Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a3b92567 | |||
| 8f9456229a | |||
| 5b602c0232 | |||
| 26c502914c | |||
| a158960c20 | |||
| 5228335f15 | |||
| 21f964a0dc | |||
| 6133313a83 | |||
| 6db095ec82 | |||
| 5a08a9bb43 | |||
| f9d92135a0 | |||
| 8779c96c3a | |||
| 4c6cff1116 | |||
| 82ab181861 | |||
| 007bf6fa73 | |||
| 8e1e89c024 | |||
| 2764aaab3f | |||
| 7b950166d7 | |||
| eb1195e59a | |||
| e13cb28267 | |||
| 956da769a3 | |||
| d7bd814fb9 | |||
| 96fde5764f | |||
| 7a108c6f87 | |||
| eb252af7b3 | |||
| 8e5a0bfb19 | |||
| d991e32b15 | |||
| 93829e3242 | |||
| 4191a9e12b | |||
| 559b7f0a84 | |||
| 2272b9b576 | |||
| 8dd3172c0f | |||
| e7ea186ea8 | |||
| 74a519875b | |||
| 4682e64e81 | |||
| 912883de05 | |||
| f704586a8d |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 104 KiB |
@@ -26,6 +26,25 @@ Before opening a pull request, please keep the following in mind:
|
||||
|
||||
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
Pull requests should provide real, observable emulator behavior rather than only suppressing errors or unresolved imports.
|
||||
|
||||
Changes that only return success, zero, or fabricated handles without implementing the expected state, output, or side effects will generally not be accepted. Functions that create resources, write output structures, register callbacks, or expose runtime state should model the behavior required by the guest.
|
||||
|
||||
When applicable, PRs should include:
|
||||
|
||||
- The affected game or application.
|
||||
- Relevant logs or failing imports.
|
||||
- Behavior before and after the change.
|
||||
- Real game testing and known limitations.
|
||||
|
||||
Avoid submitting large collections of speculative NIDs or unrelated exports. Keep each PR focused on one problem or a closely related set of changes.
|
||||
|
||||
Large architectural changes should be discussed with the maintainers before implementation. Contributors are encouraged to ask first when they are uncertain whether a proposed direction fits the project.
|
||||
|
||||
Opening a PR does not guarantee that it will be merged. Maintainers evaluate changes based on correctness, evidence, testing, scope, maintenance cost, and the long-term direction of the project.
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
|
||||
|
||||
@@ -9,11 +9,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2-beta.5</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx</_HostRidOSPrefix>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64</_HostRidArch>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64</_HostRidArch>
|
||||
<RuntimeIdentifier Condition="'$(_HostRidOSPrefix)' != ''">$(_HostRidOSPrefix)-$(_HostRidArch)</RuntimeIdentifier>
|
||||
|
||||
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
|
||||
@@ -25,6 +25,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<a href="#support">
|
||||
<img src="https://img.shields.io/badge/Support-GitHub%20Sponsors%20%26%20Crypto-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Support SharpEmu">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [!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
|
||||
@@ -136,6 +144,18 @@ Provided valuable references for filesystem handling and low-level C# implementa
|
||||
|
||||
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
|
||||
|
||||
## Support
|
||||
|
||||
Support SharpEmu via GitHub Sponsors or cryptocurrency. Every contribution helps fund ongoing development and long-term maintenance. GitHub Sponsors is the preferred way to support the project, but cryptocurrency donations are also appreciated.
|
||||
|
||||
### ETH/USDT
|
||||
|
||||
`0xF315F5d986c790bB3A58DbE60F1B2760997dEd82`
|
||||
|
||||
### BTC
|
||||
|
||||
`bc1qmr9k8899njys5ny63xsues4jgmkk96erslrkmv`
|
||||
|
||||
## Contributing
|
||||
|
||||
Before opening an issue or pull request, please read our contribution guidelines:
|
||||
|
||||
@@ -21,6 +21,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj" />
|
||||
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -9,38 +9,67 @@ 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.
|
||||
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
|
||||
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.
|
||||
|
||||
Without an adapter, Bink files remain visible to the guest and the game's
|
||||
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
|
||||
explicitly testing a title whose cinematics are optional.
|
||||
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
|
||||
(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
|
||||
FFmpeg 7.1.2; see "Supplying the FFmpeg libraries" below for where those
|
||||
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=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. Set
|
||||
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
||||
only; it does not decode the movie or alter its game logic.
|
||||
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
|
||||
being explicit about it.
|
||||
|
||||
## Supplying the adapter
|
||||
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.
|
||||
|
||||
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
|
||||
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
|
||||
The adapter deliberately contains only a three-function C ABI so the managed
|
||||
emulator never depends on RAD's private binary ABI.
|
||||
## Supplying the FFmpeg libraries
|
||||
|
||||
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
||||
executable, or point to it explicitly:
|
||||
`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
|
||||
(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
|
||||
the `FFmpeg.AutoGen` package version in `Directory.Packages.props` -- both
|
||||
need to agree on the same FFmpeg ABI) and copies its dynamically linked
|
||||
libraries into a `plugins` folder next to the published executable. No C
|
||||
toolchain is required to build SharpEmu; publishing just downloads a zip.
|
||||
`plugins` is a loose, unpacked folder rather than something embedded in the
|
||||
single-file bundle, so the OS loader can resolve the libraries' own
|
||||
inter-dependencies (`avcodec` depends on `avutil`, etc.) itself.
|
||||
|
||||
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
|
||||
./SharpEmu /path/to/eboot.bin
|
||||
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
||||
machine's own RID (see `Directory.Build.props`), so it fetches the matching
|
||||
`ffmpeg-core` archive and populates `plugins` without any extra flags.
|
||||
Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
||||
Windows) still overrides that default normally.
|
||||
|
||||
The expected exports are sharpemu_bink2_open_utf8,
|
||||
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
||||
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
||||
frame. The managed side validates dimensions and retains ownership of the
|
||||
destination buffer.
|
||||
To use a different set of FFmpeg libraries, drop them into the published
|
||||
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||
folder and does not otherwise care where the files came from.
|
||||
|
||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
||||
and retains the existing guest rendering path.
|
||||
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
||||
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
|
||||
could not open movie ...") and leaves the guest's own rendering path
|
||||
untouched, rather than crashing.
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SharpEmu Emulator Project
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*
|
||||
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
|
||||
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "bink.h"
|
||||
|
||||
typedef struct sharpemu_bink2_info {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t frames_per_second_numerator;
|
||||
uint32_t frames_per_second_denominator;
|
||||
} sharpemu_bink2_info;
|
||||
|
||||
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
|
||||
HBINK bink;
|
||||
if (!path || !movie || !info) return 0;
|
||||
|
||||
*movie = NULL;
|
||||
|
||||
bink = BinkOpen(path, 0);
|
||||
if (!bink) return 0;
|
||||
|
||||
if (bink->Width == 0 || bink->Height == 0) {
|
||||
BinkClose(bink);
|
||||
return 0;
|
||||
}
|
||||
|
||||
*movie = bink;
|
||||
info->width = bink->Width;
|
||||
info->height = bink->Height;
|
||||
info->frames_per_second_numerator = bink->FrameRate;
|
||||
info->frames_per_second_denominator = bink->FrameRateDiv;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
|
||||
uint32_t stride, uint32_t destination_bytes) {
|
||||
uint64_t needed;
|
||||
uint64_t min_stride;
|
||||
|
||||
if (!movie || !destination) return 0;
|
||||
|
||||
min_stride = (uint64_t)movie->Width * 4;
|
||||
if ((uint64_t)stride < min_stride) return 0;
|
||||
|
||||
needed = (uint64_t)stride * movie->Height;
|
||||
if (needed > destination_bytes) return 0;
|
||||
|
||||
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
|
||||
if (BinkWait(movie)) return 0;
|
||||
|
||||
if (!BinkDoFrame(movie)) return 0;
|
||||
|
||||
if (!BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA)) return 0;
|
||||
|
||||
BinkNextFrame(movie);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void sharpemu_bink2_close(HBINK movie) {
|
||||
if (movie) BinkClose(movie);
|
||||
}
|
||||
@@ -153133,6 +153133,7 @@ scePsmlMfsrGetContextBufferRequirement800M3_2
|
||||
scePsmlMfsrGetDispatchMfsrPacket1000
|
||||
scePsmlMfsrGetDispatchMfsrPacket1100
|
||||
scePsmlMfsrGetDispatchMfsrPacketSizeInDwords
|
||||
scePsmlMfsrGetDispatchMfsrPacket900
|
||||
scePsmlMfsrGetMipmapBias
|
||||
scePsmlMfsrGetSharedResourcesInitRequirement
|
||||
scePsmlMfsrInit
|
||||
|
||||
@@ -64,6 +64,7 @@ internal static partial class Program
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
PreloadGlfw();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
@@ -213,6 +214,27 @@ internal static partial class Program
|
||||
"as libvulkan.1.dylib.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharpEmu.CLI.csproj publishes glfw into a "plugins" subfolder rather
|
||||
/// than flat next to the executable, which falls outside the default OS
|
||||
/// DLL/dlopen search path. Preloading it here by full path first means
|
||||
/// any later bare-name lookup (however Silk.NET/GLFW itself resolves the
|
||||
/// library) finds it already loaded in the process and reuses it -- the
|
||||
/// same technique <see cref="PreloadMacVulkanLoader"/> already relies on
|
||||
/// for the Vulkan loader.
|
||||
/// </summary>
|
||||
private static void PreloadGlfw()
|
||||
{
|
||||
var fileName = OperatingSystem.IsWindows() ? "glfw3.dll"
|
||||
: OperatingSystem.IsMacOS() ? "libglfw.3.dylib"
|
||||
: "libglfw.so.3";
|
||||
var candidate = Path.Combine(AppContext.BaseDirectory, "plugins", fileName);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
NativeLibrary.TryLoad(candidate, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
@@ -551,12 +573,7 @@ internal static partial class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
var childArgs = new string[args.Length + 1];
|
||||
childArgs[0] = MitigatedChildFlag;
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
childArgs[i + 1] = args[i];
|
||||
}
|
||||
string[] childArgs = [MitigatedChildFlag, .. args];
|
||||
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
|
||||
@@ -20,6 +20,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<!-- A plain "dotnet publish" with no -r defaults $(RuntimeIdentifier) to
|
||||
the host's own RID; see Directory.Build.props, which is where that
|
||||
default actually has to live (PublishDir's RID suffix is decided
|
||||
there, evaluated before this file, so a default set only here would
|
||||
be too late for it). -->
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
@@ -74,7 +79,20 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
<!-- Native libraries (glfw, FFmpeg) publish into a subfolder next to the
|
||||
executable instead of sitting loose beside it, so the publish
|
||||
directory stays uncluttered as more native deps get added. The folder
|
||||
name is a fixed constant, not derived from the RID/architecture: each
|
||||
publish output only ever holds one architecture's binaries anyway, so
|
||||
varying the name added a class of bugs (RID resolution timing, host-OS
|
||||
vs. target-RID mixups) for no benefit. Runtime code (Program.cs's
|
||||
PreloadGlfw, FfmpegNativeBinkFrameSource's RootPath) uses the same
|
||||
literal "plugins" folder name. -->
|
||||
<PropertyGroup>
|
||||
<NativeLibraryFolderName>plugins</NativeLibraryFolderName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file in the native subfolder; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
@@ -83,8 +101,53 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<RelativePath>$(NativeLibraryFolderName)/%(Filename)%(Extension)</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<FfmpegRuntimeTag>2c92585</FfmpegRuntimeTag>
|
||||
<FfmpegRuntimeDir>
|
||||
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'linux-x64'">ffmpeg-linux-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-x64'">ffmpeg-macos-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">ffmpeg-macos-arm64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimeArchive>$(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)</FfmpegRuntimeArchive>
|
||||
<FfmpegRuntimeExtractDir>$(FfmpegRuntimeDir)/extracted</FfmpegRuntimeExtractDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="FetchFfmpegRuntime"
|
||||
BeforeTargets="Publish"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||
<Unzip
|
||||
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishFfmpegRuntime"
|
||||
AfterTargets="Publish"
|
||||
DependsOnTargets="FetchFfmpegRuntime"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
|
||||
e.g. linux-x64 from a Windows machine is a supported cross-publish,
|
||||
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
|
||||
depends on which platform's ffmpeg-core package was fetched. -->
|
||||
<ItemGroup>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -19,6 +19,9 @@ public sealed partial class DirectExecutionBackend
|
||||
private static int _lazyCommitTraceCount;
|
||||
private static int _guestAllocatorHoleRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultSkips;
|
||||
private nint _workerAbortStack;
|
||||
private const uint WorkerAbortStackSize = 0x10000u;
|
||||
|
||||
private unsafe void SetupExceptionHandler()
|
||||
{
|
||||
@@ -435,18 +438,91 @@ public sealed partial class DirectExecutionBackend
|
||||
void* contextRecord,
|
||||
ulong rip)
|
||||
{
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||
rip >= 0x0000000800000000UL ||
|
||||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer ThreadStatic active state; fall back to host-thread name when
|
||||
// concurrent TBB AVs race logging (tLT61: recover skipped, then Fatal).
|
||||
GuestThreadState? activeThread = _activeGuestThreadState;
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var hostName = Thread.CurrentThread.Name;
|
||||
if (hostName is null ||
|
||||
!hostName.StartsWith("SharpEmu-tbb_thead", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activeThread = FindGuestThreadStateByHostThreadId(unchecked((int)GetCurrentThreadId()));
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var skip = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultSkips);
|
||||
if (skip <= 8 || skip % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] tbb_recover skip #{skip}: rip=0x{rip:X16} " +
|
||||
$"host='{hostName}' active={(activeThread?.Name ?? "null")}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var hostExit = ActiveEntryReturnSentinelRip;
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
hostExit = unchecked((ulong)_guestReturnStub);
|
||||
}
|
||||
|
||||
// Prefer worker-abort (SetEvent + ExitThread) over host_exit→RunEpilogue:
|
||||
// the latter FailFasts the process after TBB recover (tLT28/30 silent die).
|
||||
// Do NOT abandon mutexes here — managed HLE from inside VEH can re-enter
|
||||
// and Fatal (tLT73). NativeGuestExecutor.Run abandons after detecting abort.
|
||||
var abortRip = unchecked((ulong)_workerAbortStub);
|
||||
if (abortRip >= 0x10000)
|
||||
{
|
||||
// Do NOT SetEvent from managed VEH: that wakes the renter which may
|
||||
// TerminateThread while this thread is still inside VEH return
|
||||
// (tLTA2: recover logged, no respawning, process die). Abort stub
|
||||
// SetEvent's only after CONTINUE_EXECUTION resumes at park.
|
||||
|
||||
// Prefer the entry-stub-saved host RSP (real CreateThread stack).
|
||||
// Do not treat mid-range host stacks as guest — Astro worker stacks
|
||||
// often sit in 0x02xxxxxx_xxxx and were wrongly replaced with a
|
||||
// shared VirtualAlloc abort stack (concurrent TBB AV → die).
|
||||
var hostRspSlot = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
ulong hostRsp = 0;
|
||||
if (hostRspSlot != 0)
|
||||
{
|
||||
hostRsp = *(ulong*)hostRspSlot;
|
||||
}
|
||||
|
||||
if (hostRsp < 0x10000)
|
||||
{
|
||||
hostRsp = EnsureWorkerAbortStackRsp();
|
||||
}
|
||||
|
||||
if (hostRsp >= 0x10000)
|
||||
{
|
||||
WriteCtxU64(contextRecord, 152, hostRsp & ~0xFUL);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, abortRip);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} " +
|
||||
$"host_rsp=0x{hostRsp:X16} -> worker_abort=0x{abortRip:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: parking native worker (SetEvent+park); " +
|
||||
"renter will TerminateThread+respawn — avoids ExitThread after VEH");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -458,13 +534,57 @@ public sealed partial class DirectExecutionBackend
|
||||
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, hostExit);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
var recoveryFallback = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recoveryFallback}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: resumed at host_exit (abort stub unavailable); " +
|
||||
"subsequent FastFail/CLR must not re-enter managed VEH " +
|
||||
"(live trampoline pre-filters 0xC0000409 / 0xE0434352)");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
private GuestThreadState? FindGuestThreadStateByHostThreadId(int hostThreadId)
|
||||
{
|
||||
if (hostThreadId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var thread in SnapshotGuestThreads())
|
||||
{
|
||||
if (Volatile.Read(ref thread.HostThreadId) == hostThreadId)
|
||||
{
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private unsafe ulong EnsureWorkerAbortStackRsp()
|
||||
{
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
_workerAbortStack = (nint)VirtualAlloc(null, WorkerAbortStackSize, 12288u, 4u);
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Grow-down stack: hand out near the top with alignment headroom.
|
||||
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||
|
||||
@@ -1449,6 +1449,9 @@ public sealed partial class DirectExecutionBackend
|
||||
var expectedSemaphoreTrywaitAgain =
|
||||
string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
var expectedPollSemaBusy =
|
||||
string.Equals(nid, "12wOHk8ywb0", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
var expectedNetAcceptWouldBlock =
|
||||
string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80410123);
|
||||
@@ -1463,6 +1466,7 @@ public sealed partial class DirectExecutionBackend
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedSemaphoreTrywaitAgain &&
|
||||
!expectedPollSemaBusy &&
|
||||
!expectedNetAcceptWouldBlock &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter)
|
||||
|
||||
@@ -29,9 +29,29 @@ public sealed partial class DirectExecutionBackend
|
||||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Cap concurrent native-worker Runs. Astro's tbb_thead burst overlaps many
|
||||
// UnmanagedCallersOnly prologues; a large prewarm + unbounded concurrency
|
||||
// FailFasts (0xC0000409) mid-storm with no VEH breadcrumb. Pool size and
|
||||
// in-flight Runs are separate knobs.
|
||||
private static readonly int NativeWorkerMaxConcurrent = ReadNativeWorkerMaxConcurrent();
|
||||
|
||||
private static int ReadNativeWorkerMaxConcurrent()
|
||||
{
|
||||
if (int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT"),
|
||||
out var parsed) &&
|
||||
parsed > 0)
|
||||
{
|
||||
return Math.Clamp(parsed, 1, 64);
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent);
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
@@ -49,6 +69,9 @@ public sealed partial class DirectExecutionBackend
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool TerminateThread(nint hThread, uint dwExitCode);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
@@ -56,17 +79,73 @@ public sealed partial class DirectExecutionBackend
|
||||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
|
||||
{
|
||||
var worker = RentNativeGuestExecutor();
|
||||
// Limit in-flight native Runs before renting so the idle pool is not
|
||||
// drained by threads blocked on the concurrency gate.
|
||||
_nativeWorkerRunLimiter.Wait();
|
||||
NativeGuestExecutor? worker = null;
|
||||
try
|
||||
{
|
||||
// Astro can spawn a burst of tbb_thead while workers are still in
|
||||
// TerminateThread+respawn. Wait for a native worker — never fall back
|
||||
// to managed inline (FailFast) and never throw (uncaught throw mid-
|
||||
// storm was a silent process die).
|
||||
var maxAttempts = requireNativeWorker ? 500 : 48;
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
worker = RentNativeGuestExecutor();
|
||||
if (worker is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requireNativeWorker)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(attempt < 32 ? 1 : 4);
|
||||
}
|
||||
|
||||
if (worker is null)
|
||||
{
|
||||
if (requireNativeWorker)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeWorkerRefuseCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_native_worker unavailable #{n} after {maxAttempts} attempts; " +
|
||||
"skipping run (no managed inline, no throw)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
_activeGuestThreadYieldRequested = true;
|
||||
_activeGuestThreadYieldReason = "tbb_native_worker_unavailable";
|
||||
_activeForcedGuestExit = true;
|
||||
return unchecked((int)0x80020012);
|
||||
}
|
||||
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
if (state is { Name: "tbb_thead" })
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
|
||||
if (n <= 12 || n % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] tbb_run_enter #{n} native_tid_pending handle=0x{state.ThreadHandle:X16} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
@@ -89,6 +168,58 @@ public sealed partial class DirectExecutionBackend
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nativeWorkerRunLimiter.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static int _tbbNativeRunEnterCount;
|
||||
private static int _tbbNativeWorkerRefuseCount;
|
||||
internal static int _tbbWorkerPrologueFaultCount;
|
||||
|
||||
private void PrewarmNativeGuestWorkers(int count)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled || count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var warmed = new List<NativeGuestExecutor>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
warmed.Add(worker);
|
||||
}
|
||||
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
_allNativeWorkers.Add(worker);
|
||||
_idleNativeWorkers.Push(worker);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Native guest workers prewarmed: {warmed.Count}/{count} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
@@ -400,6 +531,22 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool RestartWorkerThread()
|
||||
{
|
||||
if (_loopStub == null || _controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*(int*)_controlBlock = 0;
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool StartWorkerThread()
|
||||
{
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
@@ -445,6 +592,49 @@ public sealed partial class DirectExecutionBackend
|
||||
_runForcedExit = false;
|
||||
SignalWorkAvailable();
|
||||
WaitWorkCompleted();
|
||||
|
||||
// Normal path: RunEpilogue/ExitRun clears _entered before SetEvent(done).
|
||||
// TBB abort stub SetEvent's without ExitRun — _entered stays true.
|
||||
if (_entered)
|
||||
{
|
||||
var waitRc = WaitForSingleObject(_threadHandle, 500u);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} aborted during run; " +
|
||||
$"wait_rc=0x{waitRc:X8} respawning");
|
||||
Console.Error.Flush();
|
||||
if (_runState is { } abortedState)
|
||||
{
|
||||
_ = GuestThreadExecution.NotifyGuestThreadAbandoned(
|
||||
abortedState.ThreadHandle,
|
||||
"tbb_worker_abort");
|
||||
Volatile.Write(ref abortedState.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
_entered = false;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
// Abort stub parks (no ExitThread). Force-kill the parked OS
|
||||
// thread so we can recreate the loop without process teardown.
|
||||
if (waitRc != 0u)
|
||||
{
|
||||
_ = TerminateThread(_threadHandle, unchecked((uint)(-1)));
|
||||
_ = WaitForSingleObject(_threadHandle, 1000u);
|
||||
}
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
_nativeThreadId = 0;
|
||||
}
|
||||
if (!RestartWorkerThread())
|
||||
{
|
||||
_runPrologueFailed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_runPrologueFailed = false;
|
||||
_runForcedExit = true;
|
||||
_runNativeResult = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
@@ -452,7 +642,22 @@ public sealed partial class DirectExecutionBackend
|
||||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
// Never throw out of the native-worker rent path: an uncaught
|
||||
// exception mid-TBB storm kills the process with no FailFast
|
||||
// breadcrumb.
|
||||
var n = Interlocked.Increment(ref _tbbWorkerPrologueFaultCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_worker prologue fault #{n}; soft-fail run " +
|
||||
$"(tid={_nativeThreadId})");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
yieldRequested = true;
|
||||
yieldReason = "tbb_worker_prologue_fault";
|
||||
forcedExit = true;
|
||||
return unchecked((int)0x80020012);
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
@@ -546,6 +751,18 @@ public sealed partial class DirectExecutionBackend
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint doneHandle = OperatingSystem.IsWindows()
|
||||
? _workCompleted!.SafeWaitHandle.DangerousGetHandle()
|
||||
: _doneSemaphore;
|
||||
TlsSetValue(backend._workerDoneEventTlsIndex, doneHandle);
|
||||
}
|
||||
if (backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint eligible = _runState is { Name: "tbb_thead" } ? 1 : 0;
|
||||
TlsSetValue(backend._tbbAbortEligibleTlsIndex, eligible);
|
||||
}
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
@@ -580,6 +797,14 @@ public sealed partial class DirectExecutionBackend
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
if (_backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._workerDoneEventTlsIndex, 0);
|
||||
}
|
||||
if (_backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._tbbAbortEligibleTlsIndex, 0);
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
|
||||
@@ -214,6 +214,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private nint _guestReturnStub;
|
||||
|
||||
private nint _workerAbortStub;
|
||||
private nint _vehManagedEntryLock;
|
||||
|
||||
private uint _workerDoneEventTlsIndex = uint.MaxValue;
|
||||
|
||||
private uint _tbbAbortEligibleTlsIndex = uint.MaxValue;
|
||||
|
||||
private nint _setEventAddress;
|
||||
|
||||
private nint _rawExceptionHandler;
|
||||
|
||||
private nint _rawExceptionHandlerStub;
|
||||
@@ -1041,6 +1050,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
||||
_guestTlsBaseTlsIndex = TlsAlloc();
|
||||
_hostRspSlotTlsIndex = TlsAlloc();
|
||||
_workerDoneEventTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||
_tbbAbortEligibleTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||
if (_guestTlsBaseTlsIndex == uint.MaxValue || _hostRspSlotTlsIndex == uint.MaxValue)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate native TLS slots");
|
||||
@@ -1064,6 +1075,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
throw new InvalidOperationException("Failed to resolve kernel32 thread timing functions");
|
||||
}
|
||||
_setEventAddress = kernel32 != 0 ? GetProcAddress(kernel32, "SetEvent") : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1087,13 +1099,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate host stack slot storage");
|
||||
}
|
||||
_vehManagedEntryLock = (nint)VirtualAlloc(null, 64u, 12288u, 4u);
|
||||
if (_vehManagedEntryLock == 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate VEH managed-entry lock");
|
||||
}
|
||||
// owner (nint) + depth (int); recursive — nested VEH on same thread must reenter.
|
||||
*(nint*)_vehManagedEntryLock = 0;
|
||||
*(int*)(_vehManagedEntryLock + sizeof(nint)) = 0;
|
||||
_unresolvedReturnStub = CreateUnresolvedReturnStub();
|
||||
_guestReturnStub = CreateGuestReturnStub();
|
||||
if (_guestReturnStub == 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate guest return stub");
|
||||
}
|
||||
_workerAbortStub = CreateWorkerAbortStub();
|
||||
if (_workerAbortStub == 0 && OperatingSystem.IsWindows())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Worker abort stub unavailable; TBB execute-fault recover will use host_exit");
|
||||
}
|
||||
SetupExceptionHandler();
|
||||
// Cover the Astro TBB spawn storm (often 8–12 concurrent tbb_thead).
|
||||
PrewarmNativeGuestWorkers(Math.Max(NativeWorkerMaxConcurrent, 4));
|
||||
}
|
||||
|
||||
public bool TryExecute(CpuContext context, ulong entryPoint, Generation generation, IReadOnlyDictionary<ulong, string> importStubs, IReadOnlyDictionary<string, ulong> runtimeSymbols, CpuExecutionOptions executionOptions, out OrbisGen2Result result)
|
||||
@@ -2409,9 +2437,147 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return (nint)ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After a TBB execute-fault, VEH redirects here on a host stack.
|
||||
/// SetEvent(done) then park forever — ExitThread from VEH CONTINUE_EXECUTION
|
||||
/// was taking down the whole process (recover logged, no respawning). The
|
||||
/// renter TerminateThread's the parked worker and respawns a clean loop.
|
||||
/// </summary>
|
||||
private unsafe nint CreateWorkerAbortStub()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() ||
|
||||
_workerDoneEventTlsIndex == uint.MaxValue ||
|
||||
_tlsGetValueAddress == 0 ||
|
||||
_setEventAddress == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0;
|
||||
nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0;
|
||||
nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0;
|
||||
|
||||
const uint stubSize = 256u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (ptr == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); // sub rsp, 0x28
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _workerDoneEventTlsIndex); // mov ecx, tls
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call TlsGetValue
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||
EmitByte(code, ref offset, 0x74); EmitByte(code, ref offset, 0x0F); // jz skip SetEvent (15 bytes)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC1); // mov rcx, rax
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _setEventAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call SetEvent
|
||||
|
||||
// Breadcrumb on host stack (survives silent teardown better than managed log).
|
||||
int msgAbsSlot = -1;
|
||||
if (getStdHandle != 0 && writeFile != 0)
|
||||
{
|
||||
ReadOnlySpan<byte> msg = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x20); // extra shadow for WriteFile args
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC1); // mov rcx, handle
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC3); // mov rbx, handle (nonvolatile for flush)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC2); // mov rdx, msg
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); // lea r9, [rsp+0x20]
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
if (flushFileBuffers != 0)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xD9); // mov rcx, rbx
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = flushFileBuffers;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
}
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x20);
|
||||
}
|
||||
|
||||
// Park: do not ExitThread (process-wide silent die after VEH redirect).
|
||||
int parkOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause
|
||||
EmitByte(code, ref offset, 0xEB);
|
||||
EmitByte(code, ref offset, unchecked((byte)(parkOffset - (offset + 1)))); // jmp park
|
||||
|
||||
if (msgAbsSlot >= 0)
|
||||
{
|
||||
ReadOnlySpan<byte> msgEmbed = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8;
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + offset;
|
||||
for (int i = 0; i < msgEmbed.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msgEmbed[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (offset > (int)stubSize)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Worker abort stub overflow: used={offset} cap={stubSize}");
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint oldProtect = default;
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
|
||||
private unsafe nint CreateExceptionHandlerTrampoline(nint managedHandler)
|
||||
{
|
||||
const uint stubSize = 256u;
|
||||
// Live VEH trampoline used by SetupExceptionHandler. Must pre-filter
|
||||
// FastFail / CLR / MSVC C++ / stack-overflow the same way as
|
||||
// WindowsFaultHandling.CreateHandlerThunk: entering managed VEH while
|
||||
// the thread is in cooperative GC mode fail-fasts with
|
||||
// "UnmanagedCallersOnly method from managed code" (tLT18–22).
|
||||
// Extra headroom for native tbb abort + recursive managed-entry spinlock.
|
||||
const uint stubSize = 2048u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
|
||||
if (ptr == null)
|
||||
{
|
||||
@@ -2420,10 +2586,305 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[
|
||||
0xE0434352u, // CLR managed exception
|
||||
0xE06D7363u, // MSVC C++ exception
|
||||
0xC0000409u, // STATUS_STACK_BUFFER_OVERRUN / FailFast
|
||||
0xC00000FDu, // STATUS_STACK_OVERFLOW
|
||||
];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] ExceptionCode
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
int fastFailJumpSlot = -1;
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D);
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74);
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
if (nonManagedExceptionCodes[i] == 0xC0000409u)
|
||||
{
|
||||
fastFailJumpSlot = i;
|
||||
}
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0xE9); // jmp mainBody (rel32; FastFail breadcrumb sits between)
|
||||
var mainBodyJumpSlot = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
int fastFailPassOffset = offset;
|
||||
var fastFailLogInstalled = false;
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0;
|
||||
nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0;
|
||||
if (fastFailJumpSlot >= 0 && getStdHandle != 0 && writeFile != 0)
|
||||
{
|
||||
// Prefix + Context.Rip hex (AMD64 CONTEXT.Rip @ 0xF8) + newline.
|
||||
// Keep in sync with WindowsFaultHandling.CreateHandlerThunk.
|
||||
ReadOnlySpan<byte> msg =
|
||||
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (live trampoline; skip managed VEH) rip=0x"u8;
|
||||
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
|
||||
// rcx=EXCEPTION_POINTERS*: capture Rip into r10 before clobbering.
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
|
||||
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8] ContextRecord*
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
|
||||
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8] Rip
|
||||
EmitByte(code, ref offset, 0x50);
|
||||
EmitByte(code, ref offset, 0x51);
|
||||
EmitByte(code, ref offset, 0x52);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10 (Rip)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40 (hex buf @ +0x30)
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax stderr handle
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
var msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); // lpOverlapped slot
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
// Hex-encode Rip. Stack after sub 0x40: [rsp+0x40]=saved Rip (push r10).
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
|
||||
var hexDigitsAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint); // mov r8, hexDigits
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30] hex out
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u); // ecx = 16 nibbles
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0); // mov rax, r10
|
||||
int hexLoopOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x04); // rol rax, 4
|
||||
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); // mov edx, eax
|
||||
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F); // and edx, 0xF
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
|
||||
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10); // movzx edx, byte [r8+rdx]
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13); // mov [r11], dl
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3); // inc r11
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9); // dec ecx
|
||||
EmitByte(code, ref offset, 0x75);
|
||||
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz hexLoop (rel8)
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
|
||||
EmitByte(code, ref offset, 0x0A); // mov byte [r11], '\n'
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov rcx, [rsp+0x28] stderr
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea rdx, [rsp+0x30]
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, 17u); // 16 hex + newline
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
// Flush redirected stderr so FastFail rip survives process teardown.
|
||||
nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0;
|
||||
if (flushFileBuffers != 0)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov rcx, stderr
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = flushFileBuffers;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A); // pop r10
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
var msgOffset = offset;
|
||||
for (int i = 0; i < msg.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msg[i]);
|
||||
}
|
||||
|
||||
var hexDigitsOffset = offset;
|
||||
for (int i = 0; i < hexDigits.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, hexDigits[i]);
|
||||
}
|
||||
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
|
||||
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
|
||||
code[passJumpOffsets[fastFailJumpSlot]] =
|
||||
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
|
||||
fastFailLogInstalled = true;
|
||||
}
|
||||
|
||||
int mainBodyOffset = offset;
|
||||
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
||||
|
||||
// Native worker EXECUTE-AV abort without managed VEH.
|
||||
// Do NOT catch read/write AVs — workers need managed lazy-commit (tLTJ
|
||||
// silent-die when every worker AV was aborted). Execute faults on
|
||||
// tbb_thead are the concurrent-managed FailFast case (tLTC).
|
||||
int tbbFallthroughJump = -1;
|
||||
if (_workerAbortStub != 0 &&
|
||||
_tlsGetValueAddress != 0 &&
|
||||
_hostRspSlotTlsIndex != uint.MaxValue)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x45); EmitByte(code, ref offset, 0x00); // mov rax, [r13]
|
||||
EmitByte(code, ref offset, 0x81); EmitByte(code, ref offset, 0x38);
|
||||
EmitUInt32(code, ref offset, 0xC0000005u); // cmp dword [rax], AV
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
tbbFallthroughJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u); // jne fallthrough
|
||||
|
||||
// ExceptionInformation[0] == 8 → EXECUTE (DEP). Offset 32 on x64 RECORD.
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, 32u);
|
||||
EmitByte(code, ref offset, 0x08); // cmp qword [rax+32], 8
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
var tbbNotExecuteJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int tbbNotEligibleJump = -1;
|
||||
if (_tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _tbbAbortEligibleTlsIndex);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
tbbNotEligibleJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
var tbbNoHostRspJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x00); // mov r8, [rax] hostRsp
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
var tbbZeroRspJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xE0); EmitByte(code, ref offset, 0xF0); // and r8, ~0xF
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x08); // mov r9, [r13+8]
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0x98u); // Context.Rsp
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _workerAbortStub;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0xF8u); // Context.Rip
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x78);
|
||||
EmitUInt32(code, ref offset, 0u); // Context.Rax = 0
|
||||
|
||||
EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, unchecked((uint)-1));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xE4); // mov rsp, r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
int tbbFallthroughOffset = offset;
|
||||
*(int*)(code + tbbFallthroughJump) = tbbFallthroughOffset - (tbbFallthroughJump + sizeof(int));
|
||||
*(int*)(code + tbbNotExecuteJump) = tbbFallthroughOffset - (tbbNotExecuteJump + sizeof(int));
|
||||
if (tbbNotEligibleJump >= 0)
|
||||
{
|
||||
*(int*)(code + tbbNotEligibleJump) = tbbFallthroughOffset - (tbbNotEligibleJump + sizeof(int));
|
||||
}
|
||||
*(int*)(code + tbbNoHostRspJump) = tbbFallthroughOffset - (tbbNoHostRspJump + sizeof(int));
|
||||
*(int*)(code + tbbZeroRspJump) = tbbFallthroughOffset - (tbbZeroRspJump + sizeof(int));
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||
EmitUInt32(code, ref offset, 8u);
|
||||
@@ -2440,11 +2901,67 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
// Serialize managed VEH entry (recursive spinlock). Concurrent UnmanagedCallersOnly
|
||||
// FailFast was the tLTQ silent mid-TBB pattern (enter without abort breadcrumb).
|
||||
// Lock layout: [0]=owner UniqueThread (nint), [8]=depth (int).
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14);
|
||||
EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48]
|
||||
int hostAcquireSpin = offset;
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int hostMineJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostPauseJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11); // lock cmpxchg [r9], r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostRetryJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
EmitUInt32(code, ref offset, 1u); // mov dword [r9+8], 1
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostGotJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int hostPauseOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostPauseBackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int hostMineOffset = offset;
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); // inc dword [r9+8]
|
||||
int hostGotOffset = offset;
|
||||
*(int*)(code + hostMineJump) = hostMineOffset - (hostMineJump + sizeof(int));
|
||||
*(int*)(code + hostPauseJump) = hostPauseOffset - (hostPauseJump + sizeof(int));
|
||||
*(int*)(code + hostRetryJump) = hostAcquireSpin - (hostRetryJump + sizeof(int));
|
||||
*(int*)(code + hostGotJump) = hostGotOffset - (hostGotJump + sizeof(int));
|
||||
*(int*)(code + hostPauseBackJump) = hostAcquireSpin - (hostPauseBackJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedHandler;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8]
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostStillJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u); // mov qword [r9], 0
|
||||
int hostStillOffset = offset;
|
||||
*(int*)(code + hostStillJump) = hostStillOffset - (hostStillJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostRestoreJump = offset;
|
||||
@@ -2470,11 +2987,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14);
|
||||
EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48]
|
||||
int guestAcquireSpin = offset;
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int guestMineJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestPauseJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestRetryJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
EmitUInt32(code, ref offset, 1u);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestGotJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int guestPauseOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestPauseBackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int guestMineOffset = offset;
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
int guestGotOffset = offset;
|
||||
*(int*)(code + guestMineJump) = guestMineOffset - (guestMineJump + sizeof(int));
|
||||
*(int*)(code + guestPauseJump) = guestPauseOffset - (guestPauseJump + sizeof(int));
|
||||
*(int*)(code + guestRetryJump) = guestAcquireSpin - (guestRetryJump + sizeof(int));
|
||||
*(int*)(code + guestGotJump) = guestGotOffset - (guestGotJump + sizeof(int));
|
||||
*(int*)(code + guestPauseBackJump) = guestAcquireSpin - (guestPauseBackJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedHandler;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8]
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestStillJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u);
|
||||
int guestStillOffset = offset;
|
||||
*(int*)(code + guestStillJump) = guestStillOffset - (guestStillJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestRestoreJump = offset;
|
||||
@@ -2495,6 +3065,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
if (offset > (int)stubSize)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Exception handler trampoline overflow: used={offset} cap={stubSize}");
|
||||
VirtualFree(ptr, 0, 0x8000u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] VEH trampoline built: bytes={offset} native_worker_abort=" +
|
||||
$"{(_workerAbortStub != 0 && _hostRspSlotTlsIndex != uint.MaxValue)}");
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
@@ -5149,16 +5731,27 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
// TBB execute-AV recover needs native-worker TLS (eligible/done).
|
||||
// Other guests stay on CallNativeEntry — full native-worker migration
|
||||
// increased splash hangs / UnmanagedCallersOnly (tLTN/tLTO).
|
||||
int nativeReturn;
|
||||
if (name == "tbb_thead")
|
||||
{
|
||||
nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest thread stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
nativeReturn = CallNativeEntry(ptr);
|
||||
}
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -5304,16 +5897,24 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
int nativeReturn;
|
||||
if (name == "tbb_thead")
|
||||
{
|
||||
nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest continuation stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
nativeReturn = CallNativeEntry(ptr);
|
||||
}
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -6446,6 +7047,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
VirtualFree((void*)_hostRspSlotStorage, 0u, 32768u);
|
||||
_hostRspSlotStorage = 0;
|
||||
}
|
||||
if (_vehManagedEntryLock != 0)
|
||||
{
|
||||
VirtualFree((void*)_vehManagedEntryLock, 0u, 32768u);
|
||||
_vehManagedEntryLock = 0;
|
||||
}
|
||||
if (_workerAbortStack != 0)
|
||||
{
|
||||
VirtualFree((void*)_workerAbortStack, 0u, 32768u);
|
||||
_workerAbortStack = 0;
|
||||
}
|
||||
if (_guestTlsBaseTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsFree(_guestTlsBaseTlsIndex);
|
||||
@@ -6456,6 +7067,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
TlsFree(_hostRspSlotTlsIndex);
|
||||
_hostRspSlotTlsIndex = uint.MaxValue;
|
||||
}
|
||||
if (_workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsFree(_workerDoneEventTlsIndex);
|
||||
_workerDoneEventTlsIndex = uint.MaxValue;
|
||||
}
|
||||
if (_unresolvedReturnStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_unresolvedReturnStub, 0u, 32768u);
|
||||
@@ -6466,6 +7082,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
VirtualFree((void*)_guestReturnStub, 0u, 32768u);
|
||||
_guestReturnStub = 0;
|
||||
}
|
||||
if (_workerAbortStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_workerAbortStub, 0u, 32768u);
|
||||
_workerAbortStub = 0;
|
||||
}
|
||||
if (_guestContextTransferStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_guestContextTransferStub, 0u, 32768u);
|
||||
|
||||
@@ -24,7 +24,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
|
||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||
{
|
||||
const uint stubSize = 256u;
|
||||
const uint stubSize = 1024u;
|
||||
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||
if (ptr == null)
|
||||
{
|
||||
@@ -43,11 +43,15 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
//
|
||||
// FastFail (0xC0000409) is logged from this native path only: managed VEH never
|
||||
// sees it (tLT18–21 silent exits after TBB AV recovery).
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
int fastFailJumpSlot = -1;
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
@@ -55,13 +59,162 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
if (nonManagedExceptionCodes[i] == WindowsFaultCodes.FastFail)
|
||||
{
|
||||
fastFailJumpSlot = i;
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
}
|
||||
EmitByte(code, ref offset, 0xE9); // jmp mainBody rel32 (FastFail breadcrumb sits between)
|
||||
var mainBodyJumpSlot = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
|
||||
// FastFail: native stderr breadcrumb with Context.Rip (no managed entry), then CONTINUE_SEARCH.
|
||||
// Keep in sync with DirectExecutionBackend.CreateExceptionHandlerTrampoline.
|
||||
int fastFailPassOffset = offset;
|
||||
var fastFailLogInstalled = false;
|
||||
if (fastFailJumpSlot >= 0 &&
|
||||
NativeLibrary.TryLoad("kernel32.dll", out var kernel32) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "GetStdHandle", out var getStdHandle) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "WriteFile", out var writeFile))
|
||||
{
|
||||
ReadOnlySpan<byte> msg =
|
||||
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8;
|
||||
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
|
||||
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
|
||||
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8]
|
||||
EmitByte(code, ref offset, 0x50); // push rax
|
||||
EmitByte(code, ref offset, 0x51); // push rcx
|
||||
EmitByte(code, ref offset, 0x52); // push rdx
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); // push r8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); // push r9
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
var msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
|
||||
var hexDigitsAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30]
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0);
|
||||
int hexLoopOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x04);
|
||||
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
|
||||
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9);
|
||||
EmitByte(code, ref offset, 0x75);
|
||||
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz rel8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
|
||||
EmitByte(code, ref offset, 0x0A);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, 17u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
var msgOffset = offset;
|
||||
for (int i = 0; i < msg.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msg[i]);
|
||||
}
|
||||
|
||||
var hexDigitsOffset = offset;
|
||||
for (int i = 0; i < hexDigits.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, hexDigits[i]);
|
||||
}
|
||||
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
|
||||
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
|
||||
code[passJumpOffsets[fastFailJumpSlot]] =
|
||||
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
|
||||
fastFailLogInstalled = true;
|
||||
}
|
||||
|
||||
int mainBodyOffset = offset;
|
||||
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
|
||||
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
|
||||
@@ -238,7 +238,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
|
||||
// Reserve address space only for very large non-executable regions; commit is done lazily later.
|
||||
var reservedOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
var result = reservedOnly
|
||||
? _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite)
|
||||
: _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
if (result == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -252,6 +260,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
var state = reservedOnly ? ReserveRegion(actualAddress, alignedSize) : "n/a";
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
@@ -260,7 +270,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
IsExecutable = executable,
|
||||
IsReservedOnly = false,
|
||||
IsReservedOnly = reservedOnly,
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
@@ -269,6 +279,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
|
||||
var allocationKind = executable ? "executable memory" : "data memory";
|
||||
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||
return true;
|
||||
@@ -361,44 +372,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var actualAddress = result;
|
||||
|
||||
var lazyPrimeState = "n/a";
|
||||
if (reservedOnly)
|
||||
{
|
||||
var primeBytes = Math.Min(alignedSize, LazyReservePrimeBytes);
|
||||
if (primeBytes != 0)
|
||||
{
|
||||
ulong committedBytes = 0;
|
||||
while (committedBytes < primeBytes)
|
||||
{
|
||||
var remaining = primeBytes - committedBytes;
|
||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||
var commitAddress = actualAddress + committedBytes;
|
||||
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
committedBytes += chunkBytes;
|
||||
}
|
||||
|
||||
if (committedBytes != 0)
|
||||
{
|
||||
lazyPrimeState = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes:X}"
|
||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
}
|
||||
else
|
||||
{
|
||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lazyPrimeState = "skip:0";
|
||||
}
|
||||
}
|
||||
var lazyPrimeState = reservedOnly ? ReserveRegion(actualAddress, alignedSize) : "n/a";
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
@@ -425,6 +399,41 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return actualAddress;
|
||||
}
|
||||
|
||||
private string ReserveRegion(ulong actualAddress, ulong alignedSize)
|
||||
{
|
||||
var primeBytes = Math.Min(alignedSize, LazyReservePrimeBytes);
|
||||
if (primeBytes == 0)
|
||||
{
|
||||
return "skip:0";
|
||||
}
|
||||
|
||||
ulong committedBytes = 0;
|
||||
while (committedBytes < primeBytes)
|
||||
{
|
||||
var remaining = primeBytes - committedBytes;
|
||||
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
|
||||
var commitAddress = actualAddress + committedBytes;
|
||||
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
committedBytes += chunkBytes;
|
||||
}
|
||||
|
||||
if (committedBytes != 0)
|
||||
{
|
||||
var state = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes:X}"
|
||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||
TraceVmem($"region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
return state;
|
||||
}
|
||||
|
||||
TraceVmem($"Failed to reserve region at 0x{actualAddress:X16} ({primeBytes} bytes)!");
|
||||
return $"fail:{primeBytes:X}";
|
||||
}
|
||||
|
||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
||||
{
|
||||
if (size == 0)
|
||||
|
||||
@@ -221,6 +221,29 @@ public static class GuestThreadExecution
|
||||
|
||||
public static IGuestThreadScheduler? Scheduler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a guest thread is torn down without a clean pthread_exit
|
||||
/// (e.g. TBB execute-AV → worker_abort). Libs use this to abandon mutexes.
|
||||
/// </summary>
|
||||
public static event Func<ulong, string, int>? GuestThreadAbandoned;
|
||||
|
||||
public static int NotifyGuestThreadAbandoned(ulong threadHandle, string reason)
|
||||
{
|
||||
if (threadHandle == 0 || GuestThreadAbandoned is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GuestThreadAbandoned.Invoke(threadHandle, reason);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
|
||||
|
||||
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
||||
|
||||
@@ -1,8 +1,60 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>Which in-block address equation a <see cref="DetileParams"/> carries.</summary>
|
||||
internal enum DetileEquation
|
||||
{
|
||||
/// <summary>Unsupported mode/format; caller must use the CPU path or raw upload.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Exact AddrLib XOR equation (RDNA2 modes 5/9/24/27): factored X/Y terms.</summary>
|
||||
ExactXor,
|
||||
|
||||
/// <summary>Other modes: a precomputed in-block Morton/standard element-offset table.</summary>
|
||||
BlockTable,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backend-agnostic description of how to deswizzle one surface, produced by
|
||||
/// <see cref="GnmTiling.GetDetileParams"/>. Holds only plain integers and small
|
||||
/// int[] tables — no host graphics-API types — so it can cross the guest-GPU
|
||||
/// backend seam and drive a Vulkan (SPIR-V) or Metal (MSL) detile compute kernel
|
||||
/// identically to the CPU <see cref="GnmTiling.TryDetile"/> fallback. The single
|
||||
/// shared addressing formula both consume is:
|
||||
/// <code>
|
||||
/// inBlockByte = Equation == ExactXor
|
||||
/// ? XByteTerm[x & XMask] ^ YByteTerm[y & YMask]
|
||||
/// : BlockTable[(y % BlockHeight) * BlockWidth + (x % BlockWidth)] * BytesPerElement;
|
||||
/// srcByte = ((y / BlockHeight) * BlocksPerRow + (x / BlockWidth)) * BlockBytes + inBlockByte;
|
||||
/// </code>
|
||||
/// </summary>
|
||||
internal readonly record struct DetileParams(
|
||||
DetileEquation Equation,
|
||||
int ElementsWide,
|
||||
int ElementsHigh,
|
||||
int BytesPerElement,
|
||||
int BlockWidth,
|
||||
int BlockHeight,
|
||||
int BlockElements,
|
||||
int BlockBytes,
|
||||
int BlocksPerRow,
|
||||
// ExactXor: within-block BYTE offset = XByteTerm[x & XMask] ^ YByteTerm[y & YMask].
|
||||
int[] XByteTerm,
|
||||
int XMask,
|
||||
int[] YByteTerm,
|
||||
int YMask,
|
||||
// BlockTable: within-block ELEMENT offset = BlockTable[inBlockY * BlockWidth + inBlockX].
|
||||
int[] BlockTable)
|
||||
{
|
||||
/// <summary>False when the mode/format is not GPU-portable (Equation == None).</summary>
|
||||
public bool IsSupported => Equation != DetileEquation.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
|
||||
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
|
||||
@@ -16,8 +68,11 @@ namespace SharpEmu.Libs.Agc;
|
||||
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||
/// equations are being ported.
|
||||
/// </summary>
|
||||
internal static class GnmTiling
|
||||
internal static unsafe class GnmTiling
|
||||
{
|
||||
private const int ParallelDetileElementThreshold = 512 * 512;
|
||||
private const int MaxDetileWorkers = 4;
|
||||
|
||||
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
|
||||
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
|
||||
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||
@@ -118,6 +173,14 @@ internal static class GnmTiling
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly HashSet<uint> _reportedModes = new();
|
||||
private static readonly ConcurrentDictionary<(uint SwizzleMode, int BppLog2), PatternTerms>
|
||||
_patternTermCache = new();
|
||||
private static readonly ConcurrentDictionary<(SwizzleKind Kind, int Width, int Height), int[]>
|
||||
_blockTableCache = new();
|
||||
private static readonly ParallelOptions _parallelDetileOptions = new()
|
||||
{
|
||||
MaxDegreeOfParallelism = Math.Min(MaxDetileWorkers, Environment.ProcessorCount),
|
||||
};
|
||||
|
||||
public static bool Enabled => _enabled || !_disabled;
|
||||
|
||||
@@ -404,64 +467,218 @@ internal static class GnmTiling
|
||||
return false;
|
||||
}
|
||||
|
||||
// Precompute the within-block element offset for each (x, y) inside a
|
||||
// single block. The swizzle equation only depends on the in-block
|
||||
// coordinates, so this table is reused for every block — turning the
|
||||
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
|
||||
// Detiling a 2048x2048 texture is millions of elements; without this the
|
||||
// per-pixel math makes DETILE unusably slow during asset streaming.
|
||||
// Address tables depend only on the swizzle equation and element size,
|
||||
// so retain them across textures instead of rebuilding them per upload.
|
||||
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
||||
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
||||
{
|
||||
for (var bx = 0; bx < blockWidth; bx++)
|
||||
{
|
||||
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
||||
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
||||
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
||||
}
|
||||
}
|
||||
var patternTerms = hasExactXorPattern
|
||||
? _patternTermCache.GetOrAdd(
|
||||
(swizzleMode, bppLog2),
|
||||
_ => CreatePatternTerms(xorPattern))
|
||||
: default;
|
||||
var blockTable = hasExactXorPattern
|
||||
? []
|
||||
: _blockTableCache.GetOrAdd(
|
||||
(kind, blockWidth, blockHeight),
|
||||
static key => CreateBlockTable(key.Kind, key.Width, key.Height));
|
||||
|
||||
// The XOR equation offset factors cleanly into independent X and Y
|
||||
// fields — each output bit is parity(x & XMask) XOR parity(y & YMask),
|
||||
// and parity distributes over XOR, so offset(x, y) == xTerm(x) ^ yTerm(y).
|
||||
// Precompute the per-column X term once (reused across every row) so the
|
||||
// inner loop drops from a 16-bit interleave with 32 PopCounts per element
|
||||
// to a single array load and one XOR.
|
||||
var xTermByColumn = hasExactXorPattern ? new int[elementsWide] : [];
|
||||
for (var x = 0; hasExactXorPattern && x < elementsWide; x++)
|
||||
// Exact equations repeat at a small power-of-two period. Cached axis
|
||||
// terms reduce the inner loop to two array loads and one XOR.
|
||||
fixed (byte* tiledPointer = tiled)
|
||||
fixed (byte* linearPointer = linear)
|
||||
{
|
||||
xTermByColumn[x] = (int)PatternAxisTerm((uint)x, xorPattern, useX: true);
|
||||
}
|
||||
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
var sourceAddress = (nint)tiledPointer;
|
||||
var destinationAddress = (nint)linearPointer;
|
||||
var sourceLength = tiled.Length;
|
||||
var destinationLength = linear.Length;
|
||||
var blockWidthShift = BitLog2((uint)blockWidth);
|
||||
var blockWidthMask = blockWidth - 1;
|
||||
var detileRow = (int y) =>
|
||||
{
|
||||
var blockY = y / blockHeight;
|
||||
var inBlockY = y % blockHeight;
|
||||
var inBlockY = y & (blockHeight - 1);
|
||||
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||
var tableRowBase = inBlockY * blockWidth;
|
||||
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||
// Y term is constant across the row; hoist it out of the inner loop.
|
||||
var yTerm = hasExactXorPattern ? (int)PatternAxisTerm((uint)y, xorPattern, useX: false) : 0;
|
||||
var yTerm = hasExactXorPattern
|
||||
? patternTerms.Y[y & patternTerms.YMask]
|
||||
: 0;
|
||||
for (var x = 0; x < elementsWide; x++)
|
||||
{
|
||||
var blockX = x / blockWidth;
|
||||
var inBlockX = x % blockWidth;
|
||||
|
||||
var blockX = x >> blockWidthShift;
|
||||
var inBlockX = x & blockWidthMask;
|
||||
var blockIndex = rowBlockBase + blockX;
|
||||
var sourceByte = hasExactXorPattern
|
||||
? blockIndex * blockBytes + (xTermByColumn[x] ^ yTerm)
|
||||
? blockIndex * blockBytes + (patternTerms.X[x & patternTerms.XMask] ^ yTerm)
|
||||
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||
(long)bytesPerElement;
|
||||
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||
if (sourceByte + bytesPerElement > tiled.Length ||
|
||||
destByte + bytesPerElement > linear.Length)
|
||||
if (sourceByte < 0 ||
|
||||
sourceByte + bytesPerElement > sourceLength ||
|
||||
destByte + bytesPerElement > destinationLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tiled.Slice((int)sourceByte, bytesPerElement)
|
||||
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
||||
CopyElement(
|
||||
(byte*)sourceAddress + sourceByte,
|
||||
(byte*)destinationAddress + destByte,
|
||||
bytesPerElement);
|
||||
}
|
||||
};
|
||||
|
||||
var elementCount = (long)elementsWide * elementsHigh;
|
||||
if (elementCount >= ParallelDetileElementThreshold && Environment.ProcessorCount > 1)
|
||||
{
|
||||
Parallel.For(
|
||||
0,
|
||||
elementsHigh,
|
||||
_parallelDetileOptions,
|
||||
detileRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var y = 0; y < elementsHigh; y++)
|
||||
{
|
||||
detileRow(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the detile parameters for a surface without performing the copy,
|
||||
/// so a GPU compute kernel can run the deswizzle instead of the CPU. Returns
|
||||
/// <see cref="DetileParams.IsSupported"/> == false (Equation == None) when the
|
||||
/// mode/format is not GPU-portable, so the caller keeps the CPU
|
||||
/// <see cref="TryDetile"/> path or a raw upload. Reuses the same helpers and
|
||||
/// caches as <see cref="TryDetile"/>, so the two never disagree on addressing.
|
||||
/// </summary>
|
||||
public static DetileParams GetDetileParams(
|
||||
uint swizzleMode,
|
||||
int bytesPerElement,
|
||||
int elementsWide,
|
||||
int elementsHigh)
|
||||
{
|
||||
if (!ShouldDetile(swizzleMode) ||
|
||||
bytesPerElement <= 0 ||
|
||||
elementsWide <= 0 ||
|
||||
elementsHigh <= 0 ||
|
||||
!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var bppLog2 = BitLog2((uint)bytesPerElement);
|
||||
if (bppLog2 < 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var blockElements = blockBytes >> bppLog2;
|
||||
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
|
||||
if (blockWidth == 0 || blockHeight == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||
|
||||
if (TryGetExactXorPattern(swizzleMode, bppLog2, out var pattern))
|
||||
{
|
||||
var terms = _patternTermCache.GetOrAdd(
|
||||
(swizzleMode, bppLog2),
|
||||
_ => CreatePatternTerms(pattern));
|
||||
return new DetileParams(
|
||||
DetileEquation.ExactXor,
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
bytesPerElement,
|
||||
blockWidth,
|
||||
blockHeight,
|
||||
blockElements,
|
||||
blockBytes,
|
||||
blocksPerRow,
|
||||
terms.X,
|
||||
terms.XMask,
|
||||
terms.Y,
|
||||
terms.YMask,
|
||||
[]);
|
||||
}
|
||||
|
||||
var blockTable = _blockTableCache.GetOrAdd(
|
||||
(kind, blockWidth, blockHeight),
|
||||
static key => CreateBlockTable(key.Kind, key.Width, key.Height));
|
||||
return new DetileParams(
|
||||
DetileEquation.BlockTable,
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
bytesPerElement,
|
||||
blockWidth,
|
||||
blockHeight,
|
||||
blockElements,
|
||||
blockBytes,
|
||||
blocksPerRow,
|
||||
[],
|
||||
0,
|
||||
[],
|
||||
0,
|
||||
blockTable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CPU deswizzle driven entirely by a resolved <see cref="DetileParams"/> — the
|
||||
/// exact addressing the Vulkan/Metal compute kernel runs per texel, so a
|
||||
/// backend that packaged <paramref name="parameters"/> for the GPU path can
|
||||
/// fall back to this without re-deriving the swizzle. Copies
|
||||
/// <c>ElementsWide * ElementsHigh</c> elements from <paramref name="tiled"/>
|
||||
/// into <paramref name="linear"/>; returns false when unsupported or the output
|
||||
/// span is too small. Out-of-range source elements are left zero (matching the
|
||||
/// reference), so a truncated <paramref name="tiled"/> degrades gracefully.
|
||||
/// </summary>
|
||||
public static bool DetileWithParams(
|
||||
in DetileParams parameters,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
Span<byte> linear)
|
||||
{
|
||||
if (!parameters.IsSupported)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var width = parameters.ElementsWide;
|
||||
var height = parameters.ElementsHigh;
|
||||
var bpp = parameters.BytesPerElement;
|
||||
var requiredLinear = (long)width * height * bpp;
|
||||
if (width <= 0 || height <= 0 || bpp <= 0 || linear.Length < requiredLinear)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isExactXor = parameters.Equation == DetileEquation.ExactXor;
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var blockY = y / parameters.BlockHeight;
|
||||
var inY = y % parameters.BlockHeight;
|
||||
var yTerm = isExactXor ? parameters.YByteTerm[y & parameters.YMask] : 0;
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var blockX = x / parameters.BlockWidth;
|
||||
var inBlockByte = isExactXor
|
||||
? parameters.XByteTerm[x & parameters.XMask] ^ yTerm
|
||||
: parameters.BlockTable[inY * parameters.BlockWidth + (x % parameters.BlockWidth)] * bpp;
|
||||
var srcByte = ((long)blockY * parameters.BlocksPerRow + blockX) * parameters.BlockBytes + inBlockByte;
|
||||
var dstByte = ((long)y * width + x) * bpp;
|
||||
if (srcByte < 0 || srcByte + bpp > tiled.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tiled.Slice((int)srcByte, bpp).CopyTo(linear.Slice((int)dstByte, bpp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +691,80 @@ internal static class GnmTiling
|
||||
ZOrder,
|
||||
}
|
||||
|
||||
private readonly record struct PatternTerms(int[] X, int XMask, int[] Y, int YMask);
|
||||
|
||||
private static PatternTerms CreatePatternTerms(AddressBit[] pattern)
|
||||
{
|
||||
uint xMask = 0;
|
||||
uint yMask = 0;
|
||||
foreach (var bit in pattern)
|
||||
{
|
||||
xMask |= bit.XMask;
|
||||
yMask |= bit.YMask;
|
||||
}
|
||||
|
||||
var xLength = AxisTermPeriod(xMask);
|
||||
var yLength = AxisTermPeriod(yMask);
|
||||
var xTerms = new int[xLength];
|
||||
var yTerms = new int[yLength];
|
||||
for (var x = 0; x < xTerms.Length; x++)
|
||||
{
|
||||
xTerms[x] = (int)PatternAxisTerm((uint)x, pattern, useX: true);
|
||||
}
|
||||
|
||||
for (var y = 0; y < yTerms.Length; y++)
|
||||
{
|
||||
yTerms[y] = (int)PatternAxisTerm((uint)y, pattern, useX: false);
|
||||
}
|
||||
|
||||
return new PatternTerms(xTerms, xLength - 1, yTerms, yLength - 1);
|
||||
}
|
||||
|
||||
private static int AxisTermPeriod(uint mask) =>
|
||||
mask == 0 ? 1 : 1 << (32 - System.Numerics.BitOperations.LeadingZeroCount(mask));
|
||||
|
||||
private static int[] CreateBlockTable(SwizzleKind kind, int blockWidth, int blockHeight)
|
||||
{
|
||||
var table = new int[blockWidth * blockHeight];
|
||||
for (var y = 0; y < blockHeight; y++)
|
||||
{
|
||||
for (var x = 0; x < blockWidth; x++)
|
||||
{
|
||||
table[y * blockWidth + x] = (int)(kind == SwizzleKind.ZOrder
|
||||
? MortonInterleave((uint)x, (uint)y, blockWidth, blockHeight)
|
||||
: StandardSwizzleOffset((uint)x, (uint)y, blockWidth, blockHeight));
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyElement(byte* source, byte* destination, int bytesPerElement)
|
||||
{
|
||||
switch (bytesPerElement)
|
||||
{
|
||||
case 1:
|
||||
*destination = *source;
|
||||
break;
|
||||
case 2:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<ushort>(source));
|
||||
break;
|
||||
case 4:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<uint>(source));
|
||||
break;
|
||||
case 8:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<ulong>(source));
|
||||
break;
|
||||
case 16:
|
||||
Unsafe.WriteUnaligned(destination, Unsafe.ReadUnaligned<UInt128>(source));
|
||||
break;
|
||||
default:
|
||||
Unsafe.CopyBlockUnaligned(destination, source, (uint)bytesPerElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly AddressBit Zero = new(0, 0);
|
||||
|
||||
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||
|
||||
@@ -343,6 +343,45 @@ internal static class GpuWaitRegistry
|
||||
return expired;
|
||||
}
|
||||
|
||||
public static List<WaitingDcb>? CollectAllForMemory(object memory)
|
||||
{
|
||||
List<WaitingDcb>? collected = null;
|
||||
lock (_gate)
|
||||
{
|
||||
List<ulong>? emptied = null;
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
for (var index = list.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (!ReferenceEquals(list[index].Memory, memory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collected ??= new List<WaitingDcb>();
|
||||
collected.Add(list[index]);
|
||||
list.RemoveAt(index);
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
emptied ??= new List<ulong>();
|
||||
emptied.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (emptied is not null)
|
||||
{
|
||||
foreach (var address in emptied)
|
||||
{
|
||||
_waiters.Remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
/// <summary>Records the value a label producer wrote, for the deadlock
|
||||
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
||||
public static bool RecordProduced(object memory, ulong address, ulong value)
|
||||
|
||||
@@ -21,6 +21,7 @@ public static class AjmExports
|
||||
private const int MaxInstanceIndex = 0x2FFF;
|
||||
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
|
||||
private static int _nextContextId;
|
||||
private static int _nextBatchId;
|
||||
|
||||
private sealed class AjmContextState
|
||||
{
|
||||
@@ -227,10 +228,250 @@ public static class AjmExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a decode job on a batch. Titles call this on the Bink/AJM hot
|
||||
/// path; leaving it unresolved floods Import WARN spam. This is a silence
|
||||
/// stub, not a codec: advance the batch cursor and report the input as
|
||||
/// consumed with silence produced so the title does not spin on the same
|
||||
/// packet.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "39WxhR-ePew",
|
||||
ExportName = "sceAjmBatchJobDecode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchJobDecode(CpuContext ctx)
|
||||
{
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
var instanceId = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var inputAddress = ctx[CpuRegister.Rdx];
|
||||
var inputSize = ctx[CpuRegister.Rcx];
|
||||
var outputAddress = ctx[CpuRegister.R8];
|
||||
var outputSize = ctx[CpuRegister.R9];
|
||||
var resultAddress = ReadStackArg64(ctx, 0);
|
||||
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
// Best-effort: bump the batch cursor when the guest filled AjmBatchInfo.
|
||||
// Still succeed without it — the unresolved stub returned 0 and titles
|
||||
// keep calling; failing here would reintroduce hot-path spam via retries.
|
||||
_ = TryAppendBatchJob(ctx, infoAddress, AjmJobRunSize);
|
||||
|
||||
// Silence: clear PCM out and claim full input consumed so the guest
|
||||
// advances its bitstream cursor instead of re-submitting forever.
|
||||
if (outputAddress != 0 && outputSize != 0 && outputSize <= MaxSilentPcmBytes)
|
||||
{
|
||||
ClearGuestMemory(ctx, outputAddress, outputSize);
|
||||
}
|
||||
|
||||
WriteDecodeStreamResult(
|
||||
ctx,
|
||||
resultAddress,
|
||||
inputConsumed: inputSize > int.MaxValue ? int.MaxValue : (int)inputSize,
|
||||
outputWritten: 0,
|
||||
totalDecodedSamples: 0,
|
||||
frames: inputSize != 0 || outputSize != 0 ? 1u : 0u);
|
||||
|
||||
Trace(
|
||||
$"batch_job_decode info=0x{infoAddress:X16} instance=0x{instanceId:X8} " +
|
||||
$"in=0x{inputAddress:X16}+0x{inputSize:X} out=0x{outputAddress:X16}+0x{outputSize:X} " +
|
||||
$"result=0x{resultAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a built batch. Instant-complete silence stub: publish a batch id
|
||||
/// and clear any error out. Decode sidebands were already filled at
|
||||
/// job-enqueue time.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "5tOfnaClcqM",
|
||||
ExportName = "sceAjmBatchStart",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchStart(CpuContext ctx)
|
||||
{
|
||||
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
var priority = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var errorAddress = ctx[CpuRegister.Rcx];
|
||||
var batchOutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
if (infoAddress == 0 || batchOutAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
ClearAjmBatchError(ctx, errorAddress);
|
||||
|
||||
var batchId = unchecked((uint)Interlocked.Increment(ref _nextBatchId));
|
||||
Span<byte> batchValue = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(batchValue, batchId);
|
||||
if (!ctx.Memory.TryWrite(batchOutAddress, batchValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"batch_start context={contextId} info=0x{infoAddress:X16} " +
|
||||
$"priority={priority} batch={batchId} error=0x{errorAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-qLsfDAywIY",
|
||||
ExportName = "sceAjmBatchWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchWait(CpuContext ctx)
|
||||
{
|
||||
// Batches complete synchronously in Start; Wait is a no-op success.
|
||||
var errorAddress = ctx[CpuRegister.Rcx];
|
||||
ClearAjmBatchError(ctx, errorAddress);
|
||||
Trace(
|
||||
$"batch_wait context={unchecked((uint)ctx[CpuRegister.Rdi])} " +
|
||||
$"batch={unchecked((uint)ctx[CpuRegister.Rsi])} " +
|
||||
$"timeout={unchecked((uint)ctx[CpuRegister.Rdx])}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NVDXiUesSbA",
|
||||
ExportName = "sceAjmBatchCancel",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchCancel(CpuContext ctx)
|
||||
{
|
||||
Trace(
|
||||
$"batch_cancel context={unchecked((uint)ctx[CpuRegister.Rdi])} " +
|
||||
$"batch={unchecked((uint)ctx[CpuRegister.Rsi])}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Interlocked.Exchange(ref _nextContextId, 0);
|
||||
Interlocked.Exchange(ref _nextBatchId, 0);
|
||||
}
|
||||
|
||||
// AjmBatchInfo: buffer, offset, size, last_good_job, last_good_job_ra (5× u64).
|
||||
private const ulong AjmBatchInfoOffsetField = 8;
|
||||
private const ulong AjmBatchInfoSizeField = 16;
|
||||
private const ulong AjmBatchInfoLastGoodJobField = 24;
|
||||
private const ulong AjmJobRunSize = 64;
|
||||
private const ulong MaxSilentPcmBytes = 1 << 20;
|
||||
// AjmSidebandResult (8) + AjmSidebandStream (16) + AjmSidebandMFrame (8).
|
||||
private const int DecodeSidebandBytes = 32;
|
||||
|
||||
private static bool TryAppendBatchJob(CpuContext ctx, ulong infoAddress, ulong jobSize)
|
||||
{
|
||||
if (!TryReadUInt64(ctx, infoAddress, out var buffer) ||
|
||||
!TryReadUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, out var offset) ||
|
||||
!TryReadUInt64(ctx, infoAddress + AjmBatchInfoSizeField, out var size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer == 0 || jobSize == 0 || offset > size || size - offset < jobSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var jobAddress = buffer + offset;
|
||||
ClearGuestMemory(ctx, jobAddress, jobSize);
|
||||
return TryWriteUInt64(ctx, infoAddress + AjmBatchInfoLastGoodJobField, jobAddress) &&
|
||||
TryWriteUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, offset + jobSize);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (errorAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> error = stackalloc byte[AjmBatchErrorBytes];
|
||||
error.Clear();
|
||||
_ = ctx.Memory.TryWrite(errorAddress, error);
|
||||
}
|
||||
|
||||
private static void WriteDecodeStreamResult(
|
||||
CpuContext ctx,
|
||||
ulong resultAddress,
|
||||
int inputConsumed,
|
||||
int outputWritten,
|
||||
ulong totalDecodedSamples,
|
||||
uint frames)
|
||||
{
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> sideband = stackalloc byte[DecodeSidebandBytes];
|
||||
sideband.Clear();
|
||||
// AjmSidebandResult.result / internal_result = 0 (OK)
|
||||
BinaryPrimitives.WriteInt32LittleEndian(sideband.Slice(8, 4), inputConsumed);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(sideband.Slice(12, 4), outputWritten);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(sideband.Slice(16, 8), totalDecodedSamples);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(sideband.Slice(24, 4), frames);
|
||||
_ = ctx.Memory.TryWrite(resultAddress, sideband);
|
||||
}
|
||||
|
||||
private static void ClearGuestMemory(CpuContext ctx, ulong address, ulong byteCount)
|
||||
{
|
||||
if (address == 0 || byteCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var remaining = byteCount;
|
||||
var cursor = address;
|
||||
Span<byte> zero = stackalloc byte[256];
|
||||
while (remaining > 0)
|
||||
{
|
||||
var chunk = (int)Math.Min(remaining, (ulong)zero.Length);
|
||||
if (!ctx.Memory.TryWrite(cursor, zero[..chunk]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cursor += (ulong)chunk;
|
||||
remaining -= (ulong)chunk;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ReadStackArg64(CpuContext ctx, int index)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rsp] + sizeof(ulong) + ((ulong)index * sizeof(ulong));
|
||||
return TryReadUInt64(ctx, address, out var value) ? value : 0;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -11,8 +12,17 @@ namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOutExports
|
||||
{
|
||||
private const int AudioOutOutputParamSize = 16;
|
||||
private const int AudioOutMaximumOutputCount = 25;
|
||||
|
||||
internal const int AudioOutErrorInvalidPort = unchecked((int)0x80260003);
|
||||
internal const int AudioOutErrorInvalidPointer = unchecked((int)0x80260004);
|
||||
internal const int AudioOutErrorPortFull = unchecked((int)0x80260005);
|
||||
internal const int AudioOutErrorInvalidSize = unchecked((int)0x80260006);
|
||||
|
||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||
private static int _nextPortHandle;
|
||||
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
|
||||
|
||||
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
|
||||
// guest submits real samples or silence. Gated so it costs nothing when off.
|
||||
@@ -56,6 +66,7 @@ public static class AudioOutExports
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public IHostAudioStream? Backend { get; }
|
||||
public object SubmissionGate { get; } = new();
|
||||
public volatile float Volume = 1.0f;
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
@@ -83,7 +94,24 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Backend?.Dispose();
|
||||
public void Dispose()
|
||||
{
|
||||
lock (SubmissionGate)
|
||||
{
|
||||
Backend?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct OutputDescriptor(int Handle, ulong SourceAddress);
|
||||
|
||||
private struct ResolvedOutput
|
||||
{
|
||||
public int Handle;
|
||||
public ulong SourceAddress;
|
||||
public PortState Port;
|
||||
public byte[]? HostBuffer;
|
||||
public int HostBufferLength;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -114,11 +142,20 @@ public static class AudioOutExports
|
||||
IHostAudioStream? backend = null;
|
||||
string backendName;
|
||||
try
|
||||
{
|
||||
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
|
||||
if (streamFactory is not null)
|
||||
{
|
||||
backend = streamFactory(frequency);
|
||||
backendName = "test";
|
||||
}
|
||||
else
|
||||
{
|
||||
var audio = HostPlatform.Current.Audio;
|
||||
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||
backendName = audio.BackendName;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
backendName = "silent";
|
||||
@@ -192,6 +229,46 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w3PdaSTSwGE",
|
||||
ExportName = "sceAudioOutOutputs",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutOutputs(CpuContext ctx)
|
||||
{
|
||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||
var outputCount = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (outputCount == 0 || outputCount > AudioOutMaximumOutputCount)
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorPortFull);
|
||||
}
|
||||
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||
}
|
||||
|
||||
var count = checked((int)outputCount);
|
||||
Span<byte> parameterBytes =
|
||||
stackalloc byte[AudioOutMaximumOutputCount * AudioOutOutputParamSize];
|
||||
parameterBytes = parameterBytes[..checked(count * AudioOutOutputParamSize)];
|
||||
if (!ctx.Memory.TryRead(parameterAddress, parameterBytes))
|
||||
{
|
||||
return ctx.SetReturn(AudioOutErrorInvalidPointer);
|
||||
}
|
||||
|
||||
Span<OutputDescriptor> descriptors = stackalloc OutputDescriptor[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var entry = parameterBytes.Slice(i * AudioOutOutputParamSize, AudioOutOutputParamSize);
|
||||
descriptors[i] = new OutputDescriptor(
|
||||
BinaryPrimitives.ReadInt32LittleEndian(entry),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(entry[8..]));
|
||||
}
|
||||
|
||||
return ctx.SetReturn(SubmitOutputs(ctx, descriptors));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QOQtbeDqsT4",
|
||||
ExportName = "sceAudioOutOutput",
|
||||
@@ -225,16 +302,7 @@ public static class AudioOutExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (_traceOutput)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _outputCount);
|
||||
if (n <= 8 || n % 200 == 0)
|
||||
{
|
||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
||||
}
|
||||
}
|
||||
TraceOutput(handle, port, source);
|
||||
|
||||
if (port.Backend is null)
|
||||
{
|
||||
@@ -272,6 +340,184 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SubmitOutputs(CpuContext ctx, ReadOnlySpan<OutputDescriptor> descriptors)
|
||||
{
|
||||
var resolvedArray = ArrayPool<ResolvedOutput>.Shared.Rent(descriptors.Length);
|
||||
var resolved = resolvedArray.AsSpan(0, descriptors.Length);
|
||||
resolved.Clear();
|
||||
|
||||
Span<int> lockOrder = stackalloc int[descriptors.Length];
|
||||
var acquiredLocks = 0;
|
||||
try
|
||||
{
|
||||
uint bufferLength = 0;
|
||||
for (var i = 0; i < descriptors.Length; i++)
|
||||
{
|
||||
var descriptor = descriptors[i];
|
||||
for (var previous = 0; previous < i; previous++)
|
||||
{
|
||||
if (resolved[previous].Handle == descriptor.Handle)
|
||||
{
|
||||
return AudioOutErrorInvalidPort;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Ports.TryGetValue(descriptor.Handle, out var port))
|
||||
{
|
||||
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
bufferLength = port.BufferLength;
|
||||
}
|
||||
else if (port.BufferLength != bufferLength)
|
||||
{
|
||||
return AudioOutErrorInvalidSize;
|
||||
}
|
||||
|
||||
resolved[i].Handle = descriptor.Handle;
|
||||
resolved[i].SourceAddress = descriptor.SourceAddress;
|
||||
resolved[i].Port = port;
|
||||
lockOrder[i] = i;
|
||||
}
|
||||
|
||||
// Every batch takes port locks in handle order. Two guest threads can
|
||||
// submit overlapping batches in a different descriptor order without
|
||||
// deadlocking each other.
|
||||
for (var i = 1; i < lockOrder.Length; i++)
|
||||
{
|
||||
var index = lockOrder[i];
|
||||
var position = i;
|
||||
while (position > 0 &&
|
||||
resolved[lockOrder[position - 1]].Handle > resolved[index].Handle)
|
||||
{
|
||||
lockOrder[position] = lockOrder[position - 1];
|
||||
position--;
|
||||
}
|
||||
|
||||
lockOrder[position] = index;
|
||||
}
|
||||
|
||||
for (; acquiredLocks < lockOrder.Length; acquiredLocks++)
|
||||
{
|
||||
Monitor.Enter(resolved[lockOrder[acquiredLocks]].Port.SubmissionGate);
|
||||
}
|
||||
|
||||
// AudioOutClose removes the handle before waiting for SubmissionGate.
|
||||
// Recheck after acquiring all gates so a close racing this batch cannot
|
||||
// turn a validated submission into a write to a disposed backend.
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
if (!Ports.TryGetValue(resolved[i].Handle, out var current) ||
|
||||
!ReferenceEquals(current, resolved[i].Port))
|
||||
{
|
||||
return _shutdown ? 0 : AudioOutErrorInvalidPort;
|
||||
}
|
||||
}
|
||||
|
||||
// Stage every guest buffer before the first host submission. A bad
|
||||
// pointer in a later descriptor therefore cannot partially enqueue the
|
||||
// earlier ports.
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
ref var output = ref resolved[i];
|
||||
if (output.SourceAddress == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceBuffer = ArrayPool<byte>.Shared.Rent(output.Port.BufferByteLength);
|
||||
try
|
||||
{
|
||||
var source = sourceBuffer.AsSpan(0, output.Port.BufferByteLength);
|
||||
if (!ctx.Memory.TryRead(output.SourceAddress, source))
|
||||
{
|
||||
return AudioOutErrorInvalidPointer;
|
||||
}
|
||||
|
||||
TraceOutput(output.Handle, output.Port, source);
|
||||
|
||||
output.HostBufferLength = checked(
|
||||
(int)output.Port.BufferLength * AudioPcmConversion.OutputFrameSize);
|
||||
output.HostBuffer = ArrayPool<byte>.Shared.Rent(output.HostBufferLength);
|
||||
AudioPcmConversion.ConvertToStereoPcm16(
|
||||
source,
|
||||
output.HostBuffer.AsSpan(0, output.HostBufferLength),
|
||||
checked((int)output.Port.BufferLength),
|
||||
output.Port.Channels,
|
||||
output.Port.BytesPerSample,
|
||||
output.Port.IsFloat,
|
||||
output.Port.Volume);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(sourceBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
PortState? pacingPort = null;
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
ref var output = ref resolved[i];
|
||||
if (output.HostBuffer is null ||
|
||||
output.Port.Backend is null ||
|
||||
!output.Port.Backend.Submit(
|
||||
output.HostBuffer.AsSpan(0, output.HostBufferLength)))
|
||||
{
|
||||
if (pacingPort is null ||
|
||||
HasLongerBufferDuration(output.Port, pacingPort))
|
||||
{
|
||||
pacingPort = output.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A batch is one guest scheduling point. When one or more ports have
|
||||
// no usable backend, pace once using the longest affected buffer rather
|
||||
// than sleeping once per port.
|
||||
pacingPort?.PaceSilence();
|
||||
return checked((int)resolved[0].Port.BufferLength);
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (var i = acquiredLocks - 1; i >= 0; i--)
|
||||
{
|
||||
Monitor.Exit(resolved[lockOrder[i]].Port.SubmissionGate);
|
||||
}
|
||||
|
||||
for (var i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
if (resolved[i].HostBuffer is { } hostBuffer)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(hostBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPool<ResolvedOutput>.Shared.Return(resolvedArray, clearArray: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasLongerBufferDuration(PortState candidate, PortState current) =>
|
||||
(ulong)candidate.BufferLength * current.Frequency >
|
||||
(ulong)current.BufferLength * candidate.Frequency;
|
||||
|
||||
private static void TraceOutput(int handle, PortState port, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!_traceOutput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var n = Interlocked.Increment(ref _outputCount);
|
||||
if (n <= 8 || n % 200 == 0)
|
||||
{
|
||||
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b+uAV89IlxE",
|
||||
ExportName = "sceAudioOutSetVolume",
|
||||
@@ -362,6 +608,25 @@ public static class AudioOutExports
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
|
||||
Volatile.Write(ref _streamFactoryForTests, streamFactory);
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
foreach (var handle in Ports.Keys)
|
||||
{
|
||||
if (Ports.TryRemove(handle, out var port))
|
||||
{
|
||||
port.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_nextPortHandle = 0;
|
||||
_outputCount = 0;
|
||||
Volatile.Write(ref _shutdown, false);
|
||||
Volatile.Write(ref _streamFactoryForTests, null);
|
||||
}
|
||||
|
||||
private static bool _shutdown;
|
||||
|
||||
private static bool TryGetFormat(
|
||||
|
||||
@@ -1131,16 +1131,18 @@ public static class AvPlayerExports
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FindFfmpeg() =>
|
||||
internal static string? FindFfmpeg() =>
|
||||
FindFfmpeg(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
||||
Environment.GetEnvironmentVariable("PATH"),
|
||||
OperatingSystem.IsWindows());
|
||||
OperatingSystem.IsWindows(),
|
||||
AppContext.BaseDirectory);
|
||||
|
||||
internal static string? FindFfmpeg(
|
||||
string? configured,
|
||||
string? searchPath,
|
||||
bool isWindows)
|
||||
bool isWindows,
|
||||
string? baseDirectory = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||
{
|
||||
@@ -1148,6 +1150,21 @@ public static class AvPlayerExports
|
||||
}
|
||||
|
||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
||||
if (!string.IsNullOrWhiteSpace(baseDirectory))
|
||||
{
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, executable),
|
||||
Path.Combine(baseDirectory, "ffmpeg", executable),
|
||||
})
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var directory in (searchPath ?? string.Empty)
|
||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
|
||||
@@ -18,15 +18,43 @@ namespace SharpEmu.Libs.Bink;
|
||||
internal static class Bink2MovieBridge
|
||||
{
|
||||
private const uint MaxDimension = 16384;
|
||||
private const uint MaxHostVideoWidth = 1920;
|
||||
private const uint MaxHostVideoHeight = 1080;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static NativeAdapter? _adapter;
|
||||
private static string? _activePath;
|
||||
private static IntPtr _activeMovie;
|
||||
private static Bink2MovieInfo _activeInfo;
|
||||
private static byte[]? _frameBuffer;
|
||||
private static bool _usingDummyMovie;
|
||||
private static bool _loadAttempted;
|
||||
private static bool _availabilityReported;
|
||||
private static bool _frameBufferPresented;
|
||||
private static BinkFramePlayback? _playback;
|
||||
private static long _frameSerial;
|
||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||
|
||||
internal static bool IsHostPlaybackActive
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _playback is not null || _frameBuffer is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetPresentationSize(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
_presentationWidth = Math.Min(width, MaxHostVideoWidth);
|
||||
_presentationHeight = Math.Min(height, MaxHostVideoHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true only when movie skipping was explicitly requested. Without
|
||||
@@ -37,109 +65,106 @@ internal static class Bink2MovieBridge
|
||||
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||
ResolveMode() == MovieMode.Skip;
|
||||
|
||||
internal static void ObserveGuestMovie(string hostPath)
|
||||
/// <summary>
|
||||
/// Starts or queues host decoding. Decoded frames are only exposed as a
|
||||
/// sampled guest texture; presentation and UI composition remain guest-owned.
|
||||
/// </summary>
|
||||
internal static bool ObserveGuestMovie(string hostPath)
|
||||
{
|
||||
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||
!File.Exists(hostPath))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
return _playback is not null || _frameBuffer is not null;
|
||||
}
|
||||
|
||||
var mode = ResolveMode();
|
||||
if (mode == MovieMode.Dummy)
|
||||
if (mode is MovieMode.Guest or MovieMode.Skip)
|
||||
{
|
||||
AttachDummyMovieLocked(hostPath);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode != MovieMode.Native)
|
||||
if (_playback is not null || _frameBuffer is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var adapter = GetAdapterLocked();
|
||||
if (adapter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseActiveLocked();
|
||||
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
||||
if (PendingMoviePathSet.Add(hostPath))
|
||||
{
|
||||
PendingMoviePaths.Enqueue(hostPath);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
"[LOADER][INFO] Bink2 bridge queued: " +
|
||||
Path.GetFileName(hostPath));
|
||||
}
|
||||
return PendingMoviePathSet.Contains(hostPath);
|
||||
}
|
||||
|
||||
if (!IsValid(info))
|
||||
{
|
||||
adapter.Close(movie);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activePath = hostPath;
|
||||
_activeMovie = movie;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
AttachMovieLocked(hostPath, mode);
|
||||
return string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) &&
|
||||
(_playback is not null || _frameBuffer is not null);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryDecodeNextFrame(
|
||||
bool advanceClock,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
out uint height,
|
||||
out bool advanced,
|
||||
out long frameSerial,
|
||||
out string hostPath)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
height = 0;
|
||||
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
|
||||
advanced = false;
|
||||
frameSerial = _frameSerial;
|
||||
hostPath = _activePath ?? string.Empty;
|
||||
|
||||
if (_playback is not null)
|
||||
{
|
||||
if (_usingDummyMovie && _frameBuffer is not null)
|
||||
if (!_playback.TryGetFrame(advanceClock, out pixels, out advanced))
|
||||
{
|
||||
pixels = _frameBuffer;
|
||||
if (_playback.IsFinished)
|
||||
{
|
||||
var completedPath = _activePath;
|
||||
CloseActiveLocked();
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge completed: " +
|
||||
Path.GetFileName(completedPath));
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
if (advanced)
|
||||
{
|
||||
frameSerial = ++_frameSerial;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* destination = _frameBuffer)
|
||||
{
|
||||
if (!_adapter.DecodeNextBgra(
|
||||
_activeMovie,
|
||||
(IntPtr)destination,
|
||||
_activeInfo.Width * 4,
|
||||
(uint)_frameBuffer.Length))
|
||||
if (_frameBuffer is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels = _frameBuffer;
|
||||
width = _activeInfo.Width;
|
||||
height = _activeInfo.Height;
|
||||
advanced = !_frameBufferPresented;
|
||||
_frameBufferPresented = true;
|
||||
if (advanced)
|
||||
{
|
||||
frameSerial = ++_frameSerial;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -152,6 +177,52 @@ internal static class Bink2MovieBridge
|
||||
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||
checked((int)((ulong)info.Width * info.Height * 4));
|
||||
|
||||
private static void AttachMovieLocked(string hostPath, MovieMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case MovieMode.Dummy:
|
||||
AttachDummyMovieLocked(hostPath);
|
||||
return;
|
||||
case MovieMode.Ffmpeg:
|
||||
AttachFfmpegMovieLocked(hostPath);
|
||||
return;
|
||||
case MovieMode.Native:
|
||||
AttachNativeMovieLocked(hostPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AttachNativeMovieLocked(string hostPath)
|
||||
{
|
||||
if (!FfmpegNativeBinkFrameSource.TryOpen(
|
||||
hostPath, _presentationWidth, _presentationHeight, out var source) ||
|
||||
source is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var info = new Bink2MovieInfo(
|
||||
source.Width, source.Height, source.FramesPerSecondNumerator, source.FramesPerSecondDenominator);
|
||||
if (!IsValid(info))
|
||||
{
|
||||
source.Dispose();
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
|
||||
private static MovieMode ResolveMode()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||
@@ -170,15 +241,22 @@ internal static class Bink2MovieBridge
|
||||
return MovieMode.Skip;
|
||||
}
|
||||
|
||||
// Prefer the optional host adapter when one is supplied. Otherwise let
|
||||
// the game's statically linked Bink implementation consume the file.
|
||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
||||
EnumerateAdapterCandidates().Any(File.Exists))
|
||||
if (string.Equals(configured, "guest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Native;
|
||||
return MovieMode.Guest;
|
||||
}
|
||||
|
||||
return MovieMode.Guest;
|
||||
if (string.Equals(configured, "ffmpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MovieMode.Ffmpeg;
|
||||
}
|
||||
|
||||
// Native is the default: FfmpegNativeBinkFrameSource.TryOpen degrades
|
||||
// gracefully (falls back to the guest's own decode, logging one
|
||||
// informational line) if the FFmpeg libraries SharpEmu.CLI.csproj
|
||||
// downloads next to the executable are genuinely unavailable, so
|
||||
// defaulting to Native unconditionally is safe.
|
||||
return MovieMode.Native;
|
||||
}
|
||||
|
||||
private static void AttachDummyMovieLocked(string hostPath)
|
||||
@@ -195,22 +273,61 @@ internal static class Bink2MovieBridge
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||
_frameBufferPresented = false;
|
||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||
_usingDummyMovie = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + ".");
|
||||
}
|
||||
|
||||
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
private static void AttachFfmpegMovieLocked(string hostPath)
|
||||
{
|
||||
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink FFmpeg source has an invalid header: " +
|
||||
Path.GetFileName(hostPath));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FfmpegBinkFrameSource.TryOpen(
|
||||
hostPath,
|
||||
info.Width,
|
||||
info.Height,
|
||||
info.FramesPerSecondNumerator,
|
||||
info.FramesPerSecondDenominator,
|
||||
out var source) || source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink FFmpeg source attached: " +
|
||||
Path.GetFileName(hostPath) + " " + info.Width + "x" + info.Height + " @ " +
|
||||
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
|
||||
}
|
||||
|
||||
private static void AttachPlaybackLocked(
|
||||
string hostPath,
|
||||
Bink2MovieInfo info,
|
||||
IBinkFrameDecoder decoder)
|
||||
{
|
||||
CloseActiveLocked();
|
||||
_activePath = hostPath;
|
||||
_activeInfo = info;
|
||||
_playback = new BinkFramePlayback(decoder);
|
||||
}
|
||||
|
||||
internal static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
|
||||
{
|
||||
info = default;
|
||||
Span<byte> header = stackalloc byte[32];
|
||||
Span<byte> header = stackalloc byte[36];
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
if (stream.Read(header) != header.Length ||
|
||||
!header[..4].SequenceEqual("KB2j"u8))
|
||||
stream.ReadExactly(header);
|
||||
if (!header[..3].SequenceEqual("KB2"u8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -219,10 +336,11 @@ internal static class Bink2MovieBridge
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||
1);
|
||||
return true;
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
|
||||
return info.FramesPerSecondNumerator != 0 &&
|
||||
info.FramesPerSecondDenominator != 0;
|
||||
}
|
||||
catch (IOException)
|
||||
catch (Exception exception) when (exception is IOException or EndOfStreamException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -244,80 +362,23 @@ internal static class Bink2MovieBridge
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeAdapter? GetAdapterLocked()
|
||||
{
|
||||
if (_loadAttempted)
|
||||
{
|
||||
return _adapter;
|
||||
}
|
||||
|
||||
_loadAttempted = true;
|
||||
foreach (var candidate in EnumerateAdapterCandidates())
|
||||
{
|
||||
if (!NativeLibrary.TryLoad(candidate, out var library))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (NativeAdapter.TryCreate(library, out var adapter))
|
||||
{
|
||||
_adapter = adapter;
|
||||
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(library);
|
||||
}
|
||||
|
||||
if (!_availabilityReported)
|
||||
{
|
||||
_availabilityReported = true;
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateAdapterCandidates()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
yield return configured;
|
||||
}
|
||||
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseActiveLocked()
|
||||
{
|
||||
if (_activeMovie != IntPtr.Zero)
|
||||
{
|
||||
_adapter?.Close(_activeMovie);
|
||||
}
|
||||
|
||||
_playback?.Dispose();
|
||||
_playback = null;
|
||||
_activePath = null;
|
||||
_activeMovie = IntPtr.Zero;
|
||||
_activeInfo = default;
|
||||
_frameBuffer = null;
|
||||
_usingDummyMovie = false;
|
||||
_frameBufferPresented = false;
|
||||
|
||||
// Wake any guest _read() blocked in WaitForHostPlaybackToFinish: its
|
||||
// movie either just finished or is being pre-empted by a new attach.
|
||||
Monitor.PulseAll(Gate);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private readonly struct Bink2MovieInfo
|
||||
internal readonly struct Bink2MovieInfo
|
||||
{
|
||||
public readonly uint Width;
|
||||
public readonly uint Height;
|
||||
@@ -343,66 +404,222 @@ internal static class Bink2MovieBridge
|
||||
Skip,
|
||||
Dummy,
|
||||
Native,
|
||||
Ffmpeg,
|
||||
}
|
||||
|
||||
private sealed class NativeAdapter
|
||||
private static readonly Queue<string> PendingMoviePaths = new();
|
||||
private static readonly HashSet<string> PendingMoviePathSet =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static void AttachNextQueuedMovieLocked()
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void CloseDelegate(IntPtr movie);
|
||||
|
||||
private readonly OpenUtf8Delegate _openUtf8;
|
||||
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
||||
private readonly CloseDelegate _close;
|
||||
|
||||
private NativeAdapter(
|
||||
OpenUtf8Delegate openUtf8,
|
||||
DecodeNextBgraDelegate decodeNextBgra,
|
||||
CloseDelegate close)
|
||||
while (PendingMoviePaths.Count > 0)
|
||||
{
|
||||
_openUtf8 = openUtf8;
|
||||
_decodeNextBgra = decodeNextBgra;
|
||||
_close = close;
|
||||
var path = PendingMoviePaths.Dequeue();
|
||||
PendingMoviePathSet.Remove(path);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
||||
AttachMovieLocked(path, ResolveMode());
|
||||
if (_playback is not null || _frameBuffer is not null)
|
||||
{
|
||||
adapter = null;
|
||||
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Longest a guest _read() will block waiting for real host playback to
|
||||
// finish. A safety net, not a target: real movies finish well under
|
||||
// this. Bounds the damage if a movie fails to attach/decode after being
|
||||
// queued, so the guest thread doesn't hang forever.
|
||||
private const long MaxCompletionWaitMilliseconds = 5 * 60 * 1000;
|
||||
/// <summary>
|
||||
/// Blocks the calling (guest I/O) thread until the host has actually
|
||||
/// finished presenting <paramref name="hostPath"/> — either because it
|
||||
/// played through, or because something else took over the timeline.
|
||||
///
|
||||
/// The completion shim tells the guest's own Bink header parse "this
|
||||
/// movie is one frame and already done" so its native decoder never
|
||||
/// blocks the guest on real per-frame work. Without this wait, that lie
|
||||
/// lands the instant the guest reads the header, so guest-side game
|
||||
/// logic races far ahead of whatever the host is still showing on
|
||||
/// screen: pressing a button lands on the (already-advanced) guest
|
||||
/// state, but the video visibly keeps playing, and any real-time-gated
|
||||
/// trigger later in the guest's own flow can fire against a clock that
|
||||
/// no longer matches wall time. Gating the "done" read on real host
|
||||
/// completion keeps guest pacing and on-screen playback in lockstep.
|
||||
/// </summary>
|
||||
internal static void WaitForHostPlaybackToFinish(string hostPath)
|
||||
{
|
||||
var deadline = Environment.TickCount64 + MaxCompletionWaitMilliseconds;
|
||||
lock (Gate)
|
||||
{
|
||||
while (IsTrackedLocked(hostPath))
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge completion wait timed out for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Monitor.Wait(Gate, (int)Math.Min(remaining, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTrackedLocked(string hostPath) =>
|
||||
string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
PendingMoviePathSet.Contains(hostPath);
|
||||
|
||||
internal static bool TryTakeOverGuestMovie(
|
||||
string hostPath,
|
||||
out BinkGuestCompletionShim completionShim,
|
||||
out bool observed)
|
||||
{
|
||||
completionShim = default;
|
||||
observed = ObserveGuestMovie(hostPath);
|
||||
|
||||
// Keep the real header visible so the guest creates its movie surface
|
||||
// and draw. Host-decoded pixels replace that sampled image later; a
|
||||
// one-frame completion shim would finish before the descriptor exists.
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static void NotifyGuestMovieClosed(string hostPath)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (PendingMoviePathSet.Remove(hostPath))
|
||||
{
|
||||
var retained = PendingMoviePaths
|
||||
.Where(path => !string.Equals(
|
||||
path,
|
||||
hostPath,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
PendingMoviePaths.Clear();
|
||||
foreach (var path in retained)
|
||||
{
|
||||
PendingMoviePaths.Enqueue(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Monitor.PulseAll(Gate);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge stopped by guest close: " +
|
||||
Path.GetFileName(hostPath));
|
||||
CloseActiveLocked();
|
||||
AttachNextQueuedMovieLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadGuestCompletionShim(
|
||||
string hostPath,
|
||||
out BinkGuestCompletionShim completionShim)
|
||||
{
|
||||
completionShim = default;
|
||||
Span<byte> header = stackalloc byte[48];
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(hostPath);
|
||||
stream.ReadExactly(header);
|
||||
if (!header[..3].SequenceEqual("KB2"u8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
adapter = new NativeAdapter(
|
||||
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
||||
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
||||
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
||||
var frameCount = BinaryPrimitives.ReadUInt32LittleEndian(header[8..12]);
|
||||
var audioTrackCount = BinaryPrimitives.ReadUInt32LittleEndian(header[40..44]);
|
||||
if (frameCount < 2 || audioTrackCount > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var revision = header[3];
|
||||
var frameIndexOffset = 44L + checked(12L * audioTrackCount);
|
||||
if (revision == (byte)'m')
|
||||
{
|
||||
frameIndexOffset += 16;
|
||||
}
|
||||
else if (revision is (byte)'i' or (byte)'j' or (byte)'k' or (byte)'n')
|
||||
{
|
||||
frameIndexOffset += 4;
|
||||
}
|
||||
|
||||
Span<byte> frameOffsets = stackalloc byte[8];
|
||||
stream.Position = frameIndexOffset;
|
||||
stream.ReadExactly(frameOffsets);
|
||||
var firstFrameOffset = BinaryPrimitives.ReadUInt32LittleEndian(frameOffsets[..4]) & ~1u;
|
||||
var secondFrameOffset = BinaryPrimitives.ReadUInt32LittleEndian(frameOffsets[4..]) & ~1u;
|
||||
if (firstFrameOffset < frameIndexOffset + 8 ||
|
||||
secondFrameOffset <= firstFrameOffset ||
|
||||
secondFrameOffset > stream.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
completionShim = new BinkGuestCompletionShim(
|
||||
secondFrameOffset - 8,
|
||||
secondFrameOffset - firstFrameOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
||||
catch (Exception exception) when (
|
||||
exception is IOException or EndOfStreamException or OverflowException)
|
||||
{
|
||||
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
||||
try
|
||||
{
|
||||
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(utf8);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
||||
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
||||
internal readonly struct BinkGuestCompletionShim
|
||||
{
|
||||
private readonly uint _fileSizeMinusHeader;
|
||||
private readonly uint _largestFrameSize;
|
||||
|
||||
internal void Close(IntPtr movie) => _close(movie);
|
||||
internal BinkGuestCompletionShim(uint fileSizeMinusHeader, uint largestFrameSize)
|
||||
{
|
||||
_fileSizeMinusHeader = fileSizeMinusHeader;
|
||||
_largestFrameSize = largestFrameSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites the frame-count/size fields the guest's own Bink header
|
||||
/// parse reads, if this read covers them. Returns true when the
|
||||
/// NumFrames field (the field that tells the guest "this movie is
|
||||
/// done") was in range, so the caller can gate that specific read on
|
||||
/// the host's real playback actually finishing first.
|
||||
/// </summary>
|
||||
internal bool Patch(long fileOffset, Span<byte> bytes)
|
||||
{
|
||||
PatchUInt32(fileOffset, bytes, 4, _fileSizeMinusHeader);
|
||||
var touchedCompletionField = PatchUInt32(fileOffset, bytes, 8, 1);
|
||||
PatchUInt32(fileOffset, bytes, 12, _largestFrameSize);
|
||||
return touchedCompletionField;
|
||||
}
|
||||
|
||||
private static bool PatchUInt32(
|
||||
long fileOffset,
|
||||
Span<byte> bytes,
|
||||
long fieldOffset,
|
||||
uint value)
|
||||
{
|
||||
var relativeOffset = fieldOffset - fileOffset;
|
||||
if (relativeOffset < 0 || relativeOffset + sizeof(uint) > bytes.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
bytes.Slice((int)relativeOffset, sizeof(uint)),
|
||||
value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
internal interface IBinkFrameDecoder : IDisposable
|
||||
{
|
||||
uint Width { get; }
|
||||
|
||||
uint Height { get; }
|
||||
|
||||
uint FramesPerSecondNumerator { get; }
|
||||
|
||||
uint FramesPerSecondDenominator { get; }
|
||||
|
||||
bool TryDecodeNextFrame(Span<byte> destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps blocking codec work away from the Vulkan presentation thread and
|
||||
/// releases decoded frames according to the movie time base.
|
||||
/// </summary>
|
||||
internal sealed class BinkFramePlayback : IDisposable
|
||||
{
|
||||
private const int BufferCount = 5;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly IBinkFrameDecoder _decoder;
|
||||
private readonly Queue<byte[]> _freeBuffers = new();
|
||||
private readonly Queue<DecodedFrame> _decodedFrames = new();
|
||||
private readonly Thread _decoderThread;
|
||||
private byte[]? _currentFrame;
|
||||
private byte[]? _retiredFrame;
|
||||
private long _currentFrameIndex = -1;
|
||||
private long _nextDecodedFrameIndex;
|
||||
private long _playbackStartTimestamp;
|
||||
private bool _playbackClockStarted;
|
||||
private bool _decoderCompleted;
|
||||
private bool _stopRequested;
|
||||
private bool _finished;
|
||||
private int _disposed;
|
||||
|
||||
internal BinkFramePlayback(IBinkFrameDecoder decoder)
|
||||
{
|
||||
_decoder = decoder;
|
||||
Width = decoder.Width;
|
||||
Height = decoder.Height;
|
||||
FramesPerSecondNumerator = decoder.FramesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = decoder.FramesPerSecondDenominator;
|
||||
|
||||
var frameBytes = checked((int)((ulong)Width * Height * 4));
|
||||
for (var index = 0; index < BufferCount; index++)
|
||||
{
|
||||
_freeBuffers.Enqueue(GC.AllocateUninitializedArray<byte>(frameBytes));
|
||||
}
|
||||
|
||||
_decoderThread = new Thread(DecodeLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu Bink video decoder",
|
||||
};
|
||||
_decoderThread.Start();
|
||||
}
|
||||
|
||||
internal uint Width { get; }
|
||||
|
||||
internal uint Height { get; }
|
||||
|
||||
internal uint FramesPerSecondNumerator { get; }
|
||||
|
||||
internal uint FramesPerSecondDenominator { get; }
|
||||
|
||||
internal bool IsFinished
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryGetFrame(
|
||||
bool advanceClock,
|
||||
out byte[] pixels,
|
||||
out bool advanced)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
pixels = [];
|
||||
advanced = false;
|
||||
if (_finished)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_currentFrame is null)
|
||||
{
|
||||
if (_decodedFrames.Count == 0)
|
||||
{
|
||||
if (_decoderCompleted)
|
||||
{
|
||||
_finished = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var first = _decodedFrames.Dequeue();
|
||||
_currentFrame = first.Pixels;
|
||||
_currentFrameIndex = first.Index;
|
||||
advanced = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
if (advanceClock && !_playbackClockStarted)
|
||||
{
|
||||
_playbackStartTimestamp = Stopwatch.GetTimestamp();
|
||||
_playbackClockStarted = true;
|
||||
}
|
||||
|
||||
var elapsedSeconds = _playbackClockStarted
|
||||
? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds
|
||||
: 0;
|
||||
var targetFrameIndex = (long)Math.Floor(
|
||||
elapsedSeconds * FramesPerSecondNumerator / FramesPerSecondDenominator);
|
||||
DecodedFrame? replacement = null;
|
||||
while (_decodedFrames.Count > 0 &&
|
||||
_decodedFrames.Peek().Index <= targetFrameIndex)
|
||||
{
|
||||
if (replacement is { } skipped)
|
||||
{
|
||||
_freeBuffers.Enqueue(skipped.Pixels);
|
||||
}
|
||||
replacement = _decodedFrames.Dequeue();
|
||||
}
|
||||
|
||||
if (replacement is { } next)
|
||||
{
|
||||
if (_retiredFrame is not null)
|
||||
{
|
||||
_freeBuffers.Enqueue(_retiredFrame);
|
||||
}
|
||||
_retiredFrame = _currentFrame;
|
||||
_currentFrame = next.Pixels;
|
||||
_currentFrameIndex = next.Index;
|
||||
advanced = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
|
||||
var frameDurationSeconds =
|
||||
(double)FramesPerSecondDenominator / FramesPerSecondNumerator;
|
||||
if (_playbackClockStarted &&
|
||||
_decoderCompleted &&
|
||||
_decodedFrames.Count == 0 &&
|
||||
elapsedSeconds >= (_currentFrameIndex + 1) * frameDurationSeconds)
|
||||
{
|
||||
_finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
pixels = _currentFrame;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void DecodeLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
byte[] destination;
|
||||
lock (_gate)
|
||||
{
|
||||
while (!_stopRequested && _freeBuffers.Count == 0)
|
||||
{
|
||||
Monitor.Wait(_gate);
|
||||
}
|
||||
if (_stopRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
destination = _freeBuffers.Dequeue();
|
||||
}
|
||||
|
||||
if (!_decoder.TryDecodeNextFrame(destination))
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_freeBuffers.Enqueue(destination);
|
||||
_decoderCompleted = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_decodedFrames.Enqueue(new DecodedFrame(
|
||||
_nextDecodedFrameIndex++, destination));
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or
|
||||
InvalidOperationException)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink decoder stopped: {exception.Message}");
|
||||
lock (_gate)
|
||||
{
|
||||
_decoderCompleted = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_stopRequested = true;
|
||||
Monitor.PulseAll(_gate);
|
||||
}
|
||||
if (Thread.CurrentThread != _decoderThread &&
|
||||
!_decoderThread.Join(TimeSpan.FromMilliseconds(100)))
|
||||
{
|
||||
_decoder.Dispose();
|
||||
_decoderThread.Join(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
else
|
||||
{
|
||||
_decoder.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct DecodedFrame(long Index, byte[] Pixels);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using SharpEmu.Libs.AvPlayer;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
internal sealed class FfmpegBinkFrameSource : IBinkFrameDecoder
|
||||
{
|
||||
private readonly Process _process;
|
||||
private readonly Stream _output;
|
||||
private int _errorLines;
|
||||
private int _disposed;
|
||||
|
||||
private FfmpegBinkFrameSource(
|
||||
Process process,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
_process = process;
|
||||
_output = process.StandardOutput.BaseStream;
|
||||
Width = width;
|
||||
Height = height;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
}
|
||||
|
||||
public uint Width { get; }
|
||||
|
||||
public uint Height { get; }
|
||||
|
||||
public uint FramesPerSecondNumerator { get; }
|
||||
|
||||
public uint FramesPerSecondDenominator { get; }
|
||||
|
||||
internal static bool IsAvailable => AvPlayerExports.FindFfmpeg() is not null;
|
||||
|
||||
internal static bool TryOpen(
|
||||
string path,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator,
|
||||
out FfmpegBinkFrameSource? source)
|
||||
{
|
||||
source = null;
|
||||
var ffmpeg = AvPlayerExports.FindFfmpeg();
|
||||
if (ffmpeg is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo(ffmpeg)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
startInfo.ArgumentList.Add("-nostdin");
|
||||
startInfo.ArgumentList.Add("-hide_banner");
|
||||
startInfo.ArgumentList.Add("-loglevel");
|
||||
startInfo.ArgumentList.Add("error");
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(path);
|
||||
startInfo.ArgumentList.Add("-map");
|
||||
startInfo.ArgumentList.Add("0:v:0");
|
||||
startInfo.ArgumentList.Add("-an");
|
||||
startInfo.ArgumentList.Add("-pix_fmt");
|
||||
startInfo.ArgumentList.Add("bgra");
|
||||
startInfo.ArgumentList.Add("-f");
|
||||
startInfo.ArgumentList.Add("rawvideo");
|
||||
startInfo.ArgumentList.Add("pipe:1");
|
||||
|
||||
try
|
||||
{
|
||||
var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source = new FfmpegBinkFrameSource(
|
||||
process,
|
||||
width,
|
||||
height,
|
||||
framesPerSecondNumerator,
|
||||
framesPerSecondDenominator);
|
||||
process.ErrorDataReceived += source.OnErrorData;
|
||||
process.BeginErrorReadLine();
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or
|
||||
InvalidOperationException or
|
||||
System.ComponentModel.Win32Exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink FFmpeg decoder could not start: {exception.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < destination.Length)
|
||||
{
|
||||
var read = _output.Read(destination[offset..]);
|
||||
if (read == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or ObjectDisposedException)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Bink FFmpeg stream failed: {exception.Message}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnErrorData(object sender, DataReceivedEventArgs eventArgs)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(eventArgs.Data) ||
|
||||
Interlocked.Increment(ref _errorLines) > 20)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][FFMPEG-BINK] {eventArgs.Data}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_output.Dispose();
|
||||
try
|
||||
{
|
||||
if (!_process.HasExited)
|
||||
{
|
||||
_process.Kill(entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using FFmpeg.AutoGen;
|
||||
|
||||
namespace SharpEmu.Libs.Bink;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a .bk2 (or any FFmpeg-readable movie) directly via FFmpeg's C API
|
||||
/// through FFmpeg.AutoGen P/Invoke bindings against the dynamically linked
|
||||
/// libraries published by github.com/sharpemu/ffmpeg-core -- no native C
|
||||
/// bridge of our own to build. See docs/bink2-bridge.md.
|
||||
/// </summary>
|
||||
internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
|
||||
{
|
||||
private AVFormatContext* _formatContext;
|
||||
private AVCodecContext* _codecContext;
|
||||
private SwsContext* _swsContext;
|
||||
private AVFrame* _frame;
|
||||
private AVPacket* _packet;
|
||||
private readonly int _videoStreamIndex;
|
||||
private bool _draining;
|
||||
private int _disposed;
|
||||
|
||||
public uint Width { get; }
|
||||
|
||||
public uint Height { get; }
|
||||
|
||||
public uint FramesPerSecondNumerator { get; }
|
||||
|
||||
public uint FramesPerSecondDenominator { get; }
|
||||
|
||||
private FfmpegNativeBinkFrameSource(
|
||||
AVFormatContext* formatContext,
|
||||
AVCodecContext* codecContext,
|
||||
int videoStreamIndex,
|
||||
uint width,
|
||||
uint height,
|
||||
uint framesPerSecondNumerator,
|
||||
uint framesPerSecondDenominator)
|
||||
{
|
||||
_formatContext = formatContext;
|
||||
_codecContext = codecContext;
|
||||
_videoStreamIndex = videoStreamIndex;
|
||||
Width = width;
|
||||
Height = height;
|
||||
FramesPerSecondNumerator = framesPerSecondNumerator;
|
||||
FramesPerSecondDenominator = framesPerSecondDenominator;
|
||||
_frame = ffmpeg.av_frame_alloc();
|
||||
_packet = ffmpeg.av_packet_alloc();
|
||||
}
|
||||
|
||||
private static bool _rootPathInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Points FFmpeg.AutoGen at the FFmpeg shared libraries SharpEmu.CLI
|
||||
/// downloads next to the executable (see SharpEmu.CLI.csproj's
|
||||
/// FetchFfmpegRuntime target); kept as loose files rather than embedded
|
||||
/// in the single-file bundle so the OS loader can resolve the normal
|
||||
/// inter-library dependencies (avcodec depends on avutil, etc.) itself.
|
||||
/// </summary>
|
||||
private static void EnsureRootPathInitialized()
|
||||
{
|
||||
if (_rootPathInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rootPathInitialized = true;
|
||||
// SharpEmu.CLI.csproj publishes FFmpeg's shared libraries into a
|
||||
// "plugins" subfolder next to the executable rather than flat beside
|
||||
// it (see NativeLibraryFolderName in SharpEmu.CLI.csproj).
|
||||
ffmpeg.RootPath = Path.Combine(AppContext.BaseDirectory, "plugins");
|
||||
|
||||
// ffmpeg's static constructor runs DynamicallyLoadedBindings.Initialize()
|
||||
// itself, but that constructor fires on first touch of the ffmpeg type --
|
||||
// which is the RootPath assignment above -- so it binds against the
|
||||
// default (empty) RootPath before the assignment's own setter body runs.
|
||||
// Every function resolved during that first pass permanently throws
|
||||
// NotSupportedException. Re-running Initialize() now, with RootPath
|
||||
// actually set, rebinds everything against the real search path.
|
||||
DynamicallyLoadedBindings.Initialize();
|
||||
}
|
||||
|
||||
internal static bool TryOpen(
|
||||
string path,
|
||||
uint maximumWidth,
|
||||
uint maximumHeight,
|
||||
out FfmpegNativeBinkFrameSource? source)
|
||||
{
|
||||
source = null;
|
||||
EnsureRootPathInitialized();
|
||||
|
||||
AVFormatContext* formatContext = null;
|
||||
AVCodecContext* codecContext = null;
|
||||
try
|
||||
{
|
||||
if (ffmpeg.avformat_open_input(&formatContext, path, null, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ffmpeg.avformat_find_stream_info(formatContext, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AVCodec* decoder = null;
|
||||
var videoStreamIndex = ffmpeg.av_find_best_stream(
|
||||
formatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
|
||||
if (videoStreamIndex < 0 || decoder is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stream = formatContext->streams[videoStreamIndex];
|
||||
codecContext = ffmpeg.avcodec_alloc_context3(decoder);
|
||||
if (codecContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ffmpeg.avcodec_parameters_to_context(codecContext, stream->codecpar) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
codecContext->thread_count = 0;
|
||||
codecContext->thread_type = ffmpeg.FF_THREAD_FRAME | ffmpeg.FF_THREAD_SLICE;
|
||||
if (ffmpeg.avcodec_open2(codecContext, decoder, null) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (codecContext->width <= 0 || codecContext->height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var frameRate = ffmpeg.av_guess_frame_rate(formatContext, stream, null);
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = stream->avg_frame_rate;
|
||||
}
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = stream->r_frame_rate;
|
||||
}
|
||||
if (frameRate.num <= 0 || frameRate.den <= 0)
|
||||
{
|
||||
frameRate = new AVRational { num = 30, den = 1 };
|
||||
}
|
||||
|
||||
var outputWidth = (uint)codecContext->width;
|
||||
var outputHeight = (uint)codecContext->height;
|
||||
if (maximumWidth > 0 && maximumHeight > 0 &&
|
||||
(outputWidth > maximumWidth || outputHeight > maximumHeight))
|
||||
{
|
||||
if ((ulong)outputWidth * maximumHeight > (ulong)outputHeight * maximumWidth)
|
||||
{
|
||||
outputHeight = (uint)((ulong)outputHeight * maximumWidth / outputWidth);
|
||||
outputWidth = maximumWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
outputWidth = (uint)((ulong)outputWidth * maximumHeight / outputHeight);
|
||||
outputHeight = maximumHeight;
|
||||
}
|
||||
|
||||
outputWidth = Math.Max(1, outputWidth);
|
||||
outputHeight = Math.Max(1, outputHeight);
|
||||
}
|
||||
|
||||
source = new FfmpegNativeBinkFrameSource(
|
||||
formatContext,
|
||||
codecContext,
|
||||
videoStreamIndex,
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
(uint)frameRate.num,
|
||||
(uint)frameRate.den);
|
||||
formatContext = null;
|
||||
codecContext = null;
|
||||
return true;
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (codecContext is not null)
|
||||
{
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
}
|
||||
|
||||
if (formatContext is not null)
|
||||
{
|
||||
ffmpeg.avformat_close_input(&formatContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
var stride = checked((int)(Width * 4));
|
||||
var required = (long)stride * Height;
|
||||
if (destination.Length < required)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReceiveFrame())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_swsContext = ffmpeg.sws_getCachedContext(
|
||||
_swsContext,
|
||||
_frame->width,
|
||||
_frame->height,
|
||||
(AVPixelFormat)_frame->format,
|
||||
(int)Width,
|
||||
(int)Height,
|
||||
AVPixelFormat.AV_PIX_FMT_BGRA,
|
||||
ffmpeg.SWS_FAST_BILINEAR,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
if (_swsContext is null)
|
||||
{
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
fixed (byte* destinationPointer = destination)
|
||||
{
|
||||
var destinationPlanes = new byte*[4] { destinationPointer, null, null, null };
|
||||
var destinationStrides = new int[4] { stride, 0, 0, 0 };
|
||||
var convertedRows = ffmpeg.sws_scale(
|
||||
_swsContext,
|
||||
_frame->data,
|
||||
_frame->linesize,
|
||||
0,
|
||||
_frame->height,
|
||||
destinationPlanes,
|
||||
destinationStrides);
|
||||
ffmpeg.av_frame_unref(_frame);
|
||||
return convertedRows == (int)Height;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReceiveFrame()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
|
||||
if (receiveResult >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (receiveResult == ffmpeg.AVERROR_EOF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (receiveResult != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_draining)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFeedPacket())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFeedPacket()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var readResult = ffmpeg.av_read_frame(_formatContext, _packet);
|
||||
if (readResult < 0)
|
||||
{
|
||||
_draining = true;
|
||||
ffmpeg.avcodec_send_packet(_codecContext, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_packet->stream_index != _videoStreamIndex)
|
||||
{
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
|
||||
ffmpeg.av_packet_unref(_packet);
|
||||
if (sendResult < 0 && sendResult != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_swsContext is not null)
|
||||
{
|
||||
ffmpeg.sws_freeContext(_swsContext);
|
||||
_swsContext = null;
|
||||
}
|
||||
|
||||
if (_packet is not null)
|
||||
{
|
||||
var packet = _packet;
|
||||
ffmpeg.av_packet_free(&packet);
|
||||
_packet = null;
|
||||
}
|
||||
|
||||
if (_frame is not null)
|
||||
{
|
||||
var frame = _frame;
|
||||
ffmpeg.av_frame_free(&frame);
|
||||
_frame = null;
|
||||
}
|
||||
|
||||
if (_codecContext is not null)
|
||||
{
|
||||
var codecContext = _codecContext;
|
||||
ffmpeg.avcodec_free_context(&codecContext);
|
||||
_codecContext = null;
|
||||
}
|
||||
|
||||
if (_formatContext is not null)
|
||||
{
|
||||
var formatContext = _formatContext;
|
||||
ffmpeg.avformat_close_input(&formatContext);
|
||||
_formatContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
// The types that cross the guest-GPU backend seam. Every field is either a neutral
|
||||
@@ -10,7 +12,8 @@ namespace SharpEmu.Libs.Gpu;
|
||||
// translation for its API.
|
||||
|
||||
/// <summary>A guest texture referenced by a draw or dispatch. Format/NumberType/
|
||||
/// TileMode/DstSelect are raw guest descriptor codes.</summary>
|
||||
/// TileMode/DstSelect/Type are raw guest descriptor codes. Depth is the
|
||||
/// normalized volume depth (one for non-3D resources).</summary>
|
||||
internal sealed record GuestDrawTexture(
|
||||
ulong Address,
|
||||
uint Width,
|
||||
@@ -32,7 +35,15 @@ internal sealed record GuestDrawTexture(
|
||||
// from; -1 when the range is untracked or the pixels were not read here.
|
||||
long WriteGeneration = -1,
|
||||
bool ArrayedView = false,
|
||||
uint ArrayLayers = 1);
|
||||
uint ArrayLayers = 1,
|
||||
uint Type = 9,
|
||||
uint Depth = 1,
|
||||
// GPU-detile opt-in (SHARPEMU_GPU_DETILE): when Detile is non-null the AGC
|
||||
// layer skipped the CPU deswizzle and shipped the raw TILED bytes here in
|
||||
// TiledSource; the Vulkan backend detiles them on the GPU. RgbaPixels is
|
||||
// empty in that case. Both are neutral (no host graphics-API values).
|
||||
byte[]? TiledSource = null,
|
||||
DetileParams? Detile = null);
|
||||
|
||||
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
|
||||
internal readonly record struct GuestSampler(
|
||||
@@ -55,7 +66,9 @@ internal readonly record struct TextureContentIdentity(
|
||||
uint Pitch,
|
||||
GuestSampler Sampler,
|
||||
bool Arrayed = false,
|
||||
uint ArrayLayers = 1);
|
||||
uint ArrayLayers = 1,
|
||||
uint Type = 9,
|
||||
uint Depth = 1);
|
||||
|
||||
internal sealed record GuestMemoryBuffer(
|
||||
ulong BaseAddress,
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.ShaderCompiler.Metal;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Metal;
|
||||
|
||||
/// <summary>
|
||||
/// Metal twin of <c>VulkanDetilePass</c>: runs the ExactXor detile equation from
|
||||
/// <see cref="GnmTiling.GetDetileParams"/> as a Metal compute kernel
|
||||
/// (<see cref="MslFixedShaders.CreateDetileCompute"/>), writing a linear buffer
|
||||
/// and blitting it into the sampled texture.
|
||||
///
|
||||
/// <see cref="RecordDetile"/> records the compute dispatch + blit onto a caller's
|
||||
/// command buffer and returns its transient buffers for the caller to release
|
||||
/// once that command buffer completes — the async, non-blocking shape (Metal
|
||||
/// hazard-tracks the compute-write → blit-read → sample dependency automatically,
|
||||
/// so no manual barriers are needed).
|
||||
///
|
||||
/// Only ExactXor 4-bytes/element surfaces are handled. NOTE: authored on Windows;
|
||||
/// the MSL and every Metal call here are <b>Mac-untested</b> — mirrors the
|
||||
/// verified Vulkan logic and the existing Metal message-send conventions, but
|
||||
/// must be validated on a real Metal device.
|
||||
/// </summary>
|
||||
internal sealed unsafe class MetalDetilePass : IDisposable
|
||||
{
|
||||
private const uint LocalSize = 8;
|
||||
private const int PushConstantUints = 11;
|
||||
|
||||
private readonly nint _device;
|
||||
private nint _pipelineState;
|
||||
private bool _initialized;
|
||||
private bool _disposed;
|
||||
|
||||
public MetalDetilePass(nint device)
|
||||
{
|
||||
_device = device;
|
||||
}
|
||||
|
||||
public static bool Supports(in DetileParams parameters) =>
|
||||
(parameters.Equation == DetileEquation.ExactXor ||
|
||||
parameters.Equation == DetileEquation.BlockTable) &&
|
||||
parameters.BytesPerElement is 4 or 8 or 16;
|
||||
|
||||
/// <summary>
|
||||
/// Records the deswizzle of <paramref name="tiled"/> into
|
||||
/// <paramref name="texture"/> (<paramref name="texelWidth"/> x
|
||||
/// <paramref name="texelHeight"/> texels x <paramref name="layers"/> slices)
|
||||
/// onto <paramref name="commandBuffer"/>. The kernel iterates the element grid
|
||||
/// from <paramref name="parameters"/> (for block-compressed formats a 4x4 block
|
||||
/// is one element). Does not commit; the caller releases
|
||||
/// <paramref name="transientBuffers"/> when the command buffer completes.
|
||||
/// Returns false (empty transients) when unsupported or the pipeline could not
|
||||
/// be built.
|
||||
/// </summary>
|
||||
public bool RecordDetile(
|
||||
nint commandBuffer,
|
||||
nint texture,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters,
|
||||
out nint[] transientBuffers)
|
||||
{
|
||||
transientBuffers = [];
|
||||
var bytesPerElement = (uint)parameters.BytesPerElement;
|
||||
if (_disposed || commandBuffer == 0 || texture == 0 ||
|
||||
!Supports(parameters) || texelWidth == 0 || texelHeight == 0 || layers == 0 || tiled.IsEmpty ||
|
||||
tiled.Length % (int)(layers * bytesPerElement) != 0 ||
|
||||
!EnsurePipeline())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var elementsWide = (uint)parameters.ElementsWide;
|
||||
var elementsHigh = (uint)parameters.ElementsHigh;
|
||||
var uintsPerElement = bytesPerElement / sizeof(uint);
|
||||
|
||||
// Array slices are packed contiguously in the tiled buffer; each slice's
|
||||
// element stride is the whole buffer split evenly by layer.
|
||||
var srcSliceElements = (uint)((ulong)tiled.Length / bytesPerElement / layers);
|
||||
|
||||
// Binding 1 carries the within-block offset table. ExactXor: element-shifted
|
||||
// X/Y byte terms. BlockTable: GetDetileParams' block table (already element
|
||||
// offsets) in binding 1, a placeholder in binding 2. The two equations index
|
||||
// different-sized buffers, so the kernel branches and reads only one.
|
||||
uint[] xTerm;
|
||||
uint[] yTerm;
|
||||
uint equationValue;
|
||||
if (parameters.Equation == DetileEquation.BlockTable)
|
||||
{
|
||||
xTerm = new uint[parameters.BlockTable.Length];
|
||||
for (var index = 0; index < xTerm.Length; index++)
|
||||
{
|
||||
xTerm[index] = (uint)parameters.BlockTable[index];
|
||||
}
|
||||
|
||||
yTerm = [0];
|
||||
equationValue = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shift = BitOperations.TrailingZeroCount((uint)parameters.BytesPerElement);
|
||||
xTerm = ToElementTerms(parameters.XByteTerm, shift);
|
||||
yTerm = ToElementTerms(parameters.YByteTerm, shift);
|
||||
equationValue = 0;
|
||||
}
|
||||
|
||||
var newBufferWithBytes = MetalNative.Selector("newBufferWithBytes:length:options:");
|
||||
var newBufferWithLength = MetalNative.Selector("newBufferWithLength:options:");
|
||||
|
||||
nint tiledBuffer;
|
||||
nint xBuffer;
|
||||
nint yBuffer;
|
||||
fixed (byte* tiledPointer = tiled)
|
||||
{
|
||||
tiledBuffer = MetalNative.SendBuffer(
|
||||
_device, newBufferWithBytes, (nint)tiledPointer, (nuint)tiled.Length, 0);
|
||||
}
|
||||
|
||||
fixed (uint* xPointer = xTerm)
|
||||
{
|
||||
xBuffer = MetalNative.SendBuffer(
|
||||
_device, newBufferWithBytes, (nint)xPointer, (nuint)xTerm.Length * sizeof(uint), 0);
|
||||
}
|
||||
|
||||
fixed (uint* yPointer = yTerm)
|
||||
{
|
||||
yBuffer = MetalNative.SendBuffer(
|
||||
_device, newBufferWithBytes, (nint)yPointer, (nuint)yTerm.Length * sizeof(uint), 0);
|
||||
}
|
||||
|
||||
var outputBytes = (nuint)elementsWide * elementsHigh * bytesPerElement * layers;
|
||||
var outputBuffer = MetalNative.SendNewBuffer(_device, newBufferWithLength, outputBytes, 0);
|
||||
|
||||
Span<uint> push =
|
||||
[
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
(uint)parameters.BlockWidth,
|
||||
(uint)parameters.BlockHeight,
|
||||
(uint)parameters.BlockElements,
|
||||
(uint)parameters.BlocksPerRow,
|
||||
(uint)parameters.XMask,
|
||||
(uint)parameters.YMask,
|
||||
srcSliceElements,
|
||||
equationValue,
|
||||
uintsPerElement,
|
||||
];
|
||||
nint paramsBuffer;
|
||||
fixed (uint* pushPointer = push)
|
||||
{
|
||||
paramsBuffer = MetalNative.SendBuffer(
|
||||
_device, newBufferWithBytes, (nint)pushPointer, (nuint)PushConstantUints * sizeof(uint), 0);
|
||||
}
|
||||
|
||||
if (tiledBuffer == 0 || xBuffer == 0 || yBuffer == 0 || outputBuffer == 0 || paramsBuffer == 0)
|
||||
{
|
||||
ReleaseAll(tiledBuffer, xBuffer, yBuffer, outputBuffer, paramsBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute encoder: one thread per texel.
|
||||
var setBuffer = MetalNative.Selector("setBuffer:offset:atIndex:");
|
||||
var encoder = MetalNative.Send(commandBuffer, MetalNative.Selector("computeCommandEncoder"));
|
||||
MetalNative.Send(encoder, MetalNative.Selector("setComputePipelineState:"), _pipelineState);
|
||||
MetalNative.SendSetBuffer(encoder, setBuffer, tiledBuffer, 0, 0);
|
||||
MetalNative.SendSetBuffer(encoder, setBuffer, xBuffer, 0, 1);
|
||||
MetalNative.SendSetBuffer(encoder, setBuffer, yBuffer, 0, 2);
|
||||
MetalNative.SendSetBuffer(encoder, setBuffer, outputBuffer, 0, 3);
|
||||
MetalNative.SendSetBuffer(encoder, setBuffer, paramsBuffer, 0, 4);
|
||||
|
||||
// X is widened by uintsPerElement (each thread copies one word); one
|
||||
// grid-Z layer per array slice.
|
||||
var threadgroups = new MtlSize
|
||||
{
|
||||
Width = (nuint)((elementsWide * uintsPerElement + LocalSize - 1) / LocalSize),
|
||||
Height = (nuint)((elementsHigh + LocalSize - 1) / LocalSize),
|
||||
Depth = layers,
|
||||
};
|
||||
var threadsPerThreadgroup = new MtlSize { Width = LocalSize, Height = LocalSize, Depth = 1 };
|
||||
MetalNative.SendDispatch(
|
||||
encoder,
|
||||
MetalNative.Selector("dispatchThreadgroups:threadsPerThreadgroup:"),
|
||||
threadgroups,
|
||||
threadsPerThreadgroup);
|
||||
MetalNative.SendVoid(encoder, MetalNative.Selector("endEncoding"));
|
||||
|
||||
// Blit the layer-major linear output buffer into the sampled texture, one
|
||||
// slice per array layer (Metal copyFromBuffer targets a single slice). The
|
||||
// buffer is element/block-packed (row stride = elementsWide*bpp); the copy
|
||||
// region is in texels. Metal tracks the compute-write -> blit-read hazard.
|
||||
var blit = MetalNative.Send(commandBuffer, MetalNative.Selector("blitCommandEncoder"));
|
||||
var copySelector = MetalNative.Selector(
|
||||
"copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:" +
|
||||
"toTexture:destinationSlice:destinationLevel:destinationOrigin:");
|
||||
var sliceBytes = (nuint)elementsWide * elementsHigh * bytesPerElement;
|
||||
var rowBytes = (nuint)elementsWide * bytesPerElement;
|
||||
for (uint layer = 0; layer < layers; layer++)
|
||||
{
|
||||
MetalNative.SendCopyBufferToTexture(
|
||||
blit,
|
||||
copySelector,
|
||||
outputBuffer,
|
||||
(nuint)layer * sliceBytes,
|
||||
rowBytes,
|
||||
sliceBytes,
|
||||
new MtlSize { Width = texelWidth, Height = texelHeight, Depth = 1 },
|
||||
texture,
|
||||
layer,
|
||||
0,
|
||||
new MtlOrigin { X = 0, Y = 0, Z = 0 });
|
||||
}
|
||||
|
||||
MetalNative.SendVoid(blit, MetalNative.Selector("endEncoding"));
|
||||
|
||||
transientBuffers = [tiledBuffer, xBuffer, yBuffer, outputBuffer, paramsBuffer];
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EnsurePipeline()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return _pipelineState != 0;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
var options = MetalNative.Send(
|
||||
MetalNative.Send(MetalNative.Class("MTLCompileOptions"), MetalNative.Selector("alloc")),
|
||||
MetalNative.Selector("init"));
|
||||
MetalNative.SendVoidBool(options, MetalNative.Selector("setFastMathEnabled:"), false);
|
||||
|
||||
nint libraryError = 0;
|
||||
var library = MetalNative.Send(
|
||||
_device,
|
||||
MetalNative.Selector("newLibraryWithSource:options:error:"),
|
||||
MetalNative.NsString(MslFixedShaders.CreateDetileCompute()),
|
||||
options,
|
||||
ref libraryError);
|
||||
if (library == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU-DETILE] Metal detile library compile failed: {MetalNative.DescribeError(libraryError)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var function = MetalNative.Send(
|
||||
library, MetalNative.Selector("newFunctionWithName:"), MetalNative.NsString("detile_cs"));
|
||||
if (function == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[GPU-DETILE] Metal detile function 'detile_cs' not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
nint pipelineError = 0;
|
||||
_pipelineState = MetalNative.Send(
|
||||
_device,
|
||||
MetalNative.Selector("newComputePipelineStateWithFunction:error:"),
|
||||
function,
|
||||
ref pipelineError);
|
||||
if (_pipelineState == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU-DETILE] Metal detile pipeline failed: {MetalNative.DescribeError(pipelineError)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint[] ToElementTerms(int[] byteTerms, int shift)
|
||||
{
|
||||
var terms = new uint[byteTerms.Length];
|
||||
for (var index = 0; index < byteTerms.Length; index++)
|
||||
{
|
||||
terms[index] = (uint)byteTerms[index] >> shift;
|
||||
}
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
private static void ReleaseAll(params nint[] objects)
|
||||
{
|
||||
var release = MetalNative.Selector("release");
|
||||
foreach (var handle in objects)
|
||||
{
|
||||
if (handle != 0)
|
||||
{
|
||||
MetalNative.SendVoid(handle, release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (_pipelineState != 0)
|
||||
{
|
||||
MetalNative.SendVoid(_pipelineState, MetalNative.Selector("release"));
|
||||
_pipelineState = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Metal;
|
||||
|
||||
@@ -1583,6 +1584,45 @@ internal static partial class MetalVideoPresenter
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Default-on GPU detile packages the tiled source + resolved DetileParams
|
||||
// with empty RgbaPixels so a backend can deswizzle on the GPU. The Metal
|
||||
// GPU compute pass (MetalDetilePass / detile_compute.msl) is the intended
|
||||
// equivalent of VulkanDetilePass, but it is Mac-untested, so the active
|
||||
// Metal path CPU-detiles here via GnmTiling.DetileWithParams — the exact
|
||||
// same DetileParams addressing the kernel runs — restoring the linear-upload
|
||||
// behavior Metal had before GPU detile existed, with no regression.
|
||||
if (texture.RgbaPixels.Length == 0 &&
|
||||
texture.TiledSource is { } tiledSource &&
|
||||
texture.Detile is { } detileParameters)
|
||||
{
|
||||
// The tiled source packs the array slices contiguously (one per layer);
|
||||
// detile each into its layer-major linear region so the reconstructed
|
||||
// pixels match what the CPU array-upload path produced pre-GPU-detile.
|
||||
// A plain 2D texture is just one layer.
|
||||
var layers = Math.Max((int)texture.ArrayLayers, 1);
|
||||
var sliceLinearBytes =
|
||||
detileParameters.ElementsWide * detileParameters.ElementsHigh * detileParameters.BytesPerElement;
|
||||
var sliceTiledBytes = tiledSource.Length / layers;
|
||||
var linear = new byte[sliceLinearBytes * layers];
|
||||
var detiledAll = true;
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
if (!GnmTiling.DetileWithParams(
|
||||
detileParameters,
|
||||
tiledSource.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
linear.AsSpan(layer * sliceLinearBytes, sliceLinearBytes)))
|
||||
{
|
||||
detiledAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (detiledAll)
|
||||
{
|
||||
texture = texture with { RgbaPixels = linear };
|
||||
}
|
||||
}
|
||||
|
||||
// AGC ships the raw (detiled) source texels; create the texture in the
|
||||
// guest's native format — Mac-family GPUs sample BC blocks directly —
|
||||
// and size expectations with the same block-aware math AGC used.
|
||||
|
||||
@@ -80,6 +80,18 @@ public static class JsonExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PR5k1penBLM",
|
||||
ExportName = "_ZN3sce4Json11Initializer9terminateEv",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerTerminate(CpuContext ctx)
|
||||
{
|
||||
TraceJson("Initializer.terminate", ctx[CpuRegister.Rdi], 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Cxwy7wHq4J0",
|
||||
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_13InitParameterE",
|
||||
|
||||
@@ -97,6 +97,9 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
private static readonly object _fdGate = new();
|
||||
private static readonly Dictionary<int, FileStream> _openFiles = new();
|
||||
private static readonly Dictionary<int, Bink2MovieBridge.BinkGuestCompletionShim>
|
||||
_binkGuestCompletionShims = new();
|
||||
private static readonly Dictionary<int, string> _observedBinkGuestFiles = new();
|
||||
private static readonly Dictionary<int, OpenDirectory> _openDirectories = new();
|
||||
private static readonly object _libcAllocGate = new();
|
||||
private static readonly object _memoryGate = new();
|
||||
@@ -268,13 +271,13 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, address + mappedLength);
|
||||
_mappedRegions[address] = new MappedRegion(
|
||||
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||
address,
|
||||
mappedLength,
|
||||
OrbisProtCpuReadWrite,
|
||||
IsFlexible: false,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
DirectStart: 0));
|
||||
}
|
||||
|
||||
for (ulong offset = 0; offset < mappedLength;)
|
||||
@@ -300,13 +303,13 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
lock (_memoryGate)
|
||||
{
|
||||
_mappedRegions[address] = new MappedRegion(
|
||||
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||
address,
|
||||
length,
|
||||
Protection: 0,
|
||||
IsFlexible: false,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
DirectStart: 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1468,6 +1471,14 @@ public static partial class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
Bink2MovieBridge.BinkGuestCompletionShim binkCompletionShim = default;
|
||||
var observedBinkMovie = false;
|
||||
var useBinkCompletionShim = access == FileAccess.Read &&
|
||||
Bink2MovieBridge.TryTakeOverGuestMovie(
|
||||
hostPath,
|
||||
out binkCompletionShim,
|
||||
out observedBinkMovie);
|
||||
|
||||
if (IsMutatingOpen(flags) && IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
|
||||
@@ -1516,13 +1527,22 @@ public static partial class KernelMemoryCompatExports
|
||||
lock (_fdGate)
|
||||
{
|
||||
_openFiles[fd] = stream;
|
||||
if (useBinkCompletionShim)
|
||||
{
|
||||
_binkGuestCompletionShims[fd] = binkCompletionShim;
|
||||
}
|
||||
if (observedBinkMovie)
|
||||
{
|
||||
_observedBinkGuestFiles[fd] = hostPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Bink is linked directly into some games, so there is no media
|
||||
// import for the HLE codec layer to intercept. The successful
|
||||
// guest file open is the stable boundary at which the optional
|
||||
// host Bink bridge can attach to the same movie.
|
||||
Bink2MovieBridge.ObserveGuestMovie(hostPath);
|
||||
if (useBinkCompletionShim)
|
||||
{
|
||||
LogOpenTrace(
|
||||
"_open bink-host-shim path='" + guestPath + "' host='" + hostPath +
|
||||
"' flags=0x" + flags.ToString("X8") + " fd=" + fd);
|
||||
}
|
||||
|
||||
if (IsMutatingOpen(flags))
|
||||
{
|
||||
@@ -2053,10 +2073,21 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
var notifyBinkClose = false;
|
||||
string? observedBinkPath = null;
|
||||
lock (_fdGate)
|
||||
{
|
||||
if (_openFiles.Remove(fd, out stream))
|
||||
{
|
||||
_binkGuestCompletionShims.Remove(fd);
|
||||
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
|
||||
{
|
||||
notifyBinkClose = !_observedBinkGuestFiles.Values.Any(path =>
|
||||
string.Equals(
|
||||
path,
|
||||
observedBinkPath,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else if (_openDirectories.Remove(fd))
|
||||
{
|
||||
@@ -2069,6 +2100,10 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyBinkClose)
|
||||
{
|
||||
Bink2MovieBridge.NotifyGuestMovieClosed(observedBinkPath!);
|
||||
}
|
||||
stream.Dispose();
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -2096,9 +2131,12 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
Bink2MovieBridge.BinkGuestCompletionShim completionShim = default;
|
||||
var useBinkCompletionShim = false;
|
||||
lock (_fdGate)
|
||||
{
|
||||
_openFiles.TryGetValue(fd, out stream);
|
||||
useBinkCompletionShim = _binkGuestCompletionShims.TryGetValue(fd, out completionShim);
|
||||
}
|
||||
|
||||
if (stream is null)
|
||||
@@ -2118,6 +2156,17 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||
var read = stream.Read(buffer, 0, requested);
|
||||
if (read > 0 && useBinkCompletionShim)
|
||||
{
|
||||
// The patched NumFrames field is what tells the guest "this
|
||||
// movie is fully consumed" - hold that specific read until the
|
||||
// host has actually finished showing it, so guest-side game
|
||||
// logic can't race ahead of what's still on screen.
|
||||
if (completionShim.Patch(positionBefore, buffer.AsSpan(0, read)))
|
||||
{
|
||||
Bink2MovieBridge.WaitForHostPlaybackToFinish(stream.Name);
|
||||
}
|
||||
}
|
||||
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -3091,13 +3140,13 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
||||
_mappedRegions[mappedAddress] = new MappedRegion(
|
||||
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||
mappedAddress,
|
||||
length,
|
||||
protection,
|
||||
IsFlexible: false,
|
||||
IsDirect: true,
|
||||
DirectStart: directMemoryStart);
|
||||
DirectStart: directMemoryStart));
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||
@@ -3183,13 +3232,13 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
||||
_allocatedFlexibleBytes = Math.Min(FlexibleMemorySizeBytes, _allocatedFlexibleBytes + length);
|
||||
_mappedRegions[mappedAddress] = new MappedRegion(
|
||||
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||
mappedAddress,
|
||||
length,
|
||||
protection,
|
||||
IsFlexible: true,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
DirectStart: 0));
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||
|
||||
@@ -153,6 +153,64 @@ public static class KernelPthreadCompatExports
|
||||
static KernelPthreadCompatExports()
|
||||
{
|
||||
RunSynchronizationSelfChecks();
|
||||
GuestThreadExecution.GuestThreadAbandoned += AbandonMutexesOwnedByThread;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-release mutexes still owned by a guest thread that is being torn
|
||||
/// down without a clean unlock (TBB worker_abort, abrupt exit). Otherwise
|
||||
/// waiters can spin forever and block splash→first GPU submit.
|
||||
/// </summary>
|
||||
public static int AbandonMutexesOwnedByThread(ulong threadId, string reason)
|
||||
{
|
||||
if (threadId == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var released = 0;
|
||||
var wakeKeys = new List<string>();
|
||||
foreach (var pair in _mutexStates)
|
||||
{
|
||||
var state = pair.Value;
|
||||
string? wakeKey = null;
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId != threadId || state.RecursionCount <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.OwnerThreadId = 0;
|
||||
state.RecursionCount = 0;
|
||||
wakeKey = state.Waiters.First?.Value.Cooperative == true
|
||||
? state.Waiters.First.Value.WakeKey
|
||||
: null;
|
||||
Monitor.PulseAll(state);
|
||||
released++;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] pthread_mutex_abandon mutex=0x{pair.Key:X16} " +
|
||||
$"owner={KernelPthreadState.DescribeThreadHandle(threadId)} " +
|
||||
$"reason={reason} waiters={state.Waiters.Count}");
|
||||
}
|
||||
|
||||
if (wakeKey is not null)
|
||||
{
|
||||
wakeKeys.Add(wakeKey);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var wakeKey in wakeKeys)
|
||||
{
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(wakeKey, 1);
|
||||
}
|
||||
|
||||
if (released > 0)
|
||||
{
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
return released;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -27,8 +27,12 @@ internal static class KernelPthreadState
|
||||
internal static ulong GetCurrentThreadHandle()
|
||||
{
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _))
|
||||
// Prefer the bound guest handle even when it is not yet in Threads.
|
||||
// Falling through to a synthetic ThreadStatic handle while a guest
|
||||
// thread is bound causes mutex owner mismatches (unlock PERM → hang).
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
EnsureGuestThreadIdentity(guestThreadHandle);
|
||||
return guestThreadHandle;
|
||||
}
|
||||
|
||||
@@ -39,15 +43,27 @@ internal static class KernelPthreadState
|
||||
internal static ulong GetCurrentThreadUniqueId()
|
||||
{
|
||||
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity))
|
||||
if (guestThreadHandle != 0)
|
||||
{
|
||||
return identity.UniqueId;
|
||||
return EnsureGuestThreadIdentity(guestThreadHandle).UniqueId;
|
||||
}
|
||||
|
||||
EnsureCurrentThreadRegistered();
|
||||
return _currentThreadUniqueId;
|
||||
}
|
||||
|
||||
internal static string DescribeThreadHandle(ulong threadHandle)
|
||||
{
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
return TryGetThreadIdentity(threadHandle, out var identity)
|
||||
? $"0x{threadHandle:X16}('{identity.Name}')"
|
||||
: $"0x{threadHandle:X16}";
|
||||
}
|
||||
|
||||
internal static ulong CreateThreadHandle(string name)
|
||||
{
|
||||
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
||||
@@ -59,6 +75,18 @@ internal static class KernelPthreadState
|
||||
return Threads.TryGetValue(threadHandle, out identity);
|
||||
}
|
||||
|
||||
private static ThreadIdentity EnsureGuestThreadIdentity(ulong guestThreadHandle)
|
||||
{
|
||||
if (Threads.TryGetValue(guestThreadHandle, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var uniqueId = unchecked((ulong)Interlocked.Increment(ref _nextUniqueThreadId));
|
||||
var identity = new ThreadIdentity(uniqueId, $"Guest-0x{guestThreadHandle:X}");
|
||||
return Threads.GetOrAdd(guestThreadHandle, identity);
|
||||
}
|
||||
|
||||
private static void EnsureCurrentThreadRegistered()
|
||||
{
|
||||
if (_currentThreadHandle != 0)
|
||||
|
||||
@@ -755,6 +755,52 @@ public static class NetExports
|
||||
return true;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Kcp5d-q1Uo",
|
||||
ExportName = "sceNetInetPton",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetInetPton(CpuContext ctx)
|
||||
{
|
||||
var addressFamily = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
var destinationAddress = ctx[CpuRegister.Rdx];
|
||||
if (sourceAddress == 0 || destinationAddress == 0)
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
if (!TryReadUtf8Z(ctx, sourceAddress, MaxNameLength, out var source))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
var family = addressFamily switch
|
||||
{
|
||||
2 => AddressFamily.InterNetwork, // AF_INET
|
||||
28 => AddressFamily.InterNetworkV6, // AF_INET6
|
||||
_ => AddressFamily.Unknown,
|
||||
};
|
||||
if (family == AddressFamily.Unknown ||
|
||||
!IPAddress.TryParse(source, out var parsed) ||
|
||||
parsed.AddressFamily != family)
|
||||
{
|
||||
// Match BSD inet_pton: return 0 for a parseable-family miss.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var bytes = parsed.GetAddressBytes();
|
||||
if (!ctx.Memory.TryWrite(destinationAddress, bytes))
|
||||
{
|
||||
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
|
||||
}
|
||||
|
||||
TraceNet("inet_pton", addressFamily, sourceAddress, destinationAddress, (ulong)bytes.Length);
|
||||
ctx[CpuRegister.Rax] = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Psml;
|
||||
|
||||
public static class PsmlExports
|
||||
{
|
||||
// Empirically for Astro Bot (PPSA21567):
|
||||
// [0] must be 0x80 (sizeThis / r9); any other value в†’ Allocate length=0
|
||||
// AllocateMainDirectMemory(length=[0]*[16], alignment=[8], type=0xC)
|
||||
// So put desired byte size in [8] (becomes alignment) and page size in [16].
|
||||
private const ulong RequirementStructSize = 0x80;
|
||||
private const ulong SharedResourcesPageSize = 0x10000;
|
||||
private const ulong SharedResourcesBufferSizeBytes = 0x2000000;
|
||||
private const ulong SharedResourcesContextSizeBytes = 0x100000;
|
||||
private const ulong ContextBufferSizeBytes = 0x800000;
|
||||
private const ulong ContextBufferAuxSizeBytes = 0x100000;
|
||||
|
||||
private static int _mfsrInitialized;
|
||||
private static readonly Lock SharedResourcesGate = new();
|
||||
private static readonly Dictionary<ulong, SharedResourcesState> SharedResourcesByDescriptor = new();
|
||||
private static readonly Lock ContextGate = new();
|
||||
private static readonly Dictionary<ulong, ContextState> ContextsByAddress = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3WVD91e12ZQ",
|
||||
ExportName = "scePsmlMfsrInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrInit(CpuContext ctx)
|
||||
{
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
var arg1 = ctx[CpuRegister.Rsi];
|
||||
var arg2 = ctx[CpuRegister.Rdx];
|
||||
Interlocked.Exchange(ref _mfsrInitialized, 1);
|
||||
TracePsml($"mfsr_init arg0=0x{arg0:X} arg1=0x{arg1:X} arg2=0x{arg2:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+2KpvixvL6E",
|
||||
ExportName = "scePsmlMfsrGetSharedResourcesInitRequirement",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetSharedResourcesInitRequirement(CpuContext ctx)
|
||||
{
|
||||
var bufferRequirementAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var flags = ctx[CpuRegister.Rdx];
|
||||
var configAddress = ctx[CpuRegister.Rcx];
|
||||
if (bufferRequirementAddress == 0 || contextRequirementAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, SharedResourcesBufferSizeBytes) ||
|
||||
!WriteMemoryRequirement(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_get_shared_resources_init_requirement buf=0x{bufferRequirementAddress:X16} " +
|
||||
$"ctx=0x{contextRequirementAddress:X16} flags=0x{flags:X} config=0x{configAddress:X16} " +
|
||||
$"buf_size=0x{SharedResourcesBufferSizeBytes:X} ctx_size=0x{SharedResourcesContextSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eWoKNeB6V-k",
|
||||
ExportName = "scePsmlMfsrCreateSharedResources",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrCreateSharedResources(CpuContext ctx)
|
||||
{
|
||||
var descriptorAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var directMemoryAddress = ctx[CpuRegister.Rdx];
|
||||
if (descriptorAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var contextSizeBytes = ReadRequirementSize(ctx, contextRequirementAddress, SharedResourcesContextSizeBytes);
|
||||
var state = new SharedResourcesState(
|
||||
DescriptorAddress: descriptorAddress,
|
||||
DirectMemoryAddress: directMemoryAddress,
|
||||
BufferSizeBytes: SharedResourcesBufferSizeBytes,
|
||||
ContextSizeBytes: contextSizeBytes,
|
||||
PageSizeBytes: SharedResourcesPageSize);
|
||||
|
||||
if (!WriteSharedResourcesDescriptor(ctx, state))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (SharedResourcesGate)
|
||||
{
|
||||
SharedResourcesByDescriptor[descriptorAddress] = state;
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_create_shared_resources desc=0x{descriptorAddress:X16} req=0x{contextRequirementAddress:X16} " +
|
||||
$"direct=0x{directMemoryAddress:X16} buf_size=0x{state.BufferSizeBytes:X} " +
|
||||
$"ctx_size=0x{state.ContextSizeBytes:X} page=0x{state.PageSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ArakEpzsZo0",
|
||||
ExportName = "scePsmlMfsrGetContextBufferRequirement800M3_2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetContextBufferRequirement800M3_2(CpuContext ctx)
|
||||
{
|
||||
var bufferRequirementAddress = ctx[CpuRegister.Rdi];
|
||||
var contextRequirementAddress = ctx[CpuRegister.Rsi];
|
||||
var directMemoryAddress = ctx[CpuRegister.Rdx];
|
||||
if (bufferRequirementAddress == 0 || contextRequirementAddress == 0 || directMemoryAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!WriteMemoryRequirement(ctx, bufferRequirementAddress, ContextBufferSizeBytes) ||
|
||||
!WriteMemoryRequirement(ctx, contextRequirementAddress, ContextBufferAuxSizeBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_get_context_buffer_requirement_800m3_2 buf=0x{bufferRequirementAddress:X16} " +
|
||||
$"ctx=0x{contextRequirementAddress:X16} direct=0x{directMemoryAddress:X16} " +
|
||||
$"buf_size=0x{ContextBufferSizeBytes:X} aux_size=0x{ContextBufferAuxSizeBytes:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gxv3i+MTEzU",
|
||||
ExportName = "scePsmlMfsrCreateContext800M3_2",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrCreateContext800M3_2(CpuContext ctx)
|
||||
{
|
||||
var contextAddress = ctx[CpuRegister.Rdi];
|
||||
var requirementAddress = ctx[CpuRegister.Rsi];
|
||||
var structSize = ctx[CpuRegister.Rdx];
|
||||
var sharedDirectMemory = ctx[CpuRegister.Rcx];
|
||||
var pageSize = ctx[CpuRegister.R8];
|
||||
if (contextAddress == 0 || sharedDirectMemory == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var sharedState = TryFindSharedResources(sharedDirectMemory, contextAddress);
|
||||
var effectiveStructSize = structSize != 0 ? structSize : RequirementStructSize;
|
||||
var effectivePageSize = pageSize != 0 ? pageSize : SharedResourcesPageSize;
|
||||
var sharedDescriptor = sharedState?.DescriptorAddress ?? contextAddress - 0x30;
|
||||
var bufferSize = sharedState?.BufferSizeBytes ?? ContextBufferSizeBytes;
|
||||
|
||||
var state = new ContextState(
|
||||
ContextAddress: contextAddress,
|
||||
SharedResourcesDescriptor: sharedDescriptor,
|
||||
DirectMemoryAddress: sharedDirectMemory,
|
||||
BufferSizeBytes: bufferSize,
|
||||
PageSizeBytes: effectivePageSize,
|
||||
StructSizeBytes: effectiveStructSize);
|
||||
|
||||
if (!WriteContextObject(ctx, state))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (ContextGate)
|
||||
{
|
||||
ContextsByAddress[contextAddress] = state;
|
||||
}
|
||||
|
||||
TracePsml(
|
||||
$"mfsr_create_context_800m3_2 ctx=0x{contextAddress:X16} req=0x{requirementAddress:X16} " +
|
||||
$"struct=0x{effectiveStructSize:X} direct=0x{sharedDirectMemory:X16} " +
|
||||
$"shared_desc=0x{sharedDescriptor:X16} buf_size=0x{bufferSize:X} page=0x{effectivePageSize:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool WriteMemoryRequirement(CpuContext ctx, ulong address, ulong sizeBytes)
|
||||
{
|
||||
return ctx.TryWriteUInt64(address, RequirementStructSize) &&
|
||||
ctx.TryWriteUInt64(address + 0x08, sizeBytes) &&
|
||||
ctx.TryWriteUInt64(address + 0x10, SharedResourcesPageSize);
|
||||
}
|
||||
|
||||
private static ulong ReadRequirementSize(CpuContext ctx, ulong address, ulong fallback)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(address + 0x08, out var sizeBytes) || sizeBytes == 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Guest requirement structs use size @8; reject pointer-like garbage.
|
||||
return sizeBytes > 0x1000_0000UL ? fallback : sizeBytes;
|
||||
}
|
||||
|
||||
private static SharedResourcesState? TryFindSharedResources(ulong directMemoryAddress, ulong contextAddress)
|
||||
{
|
||||
lock (SharedResourcesGate)
|
||||
{
|
||||
foreach (var state in SharedResourcesByDescriptor.Values)
|
||||
{
|
||||
if (state.DirectMemoryAddress == directMemoryAddress)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
var inferredDescriptor = contextAddress >= 0x30 ? contextAddress - 0x30 : 0;
|
||||
if (inferredDescriptor != 0 &&
|
||||
SharedResourcesByDescriptor.TryGetValue(inferredDescriptor, out var byDescriptor))
|
||||
{
|
||||
return byDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool WriteContextObject(CpuContext ctx, ContextState state)
|
||||
{
|
||||
return ctx.TryWriteUInt64(state.ContextAddress + 0x00, state.StructSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x08, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x10, state.SharedResourcesDescriptor) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x18, state.BufferSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x20, state.PageSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x28, state.ContextAddress) &&
|
||||
ctx.TryWriteUInt64(state.ContextAddress + 0x30, state.DirectMemoryAddress);
|
||||
}
|
||||
|
||||
private static bool WriteSharedResourcesDescriptor(CpuContext ctx, SharedResourcesState state)
|
||||
{
|
||||
// Stamp a compact self-describing blob so follow-up PSML calls can treat the
|
||||
// descriptor as initialized guest memory instead of an all-zero placeholder.
|
||||
return ctx.TryWriteUInt64(state.DescriptorAddress + 0x00, RequirementStructSize) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x08, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x10, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x18, state.BufferSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x20, state.ContextSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x28, state.PageSizeBytes) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x30, state.DirectMemoryAddress) &&
|
||||
ctx.TryWriteUInt64(state.DescriptorAddress + 0x38, state.BufferSizeBytes + state.ContextSizeBytes);
|
||||
}
|
||||
|
||||
|
||||
// Astro logo path: SizeInDwords then GetDispatchMfsrPacket900 (RUNLFro+qok).
|
||||
// Unresolved 900 returns non-zero and trips GfxRenderStagePSSR.cpp:266.
|
||||
private const int SoftPacketSizeInDwords = 0x80;
|
||||
private const ulong SoftPacketSizeBytes = SoftPacketSizeInDwords * 4UL;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AHalTX9wFZY",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacketSizeInDwords",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacketSizeInDwords(CpuContext ctx)
|
||||
{
|
||||
// Logo/PSSR path: guest asserts ret == 0, then calls GetDispatchMfsrPacket900
|
||||
// with rdi=SoftPacketSizeInDwords (observed 0x80). Returning the size in rax
|
||||
// tripped :266 and skipped the 900 call (tDISP-s6). SCE_OK keeps the chain.
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
TracePsml(
|
||||
$"mfsr_get_dispatch_packet_size_dwords arg0=0x{arg0:X} " +
|
||||
$"size=0x{SoftPacketSizeInDwords:X} ret=0");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RUNLFro+qok",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket900",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket900(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "900");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "s2psNHUIdjk",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket1000",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket1000(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "1000");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "94iBp3KvIuI",
|
||||
ExportName = "scePsmlMfsrGetDispatchMfsrPacket1100",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePsml")]
|
||||
public static int PsmlMfsrGetDispatchMfsrPacket1100(CpuContext ctx) =>
|
||||
SoftGetDispatchMfsrPacket(ctx, "1100");
|
||||
|
||||
private static int SoftGetDispatchMfsrPacket(CpuContext ctx, string variant)
|
||||
{
|
||||
var arg0 = ctx[CpuRegister.Rdi];
|
||||
var arg1 = ctx[CpuRegister.Rsi];
|
||||
var arg2 = ctx[CpuRegister.Rdx];
|
||||
var arg3 = ctx[CpuRegister.Rcx];
|
||||
// Logo call shape (tDISP-s3): rdi=size_dwords (0x80), rsi/rdx = guest
|
||||
// packet/param buffers. Clear the first mapped buffer arg.
|
||||
var packetAddress = 0UL;
|
||||
foreach (var candidate in new[] { arg1, arg2, arg3 })
|
||||
{
|
||||
if (candidate >= 0x10000)
|
||||
{
|
||||
packetAddress = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var cleared = packetAddress != 0 && TryClearGuestBuffer(ctx, packetAddress, SoftPacketSizeBytes);
|
||||
TracePsml(
|
||||
$"mfsr_get_dispatch_packet_{variant} a0=0x{arg0:X16} a1=0x{arg1:X16} " +
|
||||
$"a2=0x{arg2:X16} a3=0x{arg3:X16} packet=0x{packetAddress:X16} cleared={cleared}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||
{
|
||||
Span<byte> zeroes = stackalloc byte[4096];
|
||||
zeroes.Clear();
|
||||
for (ulong offset = 0; offset < length;)
|
||||
{
|
||||
var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += unchecked((uint)chunkSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TracePsml(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PSML"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] psml.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SharedResourcesState(
|
||||
ulong DescriptorAddress,
|
||||
ulong DirectMemoryAddress,
|
||||
ulong BufferSizeBytes,
|
||||
ulong ContextSizeBytes,
|
||||
ulong PageSizeBytes);
|
||||
|
||||
private readonly record struct ContextState(
|
||||
ulong ContextAddress,
|
||||
ulong SharedResourcesDescriptor,
|
||||
ulong DirectMemoryAddress,
|
||||
ulong BufferSizeBytes,
|
||||
ulong PageSizeBytes,
|
||||
ulong StructSizeBytes);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FFmpeg.AutoGen" />
|
||||
<PackageReference Include="Silk.NET.Input" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
|
||||
@@ -59,4 +59,65 @@ public static class GameServiceStubs
|
||||
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx) => OkWithHandle(ctx, CpuRegister.Rdi);
|
||||
public static int NpUniversalDataSystemPostEvent(CpuContext ctx) => Ok(ctx);
|
||||
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "47UAEuQl+iI", ExportName = "sceNpUniversalDataSystemTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "0HBYxYAjmf0", ExportName = "sceNpGameIntentTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
|
||||
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "jqb7HntFQFc", ExportName = "sceWebBrowserDialogInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "ocHtyBwHfys", ExportName = "sceWebBrowserDialogTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
|
||||
public static int WebBrowserDialogTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")]
|
||||
public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "isruqthpYcw", ExportName = "sceSharePlayInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceSharePlay")]
|
||||
public static int SharePlayInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "0IL1keINExQ", ExportName = "sceShareTerminate",
|
||||
Target = Generation.Gen5, LibraryName = "libSceShareUtility")]
|
||||
public static int ShareTerminate(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "YBiIdcDPrxs", ExportName = "sceShareFeaturePermit",
|
||||
Target = Generation.Gen5, LibraryName = "libSceShareUtility")]
|
||||
public static int ShareFeaturePermit(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "9TrhuGzberQ", ExportName = "sceVoiceInit",
|
||||
Target = Generation.Gen5, LibraryName = "libSceVoice")]
|
||||
public static int VoiceInit(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "clyKUyi3RYU", ExportName = "sceVoiceSetThreadsParams",
|
||||
Target = Generation.Gen5, LibraryName = "libSceVoice")]
|
||||
public static int VoiceSetThreadsParams(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "dPj4ZtRcIWk", ExportName = "sceContentSearchInit",
|
||||
Target = Generation.Gen5, LibraryName = "libSceContentSearch")]
|
||||
public static int ContentSearchInit(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "zoxb0wEChEM", ExportName = "sceContentDeleteInitialize",
|
||||
Target = Generation.Gen5, LibraryName = "libSceContentDelete")]
|
||||
public static int ContentDeleteInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "Fc8qxlKINYQ", ExportName = "sceVideoRecordingSetInfo",
|
||||
Target = Generation.Gen5, LibraryName = "libSceVideoRecording")]
|
||||
public static int VideoRecordingSetInfo(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
// Captured from GTA V Enhanced (PPSA04264); not in the public NID catalog.
|
||||
// Side-effect-free success — same as unresolved stub behavior that kept boot
|
||||
// moving; reverse the ABI before writing guest memory.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceUnknownIkfdt",
|
||||
Target = Generation.Gen5, LibraryName = "libKernel")]
|
||||
public static int UnknownIkfdt(CpuContext ctx) => Ok(ctx);
|
||||
#pragma warning restore SHEM006
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class SystemServiceExports
|
||||
private const int TitleIdFieldSize = 0x10;
|
||||
|
||||
private static string? _mainAppTitleId;
|
||||
private static int _noticeScreenSkipFlag;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? titleId)
|
||||
{
|
||||
@@ -37,14 +38,40 @@ public static class SystemServiceExports
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
// No system notice screen to skip in the emulator; report "do not skip".
|
||||
// Keep the flag state even though the emulator does not display the
|
||||
// system notice screen. Titles use this service as a normal preference
|
||||
// store and expect a later get to observe the value they set.
|
||||
Span<byte> flagBytes = stackalloc byte[1];
|
||||
flagBytes[0] = 0;
|
||||
flagBytes[0] = unchecked((byte)Volatile.Read(ref _noticeScreenSkipFlag));
|
||||
return ctx.Memory.TryWrite(flagAddress, flagBytes)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Lo6Zv94aho",
|
||||
ExportName = "sceSystemServiceDisableNoticeScreenSkipFlagAutoSet",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceDisableNoticeScreenSkipFlagAutoSet(CpuContext ctx) =>
|
||||
ctx.SetReturn(0);
|
||||
|
||||
// Settings entry calls this immediately before spawning SaveModTime/Load
|
||||
// threads. An unresolved stub returns NOT_FOUND and the title can stall in
|
||||
// that path; accept the write and report success.
|
||||
[SysAbiExport(
|
||||
Nid = "Q3utJvma4Mo",
|
||||
ExportName = "sceSystemServiceSetNoticeScreenSkipFlag",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceSetNoticeScreenSkipFlag(CpuContext ctx)
|
||||
{
|
||||
// The native API takes the flag value in the first argument. Treat any
|
||||
// non-zero value as true, matching the bool-like PS5 ABI.
|
||||
Volatile.Write(ref _noticeScreenSkipFlag, ctx[CpuRegister.Rdi] != 0 ? 1 : 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4veE0XiIugA",
|
||||
ExportName = "sceSystemServiceGetMainAppTitleId",
|
||||
@@ -212,4 +239,7 @@ public static class SystemServiceExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceReportAbnormalTermination(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
internal static void ResetForTests() =>
|
||||
Volatile.Write(ref _noticeScreenSkipFlag, 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,894 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
/// <summary>
|
||||
/// Self-contained GPU deswizzle pass: runs the ExactXor detile equation from
|
||||
/// <see cref="GnmTiling.GetDetileParams"/> as a Vulkan compute shader
|
||||
/// (<see cref="SpirvFixedShaders.CreateDetileCompute"/>), writing a linear buffer
|
||||
/// and copying it into a sampled image — the GPU equivalent of the CPU
|
||||
/// <c>GnmTiling.TryDetile</c> + staging upload.
|
||||
///
|
||||
/// Two entry points share the same (verified) recording:
|
||||
/// <see cref="DetileIntoImage"/> is a self-contained one-shot (submit + wait) used
|
||||
/// by the isolation self-test; <see cref="RecordDetile"/> records into a caller's
|
||||
/// command buffer and hands back its transient buffers + descriptor pool for the
|
||||
/// caller to retire with that command buffer's fence — the render-path variant,
|
||||
/// which must never block the render thread.
|
||||
///
|
||||
/// Only ExactXor 4-bytes/element surfaces are handled; <see cref="Supports"/> lets
|
||||
/// the caller fall back to the CPU path for everything else.
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanDetilePass : IDisposable
|
||||
{
|
||||
private const uint LocalSize = 8;
|
||||
private const uint PushConstantBytes = 11 * sizeof(uint);
|
||||
|
||||
private readonly Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly Queue _queue;
|
||||
private readonly PhysicalDevice _physicalDevice;
|
||||
private readonly uint _queueFamilyIndex;
|
||||
|
||||
private ShaderModule _shaderModule;
|
||||
private DescriptorSetLayout _descriptorSetLayout;
|
||||
private PipelineLayout _pipelineLayout;
|
||||
private Pipeline _pipeline;
|
||||
private CommandPool _commandPool;
|
||||
private bool _initialized;
|
||||
private bool _disposed;
|
||||
|
||||
private PhysicalDeviceMemoryProperties _memoryProperties;
|
||||
private bool _memoryPropertiesLoaded;
|
||||
|
||||
private readonly Dictionary<int[], TermBuffer> _xorTermBuffers = new(ReferenceComparer.Instance);
|
||||
private readonly Dictionary<int[], TermBuffer> _blockTermBuffers = new(ReferenceComparer.Instance);
|
||||
private TermBuffer _placeholderTermBuffer;
|
||||
|
||||
private readonly Dictionary<(ulong Bucket, bool HostVisible), Stack<Allocation>> _bufferPool = new();
|
||||
private readonly List<Allocation> _allAllocations = new();
|
||||
|
||||
private readonly Stack<DescriptorSet> _freeDescriptorSets = new();
|
||||
private readonly List<DescriptorPool> _descriptorPools = new();
|
||||
private const uint DescriptorSetsPerPool = 64;
|
||||
private const ulong MinimumBufferBucket = 4096;
|
||||
|
||||
public VulkanDetilePass(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
_vk = vk;
|
||||
_device = device;
|
||||
_queue = queue;
|
||||
_physicalDevice = physicalDevice;
|
||||
_queueFamilyIndex = queueFamilyIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The kernel handles the exact-XOR and block-table modes at 4/8/16
|
||||
/// bytes-per-element (one, two, or four 32-bit words per element). 1/2 bpp are
|
||||
/// sub-word and stay on the CPU.
|
||||
/// </summary>
|
||||
public static bool Supports(in DetileParams parameters) =>
|
||||
(parameters.Equation == DetileEquation.ExactXor ||
|
||||
parameters.Equation == DetileEquation.BlockTable) &&
|
||||
parameters.BytesPerElement is 4 or 8 or 16;
|
||||
|
||||
/// <summary>Opaque handle to the pooled resources one recorded detile is using.
|
||||
/// The caller hands it back to <see cref="Retire"/> once the command buffer they
|
||||
/// were recorded into has completed; nothing is destroyed, the buffers and the
|
||||
/// descriptor set return to this pass's free lists for the next texture.</summary>
|
||||
public sealed class Transients
|
||||
{
|
||||
internal static readonly Transients Empty = new();
|
||||
|
||||
internal Allocation Tiled;
|
||||
internal Allocation Output;
|
||||
internal DescriptorSet Set;
|
||||
internal bool Rented;
|
||||
}
|
||||
|
||||
internal readonly record struct Allocation(
|
||||
VkBuffer Buffer,
|
||||
DeviceMemory Memory,
|
||||
ulong Capacity,
|
||||
bool HostVisible);
|
||||
|
||||
private readonly record struct TermBuffer(VkBuffer Buffer, ulong ByteSize);
|
||||
|
||||
private struct DetileResources
|
||||
{
|
||||
public Allocation Tiled;
|
||||
public Allocation Output;
|
||||
public DescriptorSet Set;
|
||||
public ulong OutputBytes;
|
||||
public uint SrcSliceElements;
|
||||
public uint EquationValue;
|
||||
public uint UintsPerElement;
|
||||
}
|
||||
|
||||
private sealed class ReferenceComparer : IEqualityComparer<int[]>
|
||||
{
|
||||
public static readonly ReferenceComparer Instance = new();
|
||||
|
||||
public bool Equals(int[]? x, int[]? y) => ReferenceEquals(x, y);
|
||||
|
||||
public int GetHashCode(int[] obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the deswizzle of <paramref name="tiled"/> into <paramref name="image"/>
|
||||
/// (<paramref name="texelWidth"/> x <paramref name="texelHeight"/> texels x
|
||||
/// <paramref name="layers"/> array slices, currently in
|
||||
/// <paramref name="currentLayout"/>) onto <paramref name="commandBuffer"/>,
|
||||
/// leaving the image <see cref="ImageLayout.ShaderReadOnlyOptimal"/>. The kernel
|
||||
/// iterates the element grid from <paramref name="parameters"/> (for
|
||||
/// block-compressed formats a 4x4 block is one element, so the element grid is
|
||||
/// smaller than the texel grid). The tiled buffer holds the array slices packed
|
||||
/// contiguously (each an independently tiled 2D surface). Does not submit; the
|
||||
/// caller retires <paramref name="transients"/> with the command buffer's fence.
|
||||
/// Returns false (with empty transients) when unsupported.
|
||||
/// </summary>
|
||||
public bool RecordDetile(
|
||||
CommandBuffer commandBuffer,
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters,
|
||||
out Transients transients)
|
||||
{
|
||||
transients = Transients.Empty;
|
||||
if (_disposed || !Supports(parameters) || texelWidth == 0 || texelHeight == 0 || layers == 0 ||
|
||||
tiled.IsEmpty || tiled.Length % (int)(layers * (uint)parameters.BytesPerElement) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsurePipeline();
|
||||
|
||||
var resources = default(DetileResources);
|
||||
try
|
||||
{
|
||||
PrepareResources(tiled, parameters, layers, ref resources);
|
||||
RecordCommands(commandBuffer, in resources, image, currentLayout, texelWidth, texelHeight, layers, in parameters);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ReleaseResources(in resources);
|
||||
throw;
|
||||
}
|
||||
|
||||
transients = new Transients
|
||||
{
|
||||
Tiled = resources.Tiled,
|
||||
Output = resources.Output,
|
||||
Set = resources.Set,
|
||||
Rented = true,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Retire(Transients transients)
|
||||
{
|
||||
if (_disposed || transients is null || !transients.Rented)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
transients.Rented = false;
|
||||
ReturnBuffer(transients.Tiled);
|
||||
ReturnBuffer(transients.Output);
|
||||
_freeDescriptorSets.Push(transients.Set);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot variant used by the isolation self-test: records the detile onto a
|
||||
/// private command buffer, submits, waits, and frees every transient. Never
|
||||
/// call this on the render thread — its blocking wait would deadlock the
|
||||
/// present pipeline; use <see cref="RecordDetile"/> there.
|
||||
/// </summary>
|
||||
public bool DetileIntoImage(
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
if (_disposed || !Supports(parameters) || texelWidth == 0 || texelHeight == 0 || layers == 0 ||
|
||||
tiled.IsEmpty || tiled.Length % (int)(layers * (uint)parameters.BytesPerElement) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsurePipeline();
|
||||
|
||||
var resources = default(DetileResources);
|
||||
CommandBuffer commandBuffer = default;
|
||||
Fence fence = default;
|
||||
try
|
||||
{
|
||||
PrepareResources(tiled, parameters, layers, ref resources);
|
||||
|
||||
commandBuffer = AllocateCommandBuffer();
|
||||
BeginCommandBuffer(commandBuffer);
|
||||
RecordCommands(commandBuffer, in resources, image, currentLayout, texelWidth, texelHeight, layers, in parameters);
|
||||
Check(_vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(detile)");
|
||||
|
||||
fence = CreateFence();
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, fence), "vkQueueSubmit(detile)");
|
||||
Check(_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue), "vkWaitForFences(detile)");
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fence.Handle != 0)
|
||||
{
|
||||
_vk.DestroyFence(_device, fence, null);
|
||||
}
|
||||
|
||||
if (commandBuffer.Handle != 0)
|
||||
{
|
||||
_vk.FreeCommandBuffers(_device, _commandPool, 1, &commandBuffer);
|
||||
}
|
||||
|
||||
ReleaseResources(in resources);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareResources(ReadOnlySpan<byte> tiled, in DetileParams parameters, uint layers, ref DetileResources resources)
|
||||
{
|
||||
// Binding 1 carries the within-block offset table, binding 2 the Y terms.
|
||||
// ExactXor: xTerm/yTerm are byte offsets; the kernel indexes a uint[], so it
|
||||
// wants element offsets — for a power-of-two element size the low
|
||||
// log2(bpp) bits of every term are 0, so the right shift is exact.
|
||||
// BlockTable: GetDetileParams' block table is already element offsets; it
|
||||
// goes in binding 1 and binding 2 is an unused placeholder.
|
||||
TermBuffer xTerm;
|
||||
TermBuffer yTerm;
|
||||
if (parameters.Equation == DetileEquation.BlockTable)
|
||||
{
|
||||
xTerm = GetTermBuffer(_blockTermBuffers, parameters.BlockTable, shift: 0);
|
||||
yTerm = GetPlaceholderTermBuffer();
|
||||
resources.EquationValue = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shift = BitOperations.TrailingZeroCount((uint)parameters.BytesPerElement);
|
||||
xTerm = GetTermBuffer(_xorTermBuffers, parameters.XByteTerm, shift);
|
||||
yTerm = GetTermBuffer(_xorTermBuffers, parameters.YByteTerm, shift);
|
||||
resources.EquationValue = 0;
|
||||
}
|
||||
|
||||
// The array slices are packed contiguously in the tiled buffer, so each
|
||||
// slice's element stride is the whole tiled buffer split evenly by layer.
|
||||
// Element sizes are in bytes-per-element; the kernel moves bpp/4 words each.
|
||||
var bytesPerElement = (uint)parameters.BytesPerElement;
|
||||
resources.UintsPerElement = bytesPerElement / sizeof(uint);
|
||||
resources.SrcSliceElements = (uint)((ulong)tiled.Length / bytesPerElement / layers);
|
||||
resources.OutputBytes =
|
||||
(ulong)parameters.ElementsWide * (ulong)parameters.ElementsHigh * bytesPerElement * layers;
|
||||
|
||||
resources.Tiled = RentBuffer((ulong)tiled.Length, hostVisible: true);
|
||||
UploadBytes(resources.Tiled.Memory, tiled);
|
||||
resources.Output = RentBuffer(resources.OutputBytes, hostVisible: false);
|
||||
|
||||
resources.Set = RentDescriptorSet();
|
||||
WriteDescriptors(
|
||||
resources.Set,
|
||||
(resources.Tiled.Buffer, (ulong)tiled.Length),
|
||||
(xTerm.Buffer, xTerm.ByteSize),
|
||||
(yTerm.Buffer, yTerm.ByteSize),
|
||||
(resources.Output.Buffer, resources.OutputBytes));
|
||||
}
|
||||
|
||||
private TermBuffer GetTermBuffer(Dictionary<int[], TermBuffer> cache, int[] table, int shift)
|
||||
{
|
||||
if (cache.TryGetValue(table, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var terms = ToElementTerms(table, shift);
|
||||
var byteSize = (ulong)terms.Length * sizeof(uint);
|
||||
var allocation = CreateBuffer(byteSize, hostVisible: true);
|
||||
UploadUInts(allocation.Memory, terms);
|
||||
var termBuffer = new TermBuffer(allocation.Buffer, byteSize);
|
||||
cache[table] = termBuffer;
|
||||
return termBuffer;
|
||||
}
|
||||
|
||||
private TermBuffer GetPlaceholderTermBuffer()
|
||||
{
|
||||
if (_placeholderTermBuffer.Buffer.Handle != 0)
|
||||
{
|
||||
return _placeholderTermBuffer;
|
||||
}
|
||||
|
||||
var allocation = CreateBuffer(sizeof(uint), hostVisible: true);
|
||||
UploadUInts(allocation.Memory, [0u]);
|
||||
_placeholderTermBuffer = new TermBuffer(allocation.Buffer, sizeof(uint));
|
||||
return _placeholderTermBuffer;
|
||||
}
|
||||
|
||||
private static ulong BucketFor(ulong size)
|
||||
{
|
||||
var bucket = MinimumBufferBucket;
|
||||
while (bucket < size)
|
||||
{
|
||||
bucket <<= 1;
|
||||
}
|
||||
|
||||
return bucket;
|
||||
}
|
||||
|
||||
private Allocation RentBuffer(ulong size, bool hostVisible)
|
||||
{
|
||||
var bucket = BucketFor(size);
|
||||
if (_bufferPool.TryGetValue((bucket, hostVisible), out var free) && free.Count > 0)
|
||||
{
|
||||
return free.Pop();
|
||||
}
|
||||
|
||||
return CreateBuffer(bucket, hostVisible);
|
||||
}
|
||||
|
||||
private void ReturnBuffer(Allocation allocation)
|
||||
{
|
||||
if (allocation.Buffer.Handle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = (allocation.Capacity, allocation.HostVisible);
|
||||
if (!_bufferPool.TryGetValue(key, out var free))
|
||||
{
|
||||
free = new Stack<Allocation>();
|
||||
_bufferPool[key] = free;
|
||||
}
|
||||
|
||||
free.Push(allocation);
|
||||
}
|
||||
|
||||
private DescriptorSet RentDescriptorSet()
|
||||
{
|
||||
if (_freeDescriptorSets.Count > 0)
|
||||
{
|
||||
return _freeDescriptorSets.Pop();
|
||||
}
|
||||
|
||||
var poolSize = new DescriptorPoolSize
|
||||
{
|
||||
Type = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 4 * DescriptorSetsPerPool,
|
||||
};
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = DescriptorSetsPerPool,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateDescriptorPool(_device, &poolInfo, null, out var pool),
|
||||
"vkCreateDescriptorPool(detile)");
|
||||
_descriptorPools.Add(pool);
|
||||
|
||||
var layouts = stackalloc DescriptorSetLayout[(int)DescriptorSetsPerPool];
|
||||
for (var index = 0; index < DescriptorSetsPerPool; index++)
|
||||
{
|
||||
layouts[index] = _descriptorSetLayout;
|
||||
}
|
||||
|
||||
var sets = stackalloc DescriptorSet[(int)DescriptorSetsPerPool];
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = DescriptorSetsPerPool,
|
||||
PSetLayouts = layouts,
|
||||
};
|
||||
Check(
|
||||
_vk.AllocateDescriptorSets(_device, &allocateInfo, sets),
|
||||
"vkAllocateDescriptorSets(detile)");
|
||||
for (var index = 0; index < DescriptorSetsPerPool; index++)
|
||||
{
|
||||
_freeDescriptorSets.Push(sets[index]);
|
||||
}
|
||||
|
||||
return _freeDescriptorSets.Pop();
|
||||
}
|
||||
|
||||
private void ReleaseResources(in DetileResources resources)
|
||||
{
|
||||
ReturnBuffer(resources.Tiled);
|
||||
ReturnBuffer(resources.Output);
|
||||
if (resources.Set.Handle != 0)
|
||||
{
|
||||
_freeDescriptorSets.Push(resources.Set);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordCommands(
|
||||
CommandBuffer commandBuffer,
|
||||
in DetileResources resources,
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
// The kernel iterates the element grid (smaller than the texel grid for
|
||||
// block-compressed formats); the image copy below uses the texel grid.
|
||||
var elementsWide = (uint)parameters.ElementsWide;
|
||||
var elementsHigh = (uint)parameters.ElementsHigh;
|
||||
|
||||
var descriptorSet = resources.Set;
|
||||
_vk.CmdBindPipeline(commandBuffer, PipelineBindPoint.Compute, _pipeline);
|
||||
_vk.CmdBindDescriptorSets(
|
||||
commandBuffer, PipelineBindPoint.Compute, _pipelineLayout, 0, 1, &descriptorSet, 0, null);
|
||||
|
||||
Span<uint> push =
|
||||
[
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
(uint)parameters.BlockWidth,
|
||||
(uint)parameters.BlockHeight,
|
||||
(uint)parameters.BlockElements,
|
||||
(uint)parameters.BlocksPerRow,
|
||||
(uint)parameters.XMask,
|
||||
(uint)parameters.YMask,
|
||||
resources.SrcSliceElements,
|
||||
resources.EquationValue,
|
||||
resources.UintsPerElement,
|
||||
];
|
||||
fixed (uint* pushPointer = push)
|
||||
{
|
||||
_vk.CmdPushConstants(
|
||||
commandBuffer, _pipelineLayout, ShaderStageFlags.ComputeBit, 0, PushConstantBytes, pushPointer);
|
||||
}
|
||||
|
||||
// X is widened by uintsPerElement (each thread copies one word); one
|
||||
// dispatch-Z layer per array slice.
|
||||
_vk.CmdDispatch(
|
||||
commandBuffer,
|
||||
(elementsWide * resources.UintsPerElement + LocalSize - 1) / LocalSize,
|
||||
(elementsHigh + LocalSize - 1) / LocalSize,
|
||||
layers);
|
||||
|
||||
// Compute store -> transfer read on the linear output buffer.
|
||||
var outputBarrier = new BufferMemoryBarrier
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = resources.Output.Buffer,
|
||||
Offset = 0,
|
||||
Size = resources.OutputBytes,
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.ComputeShaderBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&outputBarrier,
|
||||
0,
|
||||
null);
|
||||
|
||||
var initialized = currentLayout == ImageLayout.ShaderReadOnlyOptimal;
|
||||
TransitionImage(
|
||||
commandBuffer,
|
||||
image,
|
||||
currentLayout,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
initialized ? AccessFlags.ShaderReadBit : 0,
|
||||
AccessFlags.TransferWriteBit,
|
||||
initialized ? PipelineStageFlags.FragmentShaderBit : PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
layers);
|
||||
|
||||
// The output buffer is layer-major, tightly packed (BufferRowLength 0 =>
|
||||
// one element-row per texel-row, which for compressed formats is the block
|
||||
// row), so a single copy fills every array layer. Extent is in texels.
|
||||
var copyRegion = new BufferImageCopy
|
||||
{
|
||||
BufferOffset = 0,
|
||||
BufferRowLength = 0,
|
||||
BufferImageHeight = 0,
|
||||
ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, layers),
|
||||
ImageOffset = default,
|
||||
ImageExtent = new Extent3D(texelWidth, texelHeight, 1),
|
||||
};
|
||||
_vk.CmdCopyBufferToImage(
|
||||
commandBuffer, resources.Output.Buffer, image, ImageLayout.TransferDstOptimal, 1, ©Region);
|
||||
|
||||
TransitionImage(
|
||||
commandBuffer,
|
||||
image,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
AccessFlags.TransferWriteBit,
|
||||
AccessFlags.ShaderReadBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
PipelineStageFlags.FragmentShaderBit,
|
||||
layers);
|
||||
}
|
||||
|
||||
private void EnsurePipeline()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateDetileCompute();
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateShaderModule(_device, &moduleInfo, null, out _shaderModule),
|
||||
"vkCreateShaderModule(detile)");
|
||||
}
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[4];
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 4,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateDescriptorSetLayout(_device, &layoutInfo, null, out _descriptorSetLayout),
|
||||
"vkCreateDescriptorSetLayout(detile)");
|
||||
|
||||
var pushRange = new PushConstantRange
|
||||
{
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
Offset = 0,
|
||||
Size = PushConstantBytes,
|
||||
};
|
||||
var setLayout = _descriptorSetLayout;
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &setLayout,
|
||||
PushConstantRangeCount = 1,
|
||||
PPushConstantRanges = &pushRange,
|
||||
};
|
||||
Check(
|
||||
_vk.CreatePipelineLayout(_device, &pipelineLayoutInfo, null, out _pipelineLayout),
|
||||
"vkCreatePipelineLayout(detile)");
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = _pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = _shaderModule,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
Check(
|
||||
_vk.CreateComputePipelines(_device, default, 1, &pipelineInfo, null, out _pipeline),
|
||||
"vkCreateComputePipelines(detile)");
|
||||
}
|
||||
|
||||
var poolInfo = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = _queueFamilyIndex,
|
||||
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateCommandPool(_device, &poolInfo, null, out _commandPool),
|
||||
"vkCreateCommandPool(detile)");
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private static uint[] ToElementTerms(int[] byteTerms, int shift)
|
||||
{
|
||||
var terms = new uint[byteTerms.Length];
|
||||
for (var index = 0; index < byteTerms.Length; index++)
|
||||
{
|
||||
terms[index] = (uint)byteTerms[index] >> shift;
|
||||
}
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
private Allocation CreateBuffer(ulong size, bool hostVisible)
|
||||
{
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = BufferUsageFlags.StorageBufferBit | BufferUsageFlags.TransferSrcBit,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
Check(_vk.CreateBuffer(_device, &bufferInfo, null, out var buffer), "vkCreateBuffer(detile)");
|
||||
|
||||
_vk.GetBufferMemoryRequirements(_device, buffer, out var requirements);
|
||||
var required = hostVisible
|
||||
? MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit
|
||||
: MemoryPropertyFlags.DeviceLocalBit;
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, required, hostVisible),
|
||||
};
|
||||
Check(_vk.AllocateMemory(_device, &allocateInfo, null, out var memory), "vkAllocateMemory(detile)");
|
||||
Check(_vk.BindBufferMemory(_device, buffer, memory, 0), "vkBindBufferMemory(detile)");
|
||||
var allocation = new Allocation(buffer, memory, size, hostVisible);
|
||||
_allAllocations.Add(allocation);
|
||||
return allocation;
|
||||
}
|
||||
|
||||
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags, bool hostVisible)
|
||||
{
|
||||
if (!_memoryPropertiesLoaded)
|
||||
{
|
||||
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out _memoryProperties);
|
||||
_memoryPropertiesLoaded = true;
|
||||
}
|
||||
|
||||
fixed (PhysicalDeviceMemoryProperties* properties = &_memoryProperties)
|
||||
{
|
||||
var memoryTypes = &properties->MemoryTypes.Element0;
|
||||
for (uint index = 0; index < properties->MemoryTypeCount; index++)
|
||||
{
|
||||
if ((typeBits & (1u << (int)index)) != 0 &&
|
||||
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hostVisible)
|
||||
{
|
||||
for (uint index = 0; index < properties->MemoryTypeCount; index++)
|
||||
{
|
||||
if ((typeBits & (1u << (int)index)) != 0)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No compatible Vulkan memory type for detile.");
|
||||
}
|
||||
|
||||
private void UploadBytes(DeviceMemory memory, ReadOnlySpan<byte> data)
|
||||
{
|
||||
void* mapped;
|
||||
Check(_vk.MapMemory(_device, memory, 0, (ulong)data.Length, 0, &mapped), "vkMapMemory(detile)");
|
||||
data.CopyTo(new Span<byte>(mapped, data.Length));
|
||||
_vk.UnmapMemory(_device, memory);
|
||||
}
|
||||
|
||||
private void UploadUInts(DeviceMemory memory, uint[] data)
|
||||
{
|
||||
void* mapped;
|
||||
var byteCount = (ulong)data.Length * sizeof(uint);
|
||||
Check(_vk.MapMemory(_device, memory, 0, byteCount, 0, &mapped), "vkMapMemory(detile terms)");
|
||||
data.AsSpan().CopyTo(new Span<uint>(mapped, data.Length));
|
||||
_vk.UnmapMemory(_device, memory);
|
||||
}
|
||||
|
||||
private void WriteDescriptors(
|
||||
DescriptorSet descriptorSet,
|
||||
(VkBuffer Buffer, ulong Size) binding0,
|
||||
(VkBuffer Buffer, ulong Size) binding1,
|
||||
(VkBuffer Buffer, ulong Size) binding2,
|
||||
(VkBuffer Buffer, ulong Size) binding3)
|
||||
{
|
||||
var buffers = stackalloc DescriptorBufferInfo[4]
|
||||
{
|
||||
new DescriptorBufferInfo { Buffer = binding0.Buffer, Offset = 0, Range = binding0.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding1.Buffer, Offset = 0, Range = binding1.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding2.Buffer, Offset = 0, Range = binding2.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding3.Buffer, Offset = 0, Range = binding3.Size },
|
||||
};
|
||||
|
||||
var writes = stackalloc WriteDescriptorSet[4];
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
writes[index] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = descriptorSet,
|
||||
DstBinding = index,
|
||||
DstArrayElement = 0,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = &buffers[index],
|
||||
};
|
||||
}
|
||||
|
||||
_vk.UpdateDescriptorSets(_device, 4, writes, 0, null);
|
||||
}
|
||||
|
||||
private CommandBuffer AllocateCommandBuffer()
|
||||
{
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = _commandPool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
Check(
|
||||
_vk.AllocateCommandBuffers(_device, &allocateInfo, out var commandBuffer),
|
||||
"vkAllocateCommandBuffers(detile)");
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
private void BeginCommandBuffer(CommandBuffer commandBuffer)
|
||||
{
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(_vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(detile)");
|
||||
}
|
||||
|
||||
private void TransitionImage(
|
||||
CommandBuffer commandBuffer,
|
||||
Image image,
|
||||
ImageLayout oldLayout,
|
||||
ImageLayout newLayout,
|
||||
AccessFlags srcAccess,
|
||||
AccessFlags dstAccess,
|
||||
PipelineStageFlags srcStage,
|
||||
PipelineStageFlags dstStage,
|
||||
uint layers)
|
||||
{
|
||||
var barrier = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = srcAccess,
|
||||
DstAccessMask = dstAccess,
|
||||
OldLayout = oldLayout,
|
||||
NewLayout = newLayout,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image,
|
||||
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, layers),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, null, 0, null, 1, &barrier);
|
||||
}
|
||||
|
||||
private Fence CreateFence()
|
||||
{
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(_vk.CreateFence(_device, &fenceInfo, null, out var fence), "vkCreateFence(detile)");
|
||||
return fence;
|
||||
}
|
||||
|
||||
private void DestroyBuffer(VkBuffer buffer, DeviceMemory memory)
|
||||
{
|
||||
if (buffer.Handle != 0)
|
||||
{
|
||||
_vk.DestroyBuffer(_device, buffer, null);
|
||||
}
|
||||
|
||||
if (memory.Handle != 0)
|
||||
{
|
||||
_vk.FreeMemory(_device, memory, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Check(Result result, string operation)
|
||||
{
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation} failed: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
foreach (var allocation in _allAllocations)
|
||||
{
|
||||
DestroyBuffer(allocation.Buffer, allocation.Memory);
|
||||
}
|
||||
|
||||
_allAllocations.Clear();
|
||||
_bufferPool.Clear();
|
||||
_xorTermBuffers.Clear();
|
||||
_blockTermBuffers.Clear();
|
||||
_placeholderTermBuffer = default;
|
||||
_freeDescriptorSets.Clear();
|
||||
foreach (var pool in _descriptorPools)
|
||||
{
|
||||
_vk.DestroyDescriptorPool(_device, pool, null);
|
||||
}
|
||||
|
||||
_descriptorPools.Clear();
|
||||
|
||||
if (_pipeline.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipeline(_device, _pipeline, null);
|
||||
}
|
||||
|
||||
if (_pipelineLayout.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipelineLayout(_device, _pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (_descriptorSetLayout.Handle != 0)
|
||||
{
|
||||
_vk.DestroyDescriptorSetLayout(_device, _descriptorSetLayout, null);
|
||||
}
|
||||
|
||||
if (_shaderModule.Handle != 0)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, _shaderModule, null);
|
||||
}
|
||||
|
||||
if (_commandPool.Handle != 0)
|
||||
{
|
||||
_vk.DestroyCommandPool(_device, _commandPool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in GPU equivalence check for <see cref="VulkanDetilePass"/>. When
|
||||
/// SHARPEMU_DETILE_SELFTEST=1 it builds a known tiled surface, deswizzles it on
|
||||
/// the GPU into a real image, reads the image back, and compares against the CPU
|
||||
/// <see cref="GnmTiling.TryDetile"/> — the same equivalence the unit test proves
|
||||
/// for the params, now end-to-end through the actual Vulkan pass. It logs
|
||||
/// [DETILE-SELFTEST] PASS/FAIL and never throws into startup (any failure is
|
||||
/// caught and logged), so it is safe to leave wired.
|
||||
/// </summary>
|
||||
internal static unsafe class VulkanDetileSelfTest
|
||||
{
|
||||
private const uint Width = 256;
|
||||
private const uint Height = 256;
|
||||
|
||||
// (swizzleMode, bytesPerElement, image format). Mode 27 is exact-XOR, mode 8
|
||||
// (64 KiB Z) is block-table — both branches. bpp 4/8/16 exercises the
|
||||
// one/two/four-words-per-element copy. These formats are non-block-compressed
|
||||
// (element grid == texel grid), so Width/Height are both element and texel dims.
|
||||
private static readonly (uint Mode, int Bpp, Format Format)[] Cases =
|
||||
[
|
||||
(27, 4, Format.R8G8B8A8Unorm),
|
||||
(8, 4, Format.R8G8B8A8Unorm),
|
||||
(27, 8, Format.R32G32Uint),
|
||||
(27, 16, Format.R32G32B32A32Uint),
|
||||
];
|
||||
|
||||
public static void RunIfRequested(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("SHARPEMU_DETILE_SELFTEST") != "1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Run(vk, device, queue, physicalDevice, queueFamilyIndex);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[DETILE-SELFTEST] FAIL (exception): {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Run(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
using var pass = new VulkanDetilePass(vk, device, queue, physicalDevice, queueFamilyIndex);
|
||||
var commandPool = CreateCommandPool(vk, device, queueFamilyIndex);
|
||||
try
|
||||
{
|
||||
foreach (var (mode, bpp, format) in Cases)
|
||||
{
|
||||
// A plain 2D texture (1 layer) and an array texture (2 layers) — the
|
||||
// arrayed case exercises the kernel's dispatch-Z slice addressing.
|
||||
RunCase(vk, device, physicalDevice, queue, commandPool, pass, mode, bpp, format, layers: 1);
|
||||
RunCase(vk, device, physicalDevice, queue, commandPool, pass, mode, bpp, format, layers: 2);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (commandPool.Handle != 0)
|
||||
{
|
||||
vk.DestroyCommandPool(device, commandPool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunCase(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
VulkanDetilePass pass,
|
||||
uint swizzleMode,
|
||||
int bytesPerElement,
|
||||
Format format,
|
||||
uint layers)
|
||||
{
|
||||
var parameters = GnmTiling.GetDetileParams(swizzleMode, bytesPerElement, (int)Width, (int)Height);
|
||||
if (!parameters.IsSupported || !VulkanDetilePass.Supports(parameters))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[DETILE-SELFTEST] FAIL: mode {swizzleMode} bpp {bytesPerElement} not supported by the GPU pass.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Whole-block tiled source with a per-layer-distinct deterministic pattern
|
||||
// (so a slice mix-up is caught), the array slices packed contiguously.
|
||||
var blocksHigh = ((int)Height + parameters.BlockHeight - 1) / parameters.BlockHeight;
|
||||
var sliceTiledBytes = (int)((long)parameters.BlocksPerRow * blocksHigh * parameters.BlockBytes);
|
||||
var sliceLinearBytes = (int)(Width * Height * bytesPerElement);
|
||||
var tiled = new byte[sliceTiledBytes * layers];
|
||||
var expected = new byte[sliceLinearBytes * layers];
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
for (var index = 0; index < sliceTiledBytes; index++)
|
||||
{
|
||||
tiled[layer * sliceTiledBytes + index] = (byte)((index * 31 + 7 + layer * 101) & 0xFF);
|
||||
}
|
||||
|
||||
if (!GnmTiling.TryDetile(
|
||||
tiled.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
expected.AsSpan(layer * sliceLinearBytes, sliceLinearBytes),
|
||||
swizzleMode, (int)Width, (int)Height, bytesPerElement))
|
||||
{
|
||||
Console.Error.WriteLine("[DETILE-SELFTEST] FAIL: CPU TryDetile declined.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tiledBytes = tiled;
|
||||
var label = $"mode{swizzleMode} {bytesPerElement}bpp x{layers}";
|
||||
|
||||
// Phase 1: the one-shot DetileIntoImage (submit + wait in place).
|
||||
VerifyPhase(
|
||||
vk, device, physicalDevice, queue, commandPool, expected, layers, format, $"DetileIntoImage {label}",
|
||||
image => pass.DetileIntoImage(image, ImageLayout.Undefined, Width, Height, layers, tiledBytes, parameters));
|
||||
|
||||
// Phase 2: RecordDetile — the exact code path the render loop uses
|
||||
// (record into a command buffer, submit, retire the transients).
|
||||
VerifyPhase(
|
||||
vk, device, physicalDevice, queue, commandPool, expected, layers, format, $"RecordDetile {label}",
|
||||
image => RecordDetileAndSubmit(vk, device, queue, commandPool, pass, image, layers, tiledBytes, parameters));
|
||||
}
|
||||
|
||||
private static void VerifyPhase(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
byte[] expected,
|
||||
uint layers,
|
||||
Format format,
|
||||
string label,
|
||||
Func<Image, bool> detile)
|
||||
{
|
||||
var image = CreateImage(vk, device, physicalDevice, format, layers, out var imageMemory);
|
||||
var readback = CreateHostBuffer(
|
||||
vk, device, physicalDevice, (ulong)expected.Length, BufferUsageFlags.TransferDstBit, out var readbackMemory);
|
||||
try
|
||||
{
|
||||
if (!detile(image))
|
||||
{
|
||||
Console.Error.WriteLine($"[DETILE-SELFTEST] {label} FAIL: declined.");
|
||||
return;
|
||||
}
|
||||
|
||||
CopyImageToBuffer(vk, device, queue, commandPool, image, readback, layers);
|
||||
|
||||
void* mapped;
|
||||
Check(
|
||||
vk.MapMemory(device, readbackMemory, 0, (ulong)expected.Length, 0, &mapped),
|
||||
$"vkMapMemory(selftest {label})");
|
||||
var actual = new Span<byte>(mapped, expected.Length);
|
||||
var firstMismatch = -1;
|
||||
for (var index = 0; index < expected.Length; index++)
|
||||
{
|
||||
if (actual[index] != expected[index])
|
||||
{
|
||||
firstMismatch = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
vk.UnmapMemory(device, readbackMemory);
|
||||
|
||||
Console.Error.WriteLine(firstMismatch < 0
|
||||
? $"[DETILE-SELFTEST] {label} PASS: {Width}x{Height}x{layers} matches CPU detile ({expected.Length} bytes)."
|
||||
: $"[DETILE-SELFTEST] {label} FAIL: first mismatch at byte {firstMismatch}.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (readback.Handle != 0)
|
||||
{
|
||||
vk.DestroyBuffer(device, readback, null);
|
||||
}
|
||||
|
||||
if (readbackMemory.Handle != 0)
|
||||
{
|
||||
vk.FreeMemory(device, readbackMemory, null);
|
||||
}
|
||||
|
||||
if (image.Handle != 0)
|
||||
{
|
||||
vk.DestroyImage(device, image, null);
|
||||
}
|
||||
|
||||
if (imageMemory.Handle != 0)
|
||||
{
|
||||
vk.FreeMemory(device, imageMemory, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Records the detile into a fresh command buffer, submits, waits, and retires
|
||||
// the transients exactly as the presenter's batch does — verifying the render
|
||||
// path's code (RecordDetile) without needing a game to trigger it.
|
||||
private static bool RecordDetileAndSubmit(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
VulkanDetilePass pass,
|
||||
Image image,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
var commandBuffer = AllocateCommandBuffer(vk, device, commandPool);
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(selftest record)");
|
||||
|
||||
if (!pass.RecordDetile(
|
||||
commandBuffer, image, ImageLayout.Undefined, Width, Height, layers, tiled, parameters, out var transients))
|
||||
{
|
||||
_ = vk.EndCommandBuffer(commandBuffer);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(selftest record)");
|
||||
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(vk.CreateFence(device, &fenceInfo, null, out var fence), "vkCreateFence(selftest record)");
|
||||
try
|
||||
{
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(vk.QueueSubmit(queue, 1, &submitInfo, fence), "vkQueueSubmit(selftest record)");
|
||||
Check(vk.WaitForFences(device, 1, &fence, true, ulong.MaxValue), "vkWaitForFences(selftest record)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
vk.DestroyFence(device, fence, null);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
pass.Retire(transients);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Image CreateImage(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Format format,
|
||||
uint layers,
|
||||
out DeviceMemory memory)
|
||||
{
|
||||
var imageInfo = new ImageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageCreateInfo,
|
||||
ImageType = ImageType.Type2D,
|
||||
Format = format,
|
||||
Extent = new Extent3D(Width, Height, 1),
|
||||
MipLevels = 1,
|
||||
ArrayLayers = layers,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
Tiling = ImageTiling.Optimal,
|
||||
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
InitialLayout = ImageLayout.Undefined,
|
||||
};
|
||||
Check(vk.CreateImage(device, &imageInfo, null, out var image), "vkCreateImage(selftest)");
|
||||
|
||||
vk.GetImageMemoryRequirements(device, image, out var requirements);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
vk,
|
||||
physicalDevice,
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.DeviceLocalBit),
|
||||
};
|
||||
Check(vk.AllocateMemory(device, &allocateInfo, null, out memory), "vkAllocateMemory(selftest image)");
|
||||
Check(vk.BindImageMemory(device, image, memory, 0), "vkBindImageMemory(selftest)");
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void CopyImageToBuffer(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
Image image,
|
||||
VkBuffer destination,
|
||||
uint layers)
|
||||
{
|
||||
var commandBuffer = AllocateCommandBuffer(vk, device, commandPool);
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(selftest readback)");
|
||||
|
||||
// DetileIntoImage left the image ShaderReadOnly; move it to TransferSrc.
|
||||
var toTransferSrc = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.ShaderReadBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image,
|
||||
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, layers),
|
||||
};
|
||||
vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.FragmentShaderBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&toTransferSrc);
|
||||
|
||||
// Layer-major readback: one copy pulls every array slice back into the
|
||||
// buffer contiguously, matching the packed `expected` layout.
|
||||
var region = new BufferImageCopy
|
||||
{
|
||||
BufferOffset = 0,
|
||||
BufferRowLength = 0,
|
||||
BufferImageHeight = 0,
|
||||
ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, layers),
|
||||
ImageOffset = default,
|
||||
ImageExtent = new Extent3D(Width, Height, 1),
|
||||
};
|
||||
vk.CmdCopyImageToBuffer(
|
||||
commandBuffer,
|
||||
image,
|
||||
ImageLayout.TransferSrcOptimal,
|
||||
destination,
|
||||
1,
|
||||
®ion);
|
||||
|
||||
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(selftest readback)");
|
||||
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(vk.CreateFence(device, &fenceInfo, null, out var fence), "vkCreateFence(selftest)");
|
||||
try
|
||||
{
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(vk.QueueSubmit(queue, 1, &submitInfo, fence), "vkQueueSubmit(selftest readback)");
|
||||
Check(
|
||||
vk.WaitForFences(device, 1, &fence, true, ulong.MaxValue),
|
||||
"vkWaitForFences(selftest readback)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
vk.DestroyFence(device, fence, null);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandPool CreateCommandPool(Vk vk, Device device, uint queueFamilyIndex)
|
||||
{
|
||||
var poolInfo = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = queueFamilyIndex,
|
||||
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||
};
|
||||
Check(vk.CreateCommandPool(device, &poolInfo, null, out var pool), "vkCreateCommandPool(selftest)");
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static CommandBuffer AllocateCommandBuffer(Vk vk, Device device, CommandPool commandPool)
|
||||
{
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = commandPool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
Check(
|
||||
vk.AllocateCommandBuffers(device, &allocateInfo, out var commandBuffer),
|
||||
"vkAllocateCommandBuffers(selftest)");
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
private static VkBuffer CreateHostBuffer(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
ulong size,
|
||||
BufferUsageFlags usage,
|
||||
out DeviceMemory memory)
|
||||
{
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = usage,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
Check(vk.CreateBuffer(device, &bufferInfo, null, out var buffer), "vkCreateBuffer(selftest)");
|
||||
|
||||
vk.GetBufferMemoryRequirements(device, buffer, out var requirements);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
vk,
|
||||
physicalDevice,
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit),
|
||||
};
|
||||
Check(vk.AllocateMemory(device, &allocateInfo, null, out memory), "vkAllocateMemory(selftest buffer)");
|
||||
Check(vk.BindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory(selftest)");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static uint FindMemoryType(
|
||||
Vk vk,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint typeBits,
|
||||
MemoryPropertyFlags requiredFlags)
|
||||
{
|
||||
vk.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
|
||||
var memoryTypes = &properties.MemoryTypes.Element0;
|
||||
for (uint index = 0; index < properties.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((typeBits & (1u << (int)index)) != 0 &&
|
||||
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No compatible Vulkan memory type for the detile self-test.");
|
||||
}
|
||||
|
||||
private static void Check(Result result, string operation)
|
||||
{
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation} failed: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,37 @@ public static class VoiceQoSExports
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Trpt2QBZHCI",
|
||||
ExportName = "sceVoiceQoSGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSGetStatus(CpuContext ctx)
|
||||
{
|
||||
// Returns 0 to indicate connected state (voice available)
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FuXenJLkk-c",
|
||||
ExportName = "sceVoiceQoSTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSTerminate(CpuContext ctx)
|
||||
{
|
||||
// No-op: cleanup is handled by emulator shutdown
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+0lOiPZjnBI",
|
||||
ExportName = "sceVoiceQoSSetMode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSSetMode(CpuContext ctx)
|
||||
{
|
||||
// No-op: mode configuration is not emulated
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ public static class MslFixedShaders
|
||||
/// </summary>
|
||||
public static string CreateDepthOnlyFragment() => MslTemplates.Render("depth_only_fragment");
|
||||
|
||||
/// <summary>
|
||||
/// Compute kernel that deswizzles one RDNA2 exact-XOR tiled surface (swizzle
|
||||
/// modes 5/9/24/27) at 4 bytes/element into a linear output buffer — the MSL
|
||||
/// twin of <c>SpirvFixedShaders.CreateDetileCompute</c>. Entry point
|
||||
/// "detile_cs"; buffers 0=tiled, 1=xTerm, 2=yTerm, 3=out, 4=DetileParams.
|
||||
/// </summary>
|
||||
public static string CreateDetileCompute() => MslTemplates.Render("detile_compute");
|
||||
|
||||
private static string Format(float value) =>
|
||||
value.ToString("0.0######", CultureInfo.InvariantCulture) + "f";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#include <metal_stdlib>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
// GPU deswizzle for RDNA2 tiled surfaces at 4/8/16 bytes/element — the MSL twin of
|
||||
// SpirvFixedShaders.CreateDetileCompute and a direct mirror of
|
||||
// GnmTiling.GetDetileParams. Handles both supported equation families and both
|
||||
// plain 2D textures and array textures (one grid-Z layer per slice; the caller
|
||||
// packs the tiled slices contiguously). width/height are ELEMENT dims (for
|
||||
// block-compressed formats a 4x4 block is one element); each element spans
|
||||
// uintsPerElement = bpp/4 words, and the X grid is widened by that factor so each
|
||||
// thread copies one word (elemX = gidX / upe, word = gidX % upe):
|
||||
// inBlock = equation == 1 // BlockTable, modes 1/4/8
|
||||
// ? blockTable[(y % blockHeight) * blockWidth + (elemX % blockWidth)]
|
||||
// : xTerm[elemX & xMask] ^ yTerm[y & yMask]; // ExactXor 5/9/24/27
|
||||
// srcElem = z * srcSliceElements
|
||||
// + (y / blockHeight * blocksPerRow + elemX / blockWidth) * blockElements
|
||||
// + inBlock;
|
||||
// dstElem = z * width * height + y * width + elemX;
|
||||
// out[dstElem * upe + word] = tiled[srcElem * upe + word];
|
||||
// Term tables hold ELEMENT offsets. buffer(1) carries xTerm (ExactXor) OR the
|
||||
// block table (BlockTable); the two equations index different-sized buffers, so
|
||||
// exactly one branch runs. 1/2 bpp are sub-word and stay on the CPU.
|
||||
struct DetileParams
|
||||
{
|
||||
uint width;
|
||||
uint height;
|
||||
uint blockWidth;
|
||||
uint blockHeight;
|
||||
uint blockElements;
|
||||
uint blocksPerRow;
|
||||
uint xMask;
|
||||
uint yMask;
|
||||
uint srcSliceElements;
|
||||
uint equation;
|
||||
uint uintsPerElement;
|
||||
};
|
||||
|
||||
kernel void detile_cs(
|
||||
device const uint* tiled [[buffer(0)]],
|
||||
device const uint* xTermOrTable [[buffer(1)]],
|
||||
device const uint* yTerm [[buffer(2)]],
|
||||
device uint* outLinear [[buffer(3)]],
|
||||
constant DetileParams& params [[buffer(4)]],
|
||||
uint3 gid [[thread_position_in_grid]])
|
||||
{
|
||||
uint elemX = gid.x / params.uintsPerElement;
|
||||
uint word = gid.x - elemX * params.uintsPerElement;
|
||||
if (elemX >= params.width || gid.y >= params.height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint blockIndex = (gid.y / params.blockHeight) * params.blocksPerRow + (elemX / params.blockWidth);
|
||||
uint off;
|
||||
if (params.equation != 0u)
|
||||
{
|
||||
uint inX = elemX - (elemX / params.blockWidth) * params.blockWidth;
|
||||
uint inY = gid.y - (gid.y / params.blockHeight) * params.blockHeight;
|
||||
off = xTermOrTable[inY * params.blockWidth + inX];
|
||||
}
|
||||
else
|
||||
{
|
||||
off = xTermOrTable[elemX & params.xMask] ^ yTerm[gid.y & params.yMask];
|
||||
}
|
||||
|
||||
uint srcElem = gid.z * params.srcSliceElements + blockIndex * params.blockElements + off;
|
||||
uint dstElem = gid.z * params.width * params.height + gid.y * params.width + elemX;
|
||||
outLinear[dstElem * params.uintsPerElement + word] = tiled[srcElem * params.uintsPerElement + word];
|
||||
}
|
||||
@@ -314,7 +314,8 @@ public static partial class Gen5SpirvTranslator
|
||||
uint VectorType,
|
||||
ImageComponentKind ComponentKind,
|
||||
bool IsStorage,
|
||||
bool Arrayed);
|
||||
bool Arrayed,
|
||||
SpirvImageDim Dimension);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
uint Variable,
|
||||
@@ -1001,11 +1002,15 @@ public static partial class Gen5SpirvTranslator
|
||||
SpirvCapability.StorageImageExtendedFormats);
|
||||
}
|
||||
|
||||
var isArrayed = !isStorage &&
|
||||
var dimension = binding.Control.Dimension == 2
|
||||
? SpirvImageDim.Dim3D
|
||||
: SpirvImageDim.Dim2D;
|
||||
var isArrayed = dimension != SpirvImageDim.Dim3D &&
|
||||
!isStorage &&
|
||||
Gen5ShaderTranslator.IsArrayedImageBinding(binding);
|
||||
var imageType = _module.TypeImage(
|
||||
componentType,
|
||||
SpirvImageDim.Dim2D,
|
||||
dimension,
|
||||
depth: false,
|
||||
arrayed: isArrayed,
|
||||
multisampled: false,
|
||||
@@ -1035,7 +1040,8 @@ public static partial class Gen5SpirvTranslator
|
||||
_module.TypeVector(componentType, 4),
|
||||
componentKind,
|
||||
isStorage,
|
||||
isArrayed));
|
||||
isArrayed,
|
||||
dimension));
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
}
|
||||
@@ -2259,20 +2265,39 @@ public static partial class Gen5SpirvTranslator
|
||||
return false;
|
||||
}
|
||||
|
||||
var memoryOpcode = control.UsesFlatAddress
|
||||
? "Global" + instruction.Opcode["Flat".Length..]
|
||||
: instruction.Opcode;
|
||||
var vectorByteAddress = LoadV(control.VectorAddress);
|
||||
if (control.UsesFlatAddress)
|
||||
{
|
||||
// FLAT instructions carry the complete 64-bit guest address
|
||||
// in a VGPR pair. The scalar evaluator captures the buffer
|
||||
// rooted at the inferred SGPR pair, so convert the low address
|
||||
// dword back to a byte offset inside that binding. Subtraction
|
||||
// modulo 2^32 also handles an address addition that carried
|
||||
// into the high dword, while the bounded binding prevents an
|
||||
// unrelated pointer from escaping into host storage.
|
||||
vectorByteAddress = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
vectorByteAddress,
|
||||
LoadS(control.ScalarAddress));
|
||||
}
|
||||
var byteAddress = IAdd(
|
||||
LoadV(control.VectorAddress),
|
||||
vectorByteAddress,
|
||||
UInt(unchecked((uint)control.OffsetBytes)));
|
||||
byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress);
|
||||
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
|
||||
|
||||
if (instruction.Opcode is "GlobalAtomicAdd" or "GlobalAtomicUMax")
|
||||
if (memoryOpcode is "GlobalAtomicAdd" or "GlobalAtomicUMax")
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
EmitConditional(IsBufferWordInRange(bindingIndex, dwordAddress), () =>
|
||||
{
|
||||
var original = _module.AddInstruction(
|
||||
instruction.Opcode == "GlobalAtomicAdd"
|
||||
memoryOpcode == "GlobalAtomicAdd"
|
||||
? SpirvOp.AtomicIAdd
|
||||
: SpirvOp.AtomicUMax,
|
||||
_uintType,
|
||||
@@ -2289,12 +2314,12 @@ public static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("GlobalStore", StringComparison.Ordinal))
|
||||
if (memoryOpcode.StartsWith("GlobalStore", StringComparison.Ordinal))
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
if (TryGetSubdwordStoreInfo(
|
||||
instruction.Opcode,
|
||||
memoryOpcode,
|
||||
out var byteCount,
|
||||
out var sourceShift))
|
||||
{
|
||||
@@ -2324,7 +2349,7 @@ public static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
if (TryGetSubdwordLoadInfo(
|
||||
instruction.Opcode,
|
||||
memoryOpcode,
|
||||
out var loadByteCount,
|
||||
out var signExtend,
|
||||
out var d16,
|
||||
@@ -3268,6 +3293,7 @@ public static partial class Gen5SpirvTranslator
|
||||
var imageObject = Load(resource.ObjectType, resource.Variable);
|
||||
if (instruction.Opcode == "ImageGetResinfo")
|
||||
{
|
||||
var sizeComponentCount = ImageCoordinateComponentCount(resource);
|
||||
var queryImage = resource.IsStorage
|
||||
? imageObject
|
||||
: _module.AddInstruction(
|
||||
@@ -3278,7 +3304,7 @@ public static partial class Gen5SpirvTranslator
|
||||
resource.IsStorage
|
||||
? SpirvOp.ImageQuerySize
|
||||
: SpirvOp.ImageQuerySizeLod,
|
||||
_module.TypeVector(_intType, 2),
|
||||
_module.TypeVector(_intType, sizeComponentCount),
|
||||
resource.IsStorage
|
||||
? [queryImage]
|
||||
: [queryImage, UInt(0)]);
|
||||
@@ -3291,7 +3317,7 @@ public static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
uint value;
|
||||
if (component < 2)
|
||||
if (component < sizeComponentCount)
|
||||
{
|
||||
var signedValue = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
@@ -3319,7 +3345,12 @@ public static partial class Gen5SpirvTranslator
|
||||
return false;
|
||||
}
|
||||
|
||||
var coordinates = BuildIntegerCoordinates(image, 0);
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var coordinates = BuildIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
coordinateComponentCount);
|
||||
var components = new uint[4];
|
||||
uint sourceIndex = 0;
|
||||
for (var component = 0; component < components.Length; component++)
|
||||
@@ -3355,13 +3386,14 @@ public static partial class Gen5SpirvTranslator
|
||||
components);
|
||||
var imageSize = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySize,
|
||||
_module.TypeVector(_intType, 2),
|
||||
_module.TypeVector(_intType, coordinateComponentCount),
|
||||
imageObject);
|
||||
EmitBoundsCheckedImageWrite(
|
||||
coordinates,
|
||||
imageSize,
|
||||
imageObject,
|
||||
texel);
|
||||
texel,
|
||||
coordinateComponentCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -3382,14 +3414,17 @@ public static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
var signed = resource.ComponentKind == ImageComponentKind.Sint;
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var atomicImageSize = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySize,
|
||||
_module.TypeVector(_intType, 2),
|
||||
_module.TypeVector(_intType, coordinateComponentCount),
|
||||
imageObject);
|
||||
var coordinates = BuildClampedIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
atomicImageSize);
|
||||
atomicImageSize,
|
||||
coordinateComponentCount);
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var pointer = _module.AddInstruction(
|
||||
@@ -3433,14 +3468,17 @@ public static partial class Gen5SpirvTranslator
|
||||
{
|
||||
if (resource.IsStorage)
|
||||
{
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var imageSize = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySize,
|
||||
_module.TypeVector(_intType, 2),
|
||||
_module.TypeVector(_intType, coordinateComponentCount),
|
||||
imageObject);
|
||||
var coordinates = BuildClampedIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
imageSize);
|
||||
imageSize,
|
||||
coordinateComponentCount);
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.ImageRead,
|
||||
resource.VectorType,
|
||||
@@ -3454,15 +3492,18 @@ public static partial class Gen5SpirvTranslator
|
||||
SpirvOp.Image,
|
||||
resource.ImageType,
|
||||
imageObject);
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var imageSize = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySizeLod,
|
||||
_module.TypeVector(_intType, 2),
|
||||
_module.TypeVector(_intType, coordinateComponentCount),
|
||||
fetchedImage,
|
||||
UInt(mipLevel));
|
||||
var coordinates = BuildClampedIntegerCoordinates(
|
||||
image,
|
||||
0,
|
||||
imageSize);
|
||||
imageSize,
|
||||
coordinateComponentCount);
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.ImageFetch,
|
||||
resource.VectorType,
|
||||
@@ -3494,12 +3535,19 @@ public static partial class Gen5SpirvTranslator
|
||||
// lowering treated SAMPLE_D as body-first and consequently
|
||||
// sampled gradients as coordinates in every captured
|
||||
// derivative operation.
|
||||
var spatialComponentCount =
|
||||
ImageSpatialComponentCount(resource);
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var addressCursor = 0;
|
||||
var offset = 0u;
|
||||
if (hasOffset)
|
||||
{
|
||||
addressCursor = AlignFullImageAddress(image, addressCursor);
|
||||
offset = BuildImageOffset(image, addressCursor);
|
||||
offset = BuildImageOffset(
|
||||
image,
|
||||
addressCursor,
|
||||
spatialComponentCount);
|
||||
addressCursor += ImageFullAddressSlots(image);
|
||||
}
|
||||
|
||||
@@ -3523,26 +3571,33 @@ public static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
var gradientX = hasGradients
|
||||
? BuildFloatCoordinates(image, addressCursor)
|
||||
? BuildFloatCoordinates(
|
||||
image,
|
||||
addressCursor,
|
||||
spatialComponentCount)
|
||||
: 0u;
|
||||
var gradientY = hasGradients
|
||||
? BuildFloatCoordinates(image, addressCursor + 2)
|
||||
? BuildFloatCoordinates(
|
||||
image,
|
||||
addressCursor + (int)spatialComponentCount,
|
||||
spatialComponentCount)
|
||||
: 0u;
|
||||
if (hasGradients)
|
||||
{
|
||||
addressCursor += 4;
|
||||
addressCursor += checked((int)(spatialComponentCount * 2));
|
||||
}
|
||||
|
||||
var coordinates = resource.Arrayed
|
||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
||||
: BuildFloatCoordinates(image, addressCursor);
|
||||
var coordinates = BuildFloatCoordinates(
|
||||
image,
|
||||
addressCursor,
|
||||
coordinateComponentCount);
|
||||
var explicitLod = hasGradients || hasZeroLod || hasLod;
|
||||
var lod = hasZeroLod
|
||||
? Float(0)
|
||||
: hasLod
|
||||
? LoadImageFloatAddress(
|
||||
image,
|
||||
addressCursor + (resource.Arrayed ? 3 : 2))
|
||||
addressCursor + (int)coordinateComponentCount)
|
||||
: lodOrBias;
|
||||
if (hasOffset)
|
||||
{
|
||||
@@ -3608,11 +3663,18 @@ public static partial class Gen5SpirvTranslator
|
||||
instruction.Opcode.EndsWith("O", StringComparison.Ordinal);
|
||||
var hasCompare =
|
||||
instruction.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
||||
var spatialComponentCount =
|
||||
ImageSpatialComponentCount(resource);
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var addressCursor = 0;
|
||||
var offset = 0u;
|
||||
if (hasOffset)
|
||||
{
|
||||
offset = BuildImageOffset(image, addressCursor);
|
||||
offset = BuildImageOffset(
|
||||
image,
|
||||
addressCursor,
|
||||
spatialComponentCount);
|
||||
addressCursor += ImageFullAddressSlots(image);
|
||||
}
|
||||
|
||||
@@ -3627,9 +3689,10 @@ public static partial class Gen5SpirvTranslator
|
||||
addressCursor += ImageFullAddressSlots(image);
|
||||
}
|
||||
|
||||
var coordinates = resource.Arrayed
|
||||
? BuildFloatArrayCoordinates(image, addressCursor)
|
||||
: BuildFloatCoordinates(image, addressCursor);
|
||||
var coordinates = BuildFloatCoordinates(
|
||||
image,
|
||||
addressCursor,
|
||||
coordinateComponentCount);
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
@@ -3822,28 +3885,31 @@ public static partial class Gen5SpirvTranslator
|
||||
});
|
||||
}
|
||||
|
||||
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
||||
private static uint ImageSpatialComponentCount(
|
||||
SpirvImageResource resource) =>
|
||||
resource.Dimension == SpirvImageDim.Dim3D ? 3u : 2u;
|
||||
|
||||
private static uint ImageCoordinateComponentCount(
|
||||
SpirvImageResource resource) =>
|
||||
resource.Arrayed ? 3u : ImageSpatialComponentCount(resource);
|
||||
|
||||
private uint BuildFloatCoordinates(
|
||||
Gen5ImageControl image,
|
||||
int start,
|
||||
uint componentCount)
|
||||
{
|
||||
var x = LoadImageFloatAddress(image, start);
|
||||
var y = LoadImageFloatAddress(image, start + 1);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec2Type,
|
||||
x,
|
||||
y);
|
||||
var components = new uint[checked((int)componentCount)];
|
||||
for (var component = 0; component < components.Length; component++)
|
||||
{
|
||||
components[component] = LoadImageFloatAddress(
|
||||
image,
|
||||
start + component);
|
||||
}
|
||||
|
||||
private uint BuildFloatArrayCoordinates(Gen5ImageControl image, int start)
|
||||
{
|
||||
var x = LoadImageFloatAddress(image, start);
|
||||
var y = LoadImageFloatAddress(image, start + 1);
|
||||
var slice = LoadImageFloatAddress(image, start + 2);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec3Type,
|
||||
x,
|
||||
y,
|
||||
slice);
|
||||
_module.TypeVector(_floatType, componentCount),
|
||||
components);
|
||||
}
|
||||
|
||||
private static int ImageAddressRegister(
|
||||
@@ -3948,47 +4014,49 @@ public static partial class Gen5SpirvTranslator
|
||||
ShiftLeftLogical(BitwiseAnd(high, UInt(0xFFFF)), UInt(16)));
|
||||
}
|
||||
|
||||
private uint BuildIntegerCoordinates(Gen5ImageControl image, int start)
|
||||
private uint BuildIntegerCoordinates(
|
||||
Gen5ImageControl image,
|
||||
int start,
|
||||
uint componentCount)
|
||||
{
|
||||
var ivec2 = _module.TypeVector(_intType, 2);
|
||||
var x = Bitcast(_intType, LoadImageIntegerAddress(image, start));
|
||||
var y = Bitcast(_intType, LoadImageIntegerAddress(image, start + 1));
|
||||
var components = new uint[checked((int)componentCount)];
|
||||
for (var component = 0; component < components.Length; component++)
|
||||
{
|
||||
components[component] = Bitcast(
|
||||
_intType,
|
||||
LoadImageIntegerAddress(image, start + component));
|
||||
}
|
||||
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
ivec2,
|
||||
x,
|
||||
y);
|
||||
_module.TypeVector(_intType, componentCount),
|
||||
components);
|
||||
}
|
||||
|
||||
private uint BuildClampedIntegerCoordinates(
|
||||
Gen5ImageControl image,
|
||||
int start,
|
||||
uint imageSize)
|
||||
uint imageSize,
|
||||
uint componentCount)
|
||||
{
|
||||
var ivec2 = _module.TypeVector(_intType, 2);
|
||||
var x = ClampSignedCoordinate(
|
||||
var components = new uint[checked((int)componentCount)];
|
||||
for (var component = 0; component < components.Length; component++)
|
||||
{
|
||||
components[component] = ClampSignedCoordinate(
|
||||
Bitcast(
|
||||
_intType,
|
||||
LoadImageIntegerAddress(image, start)),
|
||||
LoadImageIntegerAddress(image, start + component)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
imageSize,
|
||||
0));
|
||||
var y = ClampSignedCoordinate(
|
||||
Bitcast(
|
||||
_intType,
|
||||
LoadImageIntegerAddress(image, start + 1)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
imageSize,
|
||||
1));
|
||||
(uint)component));
|
||||
}
|
||||
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
ivec2,
|
||||
x,
|
||||
y);
|
||||
_module.TypeVector(_intType, componentCount),
|
||||
components);
|
||||
}
|
||||
|
||||
private uint ClampSignedCoordinate(uint value, uint extent)
|
||||
@@ -4027,69 +4095,47 @@ public static partial class Gen5SpirvTranslator
|
||||
uint coordinates,
|
||||
uint imageSize,
|
||||
uint imageObject,
|
||||
uint texel)
|
||||
uint texel,
|
||||
uint coordinateComponentCount)
|
||||
{
|
||||
var x = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
coordinates,
|
||||
0);
|
||||
var y = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
coordinates,
|
||||
1);
|
||||
var width = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
imageSize,
|
||||
0);
|
||||
var height = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
imageSize,
|
||||
1);
|
||||
var zero = _module.Constant(_intType, 0);
|
||||
var xNonNegative = _module.AddInstruction(
|
||||
var inRange = Load(_boolType, _exec);
|
||||
for (uint component = 0;
|
||||
component < coordinateComponentCount;
|
||||
component++)
|
||||
{
|
||||
var coordinate = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
coordinates,
|
||||
component);
|
||||
var extent = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_intType,
|
||||
imageSize,
|
||||
component);
|
||||
var nonNegative = _module.AddInstruction(
|
||||
SpirvOp.SGreaterThanEqual,
|
||||
_boolType,
|
||||
x,
|
||||
coordinate,
|
||||
zero);
|
||||
var yNonNegative = _module.AddInstruction(
|
||||
SpirvOp.SGreaterThanEqual,
|
||||
_boolType,
|
||||
y,
|
||||
zero);
|
||||
var xInRange = _module.AddInstruction(
|
||||
var belowExtent = _module.AddInstruction(
|
||||
SpirvOp.SLessThan,
|
||||
_boolType,
|
||||
x,
|
||||
width);
|
||||
var yInRange = _module.AddInstruction(
|
||||
SpirvOp.SLessThan,
|
||||
_boolType,
|
||||
y,
|
||||
height);
|
||||
var lowerInRange = _module.AddInstruction(
|
||||
coordinate,
|
||||
extent);
|
||||
var componentInRange = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
xNonNegative,
|
||||
yNonNegative);
|
||||
var upperInRange = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
xInRange,
|
||||
yInRange);
|
||||
var inRange = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
lowerInRange,
|
||||
upperInRange);
|
||||
nonNegative,
|
||||
belowExtent);
|
||||
inRange = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
Load(_boolType, _exec),
|
||||
inRange);
|
||||
inRange,
|
||||
componentInRange);
|
||||
}
|
||||
|
||||
var writeLabel = _module.AllocateId();
|
||||
var mergeLabel = _module.AllocateId();
|
||||
_module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0);
|
||||
@@ -4108,30 +4154,30 @@ public static partial class Gen5SpirvTranslator
|
||||
_module.AddLabel(mergeLabel);
|
||||
}
|
||||
|
||||
private uint BuildImageOffset(Gen5ImageControl image, int component)
|
||||
private uint BuildImageOffset(
|
||||
Gen5ImageControl image,
|
||||
int component,
|
||||
uint componentCount)
|
||||
{
|
||||
var ivec2 = _module.TypeVector(_intType, 2);
|
||||
var packed = Bitcast(
|
||||
_intType,
|
||||
LoadV(image.GetAddressRegister(
|
||||
ImageAddressRegister(image, component))));
|
||||
var x = _module.AddInstruction(
|
||||
var components = new uint[checked((int)componentCount)];
|
||||
for (var index = 0; index < components.Length; index++)
|
||||
{
|
||||
components[index] = _module.AddInstruction(
|
||||
SpirvOp.BitFieldSExtract,
|
||||
_intType,
|
||||
packed,
|
||||
UInt(0),
|
||||
UInt(6));
|
||||
var y = _module.AddInstruction(
|
||||
SpirvOp.BitFieldSExtract,
|
||||
_intType,
|
||||
packed,
|
||||
UInt(8),
|
||||
UInt((uint)(index * 8)),
|
||||
UInt(6));
|
||||
}
|
||||
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
ivec2,
|
||||
x,
|
||||
y);
|
||||
_module.TypeVector(_intType, componentCount),
|
||||
components);
|
||||
}
|
||||
|
||||
private uint ApplyDynamicSampleOffset(
|
||||
@@ -4141,7 +4187,16 @@ public static partial class Gen5SpirvTranslator
|
||||
uint texelOffset,
|
||||
uint lod)
|
||||
{
|
||||
var ivec2 = _module.TypeVector(_intType, 2);
|
||||
var spatialComponentCount = ImageSpatialComponentCount(resource);
|
||||
var coordinateComponentCount =
|
||||
ImageCoordinateComponentCount(resource);
|
||||
var spatialIntegerType =
|
||||
_module.TypeVector(_intType, spatialComponentCount);
|
||||
var spatialFloatType =
|
||||
_module.TypeVector(_floatType, spatialComponentCount);
|
||||
var queryComponentCount = resource.Arrayed
|
||||
? coordinateComponentCount
|
||||
: spatialComponentCount;
|
||||
var image = _module.AddInstruction(
|
||||
SpirvOp.Image,
|
||||
resource.ImageType,
|
||||
@@ -4163,14 +4218,14 @@ public static partial class Gen5SpirvTranslator
|
||||
signedLod);
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySizeLod,
|
||||
resource.Arrayed ? _module.TypeVector(_intType, 3) : ivec2,
|
||||
_module.TypeVector(_intType, queryComponentCount),
|
||||
image,
|
||||
clampedLod);
|
||||
if (resource.Arrayed)
|
||||
{
|
||||
size = _module.AddInstruction(
|
||||
SpirvOp.VectorShuffle,
|
||||
ivec2,
|
||||
spatialIntegerType,
|
||||
size,
|
||||
size,
|
||||
0u,
|
||||
@@ -4179,27 +4234,27 @@ public static partial class Gen5SpirvTranslator
|
||||
|
||||
var sizeFloat = _module.AddInstruction(
|
||||
SpirvOp.ConvertSToF,
|
||||
_vec2Type,
|
||||
spatialFloatType,
|
||||
size);
|
||||
var offsetFloat = _module.AddInstruction(
|
||||
SpirvOp.ConvertSToF,
|
||||
_vec2Type,
|
||||
spatialFloatType,
|
||||
texelOffset);
|
||||
var normalizedOffset = _module.AddInstruction(
|
||||
SpirvOp.FDiv,
|
||||
_vec2Type,
|
||||
spatialFloatType,
|
||||
offsetFloat,
|
||||
sizeFloat);
|
||||
if (!resource.Arrayed)
|
||||
{
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.FAdd,
|
||||
_vec2Type,
|
||||
spatialFloatType,
|
||||
coordinates,
|
||||
normalizedOffset);
|
||||
}
|
||||
|
||||
var offsetVec3 = _module.AddInstruction(
|
||||
var arrayOffset = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec3Type,
|
||||
_module.AddInstruction(
|
||||
@@ -4217,7 +4272,7 @@ public static partial class Gen5SpirvTranslator
|
||||
SpirvOp.FAdd,
|
||||
_vec3Type,
|
||||
coordinates,
|
||||
offsetVec3);
|
||||
arrayOffset);
|
||||
}
|
||||
|
||||
private bool TryEmitExport(
|
||||
|
||||
@@ -260,4 +260,233 @@ public static class SpirvFixedShaders
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute kernel that deswizzles RDNA2 tiled surfaces at 4 bytes/element into
|
||||
/// a linear output buffer — one GPU thread per texel, one dispatch-Z layer per
|
||||
/// array slice. Mirrors <c>GnmTiling.GetDetileParams</c> so it is bit-identical
|
||||
/// to the CPU fallback for both supported equation families:
|
||||
/// <code>
|
||||
/// z = layer;
|
||||
/// inBlock = equation == BlockTable // modes 1/4/8
|
||||
/// ? blockTable[(y % blockHeight) * blockWidth + (x % blockWidth)]
|
||||
/// : xTerm[x & xMask] ^ yTerm[y & yMask]; // ExactXor 5/9/24/27
|
||||
/// src = z * srcSliceElements
|
||||
/// + (y / blockHeight * blocksPerRow + x / blockWidth) * blockElements
|
||||
/// + inBlock;
|
||||
/// out[z * width * height + y * width + x] = tiled[src];
|
||||
/// </code>
|
||||
/// Each array slice is an independently tiled 2D surface; the caller packs the
|
||||
/// slices contiguously in the tiled buffer (stride <c>srcSliceElements</c>) and
|
||||
/// the output ends up layer-major, matching a single multi-layer
|
||||
/// buffer->image copy. For a non-arrayed texture the caller dispatches a
|
||||
/// single Z layer with <c>srcSliceElements</c> unused (z == 0).
|
||||
///
|
||||
/// The term tables hold ELEMENT offsets. For ExactXor the caller pre-shifts the
|
||||
/// byte-unit GetDetileParams terms right by log2(bytesPerElement) (exact at 4bpp
|
||||
/// since the equation's low two byte-offset bits are 0); for BlockTable the
|
||||
/// GetDetileParams block table is already in element units. Binding 1 carries
|
||||
/// xTerm (ExactXor) OR blockTable (BlockTable) — the two equations index
|
||||
/// different-sized buffers, so the kernel branches and evaluates exactly one.
|
||||
///
|
||||
/// width/height are ELEMENT dims (for block-compressed formats a 4x4 block is
|
||||
/// one element). Each element spans uintsPerElement = bpp/4 words (4bpp -> 1,
|
||||
/// 8bpp -> 2, 16bpp -> 4); the X dispatch is widened by that factor so each
|
||||
/// thread copies one word (elemX = gidX / upe, word = gidX % upe). 1/2 bpp are
|
||||
/// sub-word and stay on the CPU.
|
||||
///
|
||||
/// Descriptor set 0: binding 0 = tiled uint[], 1 = xTerm/blockTable uint[],
|
||||
/// 2 = yTerm uint[], 3 = out uint[]. Push constants (11 x uint, offset i*4):
|
||||
/// width, height, blockWidth, blockHeight, blockElements, blocksPerRow,
|
||||
/// xMask, yMask, srcSliceElements, equation (0 = ExactXor, 1 = BlockTable),
|
||||
/// uintsPerElement. Local size 8x8x1; dispatch X = ceil(width*upe/8),
|
||||
/// Y = ceil(height/8), Z = arrayLayers.
|
||||
/// </summary>
|
||||
public static byte[] CreateDetileCompute()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var boolType = module.TypeBool();
|
||||
var uintType = module.TypeInt(32, signed: false);
|
||||
var uvec3Type = module.TypeVector(uintType, 3);
|
||||
|
||||
// One shared Block-decorated storage-buffer struct: struct { uint data[]; }.
|
||||
var runtimeArray = module.TypeRuntimeArray(uintType);
|
||||
module.AddDecoration(runtimeArray, SpirvDecoration.ArrayStride, 4);
|
||||
var bufferStruct = module.TypeStruct(runtimeArray);
|
||||
module.AddDecoration(bufferStruct, SpirvDecoration.Block);
|
||||
module.AddMemberDecoration(bufferStruct, 0, SpirvDecoration.Offset, 0);
|
||||
var bufferPtrType = module.TypePointer(SpirvStorageClass.StorageBuffer, bufferStruct);
|
||||
var uintStoragePtr = module.TypePointer(SpirvStorageClass.StorageBuffer, uintType);
|
||||
|
||||
uint MakeBuffer(uint binding, string name)
|
||||
{
|
||||
var variable = module.AddGlobalVariable(bufferPtrType, SpirvStorageClass.StorageBuffer);
|
||||
module.AddName(variable, name);
|
||||
module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0);
|
||||
module.AddDecoration(variable, SpirvDecoration.Binding, binding);
|
||||
return variable;
|
||||
}
|
||||
|
||||
var tiledVar = MakeBuffer(0, "tiled");
|
||||
var xTermVar = MakeBuffer(1, "xTerm");
|
||||
var yTermVar = MakeBuffer(2, "yTerm");
|
||||
var outVar = MakeBuffer(3, "outLinear");
|
||||
|
||||
// Push constants: struct { uint p0..p10; }, each member at offset i*4.
|
||||
var pushStruct = module.TypeStruct(
|
||||
uintType, uintType, uintType, uintType, uintType, uintType,
|
||||
uintType, uintType, uintType, uintType, uintType);
|
||||
module.AddDecoration(pushStruct, SpirvDecoration.Block);
|
||||
for (uint member = 0; member < 11; member++)
|
||||
{
|
||||
module.AddMemberDecoration(pushStruct, member, SpirvDecoration.Offset, member * 4);
|
||||
}
|
||||
|
||||
var pushPtrType = module.TypePointer(SpirvStorageClass.PushConstant, pushStruct);
|
||||
var pushMemberPtrType = module.TypePointer(SpirvStorageClass.PushConstant, uintType);
|
||||
var pushVar = module.AddGlobalVariable(pushPtrType, SpirvStorageClass.PushConstant);
|
||||
module.AddName(pushVar, "pc");
|
||||
|
||||
var inputUvec3Ptr = module.TypePointer(SpirvStorageClass.Input, uvec3Type);
|
||||
var gidVar = module.AddGlobalVariable(inputUvec3Ptr, SpirvStorageClass.Input);
|
||||
module.AddName(gidVar, "gid");
|
||||
module.AddDecoration(gidVar, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.GlobalInvocationId);
|
||||
|
||||
var uintConst = new uint[11];
|
||||
for (uint value = 0; value < 11; value++)
|
||||
{
|
||||
uintConst[value] = module.Constant(uintType, value);
|
||||
}
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
|
||||
var gid = module.AddInstruction(SpirvOp.Load, uvec3Type, gidVar);
|
||||
var gidX = module.AddInstruction(SpirvOp.CompositeExtract, uintType, gid, 0);
|
||||
var y = module.AddInstruction(SpirvOp.CompositeExtract, uintType, gid, 1);
|
||||
var z = module.AddInstruction(SpirvOp.CompositeExtract, uintType, gid, 2);
|
||||
|
||||
uint PushField(uint index)
|
||||
{
|
||||
var pointer = module.AddInstruction(
|
||||
SpirvOp.AccessChain, pushMemberPtrType, pushVar, uintConst[index]);
|
||||
return module.AddInstruction(SpirvOp.Load, uintType, pointer);
|
||||
}
|
||||
|
||||
// width/height are ELEMENT dims (for BC, a 4x4 block is one element). Each
|
||||
// element spans uintsPerElement 32-bit words (bpp/4: 4bpp->1, 8bpp->2,
|
||||
// 16bpp->4). The X dispatch is widened by uintsPerElement so each thread
|
||||
// copies exactly one word: elemX = gidX / upe, wordIndex = gidX % upe.
|
||||
var width = PushField(0);
|
||||
var height = PushField(1);
|
||||
var blockWidth = PushField(2);
|
||||
var blockHeight = PushField(3);
|
||||
var blockElements = PushField(4);
|
||||
var blocksPerRow = PushField(5);
|
||||
var xMask = PushField(6);
|
||||
var yMask = PushField(7);
|
||||
var srcSliceElements = PushField(8);
|
||||
var equation = PushField(9);
|
||||
var uintsPerElement = PushField(10);
|
||||
|
||||
var elemX = module.AddInstruction(SpirvOp.UDiv, uintType, gidX, uintsPerElement);
|
||||
var elemXTimesUpe = module.AddInstruction(SpirvOp.IMul, uintType, elemX, uintsPerElement);
|
||||
var wordIndex = module.AddInstruction(SpirvOp.ISub, uintType, gidX, elemXTimesUpe);
|
||||
|
||||
var xInRange = module.AddInstruction(SpirvOp.ULessThan, boolType, elemX, width);
|
||||
var yInRange = module.AddInstruction(SpirvOp.ULessThan, boolType, y, height);
|
||||
var inRange = module.AddInstruction(SpirvOp.LogicalAnd, boolType, xInRange, yInRange);
|
||||
|
||||
var bodyLabel = module.AllocateId();
|
||||
var mergeLabel = module.AllocateId();
|
||||
module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0);
|
||||
module.AddStatement(SpirvOp.BranchConditional, inRange, bodyLabel, mergeLabel);
|
||||
|
||||
module.AddLabel(bodyLabel);
|
||||
|
||||
// blockIdx = (y / blockHeight) * blocksPerRow + (elemX / blockWidth)
|
||||
var yDiv = module.AddInstruction(SpirvOp.UDiv, uintType, y, blockHeight);
|
||||
var blockRow = module.AddInstruction(SpirvOp.IMul, uintType, yDiv, blocksPerRow);
|
||||
var xDiv = module.AddInstruction(SpirvOp.UDiv, uintType, elemX, blockWidth);
|
||||
var blockIdx = module.AddInstruction(SpirvOp.IAdd, uintType, blockRow, xDiv);
|
||||
|
||||
// off (element offset within the block) = equation == BlockTable
|
||||
// ? blockTable[(y % blockHeight) * blockWidth + (elemX % blockWidth)]
|
||||
// : xTerm[elemX & xMask] ^ yTerm[y & yMask]
|
||||
// Binding 1 (xTermVar) doubles as the block table; the two equations index
|
||||
// different-sized buffers, so exactly one branch executes (no OOB read).
|
||||
var isBlockTable = module.AddInstruction(SpirvOp.INotEqual, boolType, equation, uintConst[0]);
|
||||
var xorLabel = module.AllocateId();
|
||||
var tableLabel = module.AllocateId();
|
||||
var offMergeLabel = module.AllocateId();
|
||||
module.AddStatement(SpirvOp.SelectionMerge, offMergeLabel, 0);
|
||||
module.AddStatement(SpirvOp.BranchConditional, isBlockTable, tableLabel, xorLabel);
|
||||
|
||||
// ExactXor: xTerm[elemX & xMask] ^ yTerm[y & yMask]
|
||||
module.AddLabel(xorLabel);
|
||||
var xIdx = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, elemX, xMask);
|
||||
var xPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, xTermVar, uintConst[0], xIdx);
|
||||
var xTerm = module.AddInstruction(SpirvOp.Load, uintType, xPtr);
|
||||
var yIdx = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, y, yMask);
|
||||
var yPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, yTermVar, uintConst[0], yIdx);
|
||||
var yTerm = module.AddInstruction(SpirvOp.Load, uintType, yPtr);
|
||||
var offXor = module.AddInstruction(SpirvOp.BitwiseXor, uintType, xTerm, yTerm);
|
||||
module.AddStatement(SpirvOp.Branch, offMergeLabel);
|
||||
|
||||
// BlockTable: blockTable[inY * blockWidth + inX], inX/inY = position in block
|
||||
module.AddLabel(tableLabel);
|
||||
var blockXBase = module.AddInstruction(SpirvOp.IMul, uintType, xDiv, blockWidth);
|
||||
var inX = module.AddInstruction(SpirvOp.ISub, uintType, elemX, blockXBase);
|
||||
var blockYBase = module.AddInstruction(SpirvOp.IMul, uintType, yDiv, blockHeight);
|
||||
var inY = module.AddInstruction(SpirvOp.ISub, uintType, y, blockYBase);
|
||||
var rowInBlock = module.AddInstruction(SpirvOp.IMul, uintType, inY, blockWidth);
|
||||
var tableIdx = module.AddInstruction(SpirvOp.IAdd, uintType, rowInBlock, inX);
|
||||
var tablePtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, xTermVar, uintConst[0], tableIdx);
|
||||
var offTable = module.AddInstruction(SpirvOp.Load, uintType, tablePtr);
|
||||
module.AddStatement(SpirvOp.Branch, offMergeLabel);
|
||||
|
||||
module.AddLabel(offMergeLabel);
|
||||
var off = module.AddInstruction(SpirvOp.Phi, uintType, offXor, xorLabel, offTable, tableLabel);
|
||||
|
||||
// srcElem = z * srcSliceElements + blockIdx * blockElements + off (in elements)
|
||||
// srcWord = srcElem * uintsPerElement + wordIndex
|
||||
var srcSliceBase = module.AddInstruction(SpirvOp.IMul, uintType, z, srcSliceElements);
|
||||
var blockBase = module.AddInstruction(SpirvOp.IMul, uintType, blockIdx, blockElements);
|
||||
var srcInSlice = module.AddInstruction(SpirvOp.IAdd, uintType, blockBase, off);
|
||||
var srcElem = module.AddInstruction(SpirvOp.IAdd, uintType, srcSliceBase, srcInSlice);
|
||||
var srcElemWords = module.AddInstruction(SpirvOp.IMul, uintType, srcElem, uintsPerElement);
|
||||
var src = module.AddInstruction(SpirvOp.IAdd, uintType, srcElemWords, wordIndex);
|
||||
var srcPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, tiledVar, uintConst[0], src);
|
||||
var word = module.AddInstruction(SpirvOp.Load, uintType, srcPtr);
|
||||
|
||||
// dstElem = z * width * height + y * width + elemX (in elements)
|
||||
// dstWord = dstElem * uintsPerElement + wordIndex
|
||||
var sliceElements = module.AddInstruction(SpirvOp.IMul, uintType, width, height);
|
||||
var dstSliceBase = module.AddInstruction(SpirvOp.IMul, uintType, z, sliceElements);
|
||||
var rowBase = module.AddInstruction(SpirvOp.IMul, uintType, y, width);
|
||||
var dstRow = module.AddInstruction(SpirvOp.IAdd, uintType, rowBase, elemX);
|
||||
var dstElem = module.AddInstruction(SpirvOp.IAdd, uintType, dstSliceBase, dstRow);
|
||||
var dstElemWords = module.AddInstruction(SpirvOp.IMul, uintType, dstElem, uintsPerElement);
|
||||
var dstIdx = module.AddInstruction(SpirvOp.IAdd, uintType, dstElemWords, wordIndex);
|
||||
var dstPtr = module.AddInstruction(SpirvOp.AccessChain, uintStoragePtr, outVar, uintConst[0], dstIdx);
|
||||
module.AddStatement(SpirvOp.Store, dstPtr, word);
|
||||
|
||||
module.AddStatement(SpirvOp.Branch, mergeLabel);
|
||||
module.AddLabel(mergeLabel);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.LocalSize, 8, 8, 1);
|
||||
module.AddEntryPoint(
|
||||
SpirvExecutionModel.GLCompute,
|
||||
main,
|
||||
"main",
|
||||
[gidVar, tiledVar, xTermVar, yTermVar, outVar, pushVar]);
|
||||
return module.Build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,8 @@ public sealed record Gen5GlobalMemoryControl(
|
||||
uint ScalarAddress,
|
||||
int OffsetBytes,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
bool Slc,
|
||||
bool UsesFlatAddress = false) : Gen5InstructionControl;
|
||||
|
||||
public sealed record Gen5BufferMemoryControl(
|
||||
uint DwordCount,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -35,6 +36,14 @@ public static class Gen5ShaderScalarEvaluator
|
||||
StringComparison.Ordinal);
|
||||
private static readonly object _scalarFallbackTraceGate = new();
|
||||
private static readonly HashSet<(ulong Shader, uint Pc)> _tracedScalarFallbacks = [];
|
||||
// Shaders whose empty SRT/EUD caused a null-base scalar pointer load.
|
||||
// Host submit of those translations has lost the Vulkan device; Agc skips
|
||||
// them before QueueSubmit.
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _emptySrtScalarPointerFallbacks =
|
||||
new();
|
||||
|
||||
public static bool WasEmptySrtScalarPointerFallback(ulong shaderAddress) =>
|
||||
_emptySrtScalarPointerFallbacks.ContainsKey(shaderAddress);
|
||||
|
||||
// Uniform forward branches select material/resource bodies that remain
|
||||
// statically present in the translated shader. Discover the skipped body's
|
||||
@@ -349,7 +358,10 @@ public static class Gen5ShaderScalarEvaluator
|
||||
if (globalMemory.ScalarAddress >= ScalarRegisterCount - 1)
|
||||
{
|
||||
error =
|
||||
$"global-address-register-range pc=0x{instruction.Pc:X} " +
|
||||
$"{(globalMemory.UsesFlatAddress
|
||||
? "flat-address-base-unresolved"
|
||||
: "global-address-register-range")} " +
|
||||
$"pc=0x{instruction.Pc:X} " +
|
||||
$"s{globalMemory.ScalarAddress}";
|
||||
return false;
|
||||
}
|
||||
@@ -364,11 +376,18 @@ public static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
|
||||
var key = (globalMemory.ScalarAddress, baseAddress);
|
||||
var writable = instruction.Opcode.StartsWith(
|
||||
var writable =
|
||||
instruction.Opcode.StartsWith(
|
||||
"GlobalStore",
|
||||
StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith(
|
||||
"GlobalAtomic",
|
||||
StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith(
|
||||
"FlatStore",
|
||||
StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith(
|
||||
"FlatAtomic",
|
||||
StringComparison.Ordinal);
|
||||
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
||||
{
|
||||
@@ -2101,6 +2120,15 @@ public static class Gen5ShaderScalarEvaluator
|
||||
$"dynamic={dynamicOffset} definitions=[{string.Join(';', definitions)}] " +
|
||||
$"user_data=[{userData}] metadata=" +
|
||||
$"{(state.Metadata is null ? "missing" : $"srt={state.Metadata.ShaderResourceTableSizeDwords},eud={state.Metadata.ExtendedUserDataSizeDwords}")}");
|
||||
if (baseAddress == 0 &&
|
||||
state.Metadata is
|
||||
{
|
||||
ShaderResourceTableSizeDwords: 0,
|
||||
ExtendedUserDataSizeDwords: 0,
|
||||
})
|
||||
{
|
||||
_emptySrtScalarPointerFallbacks.TryAdd(state.Program.Address, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
||||
@@ -305,6 +305,23 @@ public static class Gen5ShaderTranslator
|
||||
count |= 0x20;
|
||||
}
|
||||
|
||||
// Primary SH defaults leave SPI_SHADER_PGM_RSRC2_PS at 0. Draws that
|
||||
// still wrote USER_DATA_n via SetShReg would otherwise translate with
|
||||
// an empty SRT window (Astro title PS → Address-0 descriptors →
|
||||
// device lost). Recover the window from contiguous live registers.
|
||||
if (count == 0 &&
|
||||
userDataBaseRegister is not ComputeUserDataRegister)
|
||||
{
|
||||
var probed = 0;
|
||||
while (probed < MaximumHardwareUserSgprs &&
|
||||
shaderRegisters.ContainsKey(userDataBaseRegister + (uint)probed))
|
||||
{
|
||||
probed++;
|
||||
}
|
||||
|
||||
count = probed;
|
||||
}
|
||||
|
||||
if (userDataBaseRegister is not (PsUserDataRegister or
|
||||
VsUserDataRegister or
|
||||
GsUserDataRegister or
|
||||
@@ -454,7 +471,15 @@ public static class Gen5ShaderTranslator
|
||||
}
|
||||
}
|
||||
|
||||
instructions.Add(CreateInstruction(pc, encoding, name, words));
|
||||
var instruction = CreateInstruction(pc, encoding, name, words);
|
||||
if (instruction.Control is Gen5GlobalMemoryControl
|
||||
{
|
||||
UsesFlatAddress: true,
|
||||
})
|
||||
{
|
||||
instruction = ResolveFlatAddressBase(instructions, instruction);
|
||||
}
|
||||
instructions.Add(instruction);
|
||||
instructionCount++;
|
||||
|
||||
pc += sizeDwords * sizeof(uint);
|
||||
@@ -1378,35 +1403,42 @@ public static class Gen5ShaderTranslator
|
||||
var opcode = (word >> 18) & 0x7F;
|
||||
sizeDwords = 2;
|
||||
error = string.Empty;
|
||||
name = segment == 0x2
|
||||
? opcode switch
|
||||
var prefix = segment switch
|
||||
{
|
||||
0x08 => "GlobalLoadUbyte",
|
||||
0x09 => "GlobalLoadSbyte",
|
||||
0x0A => "GlobalLoadUshort",
|
||||
0x0B => "GlobalLoadSshort",
|
||||
0x0C => "GlobalLoadDword",
|
||||
0x0D => "GlobalLoadDwordx2",
|
||||
0x0E => "GlobalLoadDwordx4",
|
||||
0x0F => "GlobalLoadDwordx3",
|
||||
0x18 => "GlobalStoreByte",
|
||||
0x19 => "GlobalStoreByteD16Hi",
|
||||
0x1A => "GlobalStoreShort",
|
||||
0x1B => "GlobalStoreShortD16Hi",
|
||||
0x1C => "GlobalStoreDword",
|
||||
0x1D => "GlobalStoreDwordx2",
|
||||
0x1E => "GlobalStoreDwordx4",
|
||||
0x1F => "GlobalStoreDwordx3",
|
||||
0x20 => "GlobalLoadUbyteD16",
|
||||
0x21 => "GlobalLoadUbyteD16Hi",
|
||||
0x22 => "GlobalLoadSbyteD16",
|
||||
0x23 => "GlobalLoadSbyteD16Hi",
|
||||
0x24 => "GlobalLoadShortD16",
|
||||
0x25 => "GlobalLoadShortD16Hi",
|
||||
0x32 => "GlobalAtomicAdd",
|
||||
0x38 => "GlobalAtomicUMax",
|
||||
0x0 => "Flat",
|
||||
0x2 => "Global",
|
||||
_ => string.Empty,
|
||||
}
|
||||
};
|
||||
var suffix = opcode switch
|
||||
{
|
||||
0x08 => "LoadUbyte",
|
||||
0x09 => "LoadSbyte",
|
||||
0x0A => "LoadUshort",
|
||||
0x0B => "LoadSshort",
|
||||
0x0C => "LoadDword",
|
||||
0x0D => "LoadDwordx2",
|
||||
0x0E => "LoadDwordx4",
|
||||
0x0F => "LoadDwordx3",
|
||||
0x18 => "StoreByte",
|
||||
0x19 => "StoreByteD16Hi",
|
||||
0x1A => "StoreShort",
|
||||
0x1B => "StoreShortD16Hi",
|
||||
0x1C => "StoreDword",
|
||||
0x1D => "StoreDwordx2",
|
||||
0x1E => "StoreDwordx4",
|
||||
0x1F => "StoreDwordx3",
|
||||
0x20 => "LoadUbyteD16",
|
||||
0x21 => "LoadUbyteD16Hi",
|
||||
0x22 => "LoadSbyteD16",
|
||||
0x23 => "LoadSbyteD16Hi",
|
||||
0x24 => "LoadShortD16",
|
||||
0x25 => "LoadShortD16Hi",
|
||||
0x32 => "AtomicAdd",
|
||||
0x38 => "AtomicUMax",
|
||||
_ => string.Empty,
|
||||
};
|
||||
name = prefix.Length != 0 && suffix.Length != 0
|
||||
? prefix + suffix
|
||||
: string.Empty;
|
||||
|
||||
return FinishDecode(
|
||||
@@ -1629,6 +1661,82 @@ public static class Gen5ShaderTranslator
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static Gen5ShaderInstruction ResolveFlatAddressBase(
|
||||
IReadOnlyList<Gen5ShaderInstruction> precedingInstructions,
|
||||
Gen5ShaderInstruction instruction)
|
||||
{
|
||||
if (instruction.Control is not Gen5GlobalMemoryControl
|
||||
{
|
||||
UsesFlatAddress: true,
|
||||
} control ||
|
||||
!TryFindVectorDefinition(
|
||||
precedingInstructions,
|
||||
control.VectorAddress,
|
||||
out var lowDefinition) ||
|
||||
!TryFindVectorDefinition(
|
||||
precedingInstructions,
|
||||
control.VectorAddress + 1,
|
||||
out var highDefinition))
|
||||
{
|
||||
return instruction;
|
||||
}
|
||||
|
||||
foreach (var lowSource in lowDefinition.Sources)
|
||||
{
|
||||
if (lowSource.Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var highSource in highDefinition.Sources)
|
||||
{
|
||||
if (highSource.Kind != Gen5OperandKind.ScalarRegister ||
|
||||
highSource.Value != lowSource.Value + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return instruction with
|
||||
{
|
||||
Sources =
|
||||
[
|
||||
.. instruction.Sources,
|
||||
Gen5Operand.Scalar(lowSource.Value),
|
||||
],
|
||||
Control = control with
|
||||
{
|
||||
ScalarAddress = lowSource.Value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
private static bool TryFindVectorDefinition(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
uint register,
|
||||
out Gen5ShaderInstruction definition)
|
||||
{
|
||||
for (var index = instructions.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var candidate = instructions[index];
|
||||
foreach (var destination in candidate.Destinations)
|
||||
{
|
||||
if (destination.Kind == Gen5OperandKind.VectorRegister &&
|
||||
destination.Value == register)
|
||||
{
|
||||
definition = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
definition = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Gen5ShaderInstruction CreateInstruction(
|
||||
uint pc,
|
||||
Gen5ShaderEncoding encoding,
|
||||
@@ -2040,7 +2148,13 @@ public static class Gen5ShaderTranslator
|
||||
var vectorAddress = extra & 0xFF;
|
||||
var vectorData = (extra >> 8) & 0xFF;
|
||||
var scalarAddress = (extra >> 16) & 0x7F;
|
||||
var dwordCount = opcode switch
|
||||
var usesFlatAddress = opcode.StartsWith(
|
||||
"Flat",
|
||||
StringComparison.Ordinal);
|
||||
var memoryOpcode = usesFlatAddress
|
||||
? "Global" + opcode["Flat".Length..]
|
||||
: opcode;
|
||||
var dwordCount = memoryOpcode switch
|
||||
{
|
||||
"GlobalLoadUbyte" or
|
||||
"GlobalLoadSbyte" or
|
||||
@@ -2068,12 +2182,20 @@ public static class Gen5ShaderTranslator
|
||||
"GlobalStoreDwordx4" => 4u,
|
||||
_ => 0u,
|
||||
};
|
||||
sources =
|
||||
sources = usesFlatAddress
|
||||
?
|
||||
[
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Vector(vectorAddress + 1),
|
||||
]
|
||||
:
|
||||
[
|
||||
Gen5Operand.Vector(vectorAddress),
|
||||
Gen5Operand.Scalar(scalarAddress),
|
||||
];
|
||||
destinations = opcode.StartsWith("GlobalLoad", StringComparison.Ordinal)
|
||||
destinations = memoryOpcode.StartsWith(
|
||||
"GlobalLoad",
|
||||
StringComparison.Ordinal)
|
||||
? Enumerable
|
||||
.Range((int)vectorData, checked((int)dwordCount))
|
||||
.Select(index => Gen5Operand.Vector((uint)index))
|
||||
@@ -2083,10 +2205,11 @@ public static class Gen5ShaderTranslator
|
||||
dwordCount,
|
||||
vectorAddress,
|
||||
vectorData,
|
||||
scalarAddress,
|
||||
usesFlatAddress ? uint.MaxValue : scalarAddress,
|
||||
SignExtend(word & 0x1FFF, 13),
|
||||
((word >> 16) & 1) != 0,
|
||||
((word >> 17) & 1) != 0);
|
||||
((word >> 17) & 1) != 0,
|
||||
usesFlatAddress);
|
||||
break;
|
||||
}
|
||||
case Gen5ShaderEncoding.Mubuf:
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcPredicationTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x100;
|
||||
private const ulong PacketAddress = BaseAddress + 0x400;
|
||||
private const ulong PredicateAddress = BaseAddress + 0x800;
|
||||
|
||||
[Fact]
|
||||
public void DcbSetPredication_EmitsGen5Packet()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
ctx[CpuRegister.Rdx] = 3;
|
||||
ctx[CpuRegister.Rcx] = 1;
|
||||
ctx[CpuRegister.R8] = PredicateAddress + 7;
|
||||
ctx[CpuRegister.R9] = 2;
|
||||
|
||||
var result = AgcExports.DcbSetPredication(ctx);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0xC002_2000u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0003_1100u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(unchecked((uint)PredicateAddress), ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal((uint)(PredicateAddress >> 32), ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(PacketAddress + 16, ReadUInt64(memory, CommandBufferAddress + 0x10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPacketPredication_TogglesPacketHeaderBit()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
const uint header = 0xC003_1500;
|
||||
WriteUInt32(memory, PacketAddress, header);
|
||||
|
||||
ctx[CpuRegister.Rdi] = PacketAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.SetPacketPredication(ctx));
|
||||
Assert.Equal(header | 1u, ReadUInt32(memory, PacketAddress));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.SetPacketPredication(ctx));
|
||||
Assert.Equal(header, ReadUInt32(memory, PacketAddress));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcResourceOwnerTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong OwnerAddress = BaseAddress + 0x100;
|
||||
private const ulong NameAddress = BaseAddress + 0x200;
|
||||
private const ulong RegistrationMemoryAddress = BaseAddress + 0x400;
|
||||
|
||||
[Fact]
|
||||
public void RegisterOwner_DoesNotRequireOptionalResourceRegistryMemory()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(NameAddress, "GIRender");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress;
|
||||
ctx[CpuRegister.Rsi] = NameAddress;
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
|
||||
Assert.NotEqual(0u, ReadUInt32(memory, OwnerAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterOwner_RespectsExplicitRegistryCapacity()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
ctx[CpuRegister.Rdi] = RegistrationMemoryAddress;
|
||||
ctx[CpuRegister.Rsi] = 0x1000;
|
||||
ctx[CpuRegister.Rdx] = 1;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AgcExports.DriverInitResourceRegistration(ctx));
|
||||
|
||||
memory.WriteCString(NameAddress, "First");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress;
|
||||
ctx[CpuRegister.Rsi] = NameAddress;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverRegisterOwner(ctx));
|
||||
|
||||
memory.WriteCString(NameAddress, "Second");
|
||||
ctx[CpuRegister.Rdi] = OwnerAddress + 4;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
AgcExports.DriverRegisterOwner(ctx));
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcTextureTransportTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(10u, 4u, 4u)]
|
||||
[InlineData(10u, 0u, 1u)]
|
||||
[InlineData(9u, 4u, 1u)]
|
||||
[InlineData(13u, 4u, 1u)]
|
||||
public void GetTextureVolumeDepth_OnlyUsesDescriptorDepthFor3D(
|
||||
uint type,
|
||||
uint descriptorDepth,
|
||||
uint expectedDepth)
|
||||
{
|
||||
Assert.Equal(
|
||||
expectedDepth,
|
||||
AgcExports.GetTextureVolumeDepth(type, descriptorDepth));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTextureByteCount_MultipliesUncompressedVolumeDepth()
|
||||
{
|
||||
Assert.Equal(
|
||||
4UL * 8 * 6 * 5,
|
||||
AgcExports.GetTextureByteCount(
|
||||
format: 10,
|
||||
width: 8,
|
||||
height: 6,
|
||||
depth: 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTextureByteCount_MultipliesBlockCompressedVolumeDepth()
|
||||
{
|
||||
// Format 169 uses one eight-byte BC block for each 4x4 texel block.
|
||||
Assert.Equal(
|
||||
2UL * 2 * 8 * 3,
|
||||
AgcExports.GetTextureByteCount(
|
||||
format: 169,
|
||||
width: 7,
|
||||
height: 5,
|
||||
depth: 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTextureByteCount_LeavesTwoDimensionalSizingUnchanged()
|
||||
{
|
||||
Assert.Equal(
|
||||
AgcExports.GetTextureByteCount(10, 8, 6),
|
||||
AgcExports.GetTextureByteCount(10, 8, 6, depth: 1));
|
||||
Assert.Equal(
|
||||
AgcExports.GetTextureByteCount(10, 8, 6),
|
||||
AgcExports.GetTextureByteCount(10, 8, 6, depth: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GuestDrawTexture_CarriesRawTypeAndNormalizedDepth()
|
||||
{
|
||||
var texture = new GuestDrawTexture(
|
||||
Address: 0x1234,
|
||||
Width: 8,
|
||||
Height: 6,
|
||||
Format: 10,
|
||||
NumberType: 0,
|
||||
RgbaPixels: [],
|
||||
IsFallback: false,
|
||||
IsStorage: false,
|
||||
Type: 10,
|
||||
Depth: 5);
|
||||
|
||||
Assert.Equal(10u, texture.Type);
|
||||
Assert.Equal(5u, texture.Depth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextureContentIdentity_DistinguishesTypeAndDepth()
|
||||
{
|
||||
var twoDimensional = CreateIdentity(type: 9, depth: 1);
|
||||
var threeDimensional = CreateIdentity(type: 10, depth: 1);
|
||||
var deeperThreeDimensional = CreateIdentity(type: 10, depth: 5);
|
||||
|
||||
Assert.NotEqual(twoDimensional, threeDimensional);
|
||||
Assert.NotEqual(threeDimensional, deeperThreeDimensional);
|
||||
}
|
||||
|
||||
private static TextureContentIdentity CreateIdentity(uint type, uint depth) =>
|
||||
new(
|
||||
Address: 0x1234,
|
||||
Width: 8,
|
||||
Height: 6,
|
||||
Format: 10,
|
||||
NumberType: 0,
|
||||
DstSelect: 0xFAC,
|
||||
TileMode: 0,
|
||||
Pitch: 8,
|
||||
Sampler: default,
|
||||
Type: type,
|
||||
Depth: depth);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcWaitRegMemTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x1_0000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x100;
|
||||
private const ulong PacketAddress = BaseAddress + 0x400;
|
||||
private const ulong StackAddress = BaseAddress + 0x800;
|
||||
|
||||
[Fact]
|
||||
public void DcbWaitRegMem32_EmitsGen5PacketLayout()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
var waitAddress = BaseAddress + 0xC03;
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 0;
|
||||
ctx[CpuRegister.Rdx] = 3;
|
||||
ctx[CpuRegister.Rcx] = 4;
|
||||
ctx[CpuRegister.R8] = 2;
|
||||
ctx[CpuRegister.R9] = waitAddress;
|
||||
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
|
||||
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
|
||||
WriteUInt32(memory, StackAddress + 24, 0x123456);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
|
||||
Assert.Equal(PacketAddress, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(0xC005_1028u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 16));
|
||||
Assert.Equal(0x0400_0053u, ReadUInt32(memory, PacketAddress + 20));
|
||||
Assert.Equal(0xFFFFu, ReadUInt32(memory, PacketAddress + 24));
|
||||
Assert.Equal(PacketAddress + 28, ReadUInt64(memory, CommandBufferAddress + 0x10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DcbWaitRegMem64_EmitsGen5PacketLayout()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
var waitAddress = BaseAddress + 0xC07;
|
||||
|
||||
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
ctx[CpuRegister.Rsi] = 1;
|
||||
ctx[CpuRegister.Rdx] = 6;
|
||||
ctx[CpuRegister.Rcx] = 3;
|
||||
ctx[CpuRegister.R8] = 1;
|
||||
ctx[CpuRegister.R9] = waitAddress;
|
||||
WriteUInt64(memory, StackAddress + 8, 0x1122_3344_5566_7788);
|
||||
WriteUInt64(memory, StackAddress + 16, 0xAABB_CCDD_EEFF_0011);
|
||||
WriteUInt32(memory, StackAddress + 24, 0x320);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
|
||||
Assert.Equal(0xC007_1058u, ReadUInt32(memory, PacketAddress));
|
||||
Assert.Equal(0x0000_0C00u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
Assert.Equal(0xEEFF_0011u, ReadUInt32(memory, PacketAddress + 12));
|
||||
Assert.Equal(0xAABB_CCDDu, ReadUInt32(memory, PacketAddress + 16));
|
||||
Assert.Equal(0x5566_7788u, ReadUInt32(memory, PacketAddress + 20));
|
||||
Assert.Equal(0x1122_3344u, ReadUInt32(memory, PacketAddress + 24));
|
||||
Assert.Equal(0x0200_0156u, ReadUInt32(memory, PacketAddress + 28));
|
||||
Assert.Equal(0x32u, ReadUInt32(memory, PacketAddress + 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitRegMemPatchFunctions_UseGen5Fields()
|
||||
{
|
||||
var memory = CreateMemory(out var ctx);
|
||||
WriteUInt32(memory, PacketAddress, 0xC005_1028);
|
||||
WriteUInt32(memory, PacketAddress + 20, 0x0400_0153);
|
||||
|
||||
ctx[CpuRegister.Rdi] = PacketAddress;
|
||||
ctx[CpuRegister.Rsi] = BaseAddress + 0xD07;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchAddress(ctx));
|
||||
Assert.Equal(0x0000_0D04u, ReadUInt32(memory, PacketAddress + 4));
|
||||
Assert.Equal(1u, ReadUInt32(memory, PacketAddress + 8));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 5;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchCompareFunction(ctx));
|
||||
Assert.Equal(0x0400_0155u, ReadUInt32(memory, PacketAddress + 20));
|
||||
|
||||
ctx[CpuRegister.Rsi] = 0xDEAD_BEEF;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.WaitRegMemPatchReference(ctx));
|
||||
Assert.Equal(0xDEAD_BEEFu, ReadUInt32(memory, PacketAddress + 16));
|
||||
}
|
||||
|
||||
private static FakeCpuMemory CreateMemory(out CpuContext ctx)
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
ctx = new CpuContext(memory, Generation.Gen5);
|
||||
ctx[CpuRegister.Rsp] = StackAddress;
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, PacketAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, PacketAddress + 0x100);
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
Assert.True(memory.TryRead(address, buffer));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Structural validation of the GPU detile compute kernel. This cannot run the
|
||||
// shader without a Vulkan device, but it pins the SPIR-V is well-formed: the
|
||||
// header is correct, every instruction's word count sums to exactly the module
|
||||
// length (the classic hand-emit bug), and the compute-specific pieces are
|
||||
// present (a GLCompute entry point, a LocalSize execution mode, and a runtime
|
||||
// array for the storage buffers). Full pixel correctness is verified on a GPU.
|
||||
public sealed class DetileComputeSpirvTests
|
||||
{
|
||||
private const uint SpirvMagic = 0x07230203;
|
||||
private const uint SpirvVersion15 = 0x00010500;
|
||||
|
||||
private const ushort OpEntryPoint = 15;
|
||||
private const ushort OpExecutionMode = 16;
|
||||
private const ushort OpTypeRuntimeArray = 29;
|
||||
private const ushort OpFunction = 54;
|
||||
private const ushort OpFunctionEnd = 56;
|
||||
|
||||
private const uint ExecutionModelGLCompute = 5;
|
||||
private const uint ExecutionModeLocalSize = 17;
|
||||
|
||||
[Fact]
|
||||
public void CreateDetileCompute_EmitsWellFormedComputeModule()
|
||||
{
|
||||
var spirv = SpirvFixedShaders.CreateDetileCompute();
|
||||
|
||||
Assert.True(spirv.Length % sizeof(uint) == 0, "SPIR-V must be a whole number of words.");
|
||||
var words = new uint[spirv.Length / sizeof(uint)];
|
||||
for (var i = 0; i < words.Length; i++)
|
||||
{
|
||||
words[i] = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(i * sizeof(uint)));
|
||||
}
|
||||
|
||||
Assert.True(words.Length > 5, "Module must have a header plus instructions.");
|
||||
Assert.Equal(SpirvMagic, words[0]);
|
||||
Assert.Equal(SpirvVersion15, words[1]);
|
||||
|
||||
var bound = words[3];
|
||||
Assert.True(bound > 1, "Id bound must be set.");
|
||||
|
||||
var sawComputeEntry = false;
|
||||
var sawLocalSize = false;
|
||||
var sawRuntimeArray = false;
|
||||
var functionCount = 0;
|
||||
var functionEndCount = 0;
|
||||
|
||||
var offset = 5;
|
||||
while (offset < words.Length)
|
||||
{
|
||||
var word = words[offset];
|
||||
var wordCount = (int)(word >> 16);
|
||||
var opcode = (ushort)(word & 0xFFFF);
|
||||
|
||||
Assert.True(wordCount >= 1, $"Instruction at {offset} has a zero word count.");
|
||||
Assert.True(
|
||||
offset + wordCount <= words.Length,
|
||||
$"Instruction at {offset} (op {opcode}, wc {wordCount}) overruns the module.");
|
||||
|
||||
switch (opcode)
|
||||
{
|
||||
case OpEntryPoint when words[offset + 1] == ExecutionModelGLCompute:
|
||||
sawComputeEntry = true;
|
||||
break;
|
||||
case OpExecutionMode
|
||||
when wordCount >= 6 &&
|
||||
words[offset + 2] == ExecutionModeLocalSize &&
|
||||
words[offset + 3] == 8 &&
|
||||
words[offset + 4] == 8 &&
|
||||
words[offset + 5] == 1:
|
||||
sawLocalSize = true;
|
||||
break;
|
||||
case OpTypeRuntimeArray:
|
||||
sawRuntimeArray = true;
|
||||
break;
|
||||
case OpFunction:
|
||||
functionCount++;
|
||||
break;
|
||||
case OpFunctionEnd:
|
||||
functionEndCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
// Word counts must tile the module exactly — a wrong length lands here.
|
||||
Assert.Equal(words.Length, offset);
|
||||
Assert.True(sawComputeEntry, "Missing a GLCompute OpEntryPoint.");
|
||||
Assert.True(sawLocalSize, "Missing an 8x8x1 LocalSize execution mode.");
|
||||
Assert.True(sawRuntimeArray, "Missing a runtime array (storage buffers).");
|
||||
Assert.Equal(1, functionCount);
|
||||
Assert.Equal(1, functionEndCount);
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,12 @@ public sealed class GnmTilingDetileTests
|
||||
return offset;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation()
|
||||
[Theory]
|
||||
[InlineData(384, 200)]
|
||||
[InlineData(768, 512)]
|
||||
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation(
|
||||
int elementsWide,
|
||||
int elementsHigh)
|
||||
{
|
||||
const uint swizzleMode = 27; // 64 KiB RB+ R_X
|
||||
const int bytesPerElement = 2;
|
||||
@@ -46,8 +50,6 @@ public sealed class GnmTilingDetileTests
|
||||
// SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored.
|
||||
const int blockWidth = 256;
|
||||
const int blockHeight = 128;
|
||||
const int elementsWide = 384; // spans two block columns and a partial third
|
||||
const int elementsHigh = 200;
|
||||
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||
var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight;
|
||||
|
||||
@@ -81,4 +83,152 @@ public sealed class GnmTilingDetileTests
|
||||
Assert.Equal((ushort)i, value);
|
||||
}
|
||||
}
|
||||
|
||||
// GetDetileParams must reproduce TryDetile bit-for-bit: the CPU fallback and
|
||||
// the GPU compute kernel both consume these params, so a detile driven purely
|
||||
// by DetileParams (the shared addressing formula the kernel runs) must equal
|
||||
// the shipped CPU detile for every supported mode/bpp.
|
||||
[Theory]
|
||||
[InlineData(27u, 2, 384, 200)] // 64 KiB RB+ R_X (exact-XOR)
|
||||
[InlineData(27u, 4, 256, 256)] // 64 KiB RB+ R_X (exact-XOR)
|
||||
[InlineData(9u, 4, 300, 300)] // 64 KiB standard (exact-XOR)
|
||||
[InlineData(24u, 4, 128, 256)] // 64 KiB RB+ Z_X (exact-XOR)
|
||||
[InlineData(5u, 4, 200, 120)] // 4 KiB standard (exact-XOR)
|
||||
[InlineData(8u, 4, 128, 128)] // 64 KiB Z (block-table path)
|
||||
[InlineData(1u, 4, 64, 64)] // 256 B standard (block-table path)
|
||||
public void GetDetileParams_ReproducesTryDetile(uint mode, int bpp, int w, int h)
|
||||
{
|
||||
var p = GnmTiling.GetDetileParams(mode, bpp, w, h);
|
||||
Assert.True(p.IsSupported);
|
||||
|
||||
// Whole-block tiled buffer (block addressing overshoots the linear extent),
|
||||
// filled with a deterministic non-trivial pattern.
|
||||
var blocksHigh = (h + p.BlockHeight - 1) / p.BlockHeight;
|
||||
var tiled = new byte[(long)p.BlocksPerRow * blocksHigh * p.BlockBytes];
|
||||
for (var i = 0; i < tiled.Length; i++)
|
||||
{
|
||||
tiled[i] = (byte)((i * 31 + 7) & 0xFF);
|
||||
}
|
||||
|
||||
var expected = new byte[w * h * bpp];
|
||||
Assert.True(GnmTiling.TryDetile(tiled, expected, mode, w, h, bpp));
|
||||
|
||||
var actual = DetileViaParams(tiled, p, w, h, bpp);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// The production GnmTiling.DetileWithParams (the active CPU fallback used by
|
||||
// the Metal path under default-on GPU detile) must equal TryDetile for every
|
||||
// supported mode/bpp — same DetileParams addressing, no re-derived swizzle.
|
||||
[Theory]
|
||||
[InlineData(27u, 2, 384, 200)]
|
||||
[InlineData(27u, 4, 256, 256)]
|
||||
[InlineData(9u, 4, 300, 300)]
|
||||
[InlineData(24u, 4, 128, 256)]
|
||||
[InlineData(5u, 4, 200, 120)]
|
||||
[InlineData(8u, 4, 128, 128)]
|
||||
[InlineData(1u, 4, 64, 64)]
|
||||
[InlineData(27u, 8, 256, 256)] // 8bpp (GPU: 2 words/element)
|
||||
[InlineData(27u, 16, 128, 128)] // 16bpp (GPU: 4 words/element)
|
||||
[InlineData(9u, 8, 128, 96)]
|
||||
[InlineData(8u, 16, 64, 64)] // block-table, 16bpp
|
||||
public void DetileWithParams_MatchesTryDetile(uint mode, int bpp, int w, int h)
|
||||
{
|
||||
var p = GnmTiling.GetDetileParams(mode, bpp, w, h);
|
||||
Assert.True(p.IsSupported);
|
||||
|
||||
var blocksHigh = (h + p.BlockHeight - 1) / p.BlockHeight;
|
||||
var tiled = new byte[(long)p.BlocksPerRow * blocksHigh * p.BlockBytes];
|
||||
for (var i = 0; i < tiled.Length; i++)
|
||||
{
|
||||
tiled[i] = (byte)((i * 31 + 7) & 0xFF);
|
||||
}
|
||||
|
||||
var expected = new byte[w * h * bpp];
|
||||
Assert.True(GnmTiling.TryDetile(tiled, expected, mode, w, h, bpp));
|
||||
|
||||
var actual = new byte[w * h * bpp];
|
||||
Assert.True(GnmTiling.DetileWithParams(p, tiled, actual));
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// Array textures are packed as contiguous tiled slices and detiled one slice
|
||||
// per layer (dispatch-Z on the GPU; a per-layer loop in the CPU fallbacks)
|
||||
// into a layer-major linear buffer. This pins that packing: each slice must
|
||||
// deswizzle into its own region and match a per-slice TryDetile, and a
|
||||
// per-layer-distinct pattern catches any slice cross-talk.
|
||||
[Theory]
|
||||
[InlineData(27u, 4, 256, 256, 3)]
|
||||
[InlineData(9u, 4, 128, 96, 2)]
|
||||
[InlineData(24u, 4, 64, 128, 4)]
|
||||
public void DetileWithParams_MultiLayer_MatchesPerSliceTryDetile(uint mode, int bpp, int w, int h, int layers)
|
||||
{
|
||||
var p = GnmTiling.GetDetileParams(mode, bpp, w, h);
|
||||
Assert.True(p.IsSupported);
|
||||
|
||||
var blocksHigh = (h + p.BlockHeight - 1) / p.BlockHeight;
|
||||
var sliceTiledBytes = (int)((long)p.BlocksPerRow * blocksHigh * p.BlockBytes);
|
||||
var sliceLinearBytes = w * h * bpp;
|
||||
|
||||
var tiled = new byte[sliceTiledBytes * layers];
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
for (var i = 0; i < sliceTiledBytes; i++)
|
||||
{
|
||||
tiled[layer * sliceTiledBytes + i] = (byte)((i * 31 + 7 + layer * 101) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
// Expected: each slice detiled independently via the shipped CPU detile.
|
||||
var expected = new byte[sliceLinearBytes * layers];
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
Assert.True(GnmTiling.TryDetile(
|
||||
tiled.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
expected.AsSpan(layer * sliceLinearBytes, sliceLinearBytes),
|
||||
mode, w, h, bpp));
|
||||
}
|
||||
|
||||
// Actual: the layer-major loop the Vulkan/Metal CPU fallbacks run.
|
||||
var actual = new byte[sliceLinearBytes * layers];
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
Assert.True(GnmTiling.DetileWithParams(
|
||||
p,
|
||||
tiled.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
actual.AsSpan(layer * sliceLinearBytes, sliceLinearBytes)));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// Reference detile driven entirely by DetileParams — the single shared
|
||||
// addressing formula the Vulkan/Metal compute kernel will run per texel.
|
||||
private static byte[] DetileViaParams(byte[] tiled, DetileParams p, int w, int h, int bpp)
|
||||
{
|
||||
var linear = new byte[w * h * bpp];
|
||||
for (var y = 0; y < h; y++)
|
||||
{
|
||||
for (var x = 0; x < w; x++)
|
||||
{
|
||||
var blockX = x / p.BlockWidth;
|
||||
var blockY = y / p.BlockHeight;
|
||||
var inX = x % p.BlockWidth;
|
||||
var inY = y % p.BlockHeight;
|
||||
var inBlockByte = p.Equation == DetileEquation.ExactXor
|
||||
? p.XByteTerm[x & p.XMask] ^ p.YByteTerm[y & p.YMask]
|
||||
: p.BlockTable[inY * p.BlockWidth + inX] * p.BytesPerElement;
|
||||
var srcByte = ((long)blockY * p.BlocksPerRow + blockX) * p.BlockBytes + inBlockByte;
|
||||
var dstByte = ((long)y * w + x) * bpp;
|
||||
if (srcByte < 0 || srcByte + bpp > tiled.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Array.Copy(tiled, srcByte, linear, dstByte, bpp);
|
||||
}
|
||||
}
|
||||
|
||||
return linear;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Audio;
|
||||
|
||||
[CollectionDefinition("AudioOutState", DisableParallelization = true)]
|
||||
public sealed class AudioOutStateCollection
|
||||
{
|
||||
public const string Name = "AudioOutState";
|
||||
}
|
||||
|
||||
[Collection(AudioOutStateCollection.Name)]
|
||||
public sealed class AudioOutExportsTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const int MemorySize = 0x4000;
|
||||
private const ulong ParameterAddress = MemoryBase + 0x100;
|
||||
private const ulong FirstSourceAddress = MemoryBase + 0x1000;
|
||||
private const ulong SecondSourceAddress = MemoryBase + 0x2000;
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(MemoryBase, MemorySize);
|
||||
private readonly CpuContext _ctx;
|
||||
private readonly List<RecordingAudioStream> _streams = [];
|
||||
|
||||
public AudioOutExportsTests()
|
||||
{
|
||||
AudioOutExports.ResetForTests();
|
||||
AudioOutExports.SetStreamFactoryForTests(_ =>
|
||||
{
|
||||
var stream = new RecordingAudioStream();
|
||||
_streams.Add(stream);
|
||||
return stream;
|
||||
});
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_StagesAndSubmitsSingleStereoPort()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
Span<byte> source = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source, -32768);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[2..], 32767);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[4..], -1234);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(source[6..], 5678);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, source));
|
||||
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 1);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(2UL, _ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(source.ToArray(), Assert.Single(_streams[0].Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_SubmitsEveryPortInTheBatch()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 2);
|
||||
byte[] firstSource = [1, 0, 2, 0, 3, 0, 4, 0];
|
||||
byte[] secondSource = [5, 0, 6, 0, 7, 0, 8, 0];
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, firstSource));
|
||||
Assert.True(_memory.TryWrite(SecondSourceAddress, secondSource));
|
||||
|
||||
// Reverse handle order to exercise canonical lock ordering without
|
||||
// changing which guest buffer belongs to each port.
|
||||
WriteDescriptor(0, secondHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, firstHandle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Equal(secondSource, Assert.Single(_streams[0].Submissions));
|
||||
Assert.Equal(firstSource, Assert.Single(_streams[1].Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_AcceptsNullBufferAsSynchronizationOnly()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
WriteDescriptor(0, handle, sourceAddress: 0);
|
||||
|
||||
var result = Submit(outputCount: 1);
|
||||
|
||||
Assert.Equal(2, result);
|
||||
Assert.Empty(_streams[0].Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_FaultInLaterBufferDoesNotPartiallySubmit()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 2);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, secondHandle, MemoryBase + MemorySize);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPointer, result);
|
||||
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsDuplicateHandlesBeforeSubmission()
|
||||
{
|
||||
var handle = OpenPort(bufferLength: 2);
|
||||
Assert.True(_memory.TryWrite(FirstSourceAddress, new byte[8]));
|
||||
Assert.True(_memory.TryWrite(SecondSourceAddress, new byte[8]));
|
||||
WriteDescriptor(0, handle, FirstSourceAddress);
|
||||
WriteDescriptor(1, handle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidPort, result);
|
||||
Assert.Empty(_streams[0].Submissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsPortsWithDifferentBufferLengths()
|
||||
{
|
||||
var firstHandle = OpenPort(bufferLength: 2);
|
||||
var secondHandle = OpenPort(bufferLength: 4);
|
||||
WriteDescriptor(0, firstHandle, FirstSourceAddress);
|
||||
WriteDescriptor(1, secondHandle, SecondSourceAddress);
|
||||
|
||||
var result = Submit(outputCount: 2);
|
||||
|
||||
Assert.Equal(AudioOutExports.AudioOutErrorInvalidSize, result);
|
||||
Assert.All(_streams, stream => Assert.Empty(stream.Submissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsUnknownHandle()
|
||||
{
|
||||
WriteDescriptor(0, 99, FirstSourceAddress);
|
||||
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPort,
|
||||
Submit(outputCount: 1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u)]
|
||||
[InlineData(26u)]
|
||||
public void Outputs_RejectsInvalidOutputCount(uint outputCount)
|
||||
{
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorPortFull,
|
||||
Submit(outputCount));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outputs_RejectsNullOrUnreadableParameterArray()
|
||||
{
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||
Submit(outputCount: 1, parameterAddress: 0));
|
||||
Assert.Equal(
|
||||
AudioOutExports.AudioOutErrorInvalidPointer,
|
||||
Submit(outputCount: 1, parameterAddress: MemoryBase + MemorySize));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputsExportRegistersForBothGenerations()
|
||||
{
|
||||
foreach (var generation in new[] { Generation.Gen4, Generation.Gen5 })
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(generation));
|
||||
|
||||
Assert.True(manager.TryGetExport("w3PdaSTSwGE", out var export));
|
||||
Assert.Equal("sceAudioOutOutputs", export.Name);
|
||||
Assert.Equal("libSceAudioOut", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => AudioOutExports.ResetForTests();
|
||||
|
||||
private int OpenPort(uint bufferLength)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 1;
|
||||
_ctx[CpuRegister.Rsi] = 0;
|
||||
_ctx[CpuRegister.Rdx] = 0;
|
||||
_ctx[CpuRegister.Rcx] = bufferLength;
|
||||
_ctx[CpuRegister.R8] = 48000;
|
||||
_ctx[CpuRegister.R9] = 1;
|
||||
return AudioOutExports.AudioOutOpen(_ctx);
|
||||
}
|
||||
|
||||
private int Submit(uint outputCount, ulong parameterAddress = ParameterAddress)
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = parameterAddress;
|
||||
_ctx[CpuRegister.Rsi] = outputCount;
|
||||
return AudioOutExports.AudioOutOutputs(_ctx);
|
||||
}
|
||||
|
||||
private void WriteDescriptor(int index, int handle, ulong sourceAddress)
|
||||
{
|
||||
Span<byte> descriptor = stackalloc byte[16];
|
||||
descriptor.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(descriptor, handle);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(descriptor[8..], sourceAddress);
|
||||
Assert.True(_memory.TryWrite(
|
||||
ParameterAddress + unchecked((ulong)(index * descriptor.Length)),
|
||||
descriptor));
|
||||
}
|
||||
|
||||
private sealed class RecordingAudioStream : IHostAudioStream
|
||||
{
|
||||
public List<byte[]> Submissions { get; } = [];
|
||||
|
||||
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
|
||||
{
|
||||
Submissions.Add(stereoPcm16.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,25 @@ public sealed class AvPlayerPathTests : IDisposable
|
||||
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "ffmpeg")]
|
||||
[InlineData(true, "ffmpeg.exe")]
|
||||
public void MediaToolLookupFindsPackagedBinary(bool isWindows, string executable)
|
||||
{
|
||||
var publishDirectory = Path.Combine(_tempRoot, "publish");
|
||||
Directory.CreateDirectory(Path.Combine(publishDirectory, "ffmpeg"));
|
||||
var ffmpeg = Path.Combine(publishDirectory, "ffmpeg", executable);
|
||||
File.WriteAllBytes(ffmpeg, []);
|
||||
|
||||
Assert.Equal(
|
||||
ffmpeg,
|
||||
AvPlayerExports.FindFfmpeg(
|
||||
configured: null,
|
||||
searchPath: null,
|
||||
isWindows,
|
||||
publishDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RelativeFileUriCannotEscapeApp0()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Libs.Bink;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Bink;
|
||||
|
||||
public sealed class Bink2MovieBridgeTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"sharpemu-bink-{Guid.NewGuid():N}");
|
||||
|
||||
public Bink2MovieBridgeTests()
|
||||
{
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeaderPreservesFractionalFrameRate()
|
||||
{
|
||||
var path = WriteHeader("KB2j"u8, 3840, 2160, 30_000, 1_001);
|
||||
|
||||
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out var info));
|
||||
Assert.Equal(3840u, info.Width);
|
||||
Assert.Equal(2160u, info.Height);
|
||||
Assert.Equal(30_000u, info.FramesPerSecondNumerator);
|
||||
Assert.Equal(1_001u, info.FramesPerSecondDenominator);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("KB2g")]
|
||||
[InlineData("KB2i")]
|
||||
[InlineData("KB2j")]
|
||||
public void HeaderAcceptsBink2Revisions(string signature)
|
||||
{
|
||||
var path = WriteHeader(
|
||||
System.Text.Encoding.ASCII.GetBytes(signature),
|
||||
1920,
|
||||
1080,
|
||||
60,
|
||||
1);
|
||||
|
||||
Assert.True(Bink2MovieBridge.TryReadBinkInfo(path, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeaderRejectsMissingFrameRateDenominator()
|
||||
{
|
||||
var path = WriteHeader("KB2j"u8, 1920, 1080, 60, 0);
|
||||
|
||||
Assert.False(Bink2MovieBridge.TryReadBinkInfo(path, out _));
|
||||
}
|
||||
|
||||
private string WriteHeader(
|
||||
ReadOnlySpan<byte> signature,
|
||||
uint width,
|
||||
uint height,
|
||||
uint fpsNumerator,
|
||||
uint fpsDenominator)
|
||||
{
|
||||
var header = new byte[36];
|
||||
signature.CopyTo(header);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x14), width);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x18), height);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x1C), fpsNumerator);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(0x20), fpsDenominator);
|
||||
var path = Path.Combine(_tempDirectory, $"{Guid.NewGuid():N}.bk2");
|
||||
File.WriteAllBytes(path, header);
|
||||
return path;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(_tempDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Bink;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Bink;
|
||||
|
||||
public sealed class BinkFramePlaybackTests
|
||||
{
|
||||
[Fact]
|
||||
public void FramesAdvanceAccordingToMovieClock()
|
||||
{
|
||||
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2, 3));
|
||||
|
||||
Assert.Equal(1, WaitForAdvancedFrame(playback)[0]);
|
||||
Assert.True(playback.TryGetFrame(true, out var heldFrame, out var advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, heldFrame[0]);
|
||||
|
||||
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
|
||||
Assert.Equal(3, WaitForAdvancedFrame(playback)[0]);
|
||||
}
|
||||
|
||||
private static byte[] WaitForAdvancedFrame(BinkFramePlayback playback)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (playback.TryGetFrame(true, out var frame, out var advanced) && advanced)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The decoder did not produce a frame.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstFrameWaitsUntilPresentationStarts()
|
||||
{
|
||||
using var playback = new BinkFramePlayback(new SequenceDecoder(1, 2));
|
||||
|
||||
var first = WaitForFrame(playback, advanceClock: false);
|
||||
Assert.Equal(1, first[0]);
|
||||
Thread.Sleep(100);
|
||||
|
||||
Assert.True(playback.TryGetFrame(false, out var held, out var advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, held[0]);
|
||||
|
||||
Assert.True(playback.TryGetFrame(true, out held, out advanced));
|
||||
Assert.False(advanced);
|
||||
Assert.Equal(1, held[0]);
|
||||
Assert.Equal(2, WaitForAdvancedFrame(playback)[0]);
|
||||
}
|
||||
|
||||
private static byte[] WaitForFrame(
|
||||
BinkFramePlayback playback,
|
||||
bool advanceClock)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (playback.TryGetFrame(advanceClock, out var frame, out _))
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
throw new TimeoutException("The decoder did not produce a frame.");
|
||||
}
|
||||
|
||||
private sealed class SequenceDecoder(params byte[] values) : IBinkFrameDecoder
|
||||
{
|
||||
private int _index;
|
||||
|
||||
public uint Width => 1;
|
||||
|
||||
public uint Height => 1;
|
||||
|
||||
// Keep frame boundaries far enough apart that a loaded CI runner cannot
|
||||
// skip an expected frame between polling iterations.
|
||||
public uint FramesPerSecondNumerator => 2;
|
||||
|
||||
public uint FramesPerSecondDenominator => 1;
|
||||
|
||||
public bool TryDecodeNextFrame(Span<byte> destination)
|
||||
{
|
||||
if (_index >= values.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination.Fill(values[_index++]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,38 @@ public sealed class KernelMemoryCompatExportsTests
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualQuery_PreservesReservationPastFixedCommitAtSameBase()
|
||||
{
|
||||
const ulong memoryBase = 0x12_0000_0000;
|
||||
const ulong reservedLength = 0x1_0000;
|
||||
const ulong committedLength = 0x2000;
|
||||
const ulong inOutAddress = memoryBase + 0x1_7000;
|
||||
const ulong infoAddress = memoryBase + 0x1_8000;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x2_0000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
|
||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(memoryBase, reservedLength);
|
||||
Assert.True(memory.TryWrite(inOutAddress, BitConverter.GetBytes(memoryBase)));
|
||||
context[CpuRegister.Rdi] = inOutAddress;
|
||||
context[CpuRegister.Rsi] = committedLength;
|
||||
context[CpuRegister.Rdx] = 0x03;
|
||||
context[CpuRegister.Rcx] = 0x10; // fixed mapping
|
||||
|
||||
Assert.Equal(0, KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context));
|
||||
|
||||
context[CpuRegister.Rdi] = memoryBase + 0x8000;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
context[CpuRegister.Rdx] = infoAddress;
|
||||
context[CpuRegister.Rcx] = 0x48;
|
||||
|
||||
Assert.Equal(0, KernelMemoryCompatExports.KernelVirtualQuery(context));
|
||||
Assert.True(context.TryReadUInt64(infoAddress, out var regionStart));
|
||||
Assert.True(context.TryReadUInt64(infoAddress + 8, out var regionEnd));
|
||||
Assert.Equal(memoryBase + committedLength, regionStart);
|
||||
Assert.Equal(memoryBase + reservedLength, regionEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mprotect_ZeroAddressReturnsInvalidArgument()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,11 @@ public sealed class SystemServiceExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
|
||||
public SystemServiceExportsTests()
|
||||
{
|
||||
SystemServiceExports.ResetForTests();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNoticeScreenSkipFlagWritesOneByteAtMemoryBoundary()
|
||||
{
|
||||
@@ -26,4 +31,23 @@ public sealed class SystemServiceExportsTests
|
||||
Assert.True(memory.TryRead(MemoryBase, flag));
|
||||
Assert.Equal(0, flag[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetNoticeScreenSkipFlagRoundTripsThroughGetter()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 2);
|
||||
var context = new CpuContext(memory, Generation.Gen5)
|
||||
{
|
||||
[CpuRegister.Rdi] = 1,
|
||||
};
|
||||
|
||||
Assert.Equal(0, SystemServiceExports.SystemServiceSetNoticeScreenSkipFlag(context));
|
||||
|
||||
context[CpuRegister.Rdi] = MemoryBase;
|
||||
Assert.Equal(0, SystemServiceExports.SystemServiceGetNoticeScreenSkipFlag(context));
|
||||
|
||||
Span<byte> flag = stackalloc byte[1];
|
||||
Assert.True(memory.TryRead(MemoryBase, flag));
|
||||
Assert.Equal(1, flag[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,23 @@ public sealed class VulkanGuestImageByteCountTests
|
||||
expected,
|
||||
VulkanVideoPresenter.GetGuestImageByteCount(format, width, height));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10u, 8u, 4u, 3u, 384UL)]
|
||||
[InlineData(169u, 5u, 5u, 7u, 224UL)]
|
||||
public void MultipliesSurfaceSizeByVolumeDepth(
|
||||
uint format,
|
||||
uint width,
|
||||
uint height,
|
||||
uint depth,
|
||||
ulong expected)
|
||||
{
|
||||
Assert.Equal(
|
||||
expected,
|
||||
VulkanVideoPresenter.GetGuestImageByteCount(
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
depth));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanGuestImageTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ThreeDimensionalDescriptorsMapToVolumeImageAndViewTypes()
|
||||
{
|
||||
Assert.Equal(
|
||||
ImageType.Type3D,
|
||||
VulkanVideoPresenter.GetGuestTextureImageType(
|
||||
VulkanVideoPresenter.Gen5TextureType3D));
|
||||
Assert.Equal(
|
||||
ImageViewType.Type3D,
|
||||
VulkanVideoPresenter.GetGuestTextureViewType(
|
||||
VulkanVideoPresenter.Gen5TextureType3D,
|
||||
arrayedView: true));
|
||||
Assert.Equal(
|
||||
7u,
|
||||
VulkanVideoPresenter.GetGuestTextureDepth(
|
||||
VulkanVideoPresenter.Gen5TextureType3D,
|
||||
7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoDimensionalArraysKeepLayersSeparateFromImageDepth()
|
||||
{
|
||||
Assert.Equal(
|
||||
ImageType.Type2D,
|
||||
VulkanVideoPresenter.GetGuestTextureImageType(
|
||||
VulkanVideoPresenter.Gen5TextureType2D));
|
||||
Assert.Equal(
|
||||
ImageViewType.Type2DArray,
|
||||
VulkanVideoPresenter.GetGuestTextureViewType(
|
||||
VulkanVideoPresenter.Gen5TextureType2D,
|
||||
arrayedView: true));
|
||||
Assert.Equal(
|
||||
1u,
|
||||
VulkanVideoPresenter.GetGuestTextureDepth(
|
||||
VulkanVideoPresenter.Gen5TextureType2D,
|
||||
7));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
public sealed class Gen5FlatMemoryTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint SEndpgm = 0xBF810000;
|
||||
|
||||
[Fact]
|
||||
public void FlatLoadUbyteInfersScalarBaseAndCompiles()
|
||||
{
|
||||
var memory = new TestCpuMemory(ShaderAddress, 0x4000);
|
||||
uint[] words =
|
||||
[
|
||||
// v_add_co_u32 v1, vcc_lo, s12, v6
|
||||
0xD70F6A01,
|
||||
0x00020C0C,
|
||||
// v_add_co_ci_u32_sdwa v2, vcc_lo, 0, s13, vcc_lo
|
||||
0x50041AF9,
|
||||
0x86860680,
|
||||
// flat_load_ubyte v0, v[1:2]
|
||||
0xDC200000,
|
||||
0x007D0001,
|
||||
SEndpgm,
|
||||
];
|
||||
var shader = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
shader.AsSpan(index * sizeof(uint)),
|
||||
words[index]);
|
||||
}
|
||||
Assert.True(memory.TryWrite(ShaderAddress, shader));
|
||||
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryDecodeProgram(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
out var program,
|
||||
out var decodeError),
|
||||
decodeError);
|
||||
|
||||
var instruction = Assert.Single(
|
||||
program.Instructions,
|
||||
item => item.Opcode == "FlatLoadUbyte");
|
||||
var control = Assert.IsType<Gen5GlobalMemoryControl>(
|
||||
instruction.Control);
|
||||
Assert.True(control.UsesFlatAddress);
|
||||
Assert.Equal(1u, control.VectorAddress);
|
||||
Assert.Equal(0u, control.VectorData);
|
||||
Assert.Equal(12u, control.ScalarAddress);
|
||||
Assert.Equal(
|
||||
[
|
||||
Gen5Operand.Vector(1),
|
||||
Gen5Operand.Vector(2),
|
||||
Gen5Operand.Scalar(12),
|
||||
],
|
||||
instruction.Sources);
|
||||
|
||||
uint[] userData =
|
||||
[
|
||||
unchecked((uint)ShaderAddress),
|
||||
unchecked((uint)(ShaderAddress >> 32)),
|
||||
];
|
||||
var state = new Gen5ShaderState(
|
||||
program,
|
||||
userData,
|
||||
null,
|
||||
UserDataScalarRegisterBase: 12);
|
||||
Assert.True(
|
||||
Gen5ShaderScalarEvaluator.TryEvaluate(
|
||||
ctx,
|
||||
state,
|
||||
out var evaluation,
|
||||
out var evaluationError),
|
||||
evaluationError);
|
||||
|
||||
var binding = Assert.Single(evaluation.GlobalMemoryBindings);
|
||||
Assert.Equal(12u, binding.ScalarAddress);
|
||||
Assert.Contains(instruction.Pc, binding.InstructionPcs);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
out var compiled,
|
||||
out var compileError),
|
||||
compileError);
|
||||
Assert.Contains(
|
||||
(ushort)SpirvOp.ISub,
|
||||
ReadSpirvOpcodes(compiled.Spirv));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
|
||||
{
|
||||
Assert.Equal(0, spirv.Length % sizeof(uint));
|
||||
Assert.True(spirv.Length >= 5 * sizeof(uint));
|
||||
Assert.Equal(
|
||||
0x07230203u,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv));
|
||||
|
||||
var opcodes = new List<ushort>();
|
||||
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
|
||||
{
|
||||
var instruction =
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
|
||||
var wordCount = checked((int)(instruction >> 16));
|
||||
Assert.InRange(
|
||||
wordCount,
|
||||
1,
|
||||
(spirv.Length - offset) / sizeof(uint));
|
||||
opcodes.Add((ushort)instruction);
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
|
||||
return opcodes;
|
||||
}
|
||||
|
||||
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
|
||||
{
|
||||
private readonly byte[] _storage = new byte[size];
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(
|
||||
ulong virtualAddress,
|
||||
ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolve(
|
||||
ulong virtualAddress,
|
||||
int length,
|
||||
out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (virtualAddress < baseAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relative = virtualAddress - baseAddress;
|
||||
if (relative + (ulong)length > (ulong)_storage.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)relative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
public sealed class Gen5ImageTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_C000;
|
||||
private const uint SEndpgm = 0xBF810000;
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, SpirvImageDim.Dim2D, 2u)]
|
||||
[InlineData(2u, SpirvImageDim.Dim3D, 3u)]
|
||||
public void ImageStoreDimensionControlsImageAndCoordinateTypes(
|
||||
uint dimension,
|
||||
SpirvImageDim expectedImageDimension,
|
||||
uint expectedCoordinateComponents)
|
||||
{
|
||||
var instructions = ReadSpirvInstructions(
|
||||
CompileImageOperation("ImageStore", dimension));
|
||||
var imageType = Assert.Single(
|
||||
instructions,
|
||||
item => item.Opcode == SpirvOp.TypeImage);
|
||||
|
||||
Assert.Equal((uint)expectedImageDimension, imageType.Operands[2]);
|
||||
Assert.Equal(2u, imageType.Operands[6]);
|
||||
AssertCoordinateVectorWidth(
|
||||
instructions,
|
||||
SpirvOp.ImageWrite,
|
||||
coordinateOperand: 1,
|
||||
expectedComponents: expectedCoordinateComponents);
|
||||
|
||||
var sizeQuery = Assert.Single(
|
||||
instructions,
|
||||
item => item.Opcode == SpirvOp.ImageQuerySize);
|
||||
AssertVectorTypeWidth(
|
||||
instructions,
|
||||
sizeQuery.Operands[0],
|
||||
expectedCoordinateComponents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageSampleDim3DUsesThreeComponentSampleCoordinates()
|
||||
{
|
||||
var instructions = ReadSpirvInstructions(
|
||||
CompileImageOperation("ImageSampleLz", dimension: 2));
|
||||
var imageType = Assert.Single(
|
||||
instructions,
|
||||
item => item.Opcode == SpirvOp.TypeImage);
|
||||
|
||||
Assert.Equal((uint)SpirvImageDim.Dim3D, imageType.Operands[2]);
|
||||
Assert.Equal(1u, imageType.Operands[6]);
|
||||
AssertCoordinateVectorWidth(
|
||||
instructions,
|
||||
SpirvOp.ImageSampleExplicitLod,
|
||||
coordinateOperand: 3,
|
||||
expectedComponents: 3);
|
||||
}
|
||||
|
||||
private static byte[] CompileImageOperation(string opcode, uint dimension)
|
||||
{
|
||||
var addressRegisters = dimension == 2
|
||||
? new uint[] { 0, 1, 2 }
|
||||
: [0, 1];
|
||||
var control = new Gen5ImageControl(
|
||||
Dmask: 0xF,
|
||||
VectorAddress: 0,
|
||||
AddressRegisters: addressRegisters,
|
||||
VectorData: 4,
|
||||
ScalarResource: 8,
|
||||
ScalarSampler: 16,
|
||||
Dimension: dimension,
|
||||
IsArray: false,
|
||||
Glc: false,
|
||||
Slc: false,
|
||||
A16: false,
|
||||
D16: false);
|
||||
var imageInstruction = new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Mimg,
|
||||
opcode,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
control);
|
||||
var end = new Gen5ShaderInstruction(
|
||||
8,
|
||||
Gen5ShaderEncoding.Sopp,
|
||||
"SEndpgm",
|
||||
[SEndpgm],
|
||||
[],
|
||||
[],
|
||||
null);
|
||||
var state = new Gen5ShaderState(
|
||||
new Gen5ShaderProgram(ShaderAddress, [imageInstruction, end]),
|
||||
[],
|
||||
null);
|
||||
var scalarRegisters = new uint[256];
|
||||
var descriptor = new uint[8];
|
||||
descriptor[1] = 71u << 20; // FORMAT_16_16_16_16_FLOAT
|
||||
descriptor[3] = (dimension == 2 ? 10u : 9u) << 28;
|
||||
var evaluation = new Gen5ShaderEvaluation(
|
||||
scalarRegisters,
|
||||
scalarRegisters,
|
||||
[
|
||||
new Gen5ImageBinding(
|
||||
imageInstruction.Pc,
|
||||
imageInstruction.Opcode,
|
||||
control,
|
||||
descriptor,
|
||||
new uint[4],
|
||||
null),
|
||||
],
|
||||
[]);
|
||||
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state,
|
||||
evaluation,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
out var shader,
|
||||
out var error),
|
||||
error);
|
||||
return shader.Spirv;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ParsedSpirvInstruction> ReadSpirvInstructions(
|
||||
byte[] spirv)
|
||||
{
|
||||
var instructions = new List<ParsedSpirvInstruction>();
|
||||
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
|
||||
{
|
||||
var instruction = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
spirv.AsSpan(offset));
|
||||
var wordCount = checked((int)(instruction >> 16));
|
||||
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
|
||||
var operands = new uint[wordCount - 1];
|
||||
for (var operand = 0; operand < operands.Length; operand++)
|
||||
{
|
||||
operands[operand] = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
spirv.AsSpan(offset + (operand + 1) * sizeof(uint)));
|
||||
}
|
||||
|
||||
instructions.Add(
|
||||
new ParsedSpirvInstruction((SpirvOp)(ushort)instruction, operands));
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
private static void AssertCoordinateVectorWidth(
|
||||
IReadOnlyList<ParsedSpirvInstruction> instructions,
|
||||
SpirvOp operation,
|
||||
int coordinateOperand,
|
||||
uint expectedComponents)
|
||||
{
|
||||
var imageOperation = Assert.Single(
|
||||
instructions,
|
||||
item => item.Opcode == operation);
|
||||
var coordinateId = imageOperation.Operands[coordinateOperand];
|
||||
var coordinate = Assert.Single(
|
||||
instructions,
|
||||
item =>
|
||||
item.Opcode == SpirvOp.CompositeConstruct &&
|
||||
item.Operands.Length >= 2 &&
|
||||
item.Operands[1] == coordinateId);
|
||||
AssertVectorTypeWidth(
|
||||
instructions,
|
||||
coordinate.Operands[0],
|
||||
expectedComponents);
|
||||
}
|
||||
|
||||
private static void AssertVectorTypeWidth(
|
||||
IReadOnlyList<ParsedSpirvInstruction> instructions,
|
||||
uint vectorTypeId,
|
||||
uint expectedComponents)
|
||||
{
|
||||
var vectorType = Assert.Single(
|
||||
instructions,
|
||||
item =>
|
||||
item.Opcode == SpirvOp.TypeVector &&
|
||||
item.Operands[0] == vectorTypeId);
|
||||
Assert.Equal(expectedComponents, vectorType.Operands[2]);
|
||||
}
|
||||
|
||||
private readonly record struct ParsedSpirvInstruction(
|
||||
SpirvOp Opcode,
|
||||
uint[] Operands);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler\SharpEmu.ShaderCompiler.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.ShaderCompiler.Vulkan\SharpEmu.ShaderCompiler.Vulkan.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||