Compare commits
No commits in common. "1016_part3" and "v0.0.4-release.2" have entirely different histories.
1016_part3
...
v0.0.4-rel
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: 485 KiB After Width: | Height: | Size: 229 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 538 KiB |
|
|
@ -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.4-release.2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
|
|
|||
|
|
@ -22,9 +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" />
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -35,9 +35,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,17 +55,17 @@ 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 | Tomb Raider V Remastered |
|
||||
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| ASTRO's PLAYROOM | Dead Cells |
|
||||
| Hades | Dead Cells |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
| God of War: Sons of Sparta | Tomb Raider V Remastered |
|
||||
| PAC-MAN World Re-PAC | Cult of the Lamb |
|
||||
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ The GUI shows 14 variables on its **Options** and **Game options** pages. This l
|
|||
`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 list contains 252 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
|
||||
|
|
@ -29,18 +29,18 @@ The descriptions come from the code that reads each variable. The emulator was n
|
|||
|
||||
| Group | Variables |
|
||||
| --- | --- |
|
||||
| Behavior switches | 19 |
|
||||
| GPU and Vulkan | 59 |
|
||||
| Behavior switches | 16 |
|
||||
| GPU and Vulkan | 50 |
|
||||
| Metal | 10 |
|
||||
| Audio and video | 6 |
|
||||
| Audio and video | 7 |
|
||||
| Input | 5 |
|
||||
| Memory and CPU | 19 |
|
||||
| Memory and CPU | 17 |
|
||||
| Loader | 11 |
|
||||
| Debugger | 1 |
|
||||
| Log channels | 72 |
|
||||
| Traces | 25 |
|
||||
| Traces | 22 |
|
||||
| Data dumps | 12 |
|
||||
| Performance measurement | 17 |
|
||||
| Performance measurement | 16 |
|
||||
| Internal | 1 |
|
||||
| Tests and tools | 12 |
|
||||
|
||||
|
|
@ -52,16 +52,13 @@ These variables change how the emulator operates.
|
|||
| --- | --- | --- | --- |
|
||||
| `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_IMPORT_LOOP_GUARD_SECONDS` | number | Sets the time that an import call pattern can repeat before the import loop guard forces a guest exit. The default is 5 seconds. The value 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` |
|
||||
|
|
@ -76,7 +73,6 @@ 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` |
|
||||
|
|
@ -113,28 +109,20 @@ These variables change the GPU path or the Vulkan presenter.
|
|||
| `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` |
|
||||
| `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. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` |
|
||||
| `SHARPEMU_VK_PIPELINE_CACHE` | `0` | `0` stops the save and the load of the Vulkan pipeline cache file. The cache then stays in memory only. Default is a persistent cache file. | `VulkanVideoPresenter.Device.Setup.cs` |
|
||||
|
||||
## Metal
|
||||
|
||||
|
|
@ -161,6 +149,7 @@ These variables apply to audio output and video playback.
|
|||
| --- | --- | --- | --- |
|
||||
| `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_AUDIO_OUT2_STACK_WRITES` | `1` or list (`portstate`, `systemstate`, `speaker`) | Controls writes of AudioOut2 results to output buffers on the guest stack. The default `1` permits all writes. A list permits only the named outputs and the other calls return success without a write. | `AudioOut2Exports.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` |
|
||||
|
|
@ -193,9 +182,7 @@ These variables apply to guest memory and guest code execution.
|
|||
| `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_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. | `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` |
|
||||
|
|
@ -323,11 +310,9 @@ Each variable records a detailed sequence of events. Traces can be large.
|
|||
| `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` |
|
||||
|
|
@ -339,7 +324,6 @@ Each variable records a detailed sequence of events. Traces can be large.
|
|||
| `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
|
||||
|
||||
|
|
@ -372,7 +356,6 @@ Each variable records timings or counts.
|
|||
| `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` |
|
||||
|
|
|
|||
|
|
@ -323,16 +323,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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ 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;
|
||||
}
|
||||
|
|
@ -157,11 +157,6 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverShaInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
|
|
@ -365,13 +360,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 +378,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 +400,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 +412,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 +603,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)
|
||||
|
|
@ -1484,44 +1458,6 @@ public sealed partial class DirectExecutionBackend
|
|||
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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -185,11 +185,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))
|
||||
{
|
||||
|
|
@ -243,8 +238,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 +340,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 +1217,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(
|
||||
|
|
@ -1518,68 +1459,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 +1516,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 +1540,6 @@ public sealed partial class DirectExecutionBackend
|
|||
if (!expectedFileProbeMiss &&
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedWaitTimeout &&
|
||||
!expectedErrorCheckRelock &&
|
||||
!expectedPadNotOpen &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedSemaphoreTrywaitAgain &&
|
||||
!expectedPollSemaBusy &&
|
||||
|
|
@ -1869,20 +1732,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;
|
||||
|
|
@ -2018,7 +1867,6 @@ public sealed partial class DirectExecutionBackend
|
|||
"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
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -79,7 +77,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];
|
||||
|
|
@ -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>
|
||||
|
|
@ -198,7 +191,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 +238,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 +245,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 +262,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);
|
||||
|
|
@ -346,11 +305,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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -9,23 +9,10 @@ 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)
|
||||
private 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 ||
|
||||
if (VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -46,36 +33,6 @@ public sealed partial class DirectExecutionBackend
|
|||
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)
|
||||
{
|
||||
|
|
@ -189,18 +146,4 @@ public sealed partial class DirectExecutionBackend
|
|||
|
||||
[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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool IsNoBlockLeaf { get; }
|
||||
|
||||
// Argument-register-only exports that never block or touch the guest
|
||||
// stack; DispatchImport runs them without the full import bookkeeping.
|
||||
public bool IsTrivialLeaf { get; }
|
||||
|
||||
public bool SuppressStrlenTrace { get; }
|
||||
|
||||
public bool IsLoopGuardBoundary { get; }
|
||||
|
|
@ -78,7 +74,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
Export = export;
|
||||
IsLeaf = isLeaf;
|
||||
IsNoBlockLeaf = isNoBlockLeaf;
|
||||
IsTrivialLeaf = export is not null && IsTrivialLeafImport(nid);
|
||||
SuppressStrlenTrace = suppressStrlenTrace;
|
||||
IsLoopGuardBoundary = isLoopGuardBoundary;
|
||||
NidHash = nidHash;
|
||||
|
|
@ -156,8 +151,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
private static readonly ulong GuestThreadTlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
|
||||
|
||||
private const ulong GuestThreadStackSize = 0x0020_0000UL;
|
||||
private const ulong GuestThreadStackHeadroom = 0x0010_0000UL;
|
||||
private const ulong GuestThreadStackSizeLimit = 0x0400_0000UL;
|
||||
|
||||
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
|
||||
|
||||
|
|
@ -292,7 +285,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private readonly List<nint> _importHandlerTrampolines = new List<nint>();
|
||||
|
||||
private const int GuestContextTransferFrameQwords = 22;
|
||||
private const int GuestContextTransferFrameQwords = 20;
|
||||
|
||||
private readonly object _guestContextTransferStubGate = new();
|
||||
|
||||
|
|
@ -613,17 +606,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
private Action? _work;
|
||||
private volatile bool _stopping;
|
||||
|
||||
// Every guest call that blocks in an HLE import resumes through a managed
|
||||
// continuation frame, so a guest thread that blocks deep inside its own call
|
||||
// tree nests host frames rather than unwinding. Demon's Souls' render thread
|
||||
// overflowed the default thread stack within a minute of reaching its main
|
||||
// loop; the reservation is virtual, so a generous stack costs nothing until
|
||||
// it is touched.
|
||||
private const int GuestExecutionStackBytes = 64 * 1024 * 1024;
|
||||
|
||||
public GuestExecutionRunner(ulong guestThreadHandle, string name, ThreadPriority priority)
|
||||
{
|
||||
_thread = new Thread(() => ThreadMain(guestThreadHandle), GuestExecutionStackBytes)
|
||||
_thread = new Thread(() => ThreadMain(guestThreadHandle))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"SharpEmu-{name}",
|
||||
|
|
@ -674,30 +659,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
finally
|
||||
{
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
_workAvailable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestStop()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_stopping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_stopping = true;
|
||||
_workAvailable.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RequestStop();
|
||||
_stopping = true;
|
||||
_workAvailable.Set();
|
||||
if (!ReferenceEquals(Thread.CurrentThread, _thread))
|
||||
{
|
||||
_thread.Join(500);
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -742,7 +715,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
private int _readyGuestThreadCount;
|
||||
|
||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||
private readonly Queue<(IVirtualMemory Memory, ulong StackBase, ulong StackSize, ulong TlsBase)> _reusableGuestThreadRegions = new();
|
||||
|
||||
private readonly Dictionary<ulong, ExternalGuestThreadState> _externalGuestThreads = new Dictionary<ulong, ExternalGuestThreadState>();
|
||||
|
||||
|
|
@ -1211,9 +1183,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
Environment.GetEnvironmentVariable("SHARPEMU_PROBE_IMPORT_RET_ADDRESS"));
|
||||
_probeImportReturnAddressCount = 0;
|
||||
_importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
|
||||
_disableImportLoopGuard = !string.Equals(
|
||||
_disableImportLoopGuard = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"),
|
||||
"0",
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||
_entryReturnSentinelRip = 0uL;
|
||||
|
|
@ -1240,7 +1212,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
_patchedEa020eLookupCall = false;
|
||||
MarkExecutionProgress();
|
||||
BindTlsBase(context);
|
||||
RouteGuestAccessFaultsToSignals();
|
||||
var previousGuestThreadScheduler = GuestThreadExecution.Scheduler;
|
||||
GuestThreadExecution.Scheduler = this;
|
||||
try
|
||||
|
|
@ -1289,7 +1260,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
|
||||
ClearImportHandlerTrampolines();
|
||||
ConfigureGuestFastPath();
|
||||
_importEntries = new ImportStubEntry[importStubs.Count];
|
||||
HashSet<ulong> hashSet = new HashSet<ulong>(importStubs.Keys);
|
||||
int num = 0;
|
||||
|
|
@ -1356,10 +1326,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
LastError = "Failed to create import trampoline for NID " + text2;
|
||||
return false;
|
||||
}
|
||||
if (TryCreateGuestFastPathStub(text2, num5, out var fastPathStub))
|
||||
{
|
||||
num5 = fastPathStub;
|
||||
}
|
||||
if (_logAllImports)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][DEBUG] SetupImportStubs: Trampoline for {text2} -> 0x{num5:X16}");
|
||||
|
|
@ -1372,7 +1338,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
num2++;
|
||||
num++;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Setup {num2}/{importStubs.Count} import stubs (direct bridge, lle_redirects={num3}, fast_path_stubs={_guestFastPathStubCount})");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Setup {num2}/{importStubs.Count} import stubs (direct bridge, lle_redirects={num3})");
|
||||
return num2 == importStubs.Count;
|
||||
}
|
||||
|
||||
|
|
@ -1456,11 +1422,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
var importIndex = currentEntries.Length + i;
|
||||
var trampoline = CreateImportHandlerTrampoline(importIndex);
|
||||
if (trampoline != 0 &&
|
||||
TryCreateGuestFastPathStub(nid, trampoline, out var runtimeFastPathStub))
|
||||
{
|
||||
trampoline = runtimeFastPathStub;
|
||||
}
|
||||
if (trampoline == 0 || !PatchImportStub((nint)(long)address, trampoline))
|
||||
{
|
||||
error = $"failed to install runtime import trampoline at 0x{address:X16}";
|
||||
|
|
@ -1872,11 +1833,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private static bool IsHlePreferredNid(string nid)
|
||||
{
|
||||
// The shipped libc memset body contains a PS5-specific int 45h path
|
||||
// which is not a Windows host instruction. Use the bounds-aware HLE
|
||||
// implementation instead of redirecting this import to LLE.
|
||||
return string.Equals(nid, "8zTFvBIAIN8", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
|
@ -1925,10 +1882,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
return HasUsableLleLibcExport("tcVi5SivF7Q", "sprintf");
|
||||
}
|
||||
if (IsLibcFileObjectExport(exportName))
|
||||
{
|
||||
return HasUsableLleLibcExport(ComputePsNid(exportName), exportName);
|
||||
}
|
||||
if (string.Equals(value, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
|
|
@ -2187,13 +2140,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
|
||||
return false;
|
||||
}
|
||||
ulong stackBottom = 0;
|
||||
ulong stackTop = 0;
|
||||
if (OperatingSystem.IsWindows() && !TryGetGuestStackBounds(target.Rsp, out stackBottom, out stackTop))
|
||||
{
|
||||
error = $"guest context transfer target rsp=0x{target.Rsp:X16} has no committed host stack mapping";
|
||||
return false;
|
||||
}
|
||||
|
||||
transferStub = GetOrCreateGuestContextTransferStub();
|
||||
if (transferStub == 0)
|
||||
|
|
@ -2230,11 +2176,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
frame[17] = target.Mxcsr == 0 ? 0x1F80u : target.Mxcsr;
|
||||
frame[18] = target.FpuControlWord == 0 ? 0x037Fu : target.FpuControlWord;
|
||||
frame[19] = target.RestoreFullFpuState ? 1u : 0u;
|
||||
// The TEB stores NT_TIB.StackBase (the high address) at +0x08 and
|
||||
// NT_TIB.StackLimit (the low address) at +0x10. Keep the frame in that
|
||||
// same order so the transfer stub can store the slots straight through.
|
||||
frame[20] = stackTop;
|
||||
frame[21] = stackBottom;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2302,7 +2243,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
|
||||
Emit(0x49); Emit(0x89); Emit(0xC3); // mov r11, rax
|
||||
EmitFiberStackBounds(code, ref offset);
|
||||
// A new >=3.50 fiber receives the SDK-defined MXCSR verbatim. A
|
||||
// resumed fiber follows _sceFiberLongJmp: preserve status bits 0-5
|
||||
// while restoring the saved control bits.
|
||||
|
|
@ -2365,31 +2305,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
}
|
||||
|
||||
// A trampoline only encodes its import index, so a runtime module load that sets up every
|
||||
// module's stubs again reuses them: the stubs then keep their bytes and are not rewritten
|
||||
// under guest threads that may be executing them.
|
||||
private readonly Dictionary<int, nint> _importHandlerTrampolineByIndex = new();
|
||||
|
||||
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
||||
{
|
||||
lock (ImportStubPatchGate)
|
||||
{
|
||||
if (_importHandlerTrampolineByIndex.TryGetValue(importIndex, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var created = CreateImportHandlerTrampolineCore(importIndex);
|
||||
if (created != 0)
|
||||
{
|
||||
_importHandlerTrampolineByIndex[importIndex] = created;
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe nint CreateImportHandlerTrampolineCore(int importIndex)
|
||||
{
|
||||
const uint stubSize = 1024u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
|
||||
|
|
@ -2565,35 +2481,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
}
|
||||
|
||||
// Patches unprotect and re-protect whole pages: two threads patching code that shares a page
|
||||
// (runtime module loads, lazy TLS site patches on guest threads) would restore the other's page
|
||||
// to read-execute while it is still writing, so every patch runs under one gate.
|
||||
private static readonly object ImportStubPatchGate = new();
|
||||
|
||||
private unsafe bool PatchImportStub(nint address, nint trampoline)
|
||||
{
|
||||
lock (ImportStubPatchGate)
|
||||
{
|
||||
return PatchImportStubLocked(address, trampoline);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe bool PatchImportStubLocked(nint address, nint trampoline)
|
||||
{
|
||||
// Every runtime module load sets up the stubs of all loaded modules again; a stub that
|
||||
// already jumps to this trampoline needs no unprotect/write/restore cycle.
|
||||
Span<byte> patch = stackalloc byte[16];
|
||||
patch[0] = 0x48;
|
||||
patch[1] = 0xB8;
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(patch[2..], trampoline);
|
||||
patch[10] = 0xFF;
|
||||
patch[11] = 0xE0;
|
||||
patch[12..].Fill(0x90);
|
||||
if (new ReadOnlySpan<byte>((void*)address, 16).SequenceEqual(patch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
uint flNewProtect = default(uint);
|
||||
if (!VirtualProtect((void*)address, 16u, 64u, &flNewProtect))
|
||||
{
|
||||
|
|
@ -2630,7 +2519,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
}
|
||||
_importHandlerTrampolines.Clear();
|
||||
_importHandlerTrampolineByIndex.Clear();
|
||||
}
|
||||
|
||||
private unsafe void CreateTlsHandler()
|
||||
|
|
@ -3783,16 +3671,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
|
||||
private static unsafe bool WriteTlsInstruction(nint address, ReadOnlySpan<byte> replacement)
|
||||
{
|
||||
// Guest threads patch their TLS sites as they first reach them; two sites on one page would
|
||||
// otherwise race the unprotect/restore pair exactly like the import stubs.
|
||||
lock (ImportStubPatchGate)
|
||||
{
|
||||
return WriteTlsInstructionLocked(address, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool WriteTlsInstructionLocked(nint address, ReadOnlySpan<byte> replacement)
|
||||
{
|
||||
if (replacement.Length is < 1 or > 15 ||
|
||||
VirtualQuery((void*)address, out var information, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||
|
|
@ -3982,33 +3860,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
public bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
out ulong stackSize)
|
||||
{
|
||||
stackBase = 0;
|
||||
stackSize = 0;
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread) ||
|
||||
thread.StackBase == 0 ||
|
||||
thread.StackSize == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stackBase = thread.StackBase;
|
||||
stackSize = thread.StackSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
|
|
@ -5375,7 +5226,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryWriteGuestExceptionContext(
|
||||
private static bool TryWriteGuestExceptionContext(
|
||||
CpuContext context,
|
||||
ulong address,
|
||||
GuestCpuContinuation continuation,
|
||||
|
|
@ -5386,7 +5237,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
|
||||
|
||||
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
|
||||
const int mcontext = 0;
|
||||
// Orbis ucontext_t has a 0x10-byte signal mask and 0x30 bytes of
|
||||
// private fields before its amd64 mcontext. These offsets match the
|
||||
// platform ABI used by libScePs5Util and Unity's Boehm GC. Supplying a
|
||||
// bare mcontext here makes the collector miss live register roots.
|
||||
const int mcontext = 0x40;
|
||||
Write64(mcontext + 0x08, hasContinuation ? continuation.Rdi : context[CpuRegister.Rdi]);
|
||||
Write64(mcontext + 0x10, hasContinuation ? continuation.Rsi : context[CpuRegister.Rsi]);
|
||||
Write64(mcontext + 0x18, hasContinuation ? continuation.Rdx : context[CpuRegister.Rdx]);
|
||||
|
|
@ -5405,10 +5260,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
var rip = hasContinuation ? continuation.Rip : context.Rip;
|
||||
var rsp = hasContinuation ? continuation.Rsp : context[CpuRegister.Rsp];
|
||||
Write64(mcontext + 0xA0, rip);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : context.Rflags);
|
||||
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
|
||||
Write64(mcontext + 0xB8, rsp);
|
||||
Write64(mcontext + 0xC8, 0x480); // sizeof(Orbis mcontext_t)
|
||||
Write64(mcontext + 0xF8, rsp);
|
||||
Write64(mcontext + 0x440, hasContinuation ? continuation.FsBase : context.FsBase);
|
||||
Write64(mcontext + 0x448, hasContinuation ? continuation.GsBase : context.GsBase);
|
||||
return context.Memory.TryWrite(address, bytes);
|
||||
|
|
@ -5464,7 +5318,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
_readyGuestThreads.Clear();
|
||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||
_guestThreads.Clear();
|
||||
_reusableGuestThreadRegions.Clear();
|
||||
_externalGuestThreads.Clear();
|
||||
_pendingGuestExceptions.Clear();
|
||||
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
||||
|
|
@ -5489,64 +5342,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
error = "creator context memory is not backed by IVirtualMemory";
|
||||
return false;
|
||||
}
|
||||
var requestedStackSize = request.StackSize;
|
||||
if (requestedStackSize > GuestThreadStackSizeLimit)
|
||||
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
|
||||
{
|
||||
requestedStackSize = GuestThreadStackSizeLimit;
|
||||
return false;
|
||||
}
|
||||
|
||||
var stackSize = requestedStackSize == 0
|
||||
? GuestThreadStackSize
|
||||
: AlignUp(requestedStackSize + GuestThreadStackHeadroom, 0x1000UL);
|
||||
if (stackSize < GuestThreadStackSize)
|
||||
if (!TryMapGuestThreadTlsRegion(virtualMemory, out var tlsBase, out error))
|
||||
{
|
||||
stackSize = GuestThreadStackSize;
|
||||
}
|
||||
|
||||
ulong stackBase = 0;
|
||||
ulong tlsBase = 0;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
var count = _reusableGuestThreadRegions.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var region = _reusableGuestThreadRegions.Dequeue();
|
||||
if (stackBase == 0 && ReferenceEquals(region.Memory, virtualMemory) && region.StackSize >= stackSize)
|
||||
{
|
||||
stackBase = region.StackBase;
|
||||
stackSize = region.StackSize;
|
||||
tlsBase = region.TlsBase;
|
||||
}
|
||||
else
|
||||
{
|
||||
_reusableGuestThreadRegions.Enqueue(region);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stackBase == 0)
|
||||
{
|
||||
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out stackBase, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!TryMapGuestThreadTlsRegion(virtualMemory, out tlsBase, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<byte> zero = stackalloc byte[4096];
|
||||
zero.Clear();
|
||||
var start = tlsBase - GuestThreadTlsPrefixSize;
|
||||
for (ulong offset = 0; offset < GuestThreadTlsPrefixSize + GuestThreadTlsSize; offset += (ulong)zero.Length)
|
||||
{
|
||||
if (!virtualMemory.TryWrite(start + offset, zero[..(int)Math.Min((ulong)zero.Length, GuestThreadTlsPrefixSize + GuestThreadTlsSize - offset)]))
|
||||
{
|
||||
error = "failed to reset reused guest TLS";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var trackedMemory = new TrackedCpuMemory(virtualMemory);
|
||||
|
|
@ -5557,7 +5359,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
FsBase = tlsBase,
|
||||
GsBase = tlsBase,
|
||||
};
|
||||
context[CpuRegister.Rsp] = stackBase + stackSize - sizeof(ulong);
|
||||
context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong);
|
||||
context[CpuRegister.Rdi] = request.Argument;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
|
|
@ -5580,7 +5382,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
AffinityMask = request.AffinityMask,
|
||||
Context = context,
|
||||
StackBase = stackBase,
|
||||
StackSize = stackSize,
|
||||
StackSize = GuestThreadStackSize,
|
||||
State = GuestThreadRunState.Ready,
|
||||
};
|
||||
error = null;
|
||||
|
|
@ -5746,7 +5548,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
var hostAffinityMask = MapGuestThreadAffinity(guestAffinityMask);
|
||||
if (hostAffinityMask == 0)
|
||||
{
|
||||
SharpEmu.HLE.Host.HostLaneReservation.ApplyToGuestThread();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -5760,7 +5561,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
|
||||
private static ulong MapGuestThreadAffinity(ulong guestAffinityMask)
|
||||
{
|
||||
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue || !GuestAffinityEnabled)
|
||||
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -5820,20 +5621,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guest affinity is not applied to host threads unless
|
||||
/// SHARPEMU_GUEST_AFFINITY=1. A console title pins one spinning worker per
|
||||
/// dedicated core; on a shared host, pinning traps its renderer and any lock
|
||||
/// holder on a lane next to a busy spinner of equal priority, where it waits
|
||||
/// out whole scheduler quanta. Measured with Demon's Souls on a 16-lane host:
|
||||
/// intro 10.6 → 44.7 fps and menus 11.6 → 47.5 fps with pinning off.
|
||||
/// </summary>
|
||||
private static readonly bool GuestAffinityEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_AFFINITY") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Host lanes kept away from pinned guest threads. Measured on a 16-lane host
|
||||
/// with Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps,
|
||||
/// so the useful range is a bit over a third of the machine — too few and the
|
||||
/// Host lanes kept away from guest threads. Measured on a 16-lane host with
|
||||
/// Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps, so
|
||||
/// the useful range is a bit over a third of the machine — too few and the
|
||||
/// emulator is crowded out, too many and the guest cannot make progress.
|
||||
/// </summary>
|
||||
private static readonly int EmulatorReservedLanes =
|
||||
|
|
@ -6023,12 +5813,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
pendingAfterExecutorRelease = pending;
|
||||
}
|
||||
if (thread.State == GuestThreadRunState.Exited &&
|
||||
TryGetVirtualMemory(thread.Context, out var memory))
|
||||
{
|
||||
_reusableGuestThreadRegions.Enqueue((memory, thread.StackBase, thread.StackSize, thread.Context.FsBase));
|
||||
thread.ExecutionRunner?.RequestStop();
|
||||
}
|
||||
}
|
||||
if (pendingAfterExecutorRelease is { } pendingException &&
|
||||
!TryRaiseGuestException(
|
||||
|
|
@ -6187,7 +5971,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
RouteGuestAccessFaultsToSignals();
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
// Rosetta does not reliably permit a generated x86 thunk to write data
|
||||
// in the same page from which it is currently executing, even when the
|
||||
|
|
@ -6414,7 +6197,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
RouteGuestAccessFaultsToSignals();
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)hostRspStorage;
|
||||
var emitter = new NativeCodeEmitter(ptr2);
|
||||
|
|
@ -6645,7 +6427,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] ExecuteEntry starting at 0x{entryPoint:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] RSP=0x{context[CpuRegister.Rsp]:X16}, RDI=0x{context[CpuRegister.Rdi]:X16}");
|
||||
SharpEmu.HLE.Host.HostLaneReservation.ApplyToGuestThread();
|
||||
ulong num = context[CpuRegister.Rsp];
|
||||
if (num == 0)
|
||||
{
|
||||
|
|
@ -6688,7 +6469,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
RouteGuestAccessFaultsToSignals();
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong num2 = (ulong)hostRspStorage;
|
||||
int num3 = 0;
|
||||
|
|
@ -6994,15 +6774,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (AreAllLiveGuestThreadsExpectedlyBlocked())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s, but every live guest thread is parked in an expected blocking primitive; continuing.");
|
||||
LogStallWatchdogSnapshot();
|
||||
Console.Error.Flush();
|
||||
MarkExecutionProgress();
|
||||
continue;
|
||||
}
|
||||
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -7070,41 +6841,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool AreAllLiveGuestThreadsExpectedlyBlocked()
|
||||
{
|
||||
using (LockGate("AreAllLiveGuestThreadsExpectedlyBlocked"))
|
||||
{
|
||||
var sawBlockedThread = false;
|
||||
foreach (var thread in _guestThreads.Values)
|
||||
{
|
||||
if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (thread.State != GuestThreadRunState.Blocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref thread.LastImportNid) is not (
|
||||
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"WKAXJ4XBPQ4" or // scePthreadCondWait
|
||||
"BmMjYxmew1w" or // scePthreadCondTimedwait
|
||||
"fzyMKs9kim0" or // sceKernelWaitEqueue
|
||||
"Zxa0VhQVTsk")) // sceKernelWaitSema
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sawBlockedThread = true;
|
||||
}
|
||||
|
||||
return sawBlockedThread;
|
||||
}
|
||||
}
|
||||
|
||||
private void StopStallWatchdog()
|
||||
{
|
||||
_stallWatchdogStop = true;
|
||||
|
|
|
|||
|
|
@ -53,9 +53,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||
|
||||
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)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -371,33 +371,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
return true;
|
||||
}
|
||||
|
||||
// Aligned starts of the free host ranges in [low, highExclusive) that can hold size
|
||||
// bytes, from the host's own region map. The ranges are free when listed; another
|
||||
// thread may take one before the caller allocates it.
|
||||
public IEnumerable<ulong> EnumerateFreeHostRanges(ulong low, ulong highExclusive, ulong size, ulong alignment)
|
||||
{
|
||||
var address = low;
|
||||
while (address < highExclusive && _hostMemory.Query(address, out var info) && info.RegionSize != 0)
|
||||
{
|
||||
var regionEnd = info.BaseAddress + info.RegionSize;
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (info.State == HostRegionState.Free)
|
||||
{
|
||||
var start = (address + alignment - 1) & ~(alignment - 1);
|
||||
if (start >= address && start + size >= start && start + size <= Math.Min(regionEnd, highExclusive))
|
||||
{
|
||||
yield return start;
|
||||
}
|
||||
}
|
||||
|
||||
address = regionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
public string DescribeAddressForDiagnostics(ulong address)
|
||||
{
|
||||
if (!_hostMemory.Query(address, out var info))
|
||||
|
|
@ -1081,13 +1054,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
|
||||
public bool IsBackedRange(ulong address, ulong size)
|
||||
{
|
||||
// A backed range confirmed from the view snapshot needs no lock; a miss (partly
|
||||
// unbacked, or a view being remapped) takes the locked path.
|
||||
if (_backedSpace is { } backed && !_disposed && backed.IsBackedWithoutLock(address, size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
|
|
@ -1174,18 +1140,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
}
|
||||
}
|
||||
|
||||
public object? BackingAliasSnapshot => _backedSpace?.AliasSnapshot;
|
||||
|
||||
public bool TryEnterBackingAliasAccess() => !_disposed && _backedSpace is { } backed && backed.TryEnterAliasAccess();
|
||||
|
||||
public void ExitBackingAliasAccess() => _backedSpace?.ExitAliasAccess();
|
||||
|
||||
public bool TryResolveBackingAlias(ulong address, ulong size, out ulong alias)
|
||||
{
|
||||
alias = 0;
|
||||
return !_disposed && _backedSpace is { } backed && backed.TryResolveAlias(address, size, out alias);
|
||||
}
|
||||
|
||||
private bool HasBackingOwner() => !_disposed && _backedSpace != null;
|
||||
|
||||
// A span may cross adjacent view records; the owner validates full coverage.
|
||||
|
|
@ -1322,14 +1276,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
return true;
|
||||
}
|
||||
|
||||
// POSIX cannot query foreign host mappings. The free ranges were
|
||||
// reserved at startup, so a failed candidate means the gap is taken.
|
||||
if (!OperatingSystem.IsWindows() && reservedCandidate != 0)
|
||||
{
|
||||
address = reservedCandidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
start = candidate + GuestMemoryLayout.GuestPage;
|
||||
if (OperatingSystem.IsWindows() && _hostMemory.Query(candidate, out var info) &&
|
||||
info.BaseAddress <= candidate && info.BaseAddress < limit &&
|
||||
|
|
@ -1830,15 +1776,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
// Backed guest memory is the shared backing itself, and the backing's view table
|
||||
// is readable without a lock; only other memory needs _gate to find its region.
|
||||
// Descriptor evaluation alone reads guest words ~40 at a time per draw here.
|
||||
if (!destination.IsEmpty && _backedSpace is { } backed && !_disposed &&
|
||||
backed.TryReadSingleView(virtualAddress, destination))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
|
|
@ -1906,85 +1843,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
|
||||
{
|
||||
match = 0;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// Scan page by page in place, with the same region and protection
|
||||
// checks TryRead applies before its copy. Anything TryRead would
|
||||
// handle specially (backed views, lazy commit, protection changes)
|
||||
// is left to the caller's copying path.
|
||||
ulong offset = 0;
|
||||
while (offset < maxLength)
|
||||
{
|
||||
var current = address + offset;
|
||||
if (current < address)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = Math.Min(PageSize - (current & (PageSize - 1)), maxLength - offset);
|
||||
var region = FindRegion(current, length);
|
||||
if (region is null ||
|
||||
region.IsReservedOnly ||
|
||||
IsBackedSpan(region, current, length) ||
|
||||
!TryResolveRegionOffset(current, length, region, out var regionOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostAddress = region.VirtualAddress + regionOffset;
|
||||
if (!CanReadWithoutProtectionChange(hostAddress, length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new ReadOnlySpan<byte>((void*)hostAddress, (int)length);
|
||||
if (!findLast)
|
||||
{
|
||||
// One pass that stops at the needle or the terminator,
|
||||
// whichever comes first.
|
||||
var stop = bytes.IndexOfAny(needle, (byte)0);
|
||||
if (stop >= 0)
|
||||
{
|
||||
match = bytes[stop] == needle ? current + (ulong)stop : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
continue;
|
||||
}
|
||||
|
||||
var nulIndex = bytes.IndexOf((byte)0);
|
||||
var searched = nulIndex >= 0 ? bytes[..(nulIndex + 1)] : bytes;
|
||||
var found = findLast ? searched.LastIndexOf(needle) : searched.IndexOf(needle);
|
||||
if (found >= 0)
|
||||
{
|
||||
match = current + (ulong)found;
|
||||
if (!findLast)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) =>
|
||||
TryCompare(virtualAddress, expected, out var equal) && equal;
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
FiberExports.ResetRuntimeState();
|
||||
Http2Exports.ResetRuntimeState();
|
||||
NpAuthExports.ResetRuntimeState();
|
||||
NpUniversalDataSystemExports.ResetRuntimeState();
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
|
|
@ -668,15 +667,14 @@ 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
|
||||
// named by the main image at boot. 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, 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),
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -82,12 +82,12 @@ public sealed class GuiSettings
|
|||
|
||||
public bool OverlayEnabled { get; set; } = true;
|
||||
public string OverlayCorner { get; set; } = "TopRight";
|
||||
public string OverlayMode { get; set; } = "TitleBar";
|
||||
public string OverlayMode { get; set; } = "Full";
|
||||
|
||||
/// <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 List<string> EnvironmentToggles { get; set; } = ["SHARPEMU_WRITABLE_APP0"];
|
||||
|
||||
public string BinkPlaybackMode { get; set; } = "Guest";
|
||||
public string BinkPlaybackMode { get; set; } = "Host";
|
||||
|
||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
||||
public double RenderResolutionScale { get; set; } = 1.0;
|
||||
|
|
@ -131,7 +131,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 ?? ["SHARPEMU_WRITABLE_APP0"]);
|
||||
settings.LogLevel ??= "Info";
|
||||
settings.Language ??= "en";
|
||||
var legacyProfile = settings.EnvironmentToggles
|
||||
|
|
@ -186,9 +186,9 @@ public sealed class GuiSettings
|
|||
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.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Host", "Guest", "Skip");
|
||||
settings.OverlayCorner = NormalizeChoice(settings.OverlayCorner, "TopRight", "TopLeft", "BottomRight", "BottomLeft");
|
||||
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "TitleBar", "Full", "Minimal");
|
||||
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "Full", "Minimal", "TitleBar");
|
||||
settings.DisplayIndex = Math.Max(0, settings.DisplayIndex);
|
||||
settings.RefreshRate = Math.Clamp(settings.RefreshRate, 0, 1000);
|
||||
settings.EmbeddedConsoleHeight = NormalizeConsoleSize(settings.EmbeddedConsoleHeight, 240, 120);
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.Desc": "يقوم Host بتشغيل فيديوهات Bink عبر جسر FFmpeg، لكن قد تظهر مشكلات في بعض الألعاب.\nيستخدم Guest وحدة فك الترميز الخاصة بمحرك اللعبة، والتي يدعمها SharpEmu حاليًا بشكل جزئي فقط.\nيتخطى Skip فيديوهات Bink، مما قد يجعل التطوير أسهل.",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Skip": "تخطي"
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
"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.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; SharpEmu hazırda bunu yalnız qismən dəstəkləyir.\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",
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
"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.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, que o SharpEmu atualmente suporta apenas parcialmente.\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"
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.Desc": "Host spielt Bink-Videos über die FFmpeg-Brücke ab, bei einigen Spielen können jedoch Probleme auftreten.\nGuest verwendet den spieleigenen Decoder, den SharpEmu derzeit nur teilweise unterstützt.\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"
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.Desc": "Host afspiller Bink-videoer via FFmpeg-broen, men nogle spil kan opleve problemer.\nGuest bruger spilmotorens egen dekoder, som SharpEmu i øjeblikket kun understøtter delvist.\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"
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
"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.Desc": "Host esitab Bink-videoid FFmpegi silla kaudu, kuid mõnes mängus võib esineda probleeme.\nGuest kasutab mängumootori enda dekoodrit, mida SharpEmu praegu ainult osaliselt toetab.\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",
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
"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.Desc": "Host plays Bink videos through the FFmpeg bridge, but some games may have issues.\nGuest uses the game engine's own decoder, which SharpEmu currently supports only partially.\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",
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
"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.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, que SharpEmu actualmente solo admite de forma parcial.\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"
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
"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.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, que SharpEmu ne prend actuellement en charge que partiellement.\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"
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
"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.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, amelyet a SharpEmu jelenleg csak részben támogat.\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"
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@
|
|||
"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.Desc": "Host riproduce i video Bink tramite il bridge FFmpeg, ma alcuni giochi potrebbero avere problemi.\nGuest usa il decoder del motore di gioco, che SharpEmu attualmente supporta solo in parte.\nSkip salta i video Bink, facilitando lo sviluppo.",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Skip": "Salta"
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.Desc": "Host は FFmpeg ブリッジ経由で Bink 動画を再生しますが、一部のゲームで問題が発生する場合があります。\nGuest はゲームエンジン独自のデコーダーを使用しますが、SharpEmu は現在これを部分的にしかサポートしていません。\nSkip は Bink 動画をスキップし、開発を容易にする場合があります。",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Skip": "スキップ"
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.Desc": "Host는 FFmpeg 브리지를 통해 Bink 비디오를 재생하지만 일부 게임에서 문제가 발생할 수 있습니다.\nGuest는 게임 엔진 자체 디코더를 사용하며, SharpEmu는 현재 이를 부분적으로만 지원합니다.\nSkip은 Bink 비디오를 건너뛰어 개발을 더 쉽게 할 수 있습니다.",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Skip": "건너뛰기"
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
"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.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, die SharpEmu momenteel slechts gedeeltelijk ondersteunt.\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"
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
"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.Desc": "Host odtwarza filmy Bink przez most FFmpeg, ale w niektórych grach mogą wystąpić problemy.\nGuest używa dekodera silnika gry, który SharpEmu obecnie obsługuje tylko częściowo.\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ń",
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
"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.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, que o SharpEmu atualmente suporta apenas parcialmente.\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"
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
"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.Desc": "Host воспроизводит видео Bink через мост FFmpeg, однако в некоторых играх могут возникать проблемы.\nGuest использует собственный декодер игрового движка, который SharpEmu сейчас поддерживает лишь частично.\nSkip пропускает видео Bink, что может упростить разработку.",
|
||||
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
|
||||
"Options.Env.BinkPlayback.Guest": "Guest",
|
||||
"Options.Env.BinkPlayback.Skip": "Пропустить",
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@
|
|||
"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.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 ancak SharpEmu bunu şu anda yalnızca kısmen desteklemektedir.\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"
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ public partial class MainWindow
|
|||
"Fit");
|
||||
GameVSyncToggle.IsChecked = effective.VSync;
|
||||
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
|
||||
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
|
||||
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "Full");
|
||||
GameOverlayCornerBox.SelectedItem = FindChoice(_overlayCornerChoices, effective.OverlayCorner, "TopRight");
|
||||
GameHdrModeBox.SelectedItem = FindChoice(
|
||||
_hdrModeChoices,
|
||||
|
|
@ -285,7 +285,7 @@ public partial class MainWindow
|
|||
VSync = GameVSyncToggle.IsChecked == true,
|
||||
HdrMode = SelectedComboText(GameHdrModeBox, "Auto"),
|
||||
OverlayEnabled = GameOverlayEnabledToggle.IsChecked == true,
|
||||
OverlayMode = SelectedComboText(GameOverlayModeBox, "TitleBar"),
|
||||
OverlayMode = SelectedComboText(GameOverlayModeBox, "Full"),
|
||||
OverlayCorner = SelectedComboText(GameOverlayCornerBox, "TopRight"),
|
||||
EnvironmentToggles = BuildGameEnvironmentEntries(),
|
||||
CustomEnvironmentVariables = PerGameSettings.Load(_gameSettingsTitleId)?.CustomEnvironmentVariables,
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ public partial class MainWindow : Window
|
|||
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");
|
||||
OverlayModeBox.SelectionChanged += (_, _) => _settings.OverlayMode = SelectedComboText(OverlayModeBox, "Full");
|
||||
OverlayCornerBox.SelectionChanged += (_, _) => _settings.OverlayCorner = SelectedComboText(OverlayCornerBox, "TopRight");
|
||||
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
|
||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||
|
|
@ -344,7 +344,7 @@ public partial class MainWindow : Window
|
|||
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.IsChecked == true);
|
||||
BinkPlaybackBox.SelectionChanged += (_, _) =>
|
||||
_settings.BinkPlaybackMode = SelectedComboText(BinkPlaybackBox, "Guest");
|
||||
_settings.BinkPlaybackMode = SelectedComboText(BinkPlaybackBox, "Host");
|
||||
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||
|
|
@ -2545,11 +2545,6 @@ public partial class MainWindow : Window
|
|||
_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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -30,9 +30,6 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
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();
|
||||
|
|
@ -177,26 +174,6 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
@ -207,7 +184,6 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Add(address, size);
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -280,7 +256,6 @@ public sealed class GuestGpuMemory : IDisposable
|
|||
try
|
||||
{
|
||||
_spans.Remove(address, size);
|
||||
Interlocked.Increment(ref _spanVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -102,9 +102,6 @@ public static class GuestGpuMemoryHook
|
|||
public static bool NoteProtected(ulong address, ulong size, GuestPageProtection protection) =>
|
||||
_current != null && IsWithinGpuAddressSpace(address, size) && _current.NoteProtected(address, size, protection);
|
||||
|
||||
public static bool OverlapsRegistered(ulong address, ulong size) =>
|
||||
_current != null && IsWithinGpuAddressSpace(address, size) && _current.OverlapsRegistered(address, size);
|
||||
|
||||
public static void NoteUnmapped(ulong address, ulong size)
|
||||
{
|
||||
if (_current == null || !IsWithinGpuAddressSpace(address, size))
|
||||
|
|
|
|||
|
|
@ -15,20 +15,13 @@ public sealed class GuestPageTracker
|
|||
|
||||
private readonly PageGuard _pages;
|
||||
private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount];
|
||||
private readonly long[] _presentRegions = new long[(TrackerLayout.BlockCount + 63) / 64];
|
||||
private readonly object _regionGate = new();
|
||||
private readonly CpuDirtySummary _cpuDirtySummary = new();
|
||||
|
||||
public GuestPageTracker(PageGuard pages) => _pages = pages;
|
||||
|
||||
public bool HasCpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
if (IsKnownCpuClean(vaddr, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return VisitRegions(vaddr, size, create: true, (region, offset, bytes) =>
|
||||
{
|
||||
using var _ = region.Lock.Hold();
|
||||
|
|
@ -36,34 +29,6 @@ public sealed class GuestPageTracker
|
|||
});
|
||||
}
|
||||
|
||||
public bool MayHaveCpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
if (IsKnownCpuClean(vaddr, size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var remaining = size;
|
||||
var index = vaddr / BlockBytes;
|
||||
var offset = vaddr % BlockBytes;
|
||||
while (remaining != 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
if (Volatile.Read(ref _regions[index]) is not { } region || region.IsModified(WriteOrigin.Cpu, offset, bytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public long CpuDirtyEpoch => _cpuDirtySummary.Epoch;
|
||||
|
||||
public bool HasGpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
|
|
@ -74,31 +39,6 @@ public sealed class GuestPageTracker
|
|||
});
|
||||
}
|
||||
|
||||
// Lock-free pre-check for hot readers: only the GPU queue thread sets GPU-dirty bits,
|
||||
// so on that thread a clear answer is exact; a stale dirty answer merely sends the
|
||||
// caller to HasGpuDirtyPages. Allocation- and lock-free.
|
||||
public bool MayHaveGpuDirtyPages(ulong vaddr, ulong size)
|
||||
{
|
||||
ValidateRange(vaddr, size);
|
||||
var remaining = size;
|
||||
var index = vaddr / BlockBytes;
|
||||
var offset = vaddr % BlockBytes;
|
||||
while (remaining != 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
if (Volatile.Read(ref _regions[index]) is { } region && region.IsModified(WriteOrigin.Gpu, offset, bytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsCpuWriteHotRange(ulong vaddr, ulong size)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
|
|
@ -169,53 +109,29 @@ public sealed class GuestPageTracker
|
|||
|
||||
// Removes protection from a range; a GPU-dirty region flushes through onFlush without the lock.
|
||||
public bool InvalidateRegion(ulong vaddr, ulong size, Action onFlush)
|
||||
{
|
||||
var tracked = InvalidateRegion(vaddr, size, out var needsGpuFlush);
|
||||
if (needsGpuFlush)
|
||||
{
|
||||
onFlush();
|
||||
}
|
||||
|
||||
return tracked;
|
||||
}
|
||||
|
||||
// Marks the CPU write in every region whose bytes the GPU has not modified and
|
||||
// reports whether any region still holds GPU data the caller must download first
|
||||
// (one download of the whole range covers them all). Allocation-free: guest
|
||||
// command writes land here once per dword.
|
||||
public bool InvalidateRegion(ulong vaddr, ulong size, out bool needsGpuFlush)
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
ValidateRange(vaddr, size);
|
||||
var tracked = false;
|
||||
needsGpuFlush = false;
|
||||
var remaining = size;
|
||||
var index = vaddr / BlockBytes;
|
||||
var offset = vaddr % BlockBytes;
|
||||
while (remaining != 0)
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
if (Volatile.Read(ref _regions[index]) is { } region)
|
||||
tracked = true;
|
||||
bool shouldFlush;
|
||||
using (region.Lock.Hold())
|
||||
{
|
||||
tracked = true;
|
||||
using (region.Lock.Hold())
|
||||
shouldFlush = region.IsModified(WriteOrigin.Gpu, offset, bytes);
|
||||
if (!shouldFlush)
|
||||
{
|
||||
if (region.IsModified(WriteOrigin.Gpu, offset, bytes))
|
||||
{
|
||||
needsGpuFlush = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
|
||||
}
|
||||
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
remaining -= bytes;
|
||||
offset = 0;
|
||||
index++;
|
||||
}
|
||||
if (shouldFlush)
|
||||
{
|
||||
onFlush();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return tracked;
|
||||
}
|
||||
|
||||
|
|
@ -425,89 +341,6 @@ public sealed class GuestPageTracker
|
|||
}
|
||||
}
|
||||
|
||||
// True when every block of the range is tracked and has no CPU-dirty page. A missing
|
||||
// region is not known clean: the precise path creates it, fully dirty.
|
||||
// Visits the maximal runs of the range whose 4 MiB blocks may hold CPU-dirty pages: a block
|
||||
// with no region yet starts all dirty, and a region's summary bit is set while any of its
|
||||
// pages is dirty. Known-clean blocks are skipped without a lock, the same test
|
||||
// HasCpuDirtyPages starts with, so a caller that only uploads dirty pages loses nothing.
|
||||
public void ForEachPossiblyCpuDirtyRange(ulong vaddr, ulong size, Action<ulong, ulong> visit)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!new GuestSpan(vaddr, size).IsValid)
|
||||
{
|
||||
// Outside the tracked space nothing is known clean; the caller decides as before.
|
||||
visit(vaddr, size);
|
||||
return;
|
||||
}
|
||||
|
||||
var end = vaddr + size;
|
||||
var last = (end - 1) / BlockBytes;
|
||||
var runStart = 0UL;
|
||||
var inRun = false;
|
||||
var index = vaddr / BlockBytes;
|
||||
var loadedWord = -1;
|
||||
var possiblyDirty = 0UL;
|
||||
while (index <= last)
|
||||
{
|
||||
var word = (int)(index / 64);
|
||||
if (word != loadedWord)
|
||||
{
|
||||
loadedWord = word;
|
||||
possiblyDirty = ~(ulong)Volatile.Read(ref _presentRegions[word]) | _cpuDirtySummary.Word(word);
|
||||
}
|
||||
|
||||
var shift = (int)(index % 64);
|
||||
var pending = (inRun ? ~possiblyDirty : possiblyDirty) >> shift;
|
||||
if (pending == 0)
|
||||
{
|
||||
index = ((ulong)word + 1) * 64;
|
||||
continue;
|
||||
}
|
||||
|
||||
index += (ulong)System.Numerics.BitOperations.TrailingZeroCount(pending);
|
||||
if (index > last)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (inRun)
|
||||
{
|
||||
visit(runStart, index * BlockBytes - runStart);
|
||||
inRun = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
runStart = Math.Max(vaddr, index * BlockBytes);
|
||||
inRun = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRun)
|
||||
{
|
||||
visit(runStart, end - runStart);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsKnownCpuClean(ulong vaddr, ulong size)
|
||||
{
|
||||
ValidateRange(vaddr, size);
|
||||
var last = (vaddr + size - 1) / BlockBytes;
|
||||
for (var index = vaddr / BlockBytes; index <= last; index++)
|
||||
{
|
||||
if (Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Visits (region, offset, bytes) per 4 MiB chunk; a true result stops the walk early.
|
||||
private bool VisitRegions(ulong vaddr, ulong size, bool create, Func<TrackedRegion, ulong, ulong, bool> visit)
|
||||
{
|
||||
|
|
@ -546,9 +379,8 @@ public sealed class GuestPageTracker
|
|||
return existing;
|
||||
}
|
||||
|
||||
var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary);
|
||||
var created = new TrackedRegion(_pages, index * BlockBytes);
|
||||
Volatile.Write(ref _regions[index], created);
|
||||
Interlocked.Or(ref _presentRegions[index / 64], 1L << (int)(index % 64));
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@ public sealed class TrackedRegion
|
|||
private PageMask _recentCpuUploads;
|
||||
private PageMask _repeatedCpuWrites;
|
||||
private PageMask _hotCpuWrites;
|
||||
private readonly CpuDirtySummary? _summary;
|
||||
|
||||
public TrackedRegion(PageGuard pages, ulong baseAddress, CpuDirtySummary? summary = null)
|
||||
public TrackedRegion(PageGuard pages, ulong baseAddress)
|
||||
{
|
||||
if (baseAddress % BlockBytes != 0)
|
||||
{
|
||||
|
|
@ -27,13 +26,10 @@ public sealed class TrackedRegion
|
|||
}
|
||||
|
||||
_pages = pages;
|
||||
_summary = summary;
|
||||
BaseAddress = baseAddress;
|
||||
_cpuDirty.Fill();
|
||||
_writable.Fill();
|
||||
_readable.Fill();
|
||||
_summary?.Set(BaseAddress / BlockBytes, dirty: true);
|
||||
_summary?.NoteDirtied();
|
||||
}
|
||||
|
||||
public RegionLock Lock { get; } = new();
|
||||
|
|
@ -209,15 +205,8 @@ public sealed class TrackedRegion
|
|||
|
||||
private ref PageMask GetDirtyMask(WriteOrigin side) => ref side == WriteOrigin.Cpu ? ref _cpuDirty : ref _gpuDirty;
|
||||
|
||||
// Every change to the CPU-dirty mask ends here, so it also publishes the block summary.
|
||||
private void UpdateCpuProtection(bool track)
|
||||
{
|
||||
_summary?.Set(BaseAddress / BlockBytes, _cpuDirty.Any);
|
||||
if (_summary is not null && (_cpuDirty & ~_writable).Any)
|
||||
{
|
||||
_summary.NoteDirtied();
|
||||
}
|
||||
|
||||
var mask = _cpuDirty ^ _writable;
|
||||
_writable = _cpuDirty;
|
||||
if (mask.None)
|
||||
|
|
|
|||
|
|
@ -1,414 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
/// <summary>
|
||||
/// Per-host-thread state that emitted native x86-64 import stubs can read
|
||||
/// without entering managed code.
|
||||
///
|
||||
/// The hottest guest imports are trivial queries about the calling thread
|
||||
/// (pthread_self, pthread_getspecific). Routing them through the managed
|
||||
/// import gateway costs a guest-to-host stack switch, a full guest register
|
||||
/// pack, a GCHandle dereference and the whole dispatch bookkeeping - roughly
|
||||
/// three orders of magnitude more than the answer itself. This class publishes
|
||||
/// the few values those imports need into a flat native block whose address
|
||||
/// lives in a native TLS slot, so a stub can answer them in a handful of
|
||||
/// instructions and fall back to the managed gateway whenever the fast answer
|
||||
/// is not available.
|
||||
///
|
||||
/// The block is owned by the host thread; the values in it describe the guest
|
||||
/// thread currently bound to that host thread, and are republished by
|
||||
/// <see cref="GuestThreadExecution.EnterGuestThread"/> /
|
||||
/// <see cref="GuestThreadExecution.RestoreGuestThread"/>. A block whose
|
||||
/// ThreadHandle is zero means "no guest thread bound here": every stub then
|
||||
/// takes the managed path, which is also what happens before the fast path is
|
||||
/// enabled at all.
|
||||
/// </summary>
|
||||
public static unsafe class GuestFastPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Byte offset of the guest thread handle pthread_self must answer with.
|
||||
/// Only ever a real guest thread handle: for a host thread with no guest
|
||||
/// thread bound, pthread_self has to reach the managed export (it registers
|
||||
/// the thread's CPU context with the scheduler), so the slot stays zero.
|
||||
/// </summary>
|
||||
public const int BlockSelfHandleOffset = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Byte offset of the pointer to the pthread-specific value table of the
|
||||
/// thread handle KernelPthreadState would report - the bound guest thread,
|
||||
/// or the host thread's own synthetic handle when none is bound.
|
||||
/// </summary>
|
||||
public const int BlockTlsValuesOffset = 8;
|
||||
|
||||
/// <summary>Byte offset of the pthread_self fast-path hit counter.</summary>
|
||||
public const int BlockSelfHitsOffset = 16;
|
||||
|
||||
/// <summary>Byte offset of the pthread_getspecific fast-path hit counter.</summary>
|
||||
public const int BlockGetspecificHitsOffset = 32;
|
||||
|
||||
/// <summary>Size of the per-host-thread block. One cache line, room for future fast-path state.</summary>
|
||||
public const int BlockSize = 64;
|
||||
|
||||
/// <summary>
|
||||
/// pthread TLS keys the native table can answer. Keys are handed out
|
||||
/// densely from 1 upwards by pthread_key_create, so a small dense table
|
||||
/// covers every key a real title creates; anything at or above this limit
|
||||
/// is answered by the managed dictionary through the slow path.
|
||||
/// </summary>
|
||||
public const int TlsSlotCount = 1024;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, nint> TlsTables = new();
|
||||
|
||||
/// <summary>Values for keys at or above <see cref="TlsSlotCount"/>; effectively never used.</summary>
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> TlsOverflow = new();
|
||||
|
||||
private static readonly List<nint> Blocks = new();
|
||||
private static readonly object BlocksGate = new();
|
||||
|
||||
private static Action<nint>? _publishBlock;
|
||||
private static volatile bool _enabled;
|
||||
|
||||
[ThreadStatic]
|
||||
private static nint _block;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _hostThreadHandle;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _cachedTableHandle;
|
||||
|
||||
[ThreadStatic]
|
||||
private static nint _cachedTable;
|
||||
|
||||
/// <summary>True once a backend has installed native stubs that read the block.</summary>
|
||||
public static bool Enabled => _enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Arms the fast path. <paramref name="publishBlock"/> stores a block
|
||||
/// pointer in the native TLS slot the emitted stubs read, for the host
|
||||
/// thread that calls it.
|
||||
/// </summary>
|
||||
public static void Enable(Action<nint> publishBlock)
|
||||
{
|
||||
_publishBlock = publishBlock ?? throw new ArgumentNullException(nameof(publishBlock));
|
||||
_enabled = true;
|
||||
|
||||
// A block is published once per host thread, so a second backend (and
|
||||
// therefore a second TLS slot) must be handed the block this thread
|
||||
// already owns or its stubs would never see one.
|
||||
if (_block != 0)
|
||||
{
|
||||
_publishBlock(_block);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disarms every stub by clearing the calling thread's block; used by tests.</summary>
|
||||
public static void Disable()
|
||||
{
|
||||
_enabled = false;
|
||||
var block = _block;
|
||||
if (block != 0)
|
||||
{
|
||||
*(ulong*)(block + BlockSelfHandleOffset) = 0;
|
||||
*(nint*)(block + BlockTlsValuesOffset) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes <paramref name="guestThreadHandle"/> (and its pthread TLS
|
||||
/// table) as the guest thread bound to the calling host thread. Pass 0 to
|
||||
/// unbind, which leaves pthread_self on the managed gateway and falls the
|
||||
/// value table back to the host thread's own pthread storage.
|
||||
/// </summary>
|
||||
public static void BindGuestThread(ulong guestThreadHandle)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var block = EnsureBlock();
|
||||
if (block == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the self handle first so a stub can never answer with a handle
|
||||
// that no longer owns this host thread.
|
||||
*(ulong*)(block + BlockSelfHandleOffset) = 0;
|
||||
var tlsHandle = guestThreadHandle != 0 ? guestThreadHandle : _hostThreadHandle;
|
||||
*(nint*)(block + BlockTlsValuesOffset) = tlsHandle == 0 ? 0 : GetOrCreateTable(tlsHandle);
|
||||
*(ulong*)(block + BlockSelfHandleOffset) = guestThreadHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the calling host thread's own pthread handle - the one
|
||||
/// KernelPthreadState reports when no guest thread is bound - so
|
||||
/// pthread_getspecific can be answered on threads the guest scheduler does
|
||||
/// not own (the primary execution thread among them).
|
||||
/// </summary>
|
||||
public static void BindHostThread(ulong hostThreadHandle)
|
||||
{
|
||||
if (!_enabled || hostThreadHandle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hostThreadHandle = hostThreadHandle;
|
||||
var block = EnsureBlock();
|
||||
if (block == 0 || *(ulong*)(block + BlockSelfHandleOffset) != 0)
|
||||
{
|
||||
// A bound guest thread owns the value table while it runs.
|
||||
return;
|
||||
}
|
||||
|
||||
*(nint*)(block + BlockTlsValuesOffset) = GetOrCreateTable(hostThreadHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops every fast-path binding for the calling host thread, so each stub
|
||||
/// falls back to the managed gateway until the thread is bound again.
|
||||
/// </summary>
|
||||
public static void UnbindCurrentThread()
|
||||
{
|
||||
_hostThreadHandle = 0;
|
||||
var block = _block;
|
||||
if (block != 0)
|
||||
{
|
||||
*(ulong*)(block + BlockSelfHandleOffset) = 0;
|
||||
*(nint*)(block + BlockTlsValuesOffset) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fast-path hit counts summed over every host thread, for diagnostics.</summary>
|
||||
public static (long SelfHits, long GetspecificHits, int Blocks) SnapshotCounters()
|
||||
{
|
||||
long selfHits = 0;
|
||||
long getspecificHits = 0;
|
||||
int blocks;
|
||||
lock (BlocksGate)
|
||||
{
|
||||
blocks = Blocks.Count;
|
||||
foreach (var block in Blocks)
|
||||
{
|
||||
selfHits += *(long*)(block + BlockSelfHitsOffset);
|
||||
getspecificHits += *(long*)(block + BlockGetspecificHitsOffset);
|
||||
}
|
||||
}
|
||||
|
||||
return (selfHits, getspecificHits, blocks);
|
||||
}
|
||||
|
||||
/// <summary>Address of the calling host thread's block; allocates it on first use.</summary>
|
||||
public static nint EnsureBlock()
|
||||
{
|
||||
var block = _block;
|
||||
if (block != 0)
|
||||
{
|
||||
return block;
|
||||
}
|
||||
|
||||
block = (nint)NativeMemory.AllocZeroed(BlockSize);
|
||||
_block = block;
|
||||
lock (BlocksGate)
|
||||
{
|
||||
Blocks.Add(block);
|
||||
}
|
||||
|
||||
_publishBlock?.Invoke(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <summary>The pthread-specific value table for a guest (or host) thread handle.</summary>
|
||||
public static nint GetOrCreateTable(ulong threadHandle)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_cachedTableHandle == threadHandle && _cachedTable != 0)
|
||||
{
|
||||
return _cachedTable;
|
||||
}
|
||||
|
||||
var table = TlsTables.GetOrAdd(
|
||||
threadHandle,
|
||||
static _ => (nint)NativeMemory.AllocZeroed((nuint)TlsSlotCount, sizeof(ulong)));
|
||||
_cachedTableHandle = threadHandle;
|
||||
_cachedTable = table;
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>pthread_getspecific, for the managed (slow) path and for keys the table cannot hold.</summary>
|
||||
public static ulong GetSpecific(ulong threadHandle, int key)
|
||||
{
|
||||
if (threadHandle == 0 || key < 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((uint)key < TlsSlotCount)
|
||||
{
|
||||
return TlsTables.TryGetValue(threadHandle, out var table) && table != 0
|
||||
? ((ulong*)table)[key]
|
||||
: 0;
|
||||
}
|
||||
|
||||
return TlsOverflow.TryGetValue(threadHandle, out var overflow) && overflow.TryGetValue(key, out var value)
|
||||
? value
|
||||
: 0;
|
||||
}
|
||||
|
||||
/// <summary>pthread_setspecific. Writes the same storage the native stubs read.</summary>
|
||||
public static void SetSpecific(ulong threadHandle, int key, ulong value)
|
||||
{
|
||||
if (threadHandle == 0 || key < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint)key < TlsSlotCount)
|
||||
{
|
||||
((ulong*)GetOrCreateTable(threadHandle))[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
TlsOverflow
|
||||
.GetOrAdd(threadHandle, static _ => new ConcurrentDictionary<int, ulong>())[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare-and-set used by the destructor loop: clears <paramref name="key"/>
|
||||
/// only when it still holds <paramref name="expected"/>, so a destructor that
|
||||
/// re-sets the key is handled on the next iteration instead of being lost.
|
||||
/// </summary>
|
||||
public static bool TryClearSpecific(ulong threadHandle, int key, ulong expected)
|
||||
{
|
||||
if (threadHandle == 0 || key < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((uint)key < TlsSlotCount)
|
||||
{
|
||||
if (!TlsTables.TryGetValue(threadHandle, out var table) || table == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Interlocked.CompareExchange(ref ((ulong*)table)[key], 0, expected) == expected;
|
||||
}
|
||||
|
||||
return TlsOverflow.TryGetValue(threadHandle, out var overflow) &&
|
||||
overflow.TryUpdate(key, 0, expected);
|
||||
}
|
||||
|
||||
/// <summary>pthread_key_delete: the key reads back as unset on every thread.</summary>
|
||||
public static void ClearKeyEverywhere(int key)
|
||||
{
|
||||
if (key < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((uint)key < TlsSlotCount)
|
||||
{
|
||||
foreach (var table in TlsTables.Values)
|
||||
{
|
||||
if (table != 0)
|
||||
{
|
||||
((ulong*)table)[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var overflow in TlsOverflow.Values)
|
||||
{
|
||||
overflow.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Every non-zero (key, value) pair a thread currently holds, for the destructor loop.</summary>
|
||||
public static List<KeyValuePair<int, ulong>> SnapshotThreadValues(ulong threadHandle)
|
||||
{
|
||||
var result = new List<KeyValuePair<int, ulong>>();
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (TlsTables.TryGetValue(threadHandle, out var table) && table != 0)
|
||||
{
|
||||
var values = (ulong*)table;
|
||||
for (var key = 0; key < TlsSlotCount; key++)
|
||||
{
|
||||
if (values[key] != 0)
|
||||
{
|
||||
result.Add(new KeyValuePair<int, ulong>(key, values[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TlsOverflow.TryGetValue(threadHandle, out var overflow))
|
||||
{
|
||||
foreach (var entry in overflow)
|
||||
{
|
||||
if (entry.Value != 0)
|
||||
{
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>True when the thread has ever stored a pthread-specific value.</summary>
|
||||
public static bool HasThreadValues(ulong threadHandle) =>
|
||||
threadHandle != 0 &&
|
||||
(TlsTables.ContainsKey(threadHandle) || TlsOverflow.ContainsKey(threadHandle));
|
||||
|
||||
/// <summary>
|
||||
/// Drops a thread's pthread-specific storage on thread exit. Called from
|
||||
/// the exiting thread after its destructors have run, so no stub can still
|
||||
/// be reading the table; the calling thread's block is disarmed first.
|
||||
/// </summary>
|
||||
public static void ReleaseThread(ulong threadHandle)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var block = _block;
|
||||
if (block != 0 && *(ulong*)(block + BlockSelfHandleOffset) == threadHandle)
|
||||
{
|
||||
*(ulong*)(block + BlockSelfHandleOffset) = 0;
|
||||
*(nint*)(block + BlockTlsValuesOffset) = 0;
|
||||
}
|
||||
else if (block != 0 && _hostThreadHandle == threadHandle)
|
||||
{
|
||||
*(nint*)(block + BlockTlsValuesOffset) = 0;
|
||||
}
|
||||
|
||||
if (_cachedTableHandle == threadHandle)
|
||||
{
|
||||
_cachedTableHandle = 0;
|
||||
_cachedTable = 0;
|
||||
}
|
||||
|
||||
TlsOverflow.TryRemove(threadHandle, out _);
|
||||
if (TlsTables.TryRemove(threadHandle, out var table) && table != 0)
|
||||
{
|
||||
NativeMemory.Free((void*)table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
private readonly SortedList<ulong, ulong> _free = new();
|
||||
private readonly SortedList<ulong, OwnedRange> _mapped = new();
|
||||
private readonly List<(ulong Address, ulong Size)> _owned = new();
|
||||
private readonly bool _preReserveGuestAddressSpace;
|
||||
private bool _disposed;
|
||||
|
||||
public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false)
|
||||
|
|
@ -40,8 +39,14 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
// Create lookup views before concurrent fault handlers can read the range table.
|
||||
_ = _mapped.Keys;
|
||||
_ = _mapped.Values;
|
||||
_preReserveGuestAddressSpace = preReserveGuestAddressSpace;
|
||||
PreReserveGuestAddressSpace();
|
||||
if (preReserveGuestAddressSpace)
|
||||
{
|
||||
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
|
||||
{
|
||||
_owned.Add((range.Address, range.Size));
|
||||
AddFreeRange(range.Address, range.Size);
|
||||
}
|
||||
}
|
||||
_views = new SharedBackingViews(host, backingSize);
|
||||
if (!_views.IsAvailable)
|
||||
{
|
||||
|
|
@ -64,23 +69,10 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
|
||||
public bool TryReadBacking(ulong address, Span<byte> data) => _views.TryReadBacking(address, data);
|
||||
|
||||
// Reads through a view that covers the whole range without any lock; false otherwise.
|
||||
public bool TryReadSingleView(ulong address, Span<byte> data) => _views.TryReadSingleView(address, data);
|
||||
|
||||
public object AliasSnapshot => _views.AliasSnapshot;
|
||||
|
||||
public bool TryEnterAliasAccess() => _views.TryEnterAliasAccess();
|
||||
|
||||
public void ExitAliasAccess() => _views.ExitAliasAccess();
|
||||
|
||||
public bool TryResolveAlias(ulong address, ulong size, out ulong alias) => _views.TryResolveAlias(address, size, out alias);
|
||||
|
||||
public bool TryCopyBacking(ulong destination, ulong source, ulong size) => _views.TryCopyBacking(destination, source, size);
|
||||
|
||||
public bool IsBacked(ulong address, ulong size) => _views.Contains(address, size);
|
||||
|
||||
public bool IsBackedWithoutLock(ulong address, ulong size) => _views.ContainsWithoutLock(address, size);
|
||||
|
||||
public bool IsRestoredView(ulong address) => _views.IsRestoredView(address);
|
||||
|
||||
public bool TryReserveAddressRange(ulong address, ulong size)
|
||||
|
|
@ -503,22 +495,6 @@ public sealed class GuestSpaceOwner : IDisposable
|
|||
_owned.Clear();
|
||||
_free.Clear();
|
||||
_mapped.Clear();
|
||||
// A new image load needs the guest address space reserved again.
|
||||
PreReserveGuestAddressSpace();
|
||||
}
|
||||
}
|
||||
|
||||
private void PreReserveGuestAddressSpace()
|
||||
{
|
||||
if (!_preReserveGuestAddressSpace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
|
||||
{
|
||||
_owned.Add((range.Address, range.Size));
|
||||
AddFreeRange(range.Address, range.Size);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
private readonly SortedList<ulong, ViewRecord> _views = new();
|
||||
private bool _disposed;
|
||||
|
||||
// Guest command writes and their reads by the render thread reach TryWriteBacking and
|
||||
// TryReadBacking millions of times per second, a few bytes at a time, while views
|
||||
// change rarely. Single-view accesses search this immutable copy of _views without
|
||||
// the lock; every change to _views republishes it under the lock.
|
||||
private ViewRecord[] _snapshot = [];
|
||||
|
||||
// Single-view accesses in flight; Dispose waits for them before releasing the alias.
|
||||
private int _activeAccesses;
|
||||
|
||||
public SharedBackingViews(IHostViewMemory host, ulong size)
|
||||
{
|
||||
_host = host;
|
||||
|
|
@ -58,19 +49,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
|
||||
public bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (TryAccessSingleView(address, (ulong)data.Length, out var target))
|
||||
{
|
||||
try
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)target, data.Length));
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (IsAvailable && TryFindRecord(address, (ulong)data.Length, out var record))
|
||||
|
|
@ -99,40 +77,8 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The lock-free single-view read only; false (with nothing read) for anything else.
|
||||
public bool TryReadSingleView(ulong address, Span<byte> data)
|
||||
{
|
||||
if (!TryAccessSingleView(address, (ulong)data.Length, out var source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadBacking(ulong address, Span<byte> data)
|
||||
{
|
||||
if (TryAccessSingleView(address, (ulong)data.Length, out var source))
|
||||
{
|
||||
try
|
||||
{
|
||||
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// A read inside one mapping needs no temporary segment list.
|
||||
|
|
@ -248,7 +194,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
|
||||
_views[address] = new ViewRecord(address, size, offset, protection) { WasRestored = wasRestored };
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
failure = HostViewFailure.None;
|
||||
|
|
@ -323,7 +268,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
{
|
||||
old = record;
|
||||
_views.RemoveAt(index);
|
||||
PublishSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,49 +346,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Contains over the published snapshot, without the lock: descriptor validation asks
|
||||
// this for every bound buffer, and the lock is contended by guest threads. A true answer
|
||||
// is as current as a locked one would be once the lock were released.
|
||||
public bool ContainsWithoutLock(ulong address, ulong size)
|
||||
{
|
||||
if (size == 0 || ulong.MaxValue - address < size || Volatile.Read(ref _disposed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var snapshot = Volatile.Read(ref _snapshot);
|
||||
var end = address + size;
|
||||
var current = address;
|
||||
while (current < end)
|
||||
{
|
||||
var low = 0;
|
||||
var high = snapshot.Length - 1;
|
||||
var found = -1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (snapshot[middle].Address <= current)
|
||||
{
|
||||
found = middle;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (found < 0 || current >= snapshot[found].Address + snapshot[found].Size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = Math.Min(end, snapshot[found].Address + snapshot[found].Size);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Contains(ulong address, ulong size)
|
||||
{
|
||||
if (size == 0 || ulong.MaxValue - address < size)
|
||||
|
|
@ -484,15 +385,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
_disposed = true;
|
||||
views = new List<ViewRecord>(_views.Values);
|
||||
_views.Clear();
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
// A lock-free access that found a view before the snapshot emptied still copies
|
||||
// through the alias; let it finish before the alias is released.
|
||||
var spin = new SpinWait();
|
||||
while (Volatile.Read(ref _activeAccesses) != 0)
|
||||
{
|
||||
spin.SpinOnce();
|
||||
}
|
||||
|
||||
foreach (var view in views)
|
||||
|
|
@ -603,94 +495,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
// Must be called under _lock after every change to _views.
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var snapshot = new ViewRecord[_views.Count];
|
||||
_views.Values.CopyTo(snapshot, 0);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
// Resolves an access that lies inside one view to its alias address and registers
|
||||
// it as in flight; the caller must decrement _activeAccesses when done. Anything
|
||||
// else (spanning views, unmapped, disposed) is left to the locked path.
|
||||
private bool TryAccessSingleView(ulong address, ulong size, out ulong target)
|
||||
{
|
||||
target = 0;
|
||||
if (size == 0 || ulong.MaxValue - address < size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _activeAccesses);
|
||||
if (TryFindAlias(Volatile.Read(ref _snapshot), address, size, out target))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
return false;
|
||||
}
|
||||
|
||||
public object AliasSnapshot => Volatile.Read(ref _snapshot);
|
||||
|
||||
public bool TryEnterAliasAccess()
|
||||
{
|
||||
Interlocked.Increment(ref _activeAccesses);
|
||||
if (!Volatile.Read(ref _disposed) && _backing != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _activeAccesses);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ExitAliasAccess() => Interlocked.Decrement(ref _activeAccesses);
|
||||
|
||||
public bool TryResolveAlias(ulong address, ulong size, out ulong target)
|
||||
{
|
||||
target = 0;
|
||||
return size != 0 && ulong.MaxValue - address >= size && TryFindAlias(Volatile.Read(ref _snapshot), address, size, out target);
|
||||
}
|
||||
|
||||
private bool TryFindAlias(ViewRecord[] snapshot, ulong address, ulong size, out ulong target)
|
||||
{
|
||||
target = 0;
|
||||
var low = 0;
|
||||
var high = snapshot.Length - 1;
|
||||
var found = -1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (snapshot[middle].Address <= address)
|
||||
{
|
||||
found = middle;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (found >= 0 && !Volatile.Read(ref _disposed) && _backing != null)
|
||||
{
|
||||
var record = snapshot[found];
|
||||
if (address + size <= record.Address + record.Size)
|
||||
{
|
||||
var offset = record.Offset + address - record.Address;
|
||||
if (IsWithinBacking(offset, size))
|
||||
{
|
||||
target = AliasBase + offset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsWithinBacking(ulong offset, ulong size) =>
|
||||
size != 0 && offset < Size && size <= Size - offset;
|
||||
|
||||
|
|
@ -699,7 +503,6 @@ public sealed unsafe class SharedBackingViews : IDisposable
|
|||
lock (_lock)
|
||||
{
|
||||
_views[record.Address] = record;
|
||||
PublishSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ public readonly record struct GuestThreadStartRequest(
|
|||
ulong AttributeAddress,
|
||||
string Name,
|
||||
int Priority,
|
||||
ulong AffinityMask,
|
||||
ulong StackSize = 0);
|
||||
ulong AffinityMask);
|
||||
|
||||
public readonly record struct GuestThreadSnapshot(
|
||||
ulong ThreadHandle,
|
||||
|
|
@ -50,21 +49,6 @@ public interface IGuestThreadScheduler
|
|||
/// </summary>
|
||||
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the exact mapped guest stack owned by a scheduler thread. The
|
||||
/// default keeps alternate/test schedulers source-compatible when they do
|
||||
/// not own guest stack mappings.
|
||||
/// </summary>
|
||||
bool TryGetGuestThreadStackBounds(
|
||||
ulong threadHandle,
|
||||
out ulong stackBase,
|
||||
out ulong stackSize)
|
||||
{
|
||||
stackBase = 0;
|
||||
stackSize = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
|
|
@ -294,7 +278,6 @@ public static class GuestThreadExecution
|
|||
GpuMemory.GpuMemoryAccessProfile.InitializeCurrentThread();
|
||||
var previous = _currentGuestThreadHandle;
|
||||
_currentGuestThreadHandle = threadHandle;
|
||||
GuestFastPath.BindGuestThread(threadHandle);
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
|
|
@ -316,7 +299,6 @@ public static class GuestThreadExecution
|
|||
public static void RestoreGuestThread(ulong previousThreadHandle)
|
||||
{
|
||||
_currentGuestThreadHandle = previousThreadHandle;
|
||||
GuestFastPath.BindGuestThread(previousThreadHandle);
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ public static class HleDataSymbols
|
|||
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
|
||||
private static readonly nint _progNameBufferAddress = Allocate(ProgNameMaxBytes + 1);
|
||||
private static readonly nint _progNamePointerAddress = Allocate(nint.Size);
|
||||
private static readonly nint _argumentVectorAddress = Allocate(nint.Size * 2);
|
||||
private static readonly nint _libcNeedFlagAddress = Allocate(sizeof(uint));
|
||||
private static readonly nint _libcInternalNeedFlagAddress = Allocate(sizeof(uint));
|
||||
|
||||
|
|
@ -56,12 +55,6 @@ public static class HleDataSymbols
|
|||
yield return LibcInternalNeedFlagNid;
|
||||
}
|
||||
|
||||
public static int ProcessArgumentCount =>
|
||||
_progNameBufferAddress != 0 && _argumentVectorAddress != 0 ? 1 : 0;
|
||||
|
||||
public static ulong ProcessArgumentVectorAddress =>
|
||||
unchecked((ulong)_argumentVectorAddress);
|
||||
|
||||
public static void ConfigureProcessImageName(string? processImageName)
|
||||
{
|
||||
var effectiveName = string.IsNullOrWhiteSpace(processImageName)
|
||||
|
|
@ -72,9 +65,7 @@ public static class HleDataSymbols
|
|||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_progNameBufferAddress == 0 ||
|
||||
_progNamePointerAddress == 0 ||
|
||||
_argumentVectorAddress == 0)
|
||||
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -86,8 +77,6 @@ public static class HleDataSymbols
|
|||
|
||||
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
|
||||
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
|
||||
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
|
||||
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.HLE.Host;
|
||||
|
||||
public static unsafe class HostLaneReservation
|
||||
{
|
||||
private const int RelationProcessorCore = 0;
|
||||
private const int ThreadPriorityHighest = 2;
|
||||
|
||||
public static readonly ulong ReservedMask;
|
||||
public static readonly ulong GuestMask;
|
||||
|
||||
static HostLaneReservation()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || Environment.GetEnvironmentVariable("SHARPEMU_RENDER_CORE") == "0")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lanes = Environment.ProcessorCount;
|
||||
if (lanes < 8 || lanes > 64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var core = SelectCore();
|
||||
var all = lanes == 64 ? ulong.MaxValue : (1UL << lanes) - 1;
|
||||
if (core == 0 || (core & ~all) != 0 || (all & ~core) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReservedMask = core;
|
||||
GuestMask = all & ~core;
|
||||
}
|
||||
|
||||
public static bool Active => ReservedMask != 0;
|
||||
|
||||
public static void ApplyToRenderThread()
|
||||
{
|
||||
if (!Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var thread = GetCurrentThread();
|
||||
_ = SetThreadAffinityMask(thread, (nuint)ReservedMask);
|
||||
_ = SetThreadPriority(thread, ThreadPriorityHighest);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Render thread reserved host lanes 0x{ReservedMask:X}; guest threads use 0x{GuestMask:X}.");
|
||||
}
|
||||
|
||||
public static void ApplyToGuestThread()
|
||||
{
|
||||
if (Active)
|
||||
{
|
||||
_ = SetThreadAffinityMask(GetCurrentThread(), (nuint)GuestMask);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong SelectCore()
|
||||
{
|
||||
uint length = 0;
|
||||
_ = GetLogicalProcessorInformationEx(RelationProcessorCore, null, &length);
|
||||
if (length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var buffer = new byte[length];
|
||||
fixed (byte* start = buffer)
|
||||
{
|
||||
if (!GetLogicalProcessorInformationEx(RelationProcessorCore, start, &length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ulong selected = 0;
|
||||
var selectedClass = -1;
|
||||
for (uint offset = 0; offset + 48 <= length;)
|
||||
{
|
||||
var entry = start + offset;
|
||||
var size = *(uint*)(entry + 4);
|
||||
if (size == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var efficiencyClass = entry[9];
|
||||
var groupCount = *(ushort*)(entry + 30);
|
||||
var mask = *(ulong*)(entry + 32);
|
||||
var group = *(ushort*)(entry + 40);
|
||||
if (*(int*)entry == RelationProcessorCore && groupCount == 1 && group == 0 && mask != 0 && efficiencyClass >= selectedClass)
|
||||
{
|
||||
selected = mask;
|
||||
selectedClass = efficiencyClass;
|
||||
}
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetLogicalProcessorInformationEx(int relationshipType, byte* buffer, uint* returnedLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetCurrentThread();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nuint SetThreadAffinityMask(nint thread, nuint mask);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetThreadPriority(nint thread, int priority);
|
||||
}
|
||||
|
|
@ -328,7 +328,6 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||
}
|
||||
}
|
||||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions[(ulong)result] = new Region
|
||||
{
|
||||
Base = (ulong)result,
|
||||
|
|
@ -358,8 +357,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||
: munmap((nint)address, (nuint)region.Size) == 0;
|
||||
if (released)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions.Remove((ulong)address);
|
||||
Regions.Remove((ulong)address);
|
||||
}
|
||||
|
||||
return released;
|
||||
|
|
@ -494,7 +492,6 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
|
|||
|
||||
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
if (start == region.Base && size >= region.Size)
|
||||
{
|
||||
region.DefaultProtect = protect;
|
||||
|
|
|
|||
|
|
@ -96,60 +96,6 @@ internal sealed unsafe partial class PosixHostViews : IHostViewMemory
|
|||
return address;
|
||||
}
|
||||
|
||||
// POSIX has no VirtualQuery for foreign host mappings, so find the free
|
||||
// ranges by reserving halves until each attempt fits or falls below the
|
||||
// minimum. Occupied host ranges are few, so this needs few attempts.
|
||||
public IReadOnlyList<HostAddressRange> ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize)
|
||||
{
|
||||
var reserved = new List<HostAddressRange>();
|
||||
start = AlignUp(start, Granularity);
|
||||
end -= end % Granularity;
|
||||
if (start >= end || minimumSize == 0)
|
||||
{
|
||||
return reserved;
|
||||
}
|
||||
|
||||
lock (PosixViewRegions.Gate)
|
||||
{
|
||||
ReserveFreeAddressRangesLocked(start, end - start, Math.Max(minimumSize, Granularity), reserved);
|
||||
}
|
||||
|
||||
return reserved;
|
||||
}
|
||||
|
||||
private void ReserveFreeAddressRangesLocked(ulong address, ulong size, ulong minimumSize, List<HostAddressRange> reserved)
|
||||
{
|
||||
if (ReserveHoleCore(address, size) == address)
|
||||
{
|
||||
PosixViewRegions.Replace(address, size, HostMemory.MEM_RESERVE, HostMemory.PAGE_NOACCESS);
|
||||
if (reserved.Count != 0 && reserved[^1].Address + reserved[^1].Size == address)
|
||||
{
|
||||
reserved[^1] = reserved[^1] with { Size = reserved[^1].Size + size };
|
||||
}
|
||||
else
|
||||
{
|
||||
reserved.Add(new HostAddressRange(address, size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var half = size / 2;
|
||||
half -= half % Granularity;
|
||||
if (half < minimumSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReserveFreeAddressRangesLocked(address, half, minimumSize, reserved);
|
||||
ReserveFreeAddressRangesLocked(address + half, size - half, minimumSize, reserved);
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
{
|
||||
var remainder = value % alignment;
|
||||
return remainder == 0 ? value : value + alignment - remainder;
|
||||
}
|
||||
|
||||
public bool SplitHole(ulong address, ulong size) => size != 0;
|
||||
|
||||
public bool JoinHoles(ulong address, ulong size) => size != 0;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ internal static class PosixViewRegions
|
|||
|
||||
internal static void Replace(ulong address, ulong size, uint state, uint protection)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
var end = RoundPageEnd(address, size);
|
||||
var replacement = new List<Region>(Regions.Count + 2);
|
||||
foreach (var region in Regions)
|
||||
|
|
@ -51,7 +50,6 @@ internal static class PosixViewRegions
|
|||
// Mapping creates the storage. Fault-time protection updates must not allocate.
|
||||
internal static void ChangeProtection(ulong address, ulong size, uint protection)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
var end = RoundPageEnd(address, size);
|
||||
var pageSize = (ulong)Environment.SystemPageSize;
|
||||
foreach (var region in Regions)
|
||||
|
|
|
|||
|
|
@ -212,15 +212,7 @@ internal sealed unsafe class SdlHostAudio : IHostPcmAudioOutput
|
|||
break;
|
||||
}
|
||||
|
||||
// Sleep until the device should have drained to the cap rather than polling every
|
||||
// millisecond: this wait paces every audio grain, and 1 ms polls cost the audio
|
||||
// thread several wakeups per grain.
|
||||
var bytesPerMillisecond = _bytesPerFrame * (double)_sampleRate / 1_000;
|
||||
var drainMilliseconds = bytesPerMillisecond > 0
|
||||
? (queued - _maximumQueuedBytes) / bytesPerMillisecond
|
||||
: 1;
|
||||
var remainingMilliseconds = (deadline - Stopwatch.GetTimestamp()) * 1_000.0 / Stopwatch.Frequency;
|
||||
Thread.Sleep((int)Math.Clamp(Math.Ceiling(Math.Min(drainMilliseconds, remainingMilliseconds)), 1, MaximumWaitMilliseconds));
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
RecordSubmission(queued, blockStart, dropped: overrun, bytes: pcm.Length);
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@ namespace SharpEmu.HLE;
|
|||
/// </summary>
|
||||
public static unsafe class HostMemory
|
||||
{
|
||||
private static long _mappingGeneration;
|
||||
|
||||
// Changes whenever a POSIX mapping or its protection changes, so callers can cache a
|
||||
// query result until the address space changes.
|
||||
public static long MappingGeneration => Volatile.Read(ref _mappingGeneration);
|
||||
|
||||
internal static void OnMappingChanged() => Interlocked.Increment(ref _mappingGeneration);
|
||||
|
||||
public const uint MEM_COMMIT = 0x1000;
|
||||
public const uint MEM_RESERVE = 0x2000;
|
||||
public const uint MEM_RELEASE = 0x8000;
|
||||
|
|
@ -262,7 +254,6 @@ public static unsafe class HostMemory
|
|||
}
|
||||
}
|
||||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions[(ulong)result] = new Region
|
||||
{
|
||||
Base = (ulong)result,
|
||||
|
|
@ -286,7 +277,6 @@ public static unsafe class HostMemory
|
|||
return false;
|
||||
}
|
||||
|
||||
HostMemory.OnMappingChanged();
|
||||
Regions.Remove((ulong)address);
|
||||
return munmap((nint)address, (nuint)region.Size) == 0;
|
||||
}
|
||||
|
|
@ -449,7 +439,6 @@ public static unsafe class HostMemory
|
|||
|
||||
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
|
||||
{
|
||||
HostMemory.OnMappingChanged();
|
||||
if (start == region.Base && size >= region.Size)
|
||||
{
|
||||
region.DefaultProtect = protect;
|
||||
|
|
|
|||
|
|
@ -23,16 +23,6 @@ public interface ICpuMemory
|
|||
|
||||
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
||||
|
||||
// Finds the first (or last) byte equal to needle in the NUL-terminated string
|
||||
// at address, the terminator included, scanning mapped memory in place.
|
||||
// match is 0 when absent. False means this memory cannot scan directly and
|
||||
// the caller should read the string through TryRead instead.
|
||||
bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
|
||||
{
|
||||
match = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when the whole range is mapped guest memory; no bytes are copied.
|
||||
bool CanRead(ulong address, ulong size) => false;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,18 +24,4 @@ public interface IGuestBackedSpace
|
|||
// Copies through the backing alias only: no protection change, no store notification.
|
||||
bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data);
|
||||
bool TryReadBacking(ulong address, Span<byte> data);
|
||||
|
||||
object? BackingAliasSnapshot => null;
|
||||
|
||||
bool TryEnterBackingAliasAccess() => false;
|
||||
|
||||
void ExitBackingAliasAccess()
|
||||
{
|
||||
}
|
||||
|
||||
bool TryResolveBackingAlias(ulong address, ulong size, out ulong alias)
|
||||
{
|
||||
alias = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ namespace LibAtrac9
|
|||
public int SampleRateIndex { get; }
|
||||
/// <summary>
|
||||
/// A 3-bit value specifying one of 6 substream channel mappings.
|
||||
/// Always 0 for streams with independent mono channels.
|
||||
/// </summary>
|
||||
public int ChannelConfigIndex { get; }
|
||||
/// <summary>
|
||||
|
|
@ -71,12 +70,6 @@ namespace LibAtrac9
|
|||
/// The number of samples in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeSamples { get; }
|
||||
/// <summary>
|
||||
/// Non-zero for streams whose config header is 0x30: every channel is
|
||||
/// an independent mono stream with this many bytes per superframe,
|
||||
/// and the channels are interleaved frame by frame.
|
||||
/// </summary>
|
||||
public int IndependentBlockSuperframeBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
|
||||
|
|
@ -89,33 +82,16 @@ namespace LibAtrac9
|
|||
throw new InvalidDataException("Config data must be 4 bytes long");
|
||||
}
|
||||
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d, out int independentChannels);
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d);
|
||||
SampleRateIndex = a;
|
||||
ChannelConfigIndex = b;
|
||||
FrameBytes = c;
|
||||
SuperframeIndex = d;
|
||||
ConfigData = configData;
|
||||
|
||||
if (independentChannels > 0)
|
||||
{
|
||||
// The frame size field holds one channel's share; the frame as
|
||||
// a whole carries one mono block per channel.
|
||||
ChannelConfig = ChannelConfig.CreateIndependentMono(independentChannels);
|
||||
IndependentBlockSuperframeBytes = c << SuperframeIndex;
|
||||
FrameBytes = c * independentChannels;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ChannelConfigIndex >= Tables.ChannelConfig.Length)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
FrameBytes = c;
|
||||
}
|
||||
|
||||
FramesPerSuperframe = 1 << SuperframeIndex;
|
||||
SuperframeBytes = FrameBytes << SuperframeIndex;
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
|
||||
ChannelCount = ChannelConfig.ChannelCount;
|
||||
SampleRate = Tables.SampleRates[SampleRateIndex];
|
||||
|
|
@ -125,36 +101,21 @@ namespace LibAtrac9
|
|||
SuperframeSamples = FrameSamples * FramesPerSuperframe;
|
||||
}
|
||||
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex, out int independentChannels)
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
|
||||
{
|
||||
var reader = new BitReader(configData);
|
||||
|
||||
int header = reader.ReadInt(8);
|
||||
sampleRateIndex = reader.ReadInt(4);
|
||||
channelConfigIndex = 0;
|
||||
independentChannels = 0;
|
||||
if (header == IndependentMonoHeader)
|
||||
{
|
||||
// PS5 multichannel streams (ambisonics: 4, 9 or 16 channels)
|
||||
// replace the 3-bit channel mapping with a 6-bit channel count.
|
||||
independentChannels = reader.ReadInt(6) + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
}
|
||||
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
int validationBit = reader.ReadInt(1);
|
||||
frameBytes = reader.ReadInt(11) + 1;
|
||||
superframeIndex = reader.ReadInt(2);
|
||||
|
||||
if ((header != StandardHeader && header != IndependentMonoHeader) || validationBit != 0)
|
||||
if (header != 0xFE || validationBit != 0)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private const int StandardHeader = 0xFE;
|
||||
private const int IndependentMonoHeader = 0x30;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ namespace LibAtrac9
|
|||
|
||||
public bool FirstInSuperframe { get; set; }
|
||||
public bool ReuseBandParams { get; set; }
|
||||
public int SuperframeBytesUsed { get; set; }
|
||||
|
||||
public int BandCount { get; set; }
|
||||
public int StereoBand { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -19,13 +17,6 @@ namespace LibAtrac9
|
|||
}
|
||||
}
|
||||
|
||||
internal static ChannelConfig CreateIndependentMono(int channelCount)
|
||||
{
|
||||
var blockTypes = new BlockType[channelCount];
|
||||
Array.Fill(blockTypes, BlockType.Mono);
|
||||
return new ChannelConfig(blockTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of blocks or substreams in the ATRAC9 stream
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -10,57 +10,12 @@ namespace LibAtrac9
|
|||
{
|
||||
public static void UnpackFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
if (frame.Config.IndependentBlockSuperframeBytes != 0)
|
||||
{
|
||||
UnpackIndependentMonoFrame(reader, frame);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
UnpackBlock(reader, block);
|
||||
}
|
||||
}
|
||||
|
||||
// Each channel is a standalone mono superframe with a fixed byte budget,
|
||||
// interleaved frame by frame. Blocks of the earlier frames are packed
|
||||
// back to back; in the last frame every block gets whatever is left of
|
||||
// its channel's budget, so those slots end in padding.
|
||||
private static void UnpackIndependentMonoFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
int budget = frame.Config.IndependentBlockSuperframeBytes;
|
||||
bool lastFrame = frame.FrameIndex == frame.Config.FramesPerSuperframe - 1;
|
||||
int slotStart = reader.Position / 8;
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
if (frame.FrameIndex == 0)
|
||||
{
|
||||
block.SuperframeBytesUsed = 0;
|
||||
}
|
||||
|
||||
int usedBefore = block.SuperframeBytesUsed;
|
||||
if (lastFrame)
|
||||
{
|
||||
reader.Position = slotStart * 8;
|
||||
}
|
||||
|
||||
int start = reader.Position;
|
||||
UnpackBlock(reader, block);
|
||||
block.SuperframeBytesUsed += (reader.Position - start) / 8;
|
||||
if (block.SuperframeBytesUsed > budget)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
if (lastFrame)
|
||||
{
|
||||
slotStart += budget - usedBefore;
|
||||
reader.Position = slotStart * 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackBlock(BitReader reader, Block block)
|
||||
{
|
||||
ReadBlockHeader(reader, block);
|
||||
|
|
|
|||
|
|
@ -546,11 +546,8 @@ public static partial class AgcExports
|
|||
|
||||
var increment = (uint)(incrementRaw & 0xFF);
|
||||
var writeConfirm = (uint)(writeConfirmRaw & 0xFF);
|
||||
// A zero destination is legal: titles emit the packet into a template
|
||||
// and set the address later with sceAgcWriteDataPatchSetAddressOrOffset.
|
||||
// Rejecting it returned null, and patching that relocated null pointer
|
||||
// wrote garbage into the label the packet was meant to clear.
|
||||
if (commandBufferAddress == 0 ||
|
||||
destinationAddress == 0 ||
|
||||
dataAddress == 0 ||
|
||||
dwordCount > 0x3FFD)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -184,6 +184,24 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var dataOffset = (uint)ctx[CpuRegister.Rsi];
|
||||
var drawModifier = ctx[CpuRegister.Rdx];
|
||||
var emit = Interlocked.Increment(ref _indirectDrawEmitCount);
|
||||
|
||||
if (emit <= 12 || emit % 250 == 0)
|
||||
{
|
||||
var rcx = ctx[CpuRegister.Rcx];
|
||||
var dump = string.Empty;
|
||||
for (var word = 0; word < 8; word++)
|
||||
{
|
||||
dump += TryReadUInt32(ctx, rcx + dataOffset + ((ulong)word * 4), out var raw)
|
||||
? $" {raw}"
|
||||
: " ?";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] agc.emit_indirect#{emit} buf=0x{commandBufferAddress:X16} " +
|
||||
$"off=0x{dataOffset:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{rcx:X} " +
|
||||
$"r8=0x{ctx[CpuRegister.R8]:X} rcx_words:{dump}");
|
||||
}
|
||||
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
|
|
@ -224,7 +242,7 @@ public static partial class AgcExports
|
|||
|
||||
TraceAgc(
|
||||
$"agc.dcb_draw_indirect buf=0x{commandBufferAddress:X16} " +
|
||||
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X} modifier=0x{drawModifier:X}");
|
||||
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X}");
|
||||
|
||||
return ReturnPointer(ctx, drawCommand);
|
||||
}
|
||||
|
|
@ -456,5 +474,6 @@ public static partial class AgcExports
|
|||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
private static long _indirectDrawEmitCount;
|
||||
private static long _indirectDrawEmitRejectCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,36 +10,6 @@ public static partial class AgcExports
|
|||
{
|
||||
// This partial patches previously constructed AGC command packets.
|
||||
|
||||
// Uncatalogued name; the NID and import library identify this AGC export.
|
||||
// Observed ABI: packet, cache policy, next-buffer address, length in dwords.
|
||||
// The caller patches the previous DcbJump after allocating its continuation.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceAgcUnknownIkfdt",
|
||||
Target = Generation.Gen5, LibraryName = "libSceAgc")]
|
||||
public static int UnknownIkfdt(CpuContext ctx)
|
||||
{
|
||||
var packet = ctx[CpuRegister.Rdi];
|
||||
var cachePolicy = (uint)ctx[CpuRegister.Rsi];
|
||||
var target = ctx[CpuRegister.Rdx];
|
||||
var dwords = (uint)ctx[CpuRegister.Rcx];
|
||||
if (cachePolicy > 3 || dwords > 0xFFFFFu ||
|
||||
!TryGetPacketIdentity(ctx, packet, out var opcode, out _) ||
|
||||
opcode != ItIndirectBuffer ||
|
||||
!TryReadUInt32(ctx, packet, out var header) ||
|
||||
((header >> 16) & 0x3FFFu) != 2 ||
|
||||
!TryReadUInt32(ctx, packet + 12, out var control))
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
|
||||
// Preserve call/chain mode and every other control bit.
|
||||
control = (control & ~0x300F_FFFFu) | (cachePolicy << 28) | dwords;
|
||||
return TryWriteUInt32(ctx, packet + 4, (uint)target) &&
|
||||
TryWriteUInt32(ctx, packet + 8, (uint)(target >> 32) & 0xFFFFu) &&
|
||||
TryWriteUInt32(ctx, packet + 12, control)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vcmNN+AAXnY",
|
||||
ExportName = "sceAgcSetCxRegIndirectPatchSetAddress",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,13 @@ public static partial class AgcExports
|
|||
{
|
||||
// This partial exposes the shader headers the create export registered.
|
||||
|
||||
internal static ulong GetShaderHeaderAddress(ulong codeAddress) =>
|
||||
_shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
|
||||
internal static ulong GetShaderHeaderAddress(ulong codeAddress)
|
||||
{
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
return _shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ShaderHeaderRegistry CreateShaderHeaderRegistry(CpuContext context) =>
|
||||
new(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
|
||||
|
|
@ -33,10 +31,7 @@ public static partial class AgcExports
|
|||
private const ulong ShaderNumShRegistersOffset = 0x5C;
|
||||
private const int ShaderStructBytes = 0x60;
|
||||
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
|
||||
// Embedded GS/HS front/back descriptors can sit beyond the first 64 KiB of
|
||||
// the upload. Scan the full shader object size accepted by AGC so the
|
||||
// continuation is registered before resource planning sees the front half.
|
||||
private const int MaximumEmbeddedFusedScanBytes = 1024 * 1024;
|
||||
private const int MaximumEmbeddedFusedScanBytes = 64 * 1024;
|
||||
private const ulong FusedShaderImageAlignment = 4;
|
||||
private const byte ComputeShaderType = 0;
|
||||
private const byte PsShaderType = 1;
|
||||
|
|
@ -130,7 +125,10 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_shaderHeadersByCode[codeAddress] = headerAddress;
|
||||
lock (_submitTraceGate)
|
||||
{
|
||||
_shaderHeadersByCode[codeAddress] = headerAddress;
|
||||
}
|
||||
|
||||
TryRegisterEmbeddedFusedProgram(ctx, codeAddress, headerAddress);
|
||||
|
||||
|
|
@ -168,6 +166,27 @@ public static partial class AgcExports
|
|||
return false;
|
||||
}
|
||||
|
||||
var upload = new byte[MaximumEmbeddedFusedScanBytes];
|
||||
var bytesRead = 0;
|
||||
const int readChunkBytes = 4 * 1024;
|
||||
while (bytesRead < upload.Length)
|
||||
{
|
||||
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
|
||||
if (!ctx.Memory.TryRead(
|
||||
entryCodeAddress + (ulong)bytesRead,
|
||||
upload.AsSpan(bytesRead, chunkLength)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += chunkLength;
|
||||
}
|
||||
|
||||
if (bytesRead < ShaderStructBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredContinuationType = entryType == GsFrontShaderType
|
||||
? GsBackShaderType
|
||||
: HsBackShaderType;
|
||||
|
|
@ -182,95 +201,57 @@ public static partial class AgcExports
|
|||
ulong bestCodeAddress = 0;
|
||||
ulong bestHeaderAddress = 0;
|
||||
var bestDistance = ulong.MaxValue;
|
||||
|
||||
bool ScanForContinuation(ulong scanAddress)
|
||||
for (var offset = 0;
|
||||
offset <= bytesRead - ShaderStructBytes;
|
||||
offset += sizeof(uint))
|
||||
{
|
||||
var upload = new byte[MaximumEmbeddedFusedScanBytes];
|
||||
var bytesRead = 0;
|
||||
const int readChunkBytes = 4 * 1024;
|
||||
while (bytesRead < upload.Length)
|
||||
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
|
||||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
|
||||
{
|
||||
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
|
||||
if (!ctx.Memory.TryRead(
|
||||
scanAddress + (ulong)bytesRead,
|
||||
upload.AsSpan(bytesRead, chunkLength)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += chunkLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytesRead < ShaderStructBytes)
|
||||
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderCodeOffset..]);
|
||||
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
descriptor[(int)ShaderSizeOffset..]);
|
||||
if (continuationCodeAddress <= entryCodeAddress ||
|
||||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
|
||||
!IsValidDeclaredShaderSize(continuationSize) ||
|
||||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
|
||||
{
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var found = false;
|
||||
for (var offset = 0;
|
||||
offset <= bytesRead - ShaderStructBytes;
|
||||
offset += sizeof(uint))
|
||||
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderSpecialsOffset..]);
|
||||
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
|
||||
{
|
||||
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
|
||||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var entryStages) ||
|
||||
!TryReadUInt32(
|
||||
ctx,
|
||||
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var continuationStages) ||
|
||||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderCodeOffset..]);
|
||||
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
descriptor[(int)ShaderSizeOffset..]);
|
||||
if (continuationCodeAddress <= entryCodeAddress ||
|
||||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
|
||||
!IsValidDeclaredShaderSize(continuationSize) ||
|
||||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
|
||||
descriptor[(int)ShaderSpecialsOffset..]);
|
||||
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
|
||||
{
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var entryStages) ||
|
||||
!TryReadUInt32(
|
||||
ctx,
|
||||
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
|
||||
out var continuationStages) ||
|
||||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var distance = continuationCodeAddress - entryCodeAddress;
|
||||
if (distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistance = distance;
|
||||
bestCodeAddress = continuationCodeAddress;
|
||||
bestHeaderAddress = scanAddress + (ulong)offset;
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
var distance = continuationCodeAddress - entryCodeAddress;
|
||||
if (distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Most uploads place the second descriptor beside the code. Some
|
||||
// titles keep both descriptors in the header allocation instead, so
|
||||
// inspect that region as a fallback.
|
||||
ScanForContinuation(entryCodeAddress);
|
||||
if (bestHeaderAddress == 0)
|
||||
{
|
||||
ScanForContinuation(entryHeaderAddress);
|
||||
bestDistance = distance;
|
||||
bestCodeAddress = continuationCodeAddress;
|
||||
bestHeaderAddress = entryCodeAddress + (ulong)offset;
|
||||
}
|
||||
|
||||
if (bestHeaderAddress == 0)
|
||||
|
|
@ -354,17 +335,7 @@ public static partial class AgcExports
|
|||
ExportName = "sceAgcFuseShaderHalves",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int FuseShaderHalves(CpuContext ctx) => FuseShaderHalvesCore(ctx, legacy: false);
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nApJjpKNBl4",
|
||||
ExportName = "sceAgcFuseShaderHalves",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int FuseShaderHalvesLegacy(CpuContext ctx) => FuseShaderHalvesCore(ctx, legacy: true);
|
||||
|
||||
private static int FuseShaderHalvesCore(CpuContext ctx, bool legacy)
|
||||
public static int FuseShaderHalves(CpuContext ctx)
|
||||
{
|
||||
var fusedAddress = ctx[CpuRegister.Rdi];
|
||||
var frontAddress = ctx[CpuRegister.Rsi];
|
||||
|
|
@ -416,12 +387,6 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
ulong userDataAddress = 0;
|
||||
if (legacy && !TryReadUInt64(ctx, frontAddress + ShaderUserDataOffset, out userDataAddress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Span<byte> header = stackalloc byte[ShaderStructBytes];
|
||||
if (!ctx.Memory.TryRead(backAddress, header) ||
|
||||
!ctx.Memory.TryWrite(fusedAddress, header))
|
||||
|
|
@ -443,13 +408,13 @@ public static partial class AgcExports
|
|||
}
|
||||
|
||||
if (!TryWriteByte(ctx, fusedAddress + ShaderTypeOffset, isGeometryPair ? GsShaderType : HsShaderType) ||
|
||||
!ctx.TryWriteUInt64(fusedAddress + ShaderUserDataOffset, userDataAddress) ||
|
||||
!ctx.TryWriteUInt64(fusedAddress + ShaderUserDataOffset, 0) ||
|
||||
!ctx.TryWriteUInt64(fusedAddress + ShaderShRegistersOffset, fusedRegistersAddress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (isGeometryPair || legacy)
|
||||
if (isGeometryPair)
|
||||
{
|
||||
if (!TryReadUInt64(ctx, frontAddress + ShaderShRegistersOffset, out var frontRegistersAddress) ||
|
||||
!TryReadByte(ctx, frontAddress + ShaderNumShRegistersOffset, out var frontRegisterCount))
|
||||
|
|
@ -457,19 +422,10 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var merged = legacy
|
||||
? MergeLegacyFusedShaderRegisters(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount, isGeometryPair)
|
||||
: MergeFusedUserScalarCount(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount);
|
||||
if (!merged)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var checksumOffset = isGeometryPair ? SpiShaderPgmChksumGs : ShaderRegisterOffset.SpiShaderPgmChksumHs;
|
||||
for (var occurrence = 0; occurrence < 2; occurrence++)
|
||||
{
|
||||
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, checksumOffset, occurrence, out var fusedEntry) ||
|
||||
!TryFindShaderRegister(ctx, frontRegistersAddress, frontRegisterCount, checksumOffset, occurrence, out var frontEntry))
|
||||
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, SpiShaderPgmChksumGs, occurrence, out var fusedEntry) ||
|
||||
!TryFindShaderRegister(ctx, frontRegistersAddress, frontRegisterCount, SpiShaderPgmChksumGs, occurrence, out var frontEntry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -507,6 +463,7 @@ public static partial class AgcExports
|
|||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM004
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D9sr1xGUriE",
|
||||
|
|
@ -606,15 +563,9 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Span<byte> registers = stackalloc byte[InterpolantRegisterCount * InterpolantRegisterPairSize];
|
||||
for (var i = 0; i < InterpolantRegisterCount; i++)
|
||||
{
|
||||
SetInterpolantRegister(registers, i, (uint)i);
|
||||
}
|
||||
|
||||
if (inputSemanticsCount == 0)
|
||||
{
|
||||
if (!ctx.Memory.TryWrite(registersAddress, registers))
|
||||
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, 0))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
@ -642,74 +593,82 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> inputWords = stackalloc byte[(int)inputSemanticsCount * sizeof(uint)];
|
||||
var outputBytes = outputSemanticsCount * sizeof(uint);
|
||||
var rentedOutputWords = outputBytes > 1024 ? ArrayPool<byte>.Shared.Rent(outputBytes) : null;
|
||||
try
|
||||
for (uint inputIndex = 0; inputIndex < inputSemanticsCount; inputIndex++)
|
||||
{
|
||||
var outputWords = rentedOutputWords is null ? stackalloc byte[outputBytes] : rentedOutputWords.AsSpan(0, outputBytes);
|
||||
if (!ctx.Memory.TryRead(inputSemanticsAddress, inputWords) ||
|
||||
(outputBytes != 0 && !ctx.Memory.TryRead(outputSemanticsAddress, outputWords)))
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
inputSemanticsAddress + (inputIndex * sizeof(uint)),
|
||||
out var source))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
for (var inputIndex = 0; inputIndex < (int)inputSemanticsCount; inputIndex++)
|
||||
var hasMask = false;
|
||||
var mask = 0u;
|
||||
for (uint outputIndex = 0; outputIndex < outputSemanticsCount; outputIndex++)
|
||||
{
|
||||
var source = BinaryPrimitives.ReadUInt32LittleEndian(inputWords[(inputIndex * sizeof(uint))..]);
|
||||
var hasMask = TryFindOutputSemantic(outputWords, source, out var mask);
|
||||
|
||||
var mode = (source >> 20) & 0x3u;
|
||||
uint flags;
|
||||
if (mode == 0)
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
outputSemanticsAddress + (outputIndex * sizeof(uint)),
|
||||
out var candidate))
|
||||
{
|
||||
flags = (((source >> 24) & 0x1u) | (hasMask ? 0u : 1u)) << 5;
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if ((byte)candidate == (byte)source)
|
||||
{
|
||||
hasMask = true;
|
||||
mask = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var mode = (source >> 20) & 0x3u;
|
||||
uint flags;
|
||||
if (mode == 0)
|
||||
{
|
||||
flags = (((source >> 24) & 0x1u) | (hasMask ? 0u : 1u)) << 5;
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = ((source << 4) & 0x0300_0000u) + 0x0008_0000u;
|
||||
if (mode == 2)
|
||||
{
|
||||
flags &= 0xFFEF_FFDFu;
|
||||
flags |= hasMask ? ((~(mask & source) >> 16) & 0x20u) : 0x20u;
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 8);
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = ((source << 4) & 0x0300_0000u) + 0x0008_0000u;
|
||||
if (mode == 2)
|
||||
if (hasMask)
|
||||
{
|
||||
flags &= 0xFFEF_FFDFu;
|
||||
flags |= hasMask ? ((~(mask & source) >> 16) & 0x20u) : 0x20u;
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 8);
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
|
||||
var masked = mask & source;
|
||||
flags = (flags & 0xFFFF_FFDFu) | ((masked >> 15) & 0x20u);
|
||||
flags ^= 0x20u;
|
||||
flags = (flags & 0xFFEF_FFFFu) | ((~masked >> 1) & 0x0010_0000u);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasMask)
|
||||
{
|
||||
var masked = mask & source;
|
||||
flags = (flags & 0xFFFF_FFDFu) | ((masked >> 15) & 0x20u);
|
||||
flags ^= 0x20u;
|
||||
flags = (flags & 0xFFEF_FFFFu) | ((~masked >> 1) & 0x0010_0000u);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags |= 0x0010_0020u;
|
||||
}
|
||||
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
|
||||
flags |= 0x0010_0020u;
|
||||
}
|
||||
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
|
||||
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
|
||||
}
|
||||
|
||||
flags = hasMask
|
||||
? ApplyInterpolantFinalMask(flags, source, mask)
|
||||
: flags & 0xFFFF_FBE0u;
|
||||
SetInterpolantRegister(registers, inputIndex, flags);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedOutputWords is not null)
|
||||
|
||||
flags = hasMask
|
||||
? ApplyInterpolantFinalMask(flags, source, mask)
|
||||
: flags & 0xFFFF_FBE0u;
|
||||
if (!TryWriteInterpolantRegister(ctx, registersAddress, inputIndex, flags))
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(rentedOutputWords);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(registersAddress, registers))
|
||||
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, inputSemanticsCount))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
@ -825,17 +784,9 @@ public static partial class AgcExports
|
|||
}
|
||||
}
|
||||
|
||||
// The 32 (register, value) pairs are built here and written with one guest write; titles
|
||||
// call this per draw, and 64 separate 4-byte writes each paid the guest write path.
|
||||
Span<byte> registers = stackalloc byte[InterpolantRegisterCount * InterpolantRegisterPairSize];
|
||||
for (var i = 0; i < InterpolantRegisterCount; i++)
|
||||
{
|
||||
SetInterpolantRegister(registers, i, (uint)i);
|
||||
}
|
||||
|
||||
if (inputSemanticsCount == 0 || inputSemanticsAddress == 0)
|
||||
{
|
||||
if (!ctx.Memory.TryWrite(registersAddress, registers))
|
||||
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, 0))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
@ -859,48 +810,53 @@ public static partial class AgcExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
inputSemanticsCount = Math.Min(inputSemanticsCount, (uint)InterpolantRegisterCount);
|
||||
Span<byte> psWords = stackalloc byte[(int)inputSemanticsCount * sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(inputSemanticsAddress, psWords))
|
||||
inputSemanticsCount = Math.Min(inputSemanticsCount, 32u);
|
||||
for (uint psIndex = 0; psIndex < inputSemanticsCount; psIndex++)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var gsBytes = (outputSemanticsAddress != 0 ? outputSemanticsCount : 0) * sizeof(uint);
|
||||
var rentedGsWords = gsBytes > 1024 ? ArrayPool<byte>.Shared.Rent(gsBytes) : null;
|
||||
try
|
||||
{
|
||||
var gsWords = rentedGsWords is null ? stackalloc byte[gsBytes] : rentedGsWords.AsSpan(0, gsBytes);
|
||||
if (gsBytes != 0 && !ctx.Memory.TryRead(outputSemanticsAddress, gsWords))
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
inputSemanticsAddress + (psIndex * sizeof(uint)),
|
||||
out var psWord))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
for (var psIndex = 0; psIndex < (int)inputSemanticsCount; psIndex++)
|
||||
var psSemantic = psWord & 0xFFu;
|
||||
uint? gsWord = null;
|
||||
if (outputSemanticsAddress != 0)
|
||||
{
|
||||
var psWord = BinaryPrimitives.ReadUInt32LittleEndian(psWords[(psIndex * sizeof(uint))..]);
|
||||
uint? gsWord = TryFindOutputSemantic(gsWords, psWord, out var candidate) ? candidate : null;
|
||||
for (uint gsIndex = 0; gsIndex < outputSemanticsCount; gsIndex++)
|
||||
{
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
outputSemanticsAddress + (gsIndex * sizeof(uint)),
|
||||
out var candidate))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var value = (psWord & 0x0030_0000u) != 0
|
||||
? CreateInterpolantF16Value(psWord, gsWord)
|
||||
: CreateInterpolantNonF16Value(psWord, gsWord.HasValue);
|
||||
SetInterpolantRegister(
|
||||
registers,
|
||||
psIndex,
|
||||
gsWord is { } matched
|
||||
? CreateInterpolantMappingValue(value, psWord, matched)
|
||||
: CreateInterpolantDefaultParamValue(value, psWord));
|
||||
if ((candidate & 0xFFu) == psSemantic)
|
||||
{
|
||||
gsWord = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedGsWords is not null)
|
||||
|
||||
var value = (psWord & 0x0030_0000u) != 0
|
||||
? CreateInterpolantF16Value(psWord, gsWord)
|
||||
: CreateInterpolantNonF16Value(psWord, gsWord.HasValue);
|
||||
value = gsWord is { } matched
|
||||
? CreateInterpolantMappingValue(value, psWord, matched)
|
||||
: CreateInterpolantDefaultParamValue(value, psWord);
|
||||
|
||||
if (!TryWriteInterpolantRegister(ctx, registersAddress, psIndex, value))
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(rentedGsWords);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(registersAddress, registers))
|
||||
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, inputSemanticsCount))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
|
@ -979,31 +935,31 @@ public static partial class AgcExports
|
|||
return value;
|
||||
}
|
||||
|
||||
private const int InterpolantRegisterCount = 32;
|
||||
private const int InterpolantRegisterPairSize = 2 * sizeof(uint);
|
||||
|
||||
// The output (GS/VS export) semantic word whose semantic id (low byte) matches an input's.
|
||||
private static bool TryFindOutputSemantic(ReadOnlySpan<byte> outputWords, uint inputWord, out uint outputWord)
|
||||
private static bool TryWriteInterpolantRegister(
|
||||
CpuContext ctx,
|
||||
ulong registersAddress,
|
||||
uint index,
|
||||
uint value)
|
||||
{
|
||||
for (var offset = 0; offset + sizeof(uint) <= outputWords.Length; offset += sizeof(uint))
|
||||
var destination = registersAddress + (index * 8);
|
||||
return TryWriteUInt32(ctx, destination, SpiPsInputCntl0 + index) &&
|
||||
TryWriteUInt32(ctx, destination + sizeof(uint), value);
|
||||
}
|
||||
|
||||
private static bool TryWriteIdentityInterpolantRegisters(
|
||||
CpuContext ctx,
|
||||
ulong registersAddress,
|
||||
uint firstIndex)
|
||||
{
|
||||
for (uint i = firstIndex; i < 32u; i++)
|
||||
{
|
||||
outputWord = BinaryPrimitives.ReadUInt32LittleEndian(outputWords[offset..]);
|
||||
if ((byte)outputWord == (byte)inputWord)
|
||||
if (!TryWriteInterpolantRegister(ctx, registersAddress, i, i))
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
outputWord = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Slot i is the pair (SPI_PS_INPUT_CNTL_0 + i, value).
|
||||
private static void SetInterpolantRegister(Span<byte> registers, int index, uint value)
|
||||
{
|
||||
var pair = registers[(index * InterpolantRegisterPairSize)..];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(pair, SpiPsInputCntl0 + (uint)index);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(pair[sizeof(uint)..], value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static OrbisGen2Result PatchShaderProgramRegisters(CpuContext context, ulong headerAddress, ulong codeAddress)
|
||||
|
|
@ -1260,67 +1216,6 @@ public static partial class AgcExports
|
|||
(frontType == GsFrontShaderType && backType == GsBackShaderType) ||
|
||||
(frontType == HsFrontShaderType && backType == HsBackShaderType);
|
||||
|
||||
private static uint UserScalarCount(uint rsrc2) => ((rsrc2 >> 1) & 0x1Fu) | (((rsrc2 >> 27) & 1u) << 5);
|
||||
|
||||
private static bool MergeFusedUserScalarCount(CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount)
|
||||
{
|
||||
if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, SpiShaderPgmRsrc2Gs, 0, out var frontEntry) ||
|
||||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, SpiShaderPgmRsrc2Gs, 0, out var fusedEntry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, frontEntry + sizeof(uint), out var front) ||
|
||||
!TryReadUInt32(ctx, fusedEntry + sizeof(uint), out var fused))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = Math.Max(UserScalarCount(front), UserScalarCount(fused));
|
||||
var merged = (fused & ~((0x1Fu << 1) | (1u << 27))) | ((count & 0x1Fu) << 1) | ((count >> 5) << 27);
|
||||
return merged == fused || TryWriteUInt32(ctx, fusedEntry + sizeof(uint), merged);
|
||||
}
|
||||
|
||||
private static bool MergeLegacyFusedShaderRegisters(
|
||||
CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount, bool geometry)
|
||||
{
|
||||
var rsrc1Offset = geometry ? SpiShaderPgmRsrc1Gs : SpiShaderPgmRsrc1Hs;
|
||||
var rsrc2Offset = geometry ? SpiShaderPgmRsrc2Gs : ShaderRegisterOffset.SpiShaderPgmRsrc2Hs;
|
||||
if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, rsrc1Offset, 0, out var frontEntry1) ||
|
||||
!TryFindShaderRegister(ctx, frontRegisters, frontCount, rsrc2Offset, 0, out var frontEntry2) ||
|
||||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, rsrc1Offset, 0, out var fusedEntry1) ||
|
||||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, rsrc2Offset, 0, out var fusedEntry2))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, frontEntry1 + sizeof(uint), out var front1) ||
|
||||
!TryReadUInt32(ctx, frontEntry2 + sizeof(uint), out var front2) ||
|
||||
!TryReadUInt32(ctx, fusedEntry1 + sizeof(uint), out var fused1) ||
|
||||
!TryReadUInt32(ctx, fusedEntry2 + sizeof(uint), out var fused2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The older export takes maximum allocations, but user SGPRs and the
|
||||
// ES component flag belong to the front half, not the back half.
|
||||
fused1 = MergeMaxField(fused1, front1, 0, 0x3Fu);
|
||||
fused1 = MergeMaxField(fused1, front1, geometry ? 29 : 28, 3u);
|
||||
fused2 = MergeMaxField(fused2, front2, 28, 0xFu);
|
||||
if (geometry)
|
||||
{
|
||||
fused2 = MergeMaxField(fused2, front2, 16, 3u);
|
||||
fused2 = (fused2 & ~0x0004_0000u) | (front2 & 0x0004_0000u);
|
||||
}
|
||||
|
||||
fused2 = (fused2 & ~0x0800_003Eu) | (front2 & 0x0800_003Eu);
|
||||
return TryWriteUInt32(ctx, fusedEntry1 + sizeof(uint), fused1) &&
|
||||
TryWriteUInt32(ctx, fusedEntry2 + sizeof(uint), fused2);
|
||||
|
||||
static uint MergeMaxField(uint destination, uint source, int shift, uint mask) =>
|
||||
(destination & ~(mask << shift)) | (Math.Max((destination >> shift) & mask, (source >> shift) & mask) << shift);
|
||||
}
|
||||
|
||||
private static bool TryFindShaderRegister(
|
||||
CpuContext ctx,
|
||||
ulong registersAddress,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ public static partial class AgcExports
|
|||
private const uint SpiShaderPgmLoGs = 0x88;
|
||||
private const uint SpiShaderPgmHiGs = 0x89;
|
||||
private const uint SpiShaderPgmRsrc1Gs = 0x8A;
|
||||
private const uint SpiShaderPgmRsrc2Gs = 0x8B;
|
||||
private const uint SpiShaderPgmChksumGs = 0x80;
|
||||
private const uint SpiShaderPgmChksumPs = 0x06;
|
||||
private const uint SpiPsInputEna = 0x1B3;
|
||||
|
|
@ -201,10 +200,10 @@ public static partial class AgcExports
|
|||
private const ulong CommandBufferCallbackOffset = 0x20;
|
||||
private const ulong CommandBufferUserDataOffset = 0x28;
|
||||
private const ulong CommandBufferReservedDwOffset = 0x30;
|
||||
private static readonly object _submitTraceGate = new();
|
||||
// Every PM4 opcode with no handler, logged once with its first two
|
||||
// payload dwords as a possible target address.
|
||||
// Read by the render thread for every draw; written by guest threads creating shaders.
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
||||
private static readonly bool _traceAgc = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
||||
"1",
|
||||
|
|
|
|||
|
|
@ -965,8 +965,6 @@ public static class AmprExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int _unknownReadFileIdWarnings;
|
||||
|
||||
private static int CompleteReadFileRecord(CpuContext ctx, ulong commandBuffer, ReadFileCommand command)
|
||||
{
|
||||
var fileId = command.FileId;
|
||||
|
|
@ -984,13 +982,6 @@ public static class AmprExports
|
|||
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath))
|
||||
{
|
||||
// An unknown id stalls the guest's stream silently, so warn even without tracing.
|
||||
if (Interlocked.Increment(ref _unknownReadFileIdWarnings) <= 16)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.read_unknown_file_id id=0x{fileId:X8} size=0x{size:X} offset=0x{fileOffset:X}");
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ internal static class AmprFileRegistry
|
|||
private static readonly object _resolvedFileGate = new();
|
||||
private static readonly Dictionary<string, uint> _resolvedIdsByPath = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<uint, string> _resolvedPathsById = new();
|
||||
// Ids already handed to the guest; alias publishes must not re-poison them.
|
||||
private static readonly ConcurrentDictionary<uint, string> _aprResolvedPathsById = new();
|
||||
private static readonly ConcurrentDictionary<uint, byte> _loggedAprCollisionIds = new();
|
||||
// Resolved handles use a separate range from the 31-bit compatibility hashes.
|
||||
// Never reuse a handle while queued reads can still refer to it.
|
||||
private static uint _nextResolvedId = 0x80000000;
|
||||
|
|
@ -70,52 +67,24 @@ internal static class AmprFileRegistry
|
|||
|
||||
public static uint RegisterAprResolvedPath(string guestPath, string hostPath)
|
||||
{
|
||||
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
|
||||
{
|
||||
RegisterApp0Relative(relative, hostPath);
|
||||
}
|
||||
|
||||
// APR file ids are part of the guest ABI: ResolveFilepathsToIds returns
|
||||
// the 31-bit FNV-1a hash of the guest path. Keep the collision-safe
|
||||
// process-local handles used by Register() separate from this path so a
|
||||
// title can compare resolved ids with ids baked into its asset tables.
|
||||
// A hash shared by two files (13 /app0/ pairs in Demon's Souls) falls back to a handle.
|
||||
var fileId = ComputeFileId(guestPath);
|
||||
lock (_resolvedFileGate)
|
||||
{
|
||||
if (_aprResolvedPathsById.TryGetValue(fileId, out var claimedPath))
|
||||
{
|
||||
if (IsSameHostFile(claimedPath, hostPath))
|
||||
return fileId;
|
||||
}
|
||||
else if (!_hostPathsById.TryGetValue(fileId, out var indexed) ||
|
||||
(!indexed.Ambiguous && IsSameHostFile(indexed.HostPath, hostPath)))
|
||||
{
|
||||
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
|
||||
{
|
||||
RegisterApp0Relative(relative, hostPath);
|
||||
}
|
||||
|
||||
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
|
||||
_aprResolvedPathsById[fileId] = hostPath;
|
||||
return fileId;
|
||||
}
|
||||
}
|
||||
|
||||
var handle = Register(guestPath, hostPath);
|
||||
if (_loggedAprCollisionIds.TryAdd(fileId, 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.apr_id_collision id=0x{fileId:X8} path='{guestPath}' handle=0x{handle:X8}");
|
||||
}
|
||||
|
||||
return handle;
|
||||
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
private static bool IsSameHostFile(string left, string right) =>
|
||||
HostFsPath.Comparer.Equals(Path.GetFullPath(left), Path.GetFullPath(right));
|
||||
|
||||
public static bool TryGetHostPath(uint id, out string hostPath)
|
||||
{
|
||||
if ((id & 0x80000000) != 0)
|
||||
return _resolvedPathsById.TryGetValue(id, out hostPath!);
|
||||
if (_aprResolvedPathsById.TryGetValue(id, out hostPath!))
|
||||
return true;
|
||||
if (_hostPathsById.TryGetValue(id, out var entry) && !entry.Ambiguous)
|
||||
{
|
||||
hostPath = entry.HostPath;
|
||||
|
|
@ -132,10 +101,8 @@ internal static class AmprFileRegistry
|
|||
lock (_indexGate)
|
||||
{
|
||||
_hostPathsById.Clear();
|
||||
_loggedAprCollisionIds.Clear();
|
||||
lock (_resolvedFileGate)
|
||||
{
|
||||
_aprResolvedPathsById.Clear();
|
||||
_resolvedIdsByPath.Clear();
|
||||
_resolvedPathsById.Clear();
|
||||
_nextResolvedId = 0x80000000;
|
||||
|
|
@ -450,23 +417,7 @@ internal static class AmprFileRegistry
|
|||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"SharpEmu",
|
||||
"ampr-index");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
var probePath = Path.Combine(cacheDir, $".write-test-{Environment.ProcessId}");
|
||||
using (File.Create(probePath)) { }
|
||||
File.Delete(probePath);
|
||||
}
|
||||
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
|
||||
{
|
||||
// Sandboxed launches and mitigated child processes can lose access
|
||||
// to the profile AppData directory. Keep the index beside the
|
||||
// executable so the next launch can still reuse it.
|
||||
cacheDir = Path.Combine(AppContext.BaseDirectory, "user", "ampr-index");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.app0_index_cache_fallback path={cacheDir}: {exception.Message}");
|
||||
}
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
// Distinct roots must not share a cache file. Folding case is only
|
||||
// correct where the host filesystem folds it too.
|
||||
|
|
|
|||
|
|
@ -60,19 +60,10 @@ internal static class PakDirectoryTracker
|
|||
// related assets, and the guest streams them with locality, so disambiguate collisions
|
||||
// by choosing the unconsumed match nearest the running read cursor.
|
||||
DirectoryEntry? best = null;
|
||||
DirectoryEntry? uniqueMatch = null;
|
||||
var matchCount = 0;
|
||||
var bestDistance = ulong.MaxValue;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.FileLen != requestedSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueMatch = entry;
|
||||
matchCount++;
|
||||
if (entry.Consumed)
|
||||
if (entry.Consumed || entry.FileLen != requestedSize)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -99,12 +90,6 @@ internal static class PakDirectoryTracker
|
|||
|
||||
return best.FilePos;
|
||||
}
|
||||
|
||||
// A unique archive member may be opened again after its first read.
|
||||
if (matchCount == 1)
|
||||
{
|
||||
return uniqueMatch!.FilePos;
|
||||
}
|
||||
}
|
||||
|
||||
return state.NextOffset;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
|
@ -12,9 +11,6 @@ namespace SharpEmu.Libs.AppContent;
|
|||
public static class AppContentExports
|
||||
{
|
||||
private const ulong FallbackAvailableSpaceKb = 1024UL * 1024UL; // 1 GiB
|
||||
private const int AppContentErrorParameter = unchecked((int)0x80D90002);
|
||||
private const int AppContentErrorDrmNoEntitlement = unchecked((int)0x80D90007);
|
||||
private const int MountPointSize = 16;
|
||||
private const ulong BootParamAttrOffset = 4;
|
||||
private const string Temp0MountPoint = "/temp0";
|
||||
private const uint AppParamSkuFlag = 0;
|
||||
|
|
@ -102,29 +98,6 @@ public static class AppContentExports
|
|||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VANhIWcqYak",
|
||||
ExportName = "sceAppContentAddcontMount",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAppContent")]
|
||||
public static int AppContentAddcontMount(CpuContext ctx)
|
||||
{
|
||||
var entitlementLabelAddress = ctx[CpuRegister.Rsi];
|
||||
var mountPointAddress = ctx[CpuRegister.Rdx];
|
||||
if (entitlementLabelAddress == 0 || mountPointAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(AppContentErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> emptyMountPoint = stackalloc byte[MountPointSize];
|
||||
if (!ctx.Memory.TryWrite(mountPointAddress, emptyMountPoint))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(AppContentErrorDrmNoEntitlement);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "buYbeLOGWmA",
|
||||
ExportName = "sceAppContentTemporaryDataMount2",
|
||||
|
|
@ -138,7 +111,7 @@ public static class AppContentExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(KernelMemoryCompatExports.ResolveTemp0Root());
|
||||
Directory.CreateDirectory(ResolveTemp0Root());
|
||||
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
|
||||
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
|
||||
{
|
||||
|
|
@ -190,7 +163,7 @@ public static class AppContentExports
|
|||
{
|
||||
try
|
||||
{
|
||||
var storageRoot = KernelMemoryCompatExports.ResolveTemp0Root();
|
||||
var storageRoot = ResolveTemp0Root();
|
||||
Directory.CreateDirectory(storageRoot);
|
||||
var volumeRoot = Path.GetPathRoot(Path.GetFullPath(storageRoot));
|
||||
if (!string.IsNullOrWhiteSpace(volumeRoot))
|
||||
|
|
@ -267,4 +240,28 @@ public static class AppContentExports
|
|||
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
|
||||
}
|
||||
|
||||
private static string ResolveTemp0Root()
|
||||
{
|
||||
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
|
||||
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
|
||||
if (!string.IsNullOrWhiteSpace(configuredRoot))
|
||||
{
|
||||
return Path.GetFullPath(configuredRoot);
|
||||
}
|
||||
|
||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
var appName = string.IsNullOrWhiteSpace(app0Root)
|
||||
? "default"
|
||||
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
||||
if (string.IsNullOrWhiteSpace(appName))
|
||||
{
|
||||
appName = "default";
|
||||
}
|
||||
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "user", "temp", appName, "temp0");
|
||||
Environment.SetEnvironmentVariable(temp0VariableName, root);
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,16 +590,7 @@ public static class AjmExports
|
|||
return ctx.SetReturn(OrbisAjmErrorJobCreation);
|
||||
}
|
||||
|
||||
var status = TryGetInstance(instanceId, out var instance) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
|
||||
Span<byte> gapless = stackalloc byte[AjmSidebandGaplessDecodeBytes];
|
||||
if (status == 0 && gaplessAddress != 0 && ctx.Memory.TryRead(gaplessAddress, gapless))
|
||||
{
|
||||
instance!.Atrac9?.SetGapless(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(gapless),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(gapless[4..]),
|
||||
reset != 0);
|
||||
}
|
||||
|
||||
var status = TryGetInstance(instanceId, out _) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
|
||||
WriteBasicResult(ctx, resultAddress, status);
|
||||
Trace(
|
||||
$"batch_job_set_gapless_decode instance=0x{instanceId:X8} " +
|
||||
|
|
@ -1172,10 +1163,6 @@ public static class AjmExports
|
|||
|
||||
if ((flags & AjmJobSidebandFlagGaplessDecode) != 0 && (ulong)(offset + AjmSidebandGaplessDecodeBytes) <= size)
|
||||
{
|
||||
var gapless = instance?.Atrac9?.Gapless ?? default;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(sideband[offset..], gapless.TotalSamples);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 4)..], gapless.SkipSamples);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 6)..], gapless.SkippedSamples);
|
||||
offset += AjmSidebandGaplessDecodeBytes;
|
||||
}
|
||||
|
||||
|
|
@ -1331,12 +1318,8 @@ public static class AjmExports
|
|||
TryWriteUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, offset + jobSize);
|
||||
}
|
||||
|
||||
// SceAjmBatchError { int iErrorCode; const void *pJobAddress;
|
||||
// unsigned int uiCommandOffset; const void *pJobOriginRa; }. The two pointers
|
||||
// force 8-byte alignment, so the struct is 0x20 bytes (4+4 pad, 8, 4+4 pad, 8)
|
||||
// rather than the 24-byte naive field sum — pJobOriginRa lives at +0x18 and was
|
||||
// left holding stale guest bytes.
|
||||
private const int AjmBatchErrorBytes = 0x20;
|
||||
// AjmBatchError: int error_code; const void* job_addr; uint32_t cmd_offset; const void* job_ra;
|
||||
private const int AjmBatchErrorBytes = 24;
|
||||
|
||||
private static void ClearAjmBatchError(CpuContext ctx, ulong errorAddress)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ internal readonly record struct Atrac9DecodeResult(
|
|||
ulong TotalDecodedSamples,
|
||||
uint Frames);
|
||||
|
||||
internal readonly record struct Atrac9Gapless(uint TotalSamples, ushort SkipSamples, ushort SkippedSamples);
|
||||
|
||||
internal sealed class Atrac9DecodeState
|
||||
{
|
||||
internal const int ResultNotInitialized = 0x00000001;
|
||||
|
|
@ -49,33 +47,6 @@ internal sealed class Atrac9DecodeState
|
|||
private int _containerHeaderLength;
|
||||
private int _compressedLength;
|
||||
private ulong _totalDecodedSamples;
|
||||
private Atrac9Gapless _gaplessInit;
|
||||
private Atrac9Gapless _gapless;
|
||||
|
||||
public Atrac9Gapless Gapless
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _gapless;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGapless(uint totalSamples, ushort skipSamples, bool reset)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_gaplessInit = new Atrac9Gapless(totalSamples, skipSamples, 0);
|
||||
if (reset || _gapless == default)
|
||||
{
|
||||
_gapless = _gaplessInit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasSampleLimit => _gaplessInit.TotalSamples is not 0 and not uint.MaxValue;
|
||||
|
||||
public Atrac9Config? Config
|
||||
{
|
||||
|
|
@ -111,7 +82,6 @@ internal sealed class Atrac9DecodeState
|
|||
_compressedLength = 0;
|
||||
_totalDecodedSamples = 0;
|
||||
_containerHeaderLength = 0;
|
||||
_gapless = _gaplessInit;
|
||||
Trace(
|
||||
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
|
||||
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
|
||||
|
|
@ -144,7 +114,6 @@ internal sealed class Atrac9DecodeState
|
|||
_compressedLength = 0;
|
||||
_totalDecodedSamples = 0;
|
||||
_containerHeaderLength = 0;
|
||||
_gapless = _gaplessInit;
|
||||
if (_compressed is not null)
|
||||
{
|
||||
Array.Clear(_compressed);
|
||||
|
|
@ -184,6 +153,7 @@ internal sealed class Atrac9DecodeState
|
|||
}
|
||||
|
||||
var bytesPerSample = GetBytesPerSample(encoding);
|
||||
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
|
||||
var consumed = 0;
|
||||
var written = 0;
|
||||
uint frames = 0;
|
||||
|
|
@ -192,11 +162,6 @@ internal sealed class Atrac9DecodeState
|
|||
while (_compressedLength == config.SuperframeBytes ||
|
||||
consumed < input.Length)
|
||||
{
|
||||
if (HasSampleLimit && _gapless.TotalSamples == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
|
||||
// container rather than a pointer into its `data` chunk, so the
|
||||
// stream has to be advanced past the header before the first
|
||||
|
|
@ -237,15 +202,7 @@ internal sealed class Atrac9DecodeState
|
|||
break;
|
||||
}
|
||||
|
||||
var skip = Math.Min(config.SuperframeSamples, (int)_gapless.SkipSamples);
|
||||
var samples = config.SuperframeSamples - skip;
|
||||
if (HasSampleLimit)
|
||||
{
|
||||
samples = (int)Math.Min((uint)samples, _gapless.TotalSamples);
|
||||
}
|
||||
|
||||
var outputBytes = checked(samples * channels * bytesPerSample);
|
||||
if (output.Length - written < outputBytes)
|
||||
if (output.Length - written < outputBytesPerSuperframe)
|
||||
{
|
||||
status |= ResultNotEnoughRoom;
|
||||
break;
|
||||
|
|
@ -274,19 +231,14 @@ internal sealed class Atrac9DecodeState
|
|||
|
||||
WriteInterleaved(
|
||||
_planarPcm,
|
||||
output.Slice(written, outputBytes),
|
||||
skip,
|
||||
samples,
|
||||
output.Slice(written, outputBytesPerSuperframe),
|
||||
config.SuperframeSamples,
|
||||
channels,
|
||||
encoding);
|
||||
|
||||
_gapless = new Atrac9Gapless(
|
||||
HasSampleLimit ? _gapless.TotalSamples - (uint)samples : _gapless.TotalSamples,
|
||||
(ushort)(_gapless.SkipSamples - skip),
|
||||
(ushort)Math.Min(ushort.MaxValue, _gapless.SkippedSamples + config.SuperframeSamples - samples));
|
||||
written += outputBytes;
|
||||
written += outputBytesPerSuperframe;
|
||||
_compressedLength = 0;
|
||||
_totalDecodedSamples += unchecked((uint)samples);
|
||||
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
|
||||
frames += unchecked((uint)config.FramesPerSuperframe);
|
||||
|
||||
if (!multipleFrames)
|
||||
|
|
@ -401,13 +353,12 @@ internal sealed class Atrac9DecodeState
|
|||
private static void WriteInterleaved(
|
||||
short[][] source,
|
||||
Span<byte> destination,
|
||||
int firstSample,
|
||||
int samples,
|
||||
int channels,
|
||||
Atrac9PcmEncoding encoding)
|
||||
{
|
||||
var offset = 0;
|
||||
for (var sample = firstSample; sample < firstSample + samples; sample++)
|
||||
for (var sample = 0; sample < samples; sample++)
|
||||
{
|
||||
for (var channel = 0; channel < channels; channel++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioInExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "BohEAQ7DlUE",
|
||||
ExportName = "sceAudioInGetSilentState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioIn")]
|
||||
public static int GetSilentState(CpuContext ctx) =>
|
||||
// No AudioIn ports can be opened until an input backend is implemented.
|
||||
ctx.SetReturn(unchecked((int)0x80260101));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,13 +12,8 @@ namespace SharpEmu.Libs.Audio;
|
|||
|
||||
public static class AudioOutExports
|
||||
{
|
||||
// ABI: SceAudioOutOutputParam { int32_t handle; int32_t : 32; void *ptr; }.
|
||||
private const int AudioOutOutputParamSize = 16;
|
||||
private const int AudioOutMaximumOutputCount = 25;
|
||||
// ABI: SceAudioOutPortState { u16 output; u8 channel; u8 reserved8_1[1];
|
||||
// s16 volume; u16 rerouteCounter; u64 flag; u64 reserved64[2]; }.
|
||||
private const int AudioOutPortStateSize = 0x20;
|
||||
private const ushort AudioOutStateOutputConnectedPrimary = 1 << 0;
|
||||
|
||||
internal const int AudioOutErrorInvalidPort = unchecked((int)0x80260003);
|
||||
internal const int AudioOutErrorInvalidPointer = unchecked((int)0x80260004);
|
||||
|
|
@ -234,30 +229,34 @@ public static class AudioOutExports
|
|||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// ABI: int32_t sceAudioOutGetPortState(int32_t handle,
|
||||
// SceAudioOutPortState *state) — rsi is the only out pointer and the write
|
||||
// is exactly sizeof(SceAudioOutPortState):
|
||||
// +0x00 u16 output = STATE_OUTPUT_CONNECTED_PRIMARY
|
||||
// +0x02 u8 channel = SceAudioOutStateChannel
|
||||
// +0x03 u8 reserved8_1[1]
|
||||
// +0x04 s16 volume = -1 (invalid outside PADSPK)
|
||||
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
|
||||
// +0x08 u64 flag = SCE_AUDIO_OUT_STATE_FLAG_NONE
|
||||
// +0x10 u64 reserved64[2]
|
||||
Span<byte> state = stackalloc byte[AudioOutPortStateSize];
|
||||
// Same rule as AudioOut2 PortGetState: never bulk-write onto the caller
|
||||
// stack. Some titles place small locals next to the canary; a full
|
||||
// SceAudioOutPortState write smashes it.
|
||||
if (IsGuestStackAddress(stateAddress))
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// SceAudioOutPortState: report a connected primary output at full volume
|
||||
// so pacing/mixing code sees a live port. We do no host rerouting, so
|
||||
// rerouteCounter and flag stay zero.
|
||||
Span<byte> state = stackalloc byte[16];
|
||||
state.Clear();
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(state, 1);
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(
|
||||
state, AudioOutStateOutputConnectedPrimary);
|
||||
state[2] = (byte)Math.Clamp(port.Channels, 0, 8);
|
||||
System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(state[4..], -1);
|
||||
state[2..], (ushort)port.Channels);
|
||||
state[7] = 127;
|
||||
if (!ctx.Memory.TryWrite(stateAddress, state))
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool IsGuestStackAddress(ulong value) =>
|
||||
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w3PdaSTSwGE",
|
||||
ExportName = "sceAudioOutOutputs",
|
||||
|
|
|
|||
|
|
@ -17,21 +17,9 @@ public static class AudioPropagationExports
|
|||
{
|
||||
private const int Ok = 0;
|
||||
|
||||
// ABI: int32_t sceAudioPropagationSystemQueryMemory(
|
||||
// const SceAudioPropagationSystemOption *pOptions,
|
||||
// SceAudioPropagationSystemMemory *pOutMemorySize).
|
||||
// SceAudioPropagationSystemMemory is NOT a {size, alignment} pair: it is
|
||||
// +0x00 SceAudioPropagationStructDescriptor desc { uint32 id; size_t size; }
|
||||
// +0x10 void *pCpuMem (the caller allocates and fills this)
|
||||
// +0x18 size_t sizeCpuMem
|
||||
// +0x20 void *pGpuMem (the caller allocates and fills this)
|
||||
// +0x28 size_t sizeGpuMem
|
||||
// so only the two size fields belong to this call. Writing {size, alignment}
|
||||
// at +0x00/+0x08 overwrote the descriptor the caller had already initialized.
|
||||
private const int SystemMemoryCpuSizeOffset = 0x18;
|
||||
private const int SystemMemoryGpuSizeOffset = 0x28;
|
||||
private const ulong SystemMemoryCpuBytes = 0x10_0000;
|
||||
|
||||
// QueryMemory reports the working-set size the caller must allocate before
|
||||
// SystemCreate. rsi points at the out size/alignment; write a modest,
|
||||
// aligned block so the caller's allocation succeeds.
|
||||
[SysAbiExport(
|
||||
Nid = "7xyAxrusLko",
|
||||
ExportName = "sceAudioPropagationSystemQueryMemory",
|
||||
|
|
@ -40,15 +28,13 @@ public static class AudioPropagationExports
|
|||
public static int SystemQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var outAddress = ctx[CpuRegister.Rsi];
|
||||
if (outAddress == 0)
|
||||
if (outAddress != 0)
|
||||
{
|
||||
// SCE_AUDIO_PROPAGATION_ERROR_INVALID_POINTER.
|
||||
return ctx.SetReturn(unchecked((int)0x8A70_0003));
|
||||
// {size, alignment} — 1 MiB / 256 B covers the caller's allocation.
|
||||
ctx.TryWriteUInt64(outAddress, 0x10_0000);
|
||||
ctx.TryWriteUInt64(outAddress + sizeof(ulong), 0x100);
|
||||
}
|
||||
|
||||
// No GPU (Acm) propagation path exists here, so the GPU working set is 0.
|
||||
ctx.TryWriteUInt64(outAddress + SystemMemoryCpuSizeOffset, SystemMemoryCpuBytes);
|
||||
ctx.TryWriteUInt64(outAddress + SystemMemoryGpuSizeOffset, 0);
|
||||
return ctx.SetReturn(Ok);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -351,8 +351,6 @@ public static class AvPlayerExports
|
|||
: VideoFrameReadResult.Pending;
|
||||
}
|
||||
|
||||
public int QueuedFrameCount => _frames.Reader.Count;
|
||||
|
||||
private void DecodeFrames()
|
||||
{
|
||||
try
|
||||
|
|
@ -439,7 +437,6 @@ public static class AvPlayerExports
|
|||
public uint AvSyncMode { get; set; } = AvSyncModeDefault;
|
||||
public bool IsGen5 { get; init; }
|
||||
public bool Started { get; set; }
|
||||
public bool Stopped { get; set; }
|
||||
public bool Paused { get; set; }
|
||||
public bool Looping { get; set; }
|
||||
public bool EndOfStream { get; set; }
|
||||
|
|
@ -459,12 +456,6 @@ public static class AvPlayerExports
|
|||
public ulong LastVideoTimestamp { get; set; }
|
||||
public ulong EventDataBuffer { get; set; }
|
||||
public long NextFrameIndex { get; set; }
|
||||
public long VideoCalls { get; set; }
|
||||
public long VideoDelivered { get; set; }
|
||||
public long VideoPending { get; set; }
|
||||
public long VideoSyncWait { get; set; }
|
||||
public long VideoWriteFailures { get; set; }
|
||||
public long VideoStatsSecond { get; set; } = -1;
|
||||
public ulong AudioBufferBase { get; set; }
|
||||
public int NextAudioBuffer { get; set; }
|
||||
public long NextAudioFrameIndex { get; set; }
|
||||
|
|
@ -507,12 +498,6 @@ public static class AvPlayerExports
|
|||
DisposePlaybackResources();
|
||||
PlaybackClock.Reset();
|
||||
NextFrameIndex = 0;
|
||||
VideoCalls = 0;
|
||||
VideoDelivered = 0;
|
||||
VideoPending = 0;
|
||||
VideoSyncWait = 0;
|
||||
VideoWriteFailures = 0;
|
||||
VideoStatsSecond = -1;
|
||||
LastGuestBuffer = 0;
|
||||
LastVideoTimestamp = 0;
|
||||
SeekVideoFramePending = false;
|
||||
|
|
@ -555,9 +540,7 @@ public static class AvPlayerExports
|
|||
{
|
||||
Handle = handle,
|
||||
IsGen5 = IsGen5Target(ctx.TargetGeneration),
|
||||
AutoStart = StartsAutomatically(
|
||||
TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
||||
TryReadUInt64(ctx, initDataAddress + 88, out var eventCallbackForStart) ? eventCallbackForStart : 0),
|
||||
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
||||
GuestBuffers = new ulong[ReadOutputVideoFrameBufferCount(
|
||||
ctx,
|
||||
initDataAddress,
|
||||
|
|
@ -631,9 +614,7 @@ public static class AvPlayerExports
|
|||
{
|
||||
Handle = handle,
|
||||
IsGen5 = IsGen5Target(ctx.TargetGeneration),
|
||||
AutoStart = StartsAutomatically(
|
||||
TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
||||
TryReadUInt64(ctx, initDataAddress + 96, out var eventCallbackForStart) ? eventCallbackForStart : 0),
|
||||
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
|
||||
GuestBuffers = new ulong[ReadOutputVideoFrameBufferCount(
|
||||
ctx,
|
||||
initDataAddress,
|
||||
|
|
@ -743,7 +724,6 @@ public static class AvPlayerExports
|
|||
player = foundPlayer;
|
||||
|
||||
player.Started = true;
|
||||
player.Stopped = false;
|
||||
player.Paused = false;
|
||||
player.EndOfStream = false;
|
||||
Trace($"start handle=0x{player.Handle:X16}");
|
||||
|
|
@ -787,7 +767,6 @@ public static class AvPlayerExports
|
|||
|
||||
player.ResetPlayback();
|
||||
player.Started = false;
|
||||
player.Stopped = true;
|
||||
}
|
||||
|
||||
NotifyEvent(ctx, player, 1); // StateStop
|
||||
|
|
@ -814,7 +793,6 @@ public static class AvPlayerExports
|
|||
player.PlaybackClock.Stop();
|
||||
player.FallbackPlayback?.Pause();
|
||||
Console.Error.WriteLine($"[AVPLAYER][INFO] pause handle=0x{player.Handle:X16}");
|
||||
TraceVideoStats(player);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -881,32 +859,6 @@ public static class AvPlayerExports
|
|||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "N6Oy-EjduiY",
|
||||
ExportName = "sceAvPlayerSetAvailableBandwidth",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
[SysAbiExport(
|
||||
Nid = "+7xJ+lFxmkQ",
|
||||
ExportName = "sceAvPlayerSetAvailableBW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerSetAvailableBandwidth(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var availableBandwidth = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Players.ContainsKey(handle))
|
||||
{
|
||||
return SetReturn(ctx, InvalidParameters);
|
||||
}
|
||||
|
||||
Trace($"set_available_bandwidth handle=0x{handle:X16} bandwidth={availableBandwidth}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ODJK2sn9w4A",
|
||||
ExportName = "sceAvPlayerEnableStream",
|
||||
|
|
@ -1049,9 +1001,7 @@ public static class AvPlayerExports
|
|||
lock (StateGate)
|
||||
{
|
||||
var found = Players.TryGetValue(ctx[CpuRegister.Rdi], out var player);
|
||||
// Active from the moment a source is ready until it stops, fails or reaches its end,
|
||||
// whether or not playback has started (titles poll it before calling sceAvPlayerStart).
|
||||
var active = found && player!.SourcePath is not null && !player.Stopped && !player.EndOfStream;
|
||||
var active = found && player!.Started && !player.EndOfStream;
|
||||
TraceOnce(
|
||||
"is_active",
|
||||
$"is_active found={found} started={(found && player!.Started)} " +
|
||||
|
|
@ -1438,7 +1388,6 @@ public static class AvPlayerExports
|
|||
player.DurationMilliseconds = duration;
|
||||
player.HasAudio = hasAudio;
|
||||
player.Started = player.AutoStart;
|
||||
player.Stopped = false;
|
||||
autoStart = player.AutoStart;
|
||||
Trace(
|
||||
$"source guest='{guestPath}' host='{hostPath}' {width}x{height} " +
|
||||
|
|
@ -1670,14 +1619,6 @@ public static class AvPlayerExports
|
|||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
player.VideoCalls++;
|
||||
var statsSecond = player.PlaybackClock.ElapsedMilliseconds / 1000;
|
||||
if (statsSecond > player.VideoStatsSecond)
|
||||
{
|
||||
player.VideoStatsSecond = statsSecond;
|
||||
TraceVideoStats(player);
|
||||
}
|
||||
|
||||
if (player.Paused)
|
||||
{
|
||||
if (player.SeekVideoFramePending &&
|
||||
|
|
@ -1717,26 +1658,47 @@ public static class AvPlayerExports
|
|||
}
|
||||
|
||||
var fps = Math.Max(1.0, player.FramesPerSecond);
|
||||
var expectedFrame = player.NextFrameIndex;
|
||||
if (player.AvSyncMode == AvSyncModeDefault)
|
||||
{
|
||||
expectedFrame = CalculateExpectedDefaultSyncVideoFrame(
|
||||
var expectedFrame = CalculateExpectedDefaultSyncVideoFrame(
|
||||
player.HasAudio,
|
||||
player.NextAudioFrameIndex,
|
||||
player.PlaybackClock.Elapsed.TotalSeconds,
|
||||
fps);
|
||||
if (player.NextFrameIndex > expectedFrame)
|
||||
{
|
||||
player.VideoSyncWait++;
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
var behind = expectedFrame - player.NextFrameIndex;
|
||||
if (behind > MaxCatchUpFrames)
|
||||
{
|
||||
// Limit this call's work without changing the shared playback clock.
|
||||
expectedFrame = player.NextFrameIndex + MaxCatchUpFrames;
|
||||
TraceOnce(
|
||||
"catch_up_capped",
|
||||
$"catch_up capped behind={behind} max={MaxCatchUpFrames} " +
|
||||
$"fps={fps:F3} {player.Width}x{player.Height}");
|
||||
}
|
||||
|
||||
while (player.NextFrameIndex < expectedFrame)
|
||||
{
|
||||
var skippedFrame = ReadFrame(player);
|
||||
if (skippedFrame == VideoFrameReadResult.Pending)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
if (skippedFrame == VideoFrameReadResult.End)
|
||||
{
|
||||
return FinishStream(ctx, player);
|
||||
}
|
||||
player.NextFrameIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
var frameResult = ReadFrame(player);
|
||||
if (frameResult == VideoFrameReadResult.Pending)
|
||||
{
|
||||
player.VideoPending++;
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
if (frameResult == VideoFrameReadResult.End)
|
||||
|
|
@ -1745,23 +1707,9 @@ public static class AvPlayerExports
|
|||
}
|
||||
|
||||
var frameIndex = player.NextFrameIndex;
|
||||
// Only discard old frames when a replacement is already decoded.
|
||||
for (var skipped = 0;
|
||||
skipped < MaxCatchUpFrames &&
|
||||
player.NextFrameIndex < expectedFrame &&
|
||||
player.VideoDecoder!.QueuedFrameCount > 1;
|
||||
skipped++)
|
||||
{
|
||||
player.NextFrameIndex++;
|
||||
if (ReadFrame(player) != VideoFrameReadResult.Ready)
|
||||
{
|
||||
break;
|
||||
}
|
||||
frameIndex = player.NextFrameIndex;
|
||||
}
|
||||
var timestamp = player.StartTimeMilliseconds +
|
||||
checked((ulong)Math.Round(frameIndex * 1000.0 / fps));
|
||||
player.NextFrameIndex = frameIndex + 1;
|
||||
player.NextFrameIndex++;
|
||||
if (!WriteVideoFrame(
|
||||
ctx,
|
||||
player,
|
||||
|
|
@ -1770,28 +1718,15 @@ public static class AvPlayerExports
|
|||
frameIndex,
|
||||
extended))
|
||||
{
|
||||
player.VideoWriteFailures++;
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
player.LastVideoTimestamp = timestamp;
|
||||
player.VideoDelivered++;
|
||||
|
||||
Trace($"video_frame handle=0x{player.Handle:X16} ex={extended} ts={timestamp} data=0x{player.LastGuestBuffer:X16}");
|
||||
return SetReturn(ctx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraceVideoStats(PlayerState player)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[AVPLAYER][INFO] video_stats handle=0x{player.Handle:X16} " +
|
||||
$"clock_ms={player.PlaybackClock.ElapsedMilliseconds} audio_blocks={player.NextAudioFrameIndex} " +
|
||||
$"calls={player.VideoCalls} delivered={player.VideoDelivered} " +
|
||||
$"pending={player.VideoPending} sync_wait={player.VideoSyncWait} " +
|
||||
$"write_fail={player.VideoWriteFailures} next_frame={player.NextFrameIndex} " +
|
||||
$"queued={player.VideoDecoder?.QueuedFrameCount ?? 0} last_ts={player.LastVideoTimestamp}");
|
||||
}
|
||||
|
||||
private static int FinishStream(CpuContext ctx, PlayerState player)
|
||||
{
|
||||
TraceOnce(
|
||||
|
|
@ -2679,13 +2614,6 @@ public static class AvPlayerExports
|
|||
internal static bool IsGen5Target(Generation generation) =>
|
||||
(generation & Generation.Gen5) != 0;
|
||||
|
||||
// A player plays on its own once its source is ready when the title asks for autoStart or
|
||||
// registers no event callback: without one nothing could observe the ready state and start
|
||||
// it. Such a title never calls sceAvPlayerStart and waits for the player to go inactive at the
|
||||
// end of the stream, so leaving it unstarted kept it active forever.
|
||||
internal static bool StartsAutomatically(bool autoStartRequested, ulong eventCallback) =>
|
||||
autoStartRequested || eventCallback == 0;
|
||||
|
||||
internal static ulong GetAutoStartOffset(Generation generation, bool extended) =>
|
||||
IsGen5Target(generation)
|
||||
? extended ? 116UL : 108UL
|
||||
|
|
|
|||
|
|
@ -7,21 +7,6 @@ using SharpEmu.Libs.VideoOut;
|
|||
|
||||
namespace SharpEmu.Libs.Codec;
|
||||
|
||||
internal sealed record Videodec2DecodedFrame(
|
||||
byte[] Nv12,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint Pitch,
|
||||
bool ErrorFrame)
|
||||
{
|
||||
public ulong Pts { get; init; } = ulong.MaxValue;
|
||||
public ulong Dts { get; init; } = ulong.MaxValue;
|
||||
public ulong AttachedData { get; init; }
|
||||
public bool KeyFrame { get; init; }
|
||||
public uint Profile { get; init; }
|
||||
public uint Level { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
|
||||
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
|
||||
|
|
@ -44,8 +29,6 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
// Enough lookahead to absorb decode jitter without adding visible latency.
|
||||
private const int FrameQueueCapacity = 4;
|
||||
|
||||
private const int WorkQueueCapacity = 64;
|
||||
|
||||
// Fallback when the stream doesn't declare a usable framerate.
|
||||
private const double FallbackFps = 30.0;
|
||||
|
||||
|
|
@ -62,12 +45,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
|
||||
private bool _disposed;
|
||||
|
||||
// Unbounded: access units are small, backpressure lives on the frame queue below.
|
||||
private readonly Channel<byte[]?> _workChannel =
|
||||
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
|
||||
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
SingleWriter = true,
|
||||
});
|
||||
|
||||
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
|
||||
|
|
@ -91,10 +74,6 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
private uint _lastWidth;
|
||||
private uint _lastHeight;
|
||||
|
||||
private long _workerInputCount;
|
||||
private long _workerFrameCount;
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
|
||||
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
|
||||
{
|
||||
_codecContext = codecContext;
|
||||
|
|
@ -127,22 +106,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
|
||||
if (codec == null)
|
||||
{
|
||||
Trace("create failed: H.264 decoder not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(codec);
|
||||
if (codecContext == null)
|
||||
{
|
||||
Trace("create failed: avcodec_alloc_context3 returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: avcodec_open2 rejected H.264 decoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -161,18 +136,14 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
Trace("create failed: frame or packet allocation returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var decoder = new Videodec2Decoder(codecContext, frame, packet);
|
||||
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
|
||||
return decoder;
|
||||
return new Videodec2Decoder(codecContext, frame, packet);
|
||||
}
|
||||
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
|
||||
{
|
||||
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
|
||||
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
|
||||
if (codecContext != null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
|
|
@ -203,252 +174,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool EnqueueAccessUnit(byte[] accessUnit)
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(accessUnit);
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public bool RequestDrain()
|
||||
public void RequestDrain()
|
||||
{
|
||||
return _workChannel.Writer.TryWrite(null);
|
||||
_workChannel.Writer.TryWrite(null);
|
||||
}
|
||||
|
||||
public bool DecodeAccessUnit(
|
||||
byte[] accessUnit,
|
||||
ulong pts,
|
||||
ulong dts,
|
||||
ulong attachedData,
|
||||
out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed || accessUnit.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
|
||||
if (packetResult < 0)
|
||||
{
|
||||
Trace($"decode failed: av_new_packet={packetResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* source = accessUnit)
|
||||
{
|
||||
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
|
||||
}
|
||||
|
||||
_packet->pts = ToAvTimestamp(pts);
|
||||
_packet->dts = ToAvTimestamp(dts);
|
||||
|
||||
var havePendingFrame = false;
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (pendingResult < 0)
|
||||
{
|
||||
Trace($"decode failed: receive-before-resend={pendingResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
havePendingFrame = true;
|
||||
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
}
|
||||
|
||||
if (sendResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_send_packet={sendResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!havePendingFrame)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
|
||||
{
|
||||
decodedFrame = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
|
||||
receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult < 0)
|
||||
{
|
||||
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
decodedFrame = ConvertFrameToNv12Locked();
|
||||
return decodedFrame is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ffmpeg.avcodec_flush_buffers(_codecContext);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
}
|
||||
}
|
||||
|
||||
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
|
||||
ulong fallbackPts = ulong.MaxValue,
|
||||
ulong fallbackDts = ulong.MaxValue,
|
||||
ulong attachedData = 0)
|
||||
{
|
||||
if (_frame->width <= 0 || _frame->height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var width = checked((uint)_frame->width);
|
||||
var height = checked((uint)_frame->height);
|
||||
uint pitch;
|
||||
ulong lumaBytes;
|
||||
ulong requiredBytes;
|
||||
try
|
||||
{
|
||||
pitch = checked((width + 255u) & ~255u);
|
||||
lumaBytes = checked((ulong)pitch * height);
|
||||
var chromaRows = ((ulong)height + 1UL) / 2UL;
|
||||
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredBytes > int.MaxValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
AVPixelFormat.AV_PIX_FMT_NV12,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nv12 = new byte[(int)requiredBytes];
|
||||
fixed (byte* destination = nv12)
|
||||
{
|
||||
var destinationPlanes = new byte*[4]
|
||||
{
|
||||
destination,
|
||||
destination + checked((int)lumaBytes),
|
||||
null,
|
||||
null,
|
||||
};
|
||||
var destinationStrides = new int[4]
|
||||
{
|
||||
checked((int)pitch),
|
||||
checked((int)pitch),
|
||||
0,
|
||||
0,
|
||||
};
|
||||
var rows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
if (rows != _frame->height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Videodec2DecodedFrame(
|
||||
nv12,
|
||||
width,
|
||||
height,
|
||||
pitch,
|
||||
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
|
||||
{
|
||||
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
|
||||
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
|
||||
AttachedData = attachedData,
|
||||
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
|
||||
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
|
||||
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static long ToAvTimestamp(ulong timestamp) =>
|
||||
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
|
||||
|
||||
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
|
||||
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
|
||||
|
||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
||||
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
|
||||
{
|
||||
|
|
@ -496,28 +233,15 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
|
||||
var decodedOk = item is null
|
||||
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
|
||||
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
|
||||
if (ShouldTrace(inputOrdinal))
|
||||
{
|
||||
Trace(
|
||||
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
|
||||
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
|
||||
}
|
||||
|
||||
if (!decodedOk || !hasPicture || bgraFrame is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
|
||||
if (ShouldTrace(frameOrdinal))
|
||||
{
|
||||
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
|
||||
|
|
@ -840,22 +564,4 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
|
|
@ -16,18 +15,10 @@ public static class Videodec2Exports
|
|||
{
|
||||
private const int Ok = 0;
|
||||
|
||||
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
|
||||
private const uint FrameBufferAlignment = 0x100;
|
||||
|
||||
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
|
||||
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
|
||||
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
|
||||
|
||||
private static readonly bool TraceEnabled = IsTraceEnabled();
|
||||
private static long _decodeCallCount;
|
||||
private static long _decodedPictureCount;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RnDibcGCPKw",
|
||||
ExportName = "sceVideodec2QueryComputeMemoryInfo",
|
||||
|
|
@ -36,25 +27,24 @@ public static class Videodec2Exports
|
|||
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
// Success needs no memory writes; the game initializes from its own fields.
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
|
||||
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
|
||||
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
|
||||
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
|
||||
|
||||
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
|
||||
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
|
||||
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
|
||||
|
||||
// Opaque token the game hands back unmodified to later Videodec2 calls.
|
||||
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eD+X2SmxUt4",
|
||||
ExportName = "sceVideodec2AllocateComputeQueue",
|
||||
|
|
@ -62,14 +52,8 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var queueAddress = ctx[CpuRegister.Rdx];
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
|
||||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
|
||||
computeMemoryAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(
|
||||
queueAddress, computeMemoryAddress))
|
||||
var queueAddress = ctx[CpuRegister.Rdi];
|
||||
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -77,18 +61,7 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "UvtA3FAiF4Y",
|
||||
ExportName = "sceVideodec2ReleaseComputeQueue",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
|
||||
}
|
||||
|
||||
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
|
||||
[SysAbiExport(
|
||||
Nid = "qqMCwlULR+E",
|
||||
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
|
||||
|
|
@ -96,18 +69,12 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
|
||||
{
|
||||
var configAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (configAddress == 0 || memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
|
||||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
|
||||
if (memoryInfoAddress == 0 ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
|
||||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
|
||||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -132,9 +99,7 @@ public static class Videodec2Exports
|
|||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
|
||||
var decoder = Videodec2Decoder.TryCreate();
|
||||
Decoders[handle] = decoder;
|
||||
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
|
||||
Decoders[handle] = Videodec2Decoder.TryCreate();
|
||||
|
||||
if (!ctx.TryWriteUInt64(decoderAddress, handle))
|
||||
{
|
||||
|
|
@ -146,6 +111,7 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
|
||||
[SysAbiExport(
|
||||
Nid = "l1hXwscLuCY",
|
||||
ExportName = "sceVideodec2Flush",
|
||||
|
|
@ -154,25 +120,33 @@ public static class Videodec2Exports
|
|||
public static int Videodec2Flush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rsi];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (Decoders.TryGetValue(handle, out var decoder) &&
|
||||
decoder is not null &&
|
||||
decoder.FlushOutput(out var decodedFrame) &&
|
||||
decodedFrame is not null)
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
_ = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
// Drain in order: report an already-finished frame before queuing a new drain request.
|
||||
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
|
||||
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decoder.RequestDrain();
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// No state to reset.
|
||||
[SysAbiExport(
|
||||
Nid = "wJXikG6QFN8",
|
||||
ExportName = "sceVideodec2Reset",
|
||||
|
|
@ -180,12 +154,6 @@ public static class Videodec2Exports
|
|||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2Reset(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
|
||||
{
|
||||
decoder.Reset();
|
||||
}
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +173,8 @@ public static class Videodec2Exports
|
|||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
|
||||
// uninitialized stack and must always be written explicitly.
|
||||
[SysAbiExport(
|
||||
Nid = "852F5+q6+iM",
|
||||
ExportName = "sceVideodec2Decode",
|
||||
|
|
@ -216,9 +186,8 @@ public static class Videodec2Exports
|
|||
var inputAuStruct = ctx[CpuRegister.Rsi];
|
||||
var outputSlotObj = ctx[CpuRegister.Rdx];
|
||||
var outputInfoAddress = ctx[CpuRegister.Rcx];
|
||||
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
|
||||
|
||||
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
|
||||
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
|
@ -226,7 +195,6 @@ public static class Videodec2Exports
|
|||
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
|
||||
{
|
||||
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
|
||||
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
|
|
@ -240,235 +208,35 @@ public static class Videodec2Exports
|
|||
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
|
||||
{
|
||||
// Nothing sane to feed/fill this call; not an error.
|
||||
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
|
||||
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
|
||||
|
||||
var auBuffer = new byte[auDataSize];
|
||||
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
|
||||
{
|
||||
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
var decodeOk = decoder.DecodeAccessUnit(
|
||||
auBuffer, pts, dts, attachedData, out var decodedFrame);
|
||||
if (ShouldTrace(callOrdinal))
|
||||
{
|
||||
var prefixBytes = Math.Min(auBuffer.Length, 8);
|
||||
Trace(
|
||||
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
|
||||
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
|
||||
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
|
||||
$"picture={decodedFrame is not null}");
|
||||
}
|
||||
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
|
||||
decoder.EnqueueAccessUnit(auBuffer);
|
||||
|
||||
if (!decodeOk || decodedFrame is null)
|
||||
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
|
||||
{
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
var copied = TryWriteDecodedFrame(
|
||||
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
|
||||
if (copied)
|
||||
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
|
||||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
|
||||
{
|
||||
Interlocked.Increment(ref _decodedPictureCount);
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
|
||||
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NtXRa3dRzU0",
|
||||
ExportName = "sceVideodec2GetPictureInfo",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceVideodec2")]
|
||||
public static int Videodec2GetPictureInfo(CpuContext ctx)
|
||||
{
|
||||
var outputInfoAddress = ctx[CpuRegister.Rdi];
|
||||
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
|
||||
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
|
||||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
|
||||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
|
||||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
|
||||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
|
||||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorOutputInfo);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
|
||||
(requestedSize != 104 && requestedSize != 120))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
var picture = new byte[(int)requestedSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
|
||||
picture[0x08] = 1;
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
|
||||
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
|
||||
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
|
||||
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
|
||||
picture[0x34] = 1;
|
||||
picture[0x4E] = 1;
|
||||
picture[0x4F] = 5;
|
||||
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorInvalidArg);
|
||||
}
|
||||
|
||||
if (secondPictureInfoAddress != 0 &&
|
||||
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
|
||||
{
|
||||
return SetReturn(ctx, VideodecErrorStructSize);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"picture_info buffer=0x{pixelsAddress:X16} " +
|
||||
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
|
||||
return SetReturn(ctx, Ok);
|
||||
}
|
||||
|
||||
private static readonly byte[] NotAccepted = [0];
|
||||
private static readonly byte[] Accepted = [1];
|
||||
private static readonly byte[] NoPicture = [0, 0, 0, 0];
|
||||
private static readonly byte[] PictureReady = [1, 0, 1, 0];
|
||||
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
|
||||
|
||||
private static bool TryInitializeNoPicture(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress)
|
||||
{
|
||||
return frameBufferAddress != 0 &&
|
||||
outputInfoAddress != 0 &&
|
||||
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
|
||||
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
|
||||
}
|
||||
|
||||
internal static bool TryWriteDecodedFrame(
|
||||
CpuContext ctx,
|
||||
ulong frameBufferAddress,
|
||||
ulong outputInfoAddress,
|
||||
Videodec2DecodedFrame decodedFrame)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
|
||||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
|
||||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
|
||||
pixelsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
|
||||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
|
||||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (outputInfoBytes >= 0x38 &&
|
||||
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
|
||||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
|
||||
!ctx.Memory.TryWrite(
|
||||
outputInfoAddress + 0x08,
|
||||
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PictureInfos[pixelsAddress] = decodedFrame;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var output = new byte[(int)size];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
|
||||
return ctx.Memory.TryWrite(address, output);
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTraceEnabled()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
|
||||
return string.Equals(value, "1", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ShouldTrace(long ordinal) =>
|
||||
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
|
||||
|
||||
private static void TraceDecode(long ordinal, string message)
|
||||
{
|
||||
if (ShouldTrace(ordinal))
|
||||
{
|
||||
Trace($"decode call={ordinal} {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
{
|
||||
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
private static readonly byte[] NoPicture = [0];
|
||||
private static readonly byte[] PictureReady = [1];
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Headless lifecycle for the system web-browser dialog. SharpEmu does not yet
|
||||
/// provide a host browser, so an opened dialog completes on its first status
|
||||
/// poll instead of leaving games in an infinite RUNNING loop.
|
||||
/// </summary>
|
||||
public static class WebBrowserDialogExports
|
||||
{
|
||||
internal const int StatusNone = 0;
|
||||
internal const int StatusInitialized = 1;
|
||||
internal const int StatusRunning = 2;
|
||||
internal const int StatusFinished = 3;
|
||||
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jqb7HntFQFc",
|
||||
ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FraP7debcdg",
|
||||
ExportName = "sceWebBrowserDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpen(CpuContext ctx) => Open(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "O7dIZQrwVFY",
|
||||
ExportName = "sceWebBrowserDialogOpenForPredeterminedContent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogOpenForPredeterminedContent(CpuContext ctx) => Open(ctx);
|
||||
|
||||
private static int Open(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _status, StatusRunning);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CFTG6a8TjOU",
|
||||
ExportName = "sceWebBrowserDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "h1dR-t5ISgg",
|
||||
ExportName = "sceWebBrowserDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
|
||||
|
||||
private static int PollStatus()
|
||||
{
|
||||
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
|
||||
return Volatile.Read(ref _status);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PSK+Eik919Q",
|
||||
ExportName = "sceWebBrowserDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogClose(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vCaW0fgVQmc",
|
||||
ExportName = "sceWebBrowserDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0)
|
||||
{
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[sizeof(int)];
|
||||
result.Clear();
|
||||
if (!ctx.Memory.TryWrite(ctx[CpuRegister.Rdi], result))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wit4LjeoeX4",
|
||||
ExportName = "sceWebBrowserDialogGetEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogGetEvent(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uYELOMVnmNQ",
|
||||
ExportName = "sceWebBrowserDialogNavigate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogNavigate(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RLhKBOoNyXY",
|
||||
ExportName = "sceWebBrowserDialogSetZoom",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogSetZoom(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ocHtyBwHfys",
|
||||
ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
internal static int StatusForTests => Volatile.Read(ref _status);
|
||||
|
||||
internal static void ResetForTests() => Interlocked.Exchange(ref _status, StatusNone);
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
namespace SharpEmu.Libs.Diagnostics;
|
||||
|
||||
internal static class DccWriterTrace
|
||||
{
|
||||
private const int MaxReports = 256;
|
||||
private const int MaxFrames = 12;
|
||||
|
||||
private static readonly bool DccEnabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_DCC_WRITERS") == "1";
|
||||
|
||||
private static readonly ulong[] TargetAddresses = (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_WRITER_ADDRESS") ?? string.Empty)
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(ParseAddress)
|
||||
.Where(address => address != 0)
|
||||
.ToArray();
|
||||
|
||||
public static readonly bool Enabled = DccEnabled || TargetAddresses.Length != 0;
|
||||
|
||||
[ThreadStatic]
|
||||
public static ulong CurrentProgram;
|
||||
|
||||
private static readonly HashSet<(ulong Address, ulong Size, ulong Program)> _reported = new();
|
||||
private static readonly HashSet<(ulong Hash, string Reason)> _refusals = new();
|
||||
|
||||
private static ulong ParseAddress(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var text = value.Trim();
|
||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
text = text[2..];
|
||||
}
|
||||
|
||||
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
public static void Record(IGuestImageCache images, ulong address, ulong size)
|
||||
{
|
||||
var targeted = TargetAddresses.Any(target => target >= address && target - address < size);
|
||||
if (_reported.Count >= MaxReports || (!targeted && !(DccEnabled && images.OverlapsDccMetadata(address, size))) ||
|
||||
!_reported.Add((address, size, CurrentProgram)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var frames = new StackTrace(1, false).GetFrames();
|
||||
var names = frames.Take(MaxFrames).Select(frame => frame.GetMethod() is { } method
|
||||
? $"{method.DeclaringType?.Name}.{method.Name}"
|
||||
: "?");
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU][{(targeted ? "TARGET_WRITER" : "DCC_WRITER")}] address=0x{address:X16} size=0x{size:X} program=0x{CurrentProgram:X16} stack={string.Join(" < ", names)}");
|
||||
}
|
||||
|
||||
public static void Refuse(ulong hash, string reason)
|
||||
{
|
||||
if (!Enabled || _refusals.Count >= MaxReports || !_refusals.Add((hash, reason)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[GPU][BOUNDED_FILL_REFUSED] program=0x{hash:X16} reason={reason}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Diagnostics;
|
||||
|
||||
internal static class GpuReadTrace
|
||||
{
|
||||
private const int MaxReports = 64;
|
||||
|
||||
public static readonly bool Enabled =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GPU_READS") == "1";
|
||||
|
||||
[ThreadStatic]
|
||||
public static ulong CurrentShader;
|
||||
|
||||
[ThreadStatic]
|
||||
public static string? CurrentStage;
|
||||
|
||||
private static readonly HashSet<(ulong Shader, ulong Page, bool Table)> _reported = new();
|
||||
private static readonly Dictionary<ulong, (long Reads, long Changes, uint Last, uint First, ulong Shader)> _values = new();
|
||||
private static long _nextValueReport = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
|
||||
public static void RecordValue(ulong address, uint word)
|
||||
{
|
||||
lock (_values)
|
||||
{
|
||||
_values[address] = _values.TryGetValue(address, out var entry)
|
||||
? (entry.Reads + 1, entry.Changes + (entry.Last != word ? 1 : 0), word, entry.First, entry.Shader)
|
||||
: (1, 0, word, word, CurrentShader);
|
||||
var now = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (now < _nextValueReport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_nextValueReport = now + System.Diagnostics.Stopwatch.Frequency * 5;
|
||||
foreach (var (key, value) in _values.OrderByDescending(pair => pair.Value.Reads).Take(16))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU][SYNC_READ_VALUE] address=0x{key:X16} reads={value.Reads} changes={value.Changes} first=0x{value.First:X8} last=0x{value.Last:X8} shader=0x{value.Shader:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Record(ulong address, bool table)
|
||||
{
|
||||
var key = (CurrentShader, address & ~0xFFFUL, table);
|
||||
lock (_reported)
|
||||
{
|
||||
if (_reported.Count >= MaxReports || !_reported.Add(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU][SYNC_READ] shader=0x{CurrentShader:X16} stage={CurrentStage} address=0x{address:X16} table={table}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Diagnostics;
|
||||
|
||||
public static class RazorCpuExports
|
||||
{
|
||||
[SysAbiExport(Nid = "zw+celG7zSI", ExportName = "sceRazorCpuPushMarker",
|
||||
Target = Generation.Gen5, LibraryName = "libSceRazorCpu")]
|
||||
public static int PushMarker(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(Nid = "YpkGsMXP3ew", ExportName = "sceRazorCpuPopMarker",
|
||||
Target = Generation.Gen5, LibraryName = "libSceRazorCpu")]
|
||||
public static int PopMarker(CpuContext ctx) => ctx.SetReturn(0);
|
||||
}
|
||||
|
|
@ -49,7 +49,6 @@ public static class FiberExports
|
|||
private const int FiberMagicEndOffset = 104;
|
||||
|
||||
private static int _contextSizeCheck;
|
||||
private static readonly bool TraceEnabled = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER") == "1";
|
||||
|
||||
private static readonly object _fiberGate = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||
|
|
@ -288,10 +287,9 @@ public static class FiberExports
|
|||
|
||||
_ = GuestThreadExecution.EnterFiber(0);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
if (TraceEnabled)
|
||||
TraceFiber(
|
||||
$"return-to-thread fiber=0x{fiberAddress:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
TraceFiber(
|
||||
$"return-to-thread fiber=0x{fiberAddress:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +306,13 @@ public static class FiberExports
|
|||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, ResolveCurrentFiberAddress(ctx))
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
|
@ -337,7 +341,7 @@ public static class FiberExports
|
|||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields, readName: true))
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
|
@ -523,10 +527,9 @@ public static class FiberExports
|
|||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||
}
|
||||
|
||||
if (TraceEnabled)
|
||||
TraceFiber(
|
||||
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
|
||||
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
|
||||
TraceFiber(
|
||||
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
|
||||
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -586,8 +589,7 @@ public static class FiberExports
|
|||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
if (TraceEnabled)
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
|
|
@ -674,10 +676,9 @@ public static class FiberExports
|
|||
|
||||
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
if (TraceEnabled)
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
|
|
@ -976,7 +977,7 @@ public static class FiberExports
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields, bool readName = false)
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||
|
|
@ -985,7 +986,7 @@ public static class FiberExports
|
|||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name, decode: readName))
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1070,7 +1071,7 @@ public static class FiberExports
|
|||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value, bool decode)
|
||||
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
|
|
@ -1085,9 +1086,7 @@ public static class FiberExports
|
|||
length = buffer.Length;
|
||||
}
|
||||
|
||||
// A switch validates the same bytes but does not need a managed name.
|
||||
// Only sceFiberGetInfo exposes the decoded name to its caller.
|
||||
value = decode ? Encoding.UTF8.GetString(buffer[..length]) : string.Empty;
|
||||
value = Encoding.UTF8.GetString(buffer[..length]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1124,7 +1123,10 @@ public static class FiberExports
|
|||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct FiberFields(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace SharpEmu.Libs.Font;
|
||||
|
||||
|
|
@ -19,38 +16,6 @@ public static class FontExports
|
|||
private static readonly Stack<ulong> FreeGlyphs = new();
|
||||
private static ulong _librarySelectionAddress;
|
||||
private static ulong _rendererSelectionAddress;
|
||||
private static readonly ConcurrentDictionary<ulong, (float Width, float Height)> FontScales = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FontFace> FontFaces = new();
|
||||
private static readonly Lazy<FontFace> DefaultFont = new(() => new(SKTypeface.FromFamilyName(null)));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "oaJ1BpN2FQk",
|
||||
ExportName = "sceFontTextSourceInit",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int TextSourceInit(CpuContext ctx)
|
||||
{
|
||||
var sourceAddress = ctx[CpuRegister.Rdi];
|
||||
var textAddress = ctx[CpuRegister.Rsi];
|
||||
var textSize = (uint)ctx[CpuRegister.Rdx];
|
||||
if (sourceAddress == 0 || textAddress > ulong.MaxValue - textSize)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> source = stackalloc byte[0x60];
|
||||
source.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x08..], textAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x10..], textAddress == 0 ? 0 : textAddress + textSize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x18..], textAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x20..], ctx[CpuRegister.Rcx]);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x28..], ctx[CpuRegister.R8]);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x38..], 0x10);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(source[0x40..], textSize);
|
||||
return ctx.Memory.TryWrite(sourceAddress, source)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "whrS4oksXc4",
|
||||
|
|
@ -133,7 +98,7 @@ public static class FontExports
|
|||
ExportName = "sceFontSetScalePixel",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetScalePixel(CpuContext ctx) => SetFontHeight(ctx);
|
||||
public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TMtqoFQjjbA",
|
||||
|
|
@ -154,7 +119,7 @@ public static class FontExports
|
|||
ExportName = "sceFontSetupRenderScalePixel",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int SetupRenderScalePixel(CpuContext ctx) => SetFontHeight(ctx);
|
||||
public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lz9y9UFO2UU",
|
||||
|
|
@ -179,22 +144,13 @@ public static class FontExports
|
|||
{
|
||||
var layoutAddress = ctx[CpuRegister.Rsi];
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
layoutAddress = ctx[CpuRegister.Rdx];
|
||||
}
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var fontHandle = ctx[CpuRegister.Rdi];
|
||||
var scale = GetFontScale(fontHandle);
|
||||
float[] values = [scale.Height * 0.75f, scale.Height, 0];
|
||||
if (FontFaces.TryGetValue(fontHandle, out var face))
|
||||
{
|
||||
var metrics = face.GetMetrics(scale.Width, scale.Height);
|
||||
values = [-metrics.Ascent, metrics.Descent - metrics.Ascent + metrics.Leading, 0];
|
||||
}
|
||||
// Baseline, line advance, decoration extent: the same invented geometry
|
||||
// as GetRenderCharGlyphMetrics.
|
||||
var values = new[] { 12.0f, 16.0f, 0.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
|
|
@ -218,10 +174,6 @@ public static class FontExports
|
|||
{
|
||||
var layoutAddress = ctx[CpuRegister.Rsi];
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
layoutAddress = ctx[CpuRegister.Rdx];
|
||||
}
|
||||
if (layoutAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
|
@ -229,10 +181,7 @@ public static class FontExports
|
|||
// Baseline (horizontal offset), line advance, decoration extent.
|
||||
// Mirrors the same three-float layout as GetHorizontalLayout, but
|
||||
// interpreted for vertical writing (e.g. CJK text rendered top-to-bottom).
|
||||
// Scaled to the font's current pixel height; the default height of 16
|
||||
// reproduces the original fixed geometry.
|
||||
var scale = GetFontScale(ctx[CpuRegister.Rdi]);
|
||||
var values = new[] { scale.Width * 0.5f, scale.Width, 0.0f };
|
||||
var values = new[] { 8.0f, 16.0f, 0.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
|
|
@ -252,64 +201,16 @@ public static class FontExports
|
|||
ExportName = "sceFontOpenFontSet",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int OpenFontSet(CpuContext ctx)
|
||||
{
|
||||
var fontSet = (uint)ctx[CpuRegister.Rsi];
|
||||
var bold = (fontSet & 0xf) >= 7;
|
||||
var italic = ((fontSet >> 20) & 0xf) == 1;
|
||||
var style = bold ? (italic ? SKFontStyle.BoldItalic : SKFontStyle.Bold) :
|
||||
(italic ? SKFontStyle.Italic : SKFontStyle.Normal);
|
||||
return OpenFace(ctx, new(SKTypeface.FromFamilyName(null, style)));
|
||||
}
|
||||
public static int OpenFontSet(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KXUpebrFk1U",
|
||||
ExportName = "sceFontOpenFontMemory",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int OpenFontMemory(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
var size = (uint)ctx[CpuRegister.Rdx];
|
||||
if (address == 0 || size is < 12 or > 64 * 1024 * 1024 || ctx[CpuRegister.R8] == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
if (!ctx.Memory.CanRead(address, size))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
var bytes = new byte[(int)size];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
uint index = 0;
|
||||
if (ctx[CpuRegister.Rcx] != 0 && !ctx.TryReadUInt32(ctx[CpuRegister.Rcx] + 8, out index))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
if (index > int.MaxValue)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
using var data = SKData.CreateCopy(bytes);
|
||||
var typeface = SKTypeface.FromData(data, (int)index);
|
||||
return typeface is null
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT)
|
||||
: OpenFace(ctx, new(typeface));
|
||||
}
|
||||
|
||||
private static int OpenFace(CpuContext ctx, FontFace face)
|
||||
{
|
||||
var result = CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||
if (result == 0 && ctx.TryReadUInt64(ctx[CpuRegister.R8], out var handle))
|
||||
{
|
||||
FontFaces[handle] = face;
|
||||
FontScales.TryRemove(handle, out _);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public static int OpenFontMemory(CpuContext ctx) =>
|
||||
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JzCH3SCFnAU",
|
||||
|
|
@ -328,8 +229,6 @@ public static class FontExports
|
|||
|
||||
if (setupHandle != 0)
|
||||
{
|
||||
if (FontFaces.TryGetValue(sourceHandle, out var face)) FontFaces[setupHandle] = face;
|
||||
FontScales[setupHandle] = GetFontScale(sourceHandle);
|
||||
return ctx.TryWriteUInt64(outputAddress, setupHandle)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -350,8 +249,6 @@ public static class FontExports
|
|||
}
|
||||
|
||||
_ = TryWriteUInt16(ctx, handle, 0x0F02);
|
||||
if (FontFaces.TryGetValue(sourceHandle, out var sourceFace)) FontFaces[handle] = sourceFace;
|
||||
FontScales[handle] = GetFontScale(sourceHandle);
|
||||
return ctx.TryWriteUInt64(outputAddress, handle)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
|
@ -391,11 +288,19 @@ public static class FontExports
|
|||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var code = (uint)ctx[CpuRegister.Rsi];
|
||||
if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return WriteGlyphMetrics(ctx, metricsAddress, GetGlyph(ctx[CpuRegister.Rdi], code), GetFontScale(ctx[CpuRegister.Rdi]).Height)
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
ctx,
|
||||
metricsAddress + (ulong)(index * sizeof(float)),
|
||||
BitConverter.SingleToUInt32Bits(values[index])))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -462,26 +367,6 @@ public static class FontExports
|
|||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vRxf4d0ulPs",
|
||||
ExportName = "sceFontRenderSurfaceSetScissor",
|
||||
Target = Generation.Gen5, LibraryName = "libSceFont")]
|
||||
public static int RenderSurfaceSetScissor(CpuContext ctx)
|
||||
{
|
||||
var surfaceAddress = ctx[CpuRegister.Rdi];
|
||||
if (surfaceAddress == 0)
|
||||
{
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
return TryWriteUInt32(ctx, surfaceAddress + 0x18, (uint)ctx[CpuRegister.Rsi]) &&
|
||||
TryWriteUInt32(ctx, surfaceAddress + 0x1C, (uint)ctx[CpuRegister.Rdx]) &&
|
||||
TryWriteUInt32(ctx, surfaceAddress + 0x20, (uint)ctx[CpuRegister.Rcx]) &&
|
||||
TryWriteUInt32(ctx, surfaceAddress + 0x24, (uint)ctx[CpuRegister.R8])
|
||||
? SetSuccess(ctx)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C-4Qw5Srlyw",
|
||||
ExportName = "sceFontGenerateCharGlyph",
|
||||
|
|
@ -549,171 +434,43 @@ public static class FontExports
|
|||
LibraryName = "libSceFont")]
|
||||
public static int RenderCharGlyphImageHorizontal(CpuContext ctx)
|
||||
{
|
||||
var surfaceAddress = ctx[CpuRegister.Rdx];
|
||||
var metricsAddress = ctx[CpuRegister.Rcx];
|
||||
var resultAddress = ctx[CpuRegister.R8];
|
||||
var font = ctx[CpuRegister.Rdi];
|
||||
var code = (uint)ctx[CpuRegister.Rsi];
|
||||
if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
var glyph = GetGlyph(font, code);
|
||||
var width = glyph.Width;
|
||||
var height = glyph.Height;
|
||||
|
||||
if (metricsAddress != 0 && !WriteGlyphMetrics(ctx, metricsAddress, glyph, GetFontScale(font).Height))
|
||||
if (metricsAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (!TryWriteUInt32(
|
||||
ctx,
|
||||
metricsAddress + (ulong)(index * sizeof(float)),
|
||||
BitConverter.SingleToUInt32Bits(values[index])))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.GetXmmRegister(0, out var xBits, out _);
|
||||
ctx.GetXmmRegister(1, out var yBits, out _);
|
||||
var x = BitConverter.UInt32BitsToSingle((uint)xBits);
|
||||
var y = BitConverter.UInt32BitsToSingle((uint)yBits);
|
||||
var left = float.IsFinite(x) ? (int)MathF.Floor(x + glyph.BearingX) : 0;
|
||||
var top = float.IsFinite(y) ? (int)MathF.Floor(y - glyph.BearingY) : 0;
|
||||
|
||||
ulong buffer = 0;
|
||||
uint pitch = 0;
|
||||
uint pixelSize = 0;
|
||||
if (resultAddress != 0)
|
||||
{
|
||||
Span<byte> result = stackalloc byte[RenderOutputSize];
|
||||
result.Clear();
|
||||
if (surfaceAddress != 0 && ctx.TryReadUInt64(surfaceAddress, out buffer) &&
|
||||
ctx.TryReadUInt32(surfaceAddress + 8, out pitch) &&
|
||||
ctx.TryReadUInt32(surfaceAddress + 12, out pixelSize))
|
||||
{
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(result[8..], buffer);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch);
|
||||
result[20] = (byte)pixelSize;
|
||||
}
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)Math.Max(left, 0));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)Math.Max(top, 0));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)Math.Max(width + Math.Min(left, 0), 0));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)Math.Max(height + Math.Min(top, 0), 0));
|
||||
BinaryPrimitives.WriteSingleLittleEndian(result[40..], glyph.BearingX);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(result[44..], glyph.BearingY);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(result[48..], glyph.Advance);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(result[52..], glyph.Advance);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height);
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
Span<byte> cleared = stackalloc byte[RenderOutputSize];
|
||||
cleared.Clear();
|
||||
if (!ctx.Memory.TryWrite(resultAddress, cleared))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (surfaceAddress != 0 && !DrawGlyph(ctx, surfaceAddress, left, top, width, height, glyph.Coverage))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
private static int SetFontHeight(CpuContext ctx)
|
||||
{
|
||||
ctx.GetXmmRegister(0, out var widthBits, out _);
|
||||
ctx.GetXmmRegister(1, out var heightBits, out _);
|
||||
var width = BitConverter.UInt32BitsToSingle((uint)widthBits);
|
||||
var height = BitConverter.UInt32BitsToSingle((uint)heightBits);
|
||||
if (float.IsFinite(height) && height > 1)
|
||||
{
|
||||
FontScales[ctx[CpuRegister.Rdi]] = (
|
||||
float.IsFinite(width) && width > 1 ? Math.Clamp(width, 8, 128) : Math.Clamp(height, 8, 128),
|
||||
Math.Clamp(height, 8, 128));
|
||||
}
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
|
||||
private static (float Width, float Height) GetFontScale(ulong font) =>
|
||||
FontScales.TryGetValue(font, out var scale) ? scale : (16, 16);
|
||||
|
||||
private static bool IsValidCode(uint code) => (code & 0xffff0000) == 0x80000000 || Rune.IsValid((int)code);
|
||||
|
||||
private static FontGlyph GetGlyph(ulong font, uint code)
|
||||
{
|
||||
var scale = GetFontScale(font);
|
||||
return (FontFaces.TryGetValue(font, out var face) ? face : DefaultFont.Value).GetGlyph(code, scale.Width, scale.Height);
|
||||
}
|
||||
|
||||
private static bool WriteGlyphMetrics(CpuContext ctx, ulong address, FontGlyph glyph, float height)
|
||||
{
|
||||
Span<byte> metrics = stackalloc byte[32];
|
||||
float[] values = [glyph.Width, glyph.Height, glyph.BearingX, glyph.BearingY, glyph.Advance, 0, 0, height];
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]);
|
||||
}
|
||||
return ctx.Memory.TryWrite(address, metrics);
|
||||
}
|
||||
|
||||
internal static bool DrawGlyph(CpuContext ctx, ulong surface, int left, int top,
|
||||
int glyphWidth, int glyphHeight, byte[] coverage)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(surface, out var buffer) ||
|
||||
!ctx.TryReadUInt32(surface + 8, out var pitch) ||
|
||||
!ctx.TryReadUInt32(surface + 12, out var pixelSize) ||
|
||||
!ctx.TryReadUInt32(surface + 16, out var width) ||
|
||||
!ctx.TryReadUInt32(surface + 20, out var height) ||
|
||||
!ctx.TryReadUInt32(surface + 24, out var sx0) ||
|
||||
!ctx.TryReadUInt32(surface + 28, out var sy0) ||
|
||||
!ctx.TryReadUInt32(surface + 32, out var sx1) ||
|
||||
!ctx.TryReadUInt32(surface + 36, out var sy1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pixelSize &= 0xff;
|
||||
if (buffer == 0 || pixelSize is < 1 or > 4 || pitch == 0 || width == 0 || height == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var x0 = Math.Max(left, (int)Math.Min(sx0, width));
|
||||
var y0 = Math.Max(top, (int)Math.Min(sy0, height));
|
||||
var x1 = Math.Min(left + glyphWidth, (int)Math.Min(sx1, width));
|
||||
var y1 = Math.Min(top + glyphHeight, (int)Math.Min(sy1, height));
|
||||
if (x0 >= x1 || y0 >= y1 || (ulong)x1 * pixelSize > pitch)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var line = new byte[(x1 - x0) * pixelSize];
|
||||
for (var yy = y0; yy < y1; yy++)
|
||||
{
|
||||
var address = buffer + (ulong)yy * pitch + (ulong)x0 * pixelSize;
|
||||
if (!ctx.Memory.TryRead(address, line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (var xx = x0; xx < x1; xx++)
|
||||
{
|
||||
var alpha = coverage[(yy - top) * glyphWidth + xx - left];
|
||||
if (alpha != 0)
|
||||
{
|
||||
var pixel = line.AsSpan((xx - x0) * (int)pixelSize, (int)pixelSize);
|
||||
for (var component = 0; component < pixel.Length; component++)
|
||||
pixel[component] = (byte)(alpha + (pixel[component] * (255 - alpha) + 127) / 255);
|
||||
}
|
||||
}
|
||||
if (!ctx.Memory.TryWrite(address, line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vzHs3C8lWJk",
|
||||
ExportName = "sceFontCloseFont",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceFont")]
|
||||
public static int CloseFont(CpuContext ctx)
|
||||
{
|
||||
FontFaces.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
FontScales.TryRemove(ctx[CpuRegister.Rdi], out _);
|
||||
return SetSuccess(ctx);
|
||||
}
|
||||
public static int CloseFont(CpuContext ctx) => SetSuccess(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1QjhKxrsOB8",
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace SharpEmu.Libs.Font;
|
||||
|
||||
internal sealed class FontFace(SKTypeface typeface)
|
||||
{
|
||||
private readonly ConcurrentDictionary<(uint Code, float Width, float Height), FontGlyph> _glyphs = new();
|
||||
|
||||
public SKFontMetrics GetMetrics(float width, float height)
|
||||
{
|
||||
using var font = CreateFont(width, height);
|
||||
return font.Metrics;
|
||||
}
|
||||
|
||||
public FontGlyph GetGlyph(uint code, float width, float height)
|
||||
{
|
||||
// ponytail: bounded working set; clearing avoids retaining every glyph at every scale.
|
||||
if (_glyphs.Count >= 1024) _glyphs.Clear();
|
||||
return _glyphs.GetOrAdd((code, width, height), key => Rasterize(key.Code, key.Width, key.Height));
|
||||
}
|
||||
|
||||
private SKFont CreateFont(float width, float height) => new(typeface, height, width / height)
|
||||
{
|
||||
Edging = SKFontEdging.Antialias,
|
||||
Subpixel = false,
|
||||
};
|
||||
|
||||
private FontGlyph Rasterize(uint code, float width, float height)
|
||||
{
|
||||
using var font = CreateFont(width, height);
|
||||
var glyph = (code & 0x80000000) != 0 ? (ushort)code : font.GetGlyph((int)code);
|
||||
if (glyph == 0 && (code & 0x80000000) == 0)
|
||||
{
|
||||
// System font sets span multiple faces. Resolve by Unicode coverage, not language.
|
||||
var fallback = SKFontManager.Default.MatchCharacter(typeface.FamilyName, typeface.FontStyle, null, (int)code);
|
||||
if (fallback is not null)
|
||||
{
|
||||
font.Typeface = fallback;
|
||||
glyph = font.GetGlyph((int)code);
|
||||
}
|
||||
}
|
||||
|
||||
ReadOnlySpan<ushort> glyphs = [glyph];
|
||||
var advance = font.MeasureText(glyphs, out var bounds);
|
||||
var left = (int)MathF.Floor(bounds.Left);
|
||||
var top = (int)MathF.Floor(bounds.Top);
|
||||
var bitmapWidth = (int)MathF.Ceiling(bounds.Right) - left;
|
||||
var bitmapHeight = (int)MathF.Ceiling(bounds.Bottom) - top;
|
||||
if (bitmapWidth <= 0 || bitmapHeight <= 0)
|
||||
{
|
||||
return new(0, 0, left, -top, advance, []);
|
||||
}
|
||||
|
||||
using var bitmap = new SKBitmap(bitmapWidth, bitmapHeight, SKColorType.Alpha8, SKAlphaType.Premul);
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true };
|
||||
using var blob = SKTextBlob.Create(MemoryMarshal.AsBytes(glyphs), SKTextEncoding.GlyphId, font);
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
canvas.DrawText(blob!, -left, -top, paint);
|
||||
var coverage = new byte[bitmapWidth * bitmapHeight];
|
||||
for (var row = 0; row < bitmapHeight; row++)
|
||||
{
|
||||
Marshal.Copy(bitmap.GetPixels() + row * bitmap.RowBytes, coverage, row * bitmapWidth, bitmapWidth);
|
||||
}
|
||||
return new(bitmapWidth, bitmapHeight, left, -top, advance, coverage);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record FontGlyph(int Width, int Height, float BearingX, float BearingY, float Advance, byte[] Coverage);
|
||||
|
|
@ -17,7 +17,6 @@ public sealed unsafe class BdaFaultProcessor : IDisposable
|
|||
private const uint MaxPageFaults = 1024;
|
||||
private const ulong PageFaultAreaSize = MaxPageFaults * sizeof(ulong);
|
||||
|
||||
private static long _reportedFaults;
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly SubmissionScheduler _scheduler;
|
||||
private readonly GuestBufferCache _cache;
|
||||
|
|
@ -250,11 +249,7 @@ public sealed unsafe class BdaFaultProcessor : IDisposable
|
|||
if (SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.Traces(faults[index], _pageSize))
|
||||
SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.Trace(faults[index], _pageSize,
|
||||
$"device-address-fault scan_tick={scanTick} callback_tick={_scheduler.CurrentTick} registered={_cache.IsRegionRegistered(faults[index], _pageSize)} reported_count={(uint)faults[0]} retained_count={count}");
|
||||
var reported = Interlocked.Increment(ref _reportedFaults);
|
||||
if (reported <= 16 || (reported & (reported - 1)) == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[GPU][INFO] Accessed non-GPU cached memory at 0x{faults[index]:X16} count={reported}");
|
||||
}
|
||||
Console.Error.WriteLine($"[GPU][INFO] Accessed non-GPU cached memory at 0x{faults[index]:X16}");
|
||||
}
|
||||
|
||||
_faultRanges.ForEach((start, size) =>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ using VkBuffer = Silk.NET.Vulkan.Buffer;
|
|||
|
||||
namespace SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
// One Vulkan buffer, mapped when host-visible. Small buffers share slab chunks (GpuMemorySlabs);
|
||||
// larger ones get a dedicated allocation.
|
||||
// One Vulkan buffer with its own dedicated allocation, mapped when host-visible.
|
||||
public unsafe class GpuBuffer : IDisposable
|
||||
{
|
||||
public const BufferUsageFlags ReadFlags =
|
||||
|
|
@ -29,12 +28,8 @@ public unsafe class GpuBuffer : IDisposable
|
|||
private readonly ulong _deviceAddress;
|
||||
private VkBuffer _handle;
|
||||
private DeviceMemory _memory;
|
||||
private readonly ulong _memoryOffset;
|
||||
private readonly GpuMemorySlabs.Block? _slab;
|
||||
|
||||
// allowSlab: the caller records on the scheduler's current command buffer, which clears a recycled slab block.
|
||||
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size,
|
||||
bool allowSlab = false)
|
||||
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
|
|
@ -48,23 +43,14 @@ public unsafe class GpuBuffer : IDisposable
|
|||
Size = size;
|
||||
|
||||
var vk = device.Vk;
|
||||
var sharedFamilies = device.SharedQueueFamilies;
|
||||
fixed (uint* families = sharedFamilies)
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
// Buffers carry no layout, so concurrent sharing lets the readback queue copy
|
||||
// them without queue-family ownership transfers at no cost to other queues.
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = flags,
|
||||
SharingMode = sharedFamilies is { Length: > 1 } ? SharingMode.Concurrent : SharingMode.Exclusive,
|
||||
QueueFamilyIndexCount = sharedFamilies is { Length: > 1 } ? (uint)sharedFamilies.Length : 0,
|
||||
PQueueFamilyIndices = sharedFamilies is { Length: > 1 } ? families : null,
|
||||
};
|
||||
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
|
||||
}
|
||||
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = flags,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
|
||||
vk.GetBufferMemoryRequirements(device.Device, _handle, out var requirements);
|
||||
var withAddress = (flags & BufferUsageFlags.ShaderDeviceAddressBit) != 0;
|
||||
var flagsInfo = new MemoryAllocateFlagsInfo
|
||||
|
|
@ -81,43 +67,23 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
// The best-scored type first; a full heap falls through to the next candidate.
|
||||
var result = Result.ErrorOutOfDeviceMemory;
|
||||
var candidates = RankMemoryTypes(device, requirements.MemoryTypeBits, usage).ToArray();
|
||||
if (allowSlab && GpuMemorySlabs.Fits(requirements))
|
||||
foreach (var candidate in RankMemoryTypes(device, requirements.MemoryTypeBits, usage))
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = device.AllocateMemory(allocateInfo, out _memory);
|
||||
if (result == Result.Success)
|
||||
{
|
||||
if (device.Slabs.TryAllocate(candidate, withAddress, requirements, out var block))
|
||||
{
|
||||
_slab = block;
|
||||
_memory = block.Memory;
|
||||
_memoryOffset = block.Offset;
|
||||
_mapped = block.Mapped;
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = Result.Success;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_slab is null)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
allocateInfo.MemoryTypeIndex = candidate;
|
||||
result = device.AllocateMemory(allocateInfo, out _memory);
|
||||
if (result == Result.Success)
|
||||
{
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RequireSuccess(result, $"vkAllocateMemory({usage}, 0x{size:X} bytes)");
|
||||
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, _memoryOffset), "vkBindBufferMemory");
|
||||
_allocationSize = _slab?.Size ?? requirements.Size;
|
||||
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, 0), "vkBindBufferMemory");
|
||||
_allocationSize = requirements.Size;
|
||||
|
||||
var properties = device.GetMemoryTypeFlags(allocateInfo.MemoryTypeIndex);
|
||||
IsCoherent = (properties & MemoryPropertyFlags.HostCoherentBit) != 0;
|
||||
if (_slab is null && (properties & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
if ((properties & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
{
|
||||
void* pointer;
|
||||
RequireSuccess(vk.MapMemory(device.Device, _memory, 0, Vk.WholeSize, 0, &pointer), "vkMapMemory");
|
||||
|
|
@ -133,12 +99,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
throw SubmissionScheduler.Fatal("The buffer device address is unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
// A dedicated allocation arrives zeroed; keep that for a block another buffer used.
|
||||
if (_slab is { Recycled: true } && (Size & ~3UL) != 0)
|
||||
{
|
||||
Fill(0, Size & ~3UL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public VkBuffer Handle => _handle;
|
||||
|
|
@ -168,20 +128,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
public void AddStreamScore(int score) => StreamScore += score;
|
||||
|
||||
// The highest scheduler tick whose command buffer may write this buffer on the GPU.
|
||||
// A readback of it only has to wait for that tick, not for all queued work.
|
||||
public ulong LastGpuWriteTick { get; private set; }
|
||||
|
||||
// Called by every path that records a GPU write into this buffer.
|
||||
public void NoteGpuWrite()
|
||||
{
|
||||
var tick = _scheduler.CurrentTick;
|
||||
if (tick > LastGpuWriteTick)
|
||||
{
|
||||
LastGpuWriteTick = tick;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(ulong offset, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (_mapped == null || offset > Size || (ulong)source.Length > Size - offset)
|
||||
|
|
@ -243,7 +189,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
}
|
||||
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = stackalloc BufferMemoryBarrier2[2];
|
||||
|
|
@ -271,7 +216,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
|
||||
var command = _scheduler.Current;
|
||||
command.EndRendering();
|
||||
NoteGpuWrite();
|
||||
var vk = _device.Vk;
|
||||
var native = new CommandBuffer(command.Handle);
|
||||
var before = CreateBarrier(offset, size, MemoryAccess, AccessFlags.TransferWriteBit);
|
||||
|
|
@ -285,19 +229,6 @@ public unsafe class GpuBuffer : IDisposable
|
|||
0, null, 1, &after, 0, null);
|
||||
}
|
||||
|
||||
private int _foreignReads;
|
||||
private int _disposeDeferred;
|
||||
|
||||
public void RetainForeignRead() => Interlocked.Increment(ref _foreignReads);
|
||||
|
||||
public void ReleaseForeignRead()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _foreignReads) == 0 && Interlocked.Exchange(ref _disposeDeferred, 0) != 0)
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_handle.Handle == 0)
|
||||
|
|
@ -305,25 +236,8 @@ public unsafe class GpuBuffer : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _foreignReads) != 0)
|
||||
{
|
||||
Volatile.Write(ref _disposeDeferred, 1);
|
||||
if (Volatile.Read(ref _foreignReads) != 0 || Interlocked.Exchange(ref _disposeDeferred, 0) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_device.Vk.DestroyBuffer(_device.Device, _handle, null);
|
||||
if (_slab is { } slab)
|
||||
{
|
||||
_device.Slabs.Release(slab);
|
||||
}
|
||||
else
|
||||
{
|
||||
_device.FreeMemory(_memory);
|
||||
}
|
||||
|
||||
_device.FreeMemory(_memory);
|
||||
_handle = default;
|
||||
_memory = default;
|
||||
}
|
||||
|
|
@ -366,7 +280,7 @@ public unsafe class GpuBuffer : IDisposable
|
|||
{
|
||||
SType = StructureType.MappedMemoryRange,
|
||||
Memory = _memory,
|
||||
Offset = _memoryOffset + begin,
|
||||
Offset = begin,
|
||||
Size = end - begin,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,12 @@ namespace SharpEmu.Libs.Gpu.Buffers;
|
|||
public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
||||
{
|
||||
private PhysicalDeviceMemoryProperties _memoryProperties;
|
||||
// Queried for every draw target from the render thread and from other threads; a hit takes no lock.
|
||||
// Two threads may query the same missing key once each; the driver answers identically.
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<Format, FormatProperties> _formatProperties = new();
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
|
||||
private readonly Dictionary<Format, FormatProperties> _formatProperties = new();
|
||||
private readonly Dictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
|
||||
private readonly object _gate = new();
|
||||
private int _liveAllocations;
|
||||
private int _peakAllocations;
|
||||
|
||||
// VK_EXT_image_view_min_lod is enabled, so a view can clamp to a texture descriptor's MIN_LOD.
|
||||
public bool ImageViewMinLodSupported { get; init; }
|
||||
|
||||
public GpuDeviceInfo(Vk vk, PhysicalDevice physicalDevice, Device device)
|
||||
{
|
||||
Vk = vk;
|
||||
|
|
@ -33,18 +29,10 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
|||
MaxStorageBufferRange = properties.Limits.MaxStorageBufferRange;
|
||||
MaxMemoryAllocationCount = properties.Limits.MaxMemoryAllocationCount;
|
||||
MaxComputeWorkGroupCount = (properties.Limits.MaxComputeWorkGroupCount[0], properties.Limits.MaxComputeWorkGroupCount[1], properties.Limits.MaxComputeWorkGroupCount[2]);
|
||||
Slabs = new GpuMemorySlabs(this);
|
||||
}
|
||||
|
||||
// Shared chunks the small buffers are carved from; freed at device teardown.
|
||||
internal GpuMemorySlabs Slabs { get; }
|
||||
|
||||
public Vk Vk { get; }
|
||||
|
||||
// Queue families that may access every buffer; set before the first buffer is
|
||||
// created when a second queue family (the async readback queue) reads them.
|
||||
public uint[]? SharedQueueFamilies { get; set; }
|
||||
|
||||
public PhysicalDevice PhysicalDevice { get; }
|
||||
|
||||
public Device Device { get; }
|
||||
|
|
@ -77,27 +65,33 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
|
|||
|
||||
public FormatProperties GetFormatProperties(Format format)
|
||||
{
|
||||
if (!_formatProperties.TryGetValue(format, out var properties))
|
||||
lock (_gate)
|
||||
{
|
||||
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
|
||||
_formatProperties[format] = properties;
|
||||
}
|
||||
if (!_formatProperties.TryGetValue(format, out var properties))
|
||||
{
|
||||
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
|
||||
_formatProperties[format] = properties;
|
||||
}
|
||||
|
||||
return properties;
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetImageFormatProperties(Format format, ImageType type, ImageTiling tiling, ImageUsageFlags usage, ImageCreateFlags flags, out ImageFormatProperties properties)
|
||||
{
|
||||
var key = (format, type, tiling, usage, flags);
|
||||
if (!_imageFormatProperties.TryGetValue(key, out var entry))
|
||||
lock (_gate)
|
||||
{
|
||||
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
|
||||
entry = (result, found);
|
||||
_imageFormatProperties[key] = entry;
|
||||
}
|
||||
var key = (format, type, tiling, usage, flags);
|
||||
if (!_imageFormatProperties.TryGetValue(key, out var entry))
|
||||
{
|
||||
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
|
||||
entry = (result, found);
|
||||
_imageFormatProperties[key] = entry;
|
||||
}
|
||||
|
||||
properties = entry.Properties;
|
||||
return entry.Result == Result.Success;
|
||||
properties = entry.Properties;
|
||||
return entry.Result == Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
// Every device-memory allocation goes through here so the live count stays exact.
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
// Small buffers take a power-of-two block out of a shared, persistently mapped chunk
|
||||
// instead of a dedicated allocation. Games stream constants through fresh ranges every
|
||||
// frame, and the buffer cache created and freed a device allocation for each of them;
|
||||
// vkAllocateMemory/vkMapMemory/vkFreeMemory then cost ~15% of the render thread in busy
|
||||
// Astro Bot scenes, and every live allocation counts against maxMemoryAllocationCount.
|
||||
internal sealed unsafe class GpuMemorySlabs
|
||||
{
|
||||
public const ulong MinBlockSize = 4096;
|
||||
public const ulong MaxBlockSize = 1024 * 1024;
|
||||
private const ulong ChunkSize = 8 * 1024 * 1024;
|
||||
private static readonly int ClassCount = BitOperations.Log2(MaxBlockSize) - BitOperations.Log2(MinBlockSize) + 1;
|
||||
|
||||
private readonly GpuDeviceInfo _device;
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<(uint MemoryType, bool DeviceAddress), Pool> _pools = [];
|
||||
private readonly List<DeviceMemory> _chunks = [];
|
||||
private readonly List<Block> _quarantine = [];
|
||||
private int _foreignReaders;
|
||||
|
||||
public GpuMemorySlabs(GpuDeviceInfo device) => _device = device;
|
||||
|
||||
// Recycled blocks still hold their previous buffer's bytes; fresh ones come zeroed with their chunk.
|
||||
public readonly record struct Block(DeviceMemory Memory, ulong Offset, ulong Size, nint MappedAddress, uint MemoryType, bool DeviceAddress, bool Recycled)
|
||||
{
|
||||
public byte* Mapped => (byte*)MappedAddress;
|
||||
}
|
||||
|
||||
private sealed class Pool
|
||||
{
|
||||
public readonly Stack<(DeviceMemory Memory, ulong Offset, nint Mapped, bool Recycled)>[] Free =
|
||||
Enumerable.Range(0, ClassCount).Select(_ => new Stack<(DeviceMemory, ulong, nint, bool)>()).ToArray();
|
||||
}
|
||||
|
||||
// A readback on the second queue copies from buffers the render thread may retire (and
|
||||
// recycle) while the copy is still pending. A dedicated allocation kept its old bytes
|
||||
// after vkFreeMemory, which hid that race; a recycled block does not. Released blocks
|
||||
// therefore wait until no readback that might have picked their buffer is running.
|
||||
public ForeignReadScope BeginForeignRead()
|
||||
{
|
||||
Interlocked.Increment(ref _foreignReaders);
|
||||
return new ForeignReadScope(this);
|
||||
}
|
||||
|
||||
public readonly struct ForeignReadScope(GpuMemorySlabs owner) : IDisposable
|
||||
{
|
||||
public void Dispose() => Interlocked.Decrement(ref owner._foreignReaders);
|
||||
}
|
||||
|
||||
public static bool Fits(in MemoryRequirements requirements) =>
|
||||
requirements.Size <= MaxBlockSize && requirements.Alignment <= MaxBlockSize;
|
||||
|
||||
public bool TryAllocate(uint memoryType, bool deviceAddress, in MemoryRequirements requirements, out Block block)
|
||||
{
|
||||
block = default;
|
||||
var blockSize = Math.Max(MinBlockSize, BitOperations.RoundUpToPowerOf2(Math.Max(requirements.Size, requirements.Alignment)));
|
||||
if (blockSize > MaxBlockSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sizeClass = BitOperations.Log2(blockSize) - BitOperations.Log2(MinBlockSize);
|
||||
lock (_gate)
|
||||
{
|
||||
if (_quarantine.Count != 0 && Volatile.Read(ref _foreignReaders) == 0)
|
||||
{
|
||||
foreach (var released in _quarantine)
|
||||
{
|
||||
var releasedClass = BitOperations.Log2(released.Size) - BitOperations.Log2(MinBlockSize);
|
||||
_pools[(released.MemoryType, released.DeviceAddress)].Free[releasedClass]
|
||||
.Push((released.Memory, released.Offset, released.MappedAddress, true));
|
||||
}
|
||||
|
||||
_quarantine.Clear();
|
||||
}
|
||||
|
||||
if (!_pools.TryGetValue((memoryType, deviceAddress), out var pool))
|
||||
{
|
||||
pool = new Pool();
|
||||
_pools.Add((memoryType, deviceAddress), pool);
|
||||
}
|
||||
|
||||
var free = pool.Free[sizeClass];
|
||||
if (free.Count == 0 && !TryAddChunk(memoryType, deviceAddress, blockSize, free))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (memory, offset, mapped, recycled) = free.Pop();
|
||||
block = new Block(memory, offset, blockSize, mapped, memoryType, deviceAddress, recycled);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Release(in Block block)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_quarantine.Add(block);
|
||||
}
|
||||
}
|
||||
|
||||
// Frees every chunk; only valid once no buffer still uses one (device teardown).
|
||||
public void Destroy()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var chunk in _chunks)
|
||||
{
|
||||
_device.FreeMemory(chunk);
|
||||
}
|
||||
|
||||
_chunks.Clear();
|
||||
_pools.Clear();
|
||||
_quarantine.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAddChunk(uint memoryType, bool deviceAddress, ulong blockSize, Stack<(DeviceMemory, ulong, nint, bool)> free)
|
||||
{
|
||||
var flagsInfo = new MemoryAllocateFlagsInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateFlagsInfo,
|
||||
Flags = MemoryAllocateFlags.DeviceAddressBit,
|
||||
};
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
PNext = deviceAddress ? &flagsInfo : null,
|
||||
AllocationSize = ChunkSize,
|
||||
MemoryTypeIndex = memoryType,
|
||||
};
|
||||
if (_device.AllocateMemory(allocateInfo, out var memory) != Result.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte* mapped = null;
|
||||
if ((_device.GetMemoryTypeFlags(memoryType) & MemoryPropertyFlags.HostVisibleBit) != 0)
|
||||
{
|
||||
void* pointer;
|
||||
if (_device.Vk.MapMemory(_device.Device, memory, 0, Vk.WholeSize, 0, &pointer) != Result.Success)
|
||||
{
|
||||
_device.FreeMemory(memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
mapped = (byte*)pointer;
|
||||
}
|
||||
|
||||
_chunks.Add(memory);
|
||||
// Push in reverse so blocks come out in address order.
|
||||
for (var offset = ChunkSize - blockSize; ; offset -= blockSize)
|
||||
{
|
||||
free.Push((memory, offset, mapped == null ? 0 : (nint)(mapped + offset), false));
|
||||
if (offset == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue