mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77fe07efc0 | |||
| 5f37dd85e0 | |||
| 38621e7be9 | |||
| c9c0793059 |
@@ -14,6 +14,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<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>
|
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
<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.Desktop" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" 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="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
|
|||||||
+51
-22
@@ -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
|
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
|
||||||
decoder cannot observe or replace those frames.
|
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.
|
available, presents its decoded BGRA frames at the normal guest-flip boundary.
|
||||||
This preserves the game's own timing and lets the host Vulkan presenter display
|
This preserves the game's own timing and lets the host Vulkan presenter display
|
||||||
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
the movie without trying to execute the PS5-specific Bink GPU decode path.
|
||||||
|
|
||||||
Without an adapter, Bink files remain visible to the guest and the game's
|
The default path decodes by calling FFmpeg's own C API directly from managed
|
||||||
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
|
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
|
||||||
explicitly testing a title whose cinematics are optional.
|
[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,
|
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
|
||||||
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
|
||||||
only; it does not decode the movie or alter its game logic. Set
|
only; it does not decode the movie or alter its game logic.
|
||||||
SHARPEMU_BINK_MODE=native to force native bridge mode.
|
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,
|
## Supplying the FFmpeg libraries
|
||||||
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.
|
|
||||||
|
|
||||||
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
|
`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
|
||||||
executable, or point to it explicitly:
|
(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 \
|
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
||||||
./SharpEmu /path/to/eboot.bin
|
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,
|
To use a different set of FFmpeg libraries, drop them into the published
|
||||||
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
|
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||||
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
|
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||||
frame. The managed side validates dimensions and retains ownership of the
|
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||||
destination buffer.
|
folder and does not otherwise care where the files came from.
|
||||||
|
|
||||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
||||||
and retains the existing guest rendering path.
|
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);
|
|
||||||
}
|
|
||||||
@@ -64,6 +64,7 @@ internal static partial class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
|
PreloadGlfw();
|
||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -213,6 +214,27 @@ internal static partial class Program
|
|||||||
"as libvulkan.1.dylib.");
|
"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)
|
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|||||||
@@ -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
|
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||||
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
<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>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
@@ -60,7 +65,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\LICENSE.txt">
|
<Content Include="..\..\LICENSE.txt">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
@@ -74,17 +79,75 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</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. -->
|
library is embedded into the single-file bundle. -->
|
||||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
||||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<RelativePath>$(NativeLibraryFolderName)/%(Filename)%(Extension)</RelativePath>
|
||||||
</ResolvedFileToPublish>
|
</ResolvedFileToPublish>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</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>
|
</Project>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Bink;
|
||||||
using SharpEmu.Libs.Gpu;
|
using SharpEmu.Libs.Gpu;
|
||||||
using SharpEmu.ShaderCompiler;
|
using SharpEmu.ShaderCompiler;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
@@ -48,6 +49,7 @@ public static partial class AgcExports
|
|||||||
private const uint ItWriteData = 0x37;
|
private const uint ItWriteData = 0x37;
|
||||||
private const uint ItDispatchDirect = 0x15;
|
private const uint ItDispatchDirect = 0x15;
|
||||||
private const uint ItDispatchIndirect = 0x16;
|
private const uint ItDispatchIndirect = 0x16;
|
||||||
|
private const uint ItSetPredication = 0x20;
|
||||||
private const uint ItWaitRegMem = 0x3C;
|
private const uint ItWaitRegMem = 0x3C;
|
||||||
private const uint ItIndirectBuffer = 0x3F;
|
private const uint ItIndirectBuffer = 0x3F;
|
||||||
private const uint ItEventWrite = 0x46;
|
private const uint ItEventWrite = 0x46;
|
||||||
@@ -277,7 +279,7 @@ public static partial class AgcExports
|
|||||||
private static long _labelProducerSequence;
|
private static long _labelProducerSequence;
|
||||||
private static readonly object _labelProducerGate = new();
|
private static readonly object _labelProducerGate = new();
|
||||||
private static readonly List<LabelProducerTrace> _labelProducers = [];
|
private static readonly List<LabelProducerTrace> _labelProducers = [];
|
||||||
private static readonly HashSet<(object Memory, ulong Address, ulong SubmissionId)>
|
private static readonly HashSet<(object Memory, ulong Address)>
|
||||||
_tracedProducerlessWaits = new();
|
_tracedProducerlessWaits = new();
|
||||||
private static long _shaderTranslationMissTraceCount;
|
private static long _shaderTranslationMissTraceCount;
|
||||||
private static long _translatedDrawTraceCount;
|
private static long _translatedDrawTraceCount;
|
||||||
@@ -543,6 +545,7 @@ public static partial class AgcExports
|
|||||||
public uint IndexSize { get; set; }
|
public uint IndexSize { get; set; }
|
||||||
public uint InstanceCount { get; set; } = 1;
|
public uint InstanceCount { get; set; } = 1;
|
||||||
public uint DrawIndexOffset { get; set; }
|
public uint DrawIndexOffset { get; set; }
|
||||||
|
public bool PredicateSkip { get; set; }
|
||||||
public string QueueName { get; set; } = "graphics";
|
public string QueueName { get; set; } = "graphics";
|
||||||
public ulong ActiveSubmissionId { get; set; }
|
public ulong ActiveSubmissionId { get; set; }
|
||||||
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
||||||
@@ -1183,12 +1186,12 @@ public static partial class AgcExports
|
|||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var packetDwords = size == 0 ? 6u : 9u;
|
var packetDwords = size == 0 ? 7u : 9u;
|
||||||
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
||||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address & (size == 0 ? ~0x3u : ~0x7u)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32) & 0x3FFFFu) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
@@ -1196,8 +1199,9 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (size == 0)
|
if (size == 0)
|
||||||
{
|
{
|
||||||
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction) ||
|
if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
!TryWriteUInt32(ctx, commandAddress + 20, EncodeWaitRegMem32Control(compareFunction, 0, cachePolicy)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 24, EncodeWaitRegMemPoll(pollCycles)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
@@ -1205,8 +1209,8 @@ public static partial class AgcExports
|
|||||||
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction) ||
|
!TryWriteUInt32(ctx, commandAddress + 28, EncodeWaitRegMem64Control(compareFunction, 0, cachePolicy)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 32, pollCycles / 40))
|
!TryWriteUInt32(ctx, commandAddress + 32, EncodeWaitRegMemPoll(pollCycles)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
@@ -1826,38 +1830,21 @@ public static partial class AgcExports
|
|||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var standardWait = operation is 2 or 3;
|
var packetDwords = size == 0 ? 7u : 9u;
|
||||||
var packetDwords = standardWait ? 7u : size == 0 ? 6u : 9u;
|
|
||||||
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64;
|
||||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress))
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||||
{
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
||||||
return ReturnPointer(ctx, 0);
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address & (size == 0 ? ~0x3u : ~0x7u)) ||
|
||||||
}
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32) & 0x3FFFFu) ||
|
||||||
|
|
||||||
if (standardWait)
|
|
||||||
{
|
|
||||||
if (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItWaitRegMem, 0)) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 4, compareFunction | ((operation & 1) << 8)) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)address) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(address >> 32)) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)mask) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40))
|
|
||||||
{
|
|
||||||
return ReturnPointer(ctx, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
|
||||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
else if (size == 0)
|
else if (size == 0)
|
||||||
{
|
{
|
||||||
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction | (operation << 8)) ||
|
if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
!TryWriteUInt32(ctx, commandAddress + 20, EncodeWaitRegMem32Control(compareFunction, operation, cachePolicy)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 24, EncodeWaitRegMemPoll(pollCycles)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
@@ -1865,8 +1852,8 @@ public static partial class AgcExports
|
|||||||
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction | (operation << 8)) ||
|
!TryWriteUInt32(ctx, commandAddress + 28, EncodeWaitRegMem64Control(compareFunction, operation, cachePolicy)) ||
|
||||||
!TryWriteUInt32(ctx, commandAddress + 32, pollCycles / 40))
|
!TryWriteUInt32(ctx, commandAddress + 32, EncodeWaitRegMemPoll(pollCycles)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
@@ -2305,7 +2292,14 @@ public static partial class AgcExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
|
var wrote = op == ItNop && register is RWaitMem32 or RWaitMem64
|
||||||
|
? TryWriteUInt32(
|
||||||
|
ctx,
|
||||||
|
commandAddress + fieldOffset,
|
||||||
|
(uint)address & (register == RWaitMem32 ? ~0x3u : ~0x7u)) &&
|
||||||
|
TryWriteUInt32(ctx, commandAddress + fieldOffset + 4, (uint)(address >> 32) & 0x3FFFFu)
|
||||||
|
: ctx.TryWriteUInt64(commandAddress + fieldOffset, address);
|
||||||
|
return wrote
|
||||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -2328,7 +2322,7 @@ public static partial class AgcExports
|
|||||||
var fieldOffset = op == ItWaitRegMem
|
var fieldOffset = op == ItWaitRegMem
|
||||||
? 4UL
|
? 4UL
|
||||||
: op == ItNop && register == RWaitMem32
|
: op == ItNop && register == RWaitMem32
|
||||||
? 16UL
|
? 20UL
|
||||||
: op == ItNop && register == RWaitMem64
|
: op == ItNop && register == RWaitMem64
|
||||||
? 28UL
|
? 28UL
|
||||||
: 0;
|
: 0;
|
||||||
@@ -2357,7 +2351,7 @@ public static partial class AgcExports
|
|||||||
var wrote = op == ItWaitRegMem
|
var wrote = op == ItWaitRegMem
|
||||||
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
||||||
: op == ItNop && register == RWaitMem32
|
: op == ItNop && register == RWaitMem32
|
||||||
? TryWriteUInt32(ctx, commandAddress + 20, (uint)reference)
|
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
||||||
: op == ItNop && register == RWaitMem64 &&
|
: op == ItNop && register == RWaitMem64 &&
|
||||||
ctx.TryWriteUInt64(commandAddress + 20, reference);
|
ctx.TryWriteUInt64(commandAddress + 20, reference);
|
||||||
return wrote
|
return wrote
|
||||||
@@ -3105,6 +3099,26 @@ public static partial class AgcExports
|
|||||||
CountSubmittedOpcode(op, register);
|
CountSubmittedOpcode(op, register);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((header & 1u) != 0 && state.PredicateSkip)
|
||||||
|
{
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.predicated_skip queue={state.QueueName} " +
|
||||||
|
$"packet=0x{currentAddress:X16} op=0x{op:X2} len={length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == ItSetPredication)
|
||||||
|
{
|
||||||
|
ApplySubmittedPredication(ctx, state, currentAddress, length, tracePackets);
|
||||||
|
offset += length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (op == ItNop &&
|
if (op == ItNop &&
|
||||||
register is RDrawReset or RAcbReset &&
|
register is RDrawReset or RAcbReset &&
|
||||||
length >= 2)
|
length >= 2)
|
||||||
@@ -3823,7 +3837,7 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (!stale && producer is null &&
|
if (!stale && producer is null &&
|
||||||
!_tracedProducerlessWaits.Add(
|
!_tracedProducerlessWaits.Add(
|
||||||
(memory, waiter.WaitAddress, waiter.SubmissionId)))
|
(memory, waiter.WaitAddress)))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4019,6 +4033,111 @@ public static partial class AgcExports
|
|||||||
state.DrawIndexOffset = 0;
|
state.DrawIndexOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplySubmittedPredication(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedDcbState state,
|
||||||
|
ulong packetAddress,
|
||||||
|
uint packetLength,
|
||||||
|
bool tracePacket)
|
||||||
|
{
|
||||||
|
if (packetLength < 3 ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 4, out var first) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 8, out var second))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint flagsMask = 0x0007_1100u;
|
||||||
|
uint flags;
|
||||||
|
ulong predicateAddress;
|
||||||
|
if (packetLength >= 4 &&
|
||||||
|
(first & ~flagsMask) == 0 &&
|
||||||
|
TryReadUInt32(ctx, packetAddress + 12, out var third) &&
|
||||||
|
third <= 0xFFFFu)
|
||||||
|
{
|
||||||
|
flags = first;
|
||||||
|
predicateAddress = ((ulong)third << 32) | (second & 0xFFFF_FFF0u);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flags = second;
|
||||||
|
predicateAddress = (first & 0xFFFF_FFF0u) | ((ulong)(second & 0xFFu) << 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
var operation = (flags >> 16) & 0x7u;
|
||||||
|
if (operation == 0)
|
||||||
|
{
|
||||||
|
state.PredicateSkip = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation != 3)
|
||||||
|
{
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.predication_unsupported packet=0x{packetAddress:X16} " +
|
||||||
|
$"op={operation} addr=0x{predicateAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var waitOperation = (flags >> 12) & 1u;
|
||||||
|
var value = 0UL;
|
||||||
|
var readSucceeded = false;
|
||||||
|
void ReadPredicate() =>
|
||||||
|
readSucceeded = ctx.TryReadUInt64(predicateAddress, out value);
|
||||||
|
|
||||||
|
if (waitOperation != 0)
|
||||||
|
{
|
||||||
|
var sequence = GuestGpu.Current.SubmitOrderedGuestAction(
|
||||||
|
ReadPredicate,
|
||||||
|
$"set_predication read 0x{predicateAddress:X16}");
|
||||||
|
if (sequence == 0)
|
||||||
|
{
|
||||||
|
ReadPredicate();
|
||||||
|
}
|
||||||
|
else if (!GuestGpu.Current.WaitForGuestWork(sequence))
|
||||||
|
{
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.predication_wait_failed packet=0x{packetAddress:X16} " +
|
||||||
|
$"addr=0x{predicateAddress:X16} sequence={sequence}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReadPredicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readSucceeded)
|
||||||
|
{
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.predication_read_failed packet=0x{packetAddress:X16} " +
|
||||||
|
$"addr=0x{predicateAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var condition = (flags >> 8) & 1u;
|
||||||
|
state.PredicateSkip = condition == 0 ? value != 0 : value == 0;
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.predication packet=0x{packetAddress:X16} " +
|
||||||
|
$"addr=0x{predicateAddress:X16} value=0x{value:X16} " +
|
||||||
|
$"condition={condition} wait={waitOperation} skip={state.PredicateSkip}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool RangesOverlap(
|
private static bool RangesOverlap(
|
||||||
ulong leftAddress,
|
ulong leftAddress,
|
||||||
ulong leftLength,
|
ulong leftLength,
|
||||||
@@ -4561,6 +4680,7 @@ public static partial class AgcExports
|
|||||||
private static bool TryParseSubmittedWait(
|
private static bool TryParseSubmittedWait(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong packetAddress,
|
ulong packetAddress,
|
||||||
|
uint packetLength,
|
||||||
bool is64Bit,
|
bool is64Bit,
|
||||||
bool isStandard,
|
bool isStandard,
|
||||||
out ulong waitAddress,
|
out ulong waitAddress,
|
||||||
@@ -4591,8 +4711,10 @@ public static partial class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var legacyWait32 = !is64Bit && packetLength == 6;
|
||||||
|
var controlOffset = is64Bit ? 28u : legacyWait32 ? 16u : 20u;
|
||||||
if (!TryReadUInt64(ctx, packetAddress + 4, out waitAddress) ||
|
if (!TryReadUInt64(ctx, packetAddress + 4, out waitAddress) ||
|
||||||
!TryReadUInt32(ctx, packetAddress + (is64Bit ? 28u : 16u), out var control))
|
!TryReadUInt32(ctx, packetAddress + controlOffset, out var control))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -4605,8 +4727,9 @@ public static partial class AgcExports
|
|||||||
TryReadUInt64(ctx, packetAddress + 20, out reference);
|
TryReadUInt64(ctx, packetAddress + 20, out reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var referenceOffset = legacyWait32 ? 20u : 16u;
|
||||||
if (!TryReadUInt32(ctx, packetAddress + 12, out var mask32) ||
|
if (!TryReadUInt32(ctx, packetAddress + 12, out var mask32) ||
|
||||||
!TryReadUInt32(ctx, packetAddress + 20, out var reference32))
|
!TryReadUInt32(ctx, packetAddress + referenceOffset, out var reference32))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -4711,7 +4834,7 @@ public static partial class AgcExports
|
|||||||
bool tracePacket)
|
bool tracePacket)
|
||||||
{
|
{
|
||||||
if (!TryParseSubmittedWait(
|
if (!TryParseSubmittedWait(
|
||||||
ctx, packetAddress, is64Bit, isStandard,
|
ctx, packetAddress, length, is64Bit, isStandard,
|
||||||
out var waitAddress, out var reference, out var mask, out var compareFunction,
|
out var waitAddress, out var reference, out var mask, out var compareFunction,
|
||||||
out var controlValue))
|
out var controlValue))
|
||||||
{
|
{
|
||||||
@@ -10910,6 +11033,23 @@ public static partial class AgcExports
|
|||||||
((op & 0xFFu) << 8) |
|
((op & 0xFFu) << 8) |
|
||||||
((register & 0x3Fu) << 2);
|
((register & 0x3Fu) << 2);
|
||||||
|
|
||||||
|
private static uint EncodeWaitRegMemPoll(uint pollCycles) =>
|
||||||
|
Math.Min(pollCycles >> 4, 0xFFFFu);
|
||||||
|
|
||||||
|
private static uint EncodeWaitRegMem32Control(uint compareFunction, uint operation, uint cachePolicy) =>
|
||||||
|
0x10u |
|
||||||
|
(compareFunction & 0x7u) |
|
||||||
|
((operation & 0x3u) << 8) |
|
||||||
|
((operation & 0xCu) << 4) |
|
||||||
|
((cachePolicy & 0x3u) << 25);
|
||||||
|
|
||||||
|
private static uint EncodeWaitRegMem64Control(uint compareFunction, uint operation, uint cachePolicy) =>
|
||||||
|
0x10u |
|
||||||
|
(compareFunction & 0x7u) |
|
||||||
|
((operation & 0x1u) << 8) |
|
||||||
|
((operation & 0x6u) << 5) |
|
||||||
|
((cachePolicy & 0x3u) << 25);
|
||||||
|
|
||||||
private static uint Pm4Length(uint header) =>
|
private static uint Pm4Length(uint header) =>
|
||||||
((header >> 16) & 0x3FFFu) + 2u;
|
((header >> 16) & 0x3FFFu) + 2u;
|
||||||
|
|
||||||
@@ -11364,16 +11504,21 @@ public static partial class AgcExports
|
|||||||
public static int DcbSetPredication(CpuContext ctx)
|
public static int DcbSetPredication(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var dcb = ctx[CpuRegister.Rdi];
|
var dcb = ctx[CpuRegister.Rdi];
|
||||||
var address = ctx[CpuRegister.Rsi];
|
var condition = (uint)(ctx[CpuRegister.Rsi] & 1u);
|
||||||
|
var operation = (uint)(ctx[CpuRegister.Rdx] & 0x7u);
|
||||||
|
var waitOperation = (uint)(ctx[CpuRegister.Rcx] & 1u);
|
||||||
|
var address = ctx[CpuRegister.R8];
|
||||||
if (dcb == 0)
|
if (dcb == 0)
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
var flags = (condition << 8) | (waitOperation << 12) | (operation << 16);
|
||||||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
!ctx.TryWriteUInt32(cmd, Pm4(4, ItSetPredication, RZero)) ||
|
||||||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
!ctx.TryWriteUInt32(cmd + 4, flags) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)address & 0xFFFF_FFF0u) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 12, (uint)(address >> 32)))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
@@ -11388,9 +11533,17 @@ public static partial class AgcExports
|
|||||||
LibraryName = "libSceAgc")]
|
LibraryName = "libSceAgc")]
|
||||||
public static int SetPacketPredication(CpuContext ctx)
|
public static int SetPacketPredication(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// Global predication toggle on a packet; a no-op is safe for rendering.
|
var packetAddress = ctx[CpuRegister.Rdi];
|
||||||
ctx[CpuRegister.Rax] = 0;
|
var predication = ctx[CpuRegister.Rsi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
if (packetAddress == 0 || !TryReadUInt32(ctx, packetAddress, out var header))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
header = (header & ~1u) | (predication == 1 ? 1u : 0u);
|
||||||
|
return !ctx.TryWriteUInt32(packetAddress, header)
|
||||||
|
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)
|
||||||
|
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
|
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
|
||||||
@@ -11565,7 +11718,7 @@ public static partial class AgcExports
|
|||||||
uint owner;
|
uint owner;
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (!state.ResourceRegistrationInitialized ||
|
if (state.ResourceRegistrationInitialized &&
|
||||||
state.ResourceRegistrationMaxOwners != 0 &&
|
state.ResourceRegistrationMaxOwners != 0 &&
|
||||||
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
|
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -16,8 +19,11 @@ namespace SharpEmu.Libs.Agc;
|
|||||||
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
|
||||||
/// equations are being ported.
|
/// equations are being ported.
|
||||||
/// </summary>
|
/// </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
|
// 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
|
// 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.
|
// topology. Each entry describes one address bit as an XOR of X/Y bits.
|
||||||
@@ -118,6 +124,14 @@ internal static class GnmTiling
|
|||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
private static readonly HashSet<uint> _reportedModes = new();
|
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;
|
public static bool Enabled => _enabled || !_disabled;
|
||||||
|
|
||||||
@@ -404,64 +418,83 @@ internal static class GnmTiling
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precompute the within-block element offset for each (x, y) inside a
|
// Address tables depend only on the swizzle equation and element size,
|
||||||
// single block. The swizzle equation only depends on the in-block
|
// so retain them across textures instead of rebuilding them per upload.
|
||||||
// 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.
|
|
||||||
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
|
||||||
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
|
var patternTerms = hasExactXorPattern
|
||||||
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
|
? _patternTermCache.GetOrAdd(
|
||||||
{
|
(swizzleMode, bppLog2),
|
||||||
for (var bx = 0; bx < blockWidth; bx++)
|
_ => CreatePatternTerms(xorPattern))
|
||||||
{
|
: default;
|
||||||
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
|
var blockTable = hasExactXorPattern
|
||||||
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
|
? []
|
||||||
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
|
: _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
|
// The XOR equation offset factors cleanly into independent X and Y
|
||||||
// fields — each output bit is parity(x & XMask) XOR parity(y & YMask),
|
// 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).
|
// 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
|
// Exact equations repeat at a small power-of-two period. Cached axis
|
||||||
// inner loop drops from a 16-bit interleave with 32 PopCounts per element
|
// terms reduce the inner loop to two array loads and one XOR.
|
||||||
// to a single array load and one XOR.
|
fixed (byte* tiledPointer = tiled)
|
||||||
var xTermByColumn = hasExactXorPattern ? new int[elementsWide] : [];
|
fixed (byte* linearPointer = linear)
|
||||||
for (var x = 0; hasExactXorPattern && x < elementsWide; x++)
|
|
||||||
{
|
{
|
||||||
xTermByColumn[x] = (int)PatternAxisTerm((uint)x, xorPattern, useX: true);
|
var sourceAddress = (nint)tiledPointer;
|
||||||
}
|
var destinationAddress = (nint)linearPointer;
|
||||||
|
var sourceLength = tiled.Length;
|
||||||
for (var y = 0; y < elementsHigh; y++)
|
var destinationLength = linear.Length;
|
||||||
{
|
var blockWidthShift = BitLog2((uint)blockWidth);
|
||||||
var blockY = y / blockHeight;
|
var blockWidthMask = blockWidth - 1;
|
||||||
var inBlockY = y % blockHeight;
|
var detileRow = (int y) =>
|
||||||
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;
|
|
||||||
for (var x = 0; x < elementsWide; x++)
|
|
||||||
{
|
{
|
||||||
var blockX = x / blockWidth;
|
var blockY = y / blockHeight;
|
||||||
var inBlockX = x % blockWidth;
|
var inBlockY = y & (blockHeight - 1);
|
||||||
|
var rowBlockBase = (long)blockY * blocksPerRow;
|
||||||
var blockIndex = rowBlockBase + blockX;
|
var tableRowBase = inBlockY * blockWidth;
|
||||||
var sourceByte = hasExactXorPattern
|
var destRowBase = (long)y * elementsWide * bytesPerElement;
|
||||||
? blockIndex * blockBytes + (xTermByColumn[x] ^ yTerm)
|
var yTerm = hasExactXorPattern
|
||||||
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
? patternTerms.Y[y & patternTerms.YMask]
|
||||||
(long)bytesPerElement;
|
: 0;
|
||||||
var destByte = destRowBase + (long)x * bytesPerElement;
|
for (var x = 0; x < elementsWide; x++)
|
||||||
if (sourceByte + bytesPerElement > tiled.Length ||
|
|
||||||
destByte + bytesPerElement > linear.Length)
|
|
||||||
{
|
{
|
||||||
continue;
|
var blockX = x >> blockWidthShift;
|
||||||
}
|
var inBlockX = x & blockWidthMask;
|
||||||
|
var blockIndex = rowBlockBase + blockX;
|
||||||
|
var sourceByte = hasExactXorPattern
|
||||||
|
? blockIndex * blockBytes + (patternTerms.X[x & patternTerms.XMask] ^ yTerm)
|
||||||
|
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
|
||||||
|
(long)bytesPerElement;
|
||||||
|
var destByte = destRowBase + (long)x * bytesPerElement;
|
||||||
|
if (sourceByte < 0 ||
|
||||||
|
sourceByte + bytesPerElement > sourceLength ||
|
||||||
|
destByte + bytesPerElement > destinationLength)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
tiled.Slice((int)sourceByte, bytesPerElement)
|
CopyElement(
|
||||||
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
|
(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +507,80 @@ internal static class GnmTiling
|
|||||||
ZOrder,
|
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 readonly AddressBit Zero = new(0, 0);
|
||||||
|
|
||||||
private static AddressBit X(int bit) => new(1u << bit, 0);
|
private static AddressBit X(int bit) => new(1u << bit, 0);
|
||||||
|
|||||||
@@ -343,6 +343,45 @@ internal static class GpuWaitRegistry
|
|||||||
return expired;
|
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
|
/// <summary>Records the value a label producer wrote, for the deadlock
|
||||||
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
|
||||||
public static bool RecordProduced(object memory, ulong address, ulong value)
|
public static bool RecordProduced(object memory, ulong address, ulong value)
|
||||||
|
|||||||
@@ -1131,16 +1131,18 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FindFfmpeg() =>
|
internal static string? FindFfmpeg() =>
|
||||||
FindFfmpeg(
|
FindFfmpeg(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
Environment.GetEnvironmentVariable("SHARPEMU_FFMPEG_PATH"),
|
||||||
Environment.GetEnvironmentVariable("PATH"),
|
Environment.GetEnvironmentVariable("PATH"),
|
||||||
OperatingSystem.IsWindows());
|
OperatingSystem.IsWindows(),
|
||||||
|
AppContext.BaseDirectory);
|
||||||
|
|
||||||
internal static string? FindFfmpeg(
|
internal static string? FindFfmpeg(
|
||||||
string? configured,
|
string? configured,
|
||||||
string? searchPath,
|
string? searchPath,
|
||||||
bool isWindows)
|
bool isWindows,
|
||||||
|
string? baseDirectory = null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||||
{
|
{
|
||||||
@@ -1148,6 +1150,21 @@ public static class AvPlayerExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var executable = isWindows ? "ffmpeg.exe" : "ffmpeg";
|
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)
|
foreach (var directory in (searchPath ?? string.Empty)
|
||||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,15 +18,43 @@ namespace SharpEmu.Libs.Bink;
|
|||||||
internal static class Bink2MovieBridge
|
internal static class Bink2MovieBridge
|
||||||
{
|
{
|
||||||
private const uint MaxDimension = 16384;
|
private const uint MaxDimension = 16384;
|
||||||
|
private const uint MaxHostVideoWidth = 1920;
|
||||||
|
private const uint MaxHostVideoHeight = 1080;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static NativeAdapter? _adapter;
|
|
||||||
private static string? _activePath;
|
private static string? _activePath;
|
||||||
private static IntPtr _activeMovie;
|
|
||||||
private static Bink2MovieInfo _activeInfo;
|
private static Bink2MovieInfo _activeInfo;
|
||||||
private static byte[]? _frameBuffer;
|
private static byte[]? _frameBuffer;
|
||||||
private static bool _usingDummyMovie;
|
private static bool _frameBufferPresented;
|
||||||
private static bool _loadAttempted;
|
private static BinkFramePlayback? _playback;
|
||||||
private static bool _availabilityReported;
|
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>
|
/// <summary>
|
||||||
/// Returns true only when movie skipping was explicitly requested. Without
|
/// Returns true only when movie skipping was explicitly requested. Without
|
||||||
@@ -37,109 +65,106 @@ internal static class Bink2MovieBridge
|
|||||||
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
|
||||||
ResolveMode() == MovieMode.Skip;
|
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) ||
|
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
!File.Exists(hostPath))
|
!File.Exists(hostPath))
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return _playback is not null || _frameBuffer is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mode = ResolveMode();
|
var mode = ResolveMode();
|
||||||
if (mode == MovieMode.Dummy)
|
if (mode is MovieMode.Guest or MovieMode.Skip)
|
||||||
{
|
{
|
||||||
AttachDummyMovieLocked(hostPath);
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode != MovieMode.Native)
|
if (_playback is not null || _frameBuffer is not null)
|
||||||
{
|
{
|
||||||
return;
|
if (PendingMoviePathSet.Add(hostPath))
|
||||||
|
{
|
||||||
|
PendingMoviePaths.Enqueue(hostPath);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge queued: " +
|
||||||
|
Path.GetFileName(hostPath));
|
||||||
|
}
|
||||||
|
return PendingMoviePathSet.Contains(hostPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var adapter = GetAdapterLocked();
|
AttachMovieLocked(hostPath, mode);
|
||||||
if (adapter is null)
|
return string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase) &&
|
||||||
{
|
(_playback is not null || _frameBuffer is not null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseActiveLocked();
|
|
||||||
if (!adapter.TryOpen(hostPath, out var movie, out var info))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
|
||||||
Path.GetFileName(hostPath) + "'.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool TryDecodeNextFrame(
|
internal static bool TryDecodeNextFrame(
|
||||||
|
bool advanceClock,
|
||||||
out byte[] pixels,
|
out byte[] pixels,
|
||||||
out uint width,
|
out uint width,
|
||||||
out uint height)
|
out uint height,
|
||||||
|
out bool advanced,
|
||||||
|
out long frameSerial,
|
||||||
|
out string hostPath)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
pixels = [];
|
pixels = [];
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 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)
|
||||||
width = _activeInfo.Width;
|
{
|
||||||
height = _activeInfo.Height;
|
var completedPath = _activePath;
|
||||||
return true;
|
CloseActiveLocked();
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
"[LOADER][INFO] Bink2 bridge completed: " +
|
||||||
|
Path.GetFileName(completedPath));
|
||||||
|
AttachNextQueuedMovieLocked();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
width = _activeInfo.Width;
|
||||||
|
height = _activeInfo.Height;
|
||||||
|
if (advanced)
|
||||||
|
{
|
||||||
|
frameSerial = ++_frameSerial;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe
|
if (_frameBuffer is null)
|
||||||
{
|
{
|
||||||
fixed (byte* destination = _frameBuffer)
|
return false;
|
||||||
{
|
|
||||||
if (!_adapter.DecodeNextBgra(
|
|
||||||
_activeMovie,
|
|
||||||
(IntPtr)destination,
|
|
||||||
_activeInfo.Width * 4,
|
|
||||||
(uint)_frameBuffer.Length))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pixels = _frameBuffer;
|
pixels = _frameBuffer;
|
||||||
width = _activeInfo.Width;
|
width = _activeInfo.Width;
|
||||||
height = _activeInfo.Height;
|
height = _activeInfo.Height;
|
||||||
|
advanced = !_frameBufferPresented;
|
||||||
|
_frameBufferPresented = true;
|
||||||
|
if (advanced)
|
||||||
|
{
|
||||||
|
frameSerial = ++_frameSerial;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +177,52 @@ internal static class Bink2MovieBridge
|
|||||||
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
|
||||||
checked((int)((ulong)info.Width * info.Height * 4));
|
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()
|
private static MovieMode ResolveMode()
|
||||||
{
|
{
|
||||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
|
||||||
@@ -170,15 +241,22 @@ internal static class Bink2MovieBridge
|
|||||||
return MovieMode.Skip;
|
return MovieMode.Skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the optional host adapter when one is supplied. Otherwise let
|
if (string.Equals(configured, "guest", StringComparison.OrdinalIgnoreCase))
|
||||||
// the game's statically linked Bink implementation consume the file.
|
|
||||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
|
|
||||||
EnumerateAdapterCandidates().Any(File.Exists))
|
|
||||||
{
|
{
|
||||||
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)
|
private static void AttachDummyMovieLocked(string hostPath)
|
||||||
@@ -195,22 +273,61 @@ internal static class Bink2MovieBridge
|
|||||||
_activePath = hostPath;
|
_activePath = hostPath;
|
||||||
_activeInfo = info;
|
_activeInfo = info;
|
||||||
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
|
||||||
|
_frameBufferPresented = false;
|
||||||
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
FillDummyFrame(_frameBuffer, info.Width, info.Height);
|
||||||
_usingDummyMovie = true;
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
info.Width + "x" + info.Height + ".");
|
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;
|
info = default;
|
||||||
Span<byte> header = stackalloc byte[32];
|
Span<byte> header = stackalloc byte[36];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(path);
|
using var stream = File.OpenRead(path);
|
||||||
if (stream.Read(header) != header.Length ||
|
stream.ReadExactly(header);
|
||||||
!header[..4].SequenceEqual("KB2j"u8))
|
if (!header[..3].SequenceEqual("KB2"u8))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -219,10 +336,11 @@ internal static class Bink2MovieBridge
|
|||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
|
||||||
1);
|
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x20, 4)));
|
||||||
return true;
|
return info.FramesPerSecondNumerator != 0 &&
|
||||||
|
info.FramesPerSecondDenominator != 0;
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (Exception exception) when (exception is IOException or EndOfStreamException)
|
||||||
{
|
{
|
||||||
return false;
|
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()
|
private static void CloseActiveLocked()
|
||||||
{
|
{
|
||||||
if (_activeMovie != IntPtr.Zero)
|
_playback?.Dispose();
|
||||||
{
|
_playback = null;
|
||||||
_adapter?.Close(_activeMovie);
|
|
||||||
}
|
|
||||||
|
|
||||||
_activePath = null;
|
_activePath = null;
|
||||||
_activeMovie = IntPtr.Zero;
|
|
||||||
_activeInfo = default;
|
_activeInfo = default;
|
||||||
_frameBuffer = null;
|
_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)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private readonly struct Bink2MovieInfo
|
internal readonly struct Bink2MovieInfo
|
||||||
{
|
{
|
||||||
public readonly uint Width;
|
public readonly uint Width;
|
||||||
public readonly uint Height;
|
public readonly uint Height;
|
||||||
@@ -343,66 +404,222 @@ internal static class Bink2MovieBridge
|
|||||||
Skip,
|
Skip,
|
||||||
Dummy,
|
Dummy,
|
||||||
Native,
|
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)]
|
while (PendingMoviePaths.Count > 0)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_openUtf8 = openUtf8;
|
var path = PendingMoviePaths.Dequeue();
|
||||||
_decodeNextBgra = decodeNextBgra;
|
PendingMoviePathSet.Remove(path);
|
||||||
_close = close;
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachMovieLocked(path, ResolveMode());
|
||||||
|
if (_playback is not null || _frameBuffer is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
// 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)
|
||||||
{
|
{
|
||||||
adapter = null;
|
while (IsTrackedLocked(hostPath))
|
||||||
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
{
|
||||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
var remaining = deadline - Environment.TickCount64;
|
||||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = new NativeAdapter(
|
var frameCount = BinaryPrimitives.ReadUInt32LittleEndian(header[8..12]);
|
||||||
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
var audioTrackCount = BinaryPrimitives.ReadUInt32LittleEndian(header[40..44]);
|
||||||
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
if (frameCount < 2 || audioTrackCount > 256)
|
||||||
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
{
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
|
exception is IOException or EndOfStreamException or OverflowException)
|
||||||
{
|
{
|
||||||
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
return false;
|
||||||
try
|
}
|
||||||
{
|
}
|
||||||
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
|
|
||||||
}
|
internal readonly struct BinkGuestCompletionShim
|
||||||
finally
|
{
|
||||||
{
|
private readonly uint _fileSizeMinusHeader;
|
||||||
Marshal.FreeCoTaskMem(utf8);
|
private readonly uint _largestFrameSize;
|
||||||
}
|
|
||||||
|
internal BinkGuestCompletionShim(uint fileSizeMinusHeader, uint largestFrameSize)
|
||||||
|
{
|
||||||
|
_fileSizeMinusHeader = fileSizeMinusHeader;
|
||||||
|
_largestFrameSize = largestFrameSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
/// <summary>
|
||||||
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
/// 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;
|
||||||
|
}
|
||||||
|
|
||||||
internal void Close(IntPtr movie) => _close(movie);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,6 +97,9 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
private static readonly object _fdGate = new();
|
private static readonly object _fdGate = new();
|
||||||
private static readonly Dictionary<int, FileStream> _openFiles = 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 Dictionary<int, OpenDirectory> _openDirectories = new();
|
||||||
private static readonly object _libcAllocGate = new();
|
private static readonly object _libcAllocGate = new();
|
||||||
private static readonly object _memoryGate = new();
|
private static readonly object _memoryGate = new();
|
||||||
@@ -268,13 +271,13 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, address + mappedLength);
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, address + mappedLength);
|
||||||
_mappedRegions[address] = new MappedRegion(
|
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||||
address,
|
address,
|
||||||
mappedLength,
|
mappedLength,
|
||||||
OrbisProtCpuReadWrite,
|
OrbisProtCpuReadWrite,
|
||||||
IsFlexible: false,
|
IsFlexible: false,
|
||||||
IsDirect: false,
|
IsDirect: false,
|
||||||
DirectStart: 0);
|
DirectStart: 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ulong offset = 0; offset < mappedLength;)
|
for (ulong offset = 0; offset < mappedLength;)
|
||||||
@@ -300,13 +303,13 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
lock (_memoryGate)
|
lock (_memoryGate)
|
||||||
{
|
{
|
||||||
_mappedRegions[address] = new MappedRegion(
|
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||||
address,
|
address,
|
||||||
length,
|
length,
|
||||||
Protection: 0,
|
Protection: 0,
|
||||||
IsFlexible: false,
|
IsFlexible: false,
|
||||||
IsDirect: false,
|
IsDirect: false,
|
||||||
DirectStart: 0);
|
DirectStart: 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1468,6 +1471,14 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
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))
|
if (IsMutatingOpen(flags) && IsReadOnlyGuestMutationPath(guestPath))
|
||||||
{
|
{
|
||||||
LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
|
LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
|
||||||
@@ -1516,13 +1527,22 @@ public static partial class KernelMemoryCompatExports
|
|||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
_openFiles[fd] = stream;
|
_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
|
if (useBinkCompletionShim)
|
||||||
// import for the HLE codec layer to intercept. The successful
|
{
|
||||||
// guest file open is the stable boundary at which the optional
|
LogOpenTrace(
|
||||||
// host Bink bridge can attach to the same movie.
|
"_open bink-host-shim path='" + guestPath + "' host='" + hostPath +
|
||||||
Bink2MovieBridge.ObserveGuestMovie(hostPath);
|
"' flags=0x" + flags.ToString("X8") + " fd=" + fd);
|
||||||
|
}
|
||||||
|
|
||||||
if (IsMutatingOpen(flags))
|
if (IsMutatingOpen(flags))
|
||||||
{
|
{
|
||||||
@@ -2053,10 +2073,21 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileStream? stream;
|
FileStream? stream;
|
||||||
|
var notifyBinkClose = false;
|
||||||
|
string? observedBinkPath = null;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
if (_openFiles.Remove(fd, out stream))
|
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))
|
else if (_openDirectories.Remove(fd))
|
||||||
{
|
{
|
||||||
@@ -2069,6 +2100,10 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notifyBinkClose)
|
||||||
|
{
|
||||||
|
Bink2MovieBridge.NotifyGuestMovieClosed(observedBinkPath!);
|
||||||
|
}
|
||||||
stream.Dispose();
|
stream.Dispose();
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -2096,9 +2131,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileStream? stream;
|
FileStream? stream;
|
||||||
|
Bink2MovieBridge.BinkGuestCompletionShim completionShim = default;
|
||||||
|
var useBinkCompletionShim = false;
|
||||||
lock (_fdGate)
|
lock (_fdGate)
|
||||||
{
|
{
|
||||||
_openFiles.TryGetValue(fd, out stream);
|
_openFiles.TryGetValue(fd, out stream);
|
||||||
|
useBinkCompletionShim = _binkGuestCompletionShims.TryGetValue(fd, out completionShim);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream is null)
|
if (stream is null)
|
||||||
@@ -2118,6 +2156,17 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||||
var read = stream.Read(buffer, 0, 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)))
|
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -3091,13 +3140,13 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
||||||
_mappedRegions[mappedAddress] = new MappedRegion(
|
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||||
mappedAddress,
|
mappedAddress,
|
||||||
length,
|
length,
|
||||||
protection,
|
protection,
|
||||||
IsFlexible: false,
|
IsFlexible: false,
|
||||||
IsDirect: true,
|
IsDirect: true,
|
||||||
DirectStart: directMemoryStart);
|
DirectStart: directMemoryStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
@@ -3183,13 +3232,13 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
||||||
_allocatedFlexibleBytes = Math.Min(FlexibleMemorySizeBytes, _allocatedFlexibleBytes + length);
|
_allocatedFlexibleBytes = Math.Min(FlexibleMemorySizeBytes, _allocatedFlexibleBytes + length);
|
||||||
_mappedRegions[mappedAddress] = new MappedRegion(
|
ReplaceMappedRegionRangeLocked(new MappedRegion(
|
||||||
mappedAddress,
|
mappedAddress,
|
||||||
length,
|
length,
|
||||||
protection,
|
protection,
|
||||||
IsFlexible: true,
|
IsFlexible: true,
|
||||||
IsDirect: false,
|
IsDirect: false,
|
||||||
DirectStart: 0);
|
DirectStart: 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FFmpeg.AutoGen" />
|
||||||
<PackageReference Include="Silk.NET.Input" />
|
<PackageReference Include="Silk.NET.Input" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||||
|
|||||||
@@ -2230,7 +2230,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (IsGuestWorkCompletedLocked(pending.RequiredGuestWorkSequence))
|
if (IsGuestWorkCompletedLocked(pending.RequiredGuestWorkSequence))
|
||||||
{
|
{
|
||||||
presentation = _pendingGuestImagePresentations.Dequeue();
|
presentation = _pendingGuestImagePresentations.Dequeue();
|
||||||
TryReplaceWithBinkFrame(ref presentation);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2263,29 +2262,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
presentation = latest;
|
presentation = latest;
|
||||||
TryReplaceWithBinkFrame(ref presentation);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryReplaceWithBinkFrame(ref Presentation presentation)
|
|
||||||
{
|
|
||||||
if (!Bink2MovieBridge.TryDecodeNextFrame(out var pixels, out var width, out var height))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation = new Presentation(
|
|
||||||
pixels,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
presentation.Sequence,
|
|
||||||
GuestDrawKind.None,
|
|
||||||
TranslatedDraw: null,
|
|
||||||
presentation.RequiredGuestWorkSequence,
|
|
||||||
IsSplash: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();
|
private static readonly HashSet<long> _tracedGuestImagePresentRejections = new();
|
||||||
|
|
||||||
private static bool HasPendingGuestPresentation(long presentedSequence)
|
private static bool HasPendingGuestPresentation(long presentedSequence)
|
||||||
@@ -2835,6 +2815,40 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private VkBuffer _stagingBuffer;
|
private VkBuffer _stagingBuffer;
|
||||||
private DeviceMemory _stagingMemory;
|
private DeviceMemory _stagingMemory;
|
||||||
private ulong _stagingSize;
|
private ulong _stagingSize;
|
||||||
|
private VkBuffer[] _frameUploadBuffers = [];
|
||||||
|
private DeviceMemory[] _frameUploadMemory = [];
|
||||||
|
private nint[] _frameUploadMapped = [];
|
||||||
|
private Image _hostMovieImage;
|
||||||
|
private DeviceMemory _hostMovieImageMemory;
|
||||||
|
private ImageView _hostMovieImageView;
|
||||||
|
private uint _hostMovieImageWidth;
|
||||||
|
private uint _hostMovieImageHeight;
|
||||||
|
private Format _hostMovieImageFormat;
|
||||||
|
private uint _hostMovieImageDstSelect;
|
||||||
|
private bool _hostMovieImageInitialized;
|
||||||
|
private Image _hostMovieChromaImage;
|
||||||
|
private DeviceMemory _hostMovieChromaImageMemory;
|
||||||
|
private ImageView _hostMovieChromaImageView;
|
||||||
|
private uint _hostMovieChromaImageWidth;
|
||||||
|
private uint _hostMovieChromaImageHeight;
|
||||||
|
private uint _hostMovieChromaImageDstSelect;
|
||||||
|
private bool _hostMovieChromaImageInitialized;
|
||||||
|
private byte[]? _hostMovieFramePixels;
|
||||||
|
private byte[]? _hostMovieLumaPixels;
|
||||||
|
private byte[]? _hostMovieChromaPixels;
|
||||||
|
private uint _hostMovieFrameWidth;
|
||||||
|
private uint _hostMovieFrameHeight;
|
||||||
|
private long _hostMovieFrameSerial;
|
||||||
|
private long _hostMovieConvertedFrameSerial = -1;
|
||||||
|
private long _hostMovieLumaUploadedFrameSerial = -1;
|
||||||
|
private long _hostMovieChromaUploadedFrameSerial = -1;
|
||||||
|
private string? _hostMovieFramePath;
|
||||||
|
private ulong _hostMovieLumaTextureAddress;
|
||||||
|
private ulong _hostMovieChromaTextureAddress;
|
||||||
|
private uint _hostMovieLumaDstSelect;
|
||||||
|
private uint _hostMovieChromaDstSelect;
|
||||||
|
private readonly HashSet<string> _tracedHostMovieTextureBindings =
|
||||||
|
new(StringComparer.Ordinal);
|
||||||
// Perf overlay: CPU-rasterized panel copied through per-slot staging
|
// Perf overlay: CPU-rasterized panel copied through per-slot staging
|
||||||
// buffers into one image, then blitted onto the swapchain.
|
// buffers into one image, then blitted onto the swapchain.
|
||||||
private Image _overlayImage;
|
private Image _overlayImage;
|
||||||
@@ -3027,6 +3041,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
public bool OwnsStorage;
|
public bool OwnsStorage;
|
||||||
public bool IsStorage;
|
public bool IsStorage;
|
||||||
public bool Cached;
|
public bool Cached;
|
||||||
|
public bool IsHostMovie;
|
||||||
|
public int HostMoviePlane = -1;
|
||||||
|
public long HostMovieFrameSerial;
|
||||||
public ulong CpuContentFingerprint;
|
public ulong CpuContentFingerprint;
|
||||||
public bool UpdatesCpuContent;
|
public bool UpdatesCpuContent;
|
||||||
public GuestSampler SamplerState;
|
public GuestSampler SamplerState;
|
||||||
@@ -4328,6 +4345,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var surfaceFormat = ChooseSurfaceFormat(formats);
|
var surfaceFormat = ChooseSurfaceFormat(formats);
|
||||||
_swapchainFormat = surfaceFormat.Format;
|
_swapchainFormat = surfaceFormat.Format;
|
||||||
_extent = ChooseExtent(capabilities);
|
_extent = ChooseExtent(capabilities);
|
||||||
|
Bink2MovieBridge.SetPresentationSize(_extent.Width, _extent.Height);
|
||||||
var presentMode = ChoosePresentMode();
|
var presentMode = ChoosePresentMode();
|
||||||
var imageCount = capabilities.MinImageCount + 1;
|
var imageCount = capabilities.MinImageCount + 1;
|
||||||
if (capabilities.MaxImageCount != 0)
|
if (capabilities.MaxImageCount != 0)
|
||||||
@@ -4474,6 +4492,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_presentationCommandBuffer = _commandBuffer;
|
_presentationCommandBuffer = _commandBuffer;
|
||||||
|
|
||||||
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
||||||
|
CreateFrameUploadBuffers(_stagingSize);
|
||||||
CreateOverlayResources();
|
CreateOverlayResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6076,10 +6095,15 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hostMovieTextures = FindHostMovieTextureBindings(draw.Textures);
|
||||||
for (var index = 0; index < draw.Textures.Count; index++)
|
for (var index = 0; index < draw.Textures.Count; index++)
|
||||||
{
|
{
|
||||||
var texture = draw.Textures[index];
|
var texture = draw.Textures[index];
|
||||||
var resolved = ResolveTextureResource(texture);
|
var resolved = index == hostMovieTextures.Luma
|
||||||
|
? CreateHostMovieTextureResource(texture, plane: 0)
|
||||||
|
: index == hostMovieTextures.Chroma
|
||||||
|
? CreateHostMovieTextureResource(texture, plane: 1)
|
||||||
|
: ResolveTextureResource(texture);
|
||||||
var feedbackTarget = !texture.IsStorage
|
var feedbackTarget = !texture.IsStorage
|
||||||
? feedbackTargets?.FirstOrDefault(target =>
|
? feedbackTargets?.FirstOrDefault(target =>
|
||||||
ReferenceEquals(resolved.GuestImage, target))
|
ReferenceEquals(resolved.GuestImage, target))
|
||||||
@@ -6934,6 +6958,343 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PumpHostMovieFrame()
|
||||||
|
{
|
||||||
|
if (!Bink2MovieBridge.TryDecodeNextFrame(
|
||||||
|
advanceClock: _hostMovieLumaTextureAddress != 0 &&
|
||||||
|
_hostMovieChromaTextureAddress != 0,
|
||||||
|
out var pixels,
|
||||||
|
out var width,
|
||||||
|
out var height,
|
||||||
|
out var advanced,
|
||||||
|
out var frameSerial,
|
||||||
|
out var hostPath))
|
||||||
|
{
|
||||||
|
// Keep the last decoded image until a replacement arrives.
|
||||||
|
// Movie sessions are queued asynchronously; clearing here
|
||||||
|
// exposes the guest decoder's neutral surfaces between the
|
||||||
|
// final frame of one movie and the first frame of the next.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(
|
||||||
|
_hostMovieFramePath,
|
||||||
|
hostPath,
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_hostMovieFramePath = hostPath;
|
||||||
|
_hostMovieLumaTextureAddress = 0;
|
||||||
|
_hostMovieChromaTextureAddress = 0;
|
||||||
|
_hostMovieLumaDstSelect = 0;
|
||||||
|
_hostMovieChromaDstSelect = 0;
|
||||||
|
_hostMovieConvertedFrameSerial = -1;
|
||||||
|
_hostMovieLumaUploadedFrameSerial = -1;
|
||||||
|
_hostMovieChromaUploadedFrameSerial = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!advanced && _hostMovieFramePixels is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hostMovieFramePixels = pixels;
|
||||||
|
_hostMovieFrameWidth = width;
|
||||||
|
_hostMovieFrameHeight = height;
|
||||||
|
_hostMovieFrameSerial = frameSerial;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct HostMovieTextureBindings(int Luma, int Chroma)
|
||||||
|
{
|
||||||
|
public static HostMovieTextureBindings None { get; } = new(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HostMovieTextureBindings FindHostMovieTextureBindings(
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures)
|
||||||
|
{
|
||||||
|
if (_hostMovieFramePixels is null ||
|
||||||
|
_hostMovieFrameWidth == 0 ||
|
||||||
|
_hostMovieFrameHeight == 0)
|
||||||
|
{
|
||||||
|
return HostMovieTextureBindings.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hostMovieLumaTextureAddress != 0 &&
|
||||||
|
_hostMovieChromaTextureAddress != 0)
|
||||||
|
{
|
||||||
|
var lumaIndex = -1;
|
||||||
|
var chromaIndex = -1;
|
||||||
|
for (var index = 0; index < textures.Count; index++)
|
||||||
|
{
|
||||||
|
if (textures[index].Address == _hostMovieLumaTextureAddress)
|
||||||
|
{
|
||||||
|
lumaIndex = index;
|
||||||
|
}
|
||||||
|
else if (textures[index].Address == _hostMovieChromaTextureAddress)
|
||||||
|
{
|
||||||
|
chromaIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lumaIndex >= 0 && chromaIndex >= 0)
|
||||||
|
{
|
||||||
|
return RememberHostMovieTextureMappings(
|
||||||
|
textures,
|
||||||
|
lumaIndex,
|
||||||
|
chromaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluepoint alternates decoder output between multiple Y/UV
|
||||||
|
// surface pairs. Fall through and discover the active pair
|
||||||
|
// instead of sampling the stale guest surface on every other
|
||||||
|
// movie draw.
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestLumaIndex = -1;
|
||||||
|
var bestChromaIndex = -1;
|
||||||
|
ulong bestArea = 0;
|
||||||
|
for (var lumaIndex = 0; lumaIndex < textures.Count; lumaIndex++)
|
||||||
|
{
|
||||||
|
var luma = textures[lumaIndex];
|
||||||
|
if (!IsHostMovieLumaCandidate(luma))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var chromaIndex = 0; chromaIndex < textures.Count; chromaIndex++)
|
||||||
|
{
|
||||||
|
var chroma = textures[chromaIndex];
|
||||||
|
if (!IsHostMovieChromaCandidate(luma, chroma))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var area = (ulong)luma.Width * luma.Height;
|
||||||
|
if (bestLumaIndex < 0 || area > bestArea)
|
||||||
|
{
|
||||||
|
bestLumaIndex = lumaIndex;
|
||||||
|
bestChromaIndex = chromaIndex;
|
||||||
|
bestArea = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestLumaIndex < 0 || bestChromaIndex < 0)
|
||||||
|
{
|
||||||
|
return HostMovieTextureBindings.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lumaTexture = textures[bestLumaIndex];
|
||||||
|
var chromaTexture = textures[bestChromaIndex];
|
||||||
|
_hostMovieLumaTextureAddress = lumaTexture.Address;
|
||||||
|
_hostMovieChromaTextureAddress = chromaTexture.Address;
|
||||||
|
_hostMovieLumaDstSelect = lumaTexture.DstSelect;
|
||||||
|
_hostMovieChromaDstSelect = chromaTexture.DstSelect;
|
||||||
|
var traceKey =
|
||||||
|
$"{_hostMovieFramePath}|{lumaTexture.Address:X16}|{chromaTexture.Address:X16}";
|
||||||
|
if (_tracedHostMovieTextureBindings.Add(traceKey))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Bink2 YUV textures bound: " +
|
||||||
|
$"{Path.GetFileName(_hostMovieFramePath)} " +
|
||||||
|
$"y={bestLumaIndex}:0x{lumaTexture.Address:X16}:" +
|
||||||
|
$"{lumaTexture.Width}x{lumaTexture.Height}:dst=0x{lumaTexture.DstSelect:X} " +
|
||||||
|
$"uv={bestChromaIndex}:0x{chromaTexture.Address:X16}:" +
|
||||||
|
$"{chromaTexture.Width}x{chromaTexture.Height}:dst=0x{chromaTexture.DstSelect:X} " +
|
||||||
|
$"host={_hostMovieFrameWidth}x{_hostMovieFrameHeight}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HostMovieTextureBindings(bestLumaIndex, bestChromaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HostMovieTextureBindings RememberHostMovieTextureMappings(
|
||||||
|
IReadOnlyList<GuestDrawTexture> textures,
|
||||||
|
int lumaIndex,
|
||||||
|
int chromaIndex)
|
||||||
|
{
|
||||||
|
_hostMovieLumaDstSelect = textures[lumaIndex].DstSelect;
|
||||||
|
_hostMovieChromaDstSelect = textures[chromaIndex].DstSelect;
|
||||||
|
return new HostMovieTextureBindings(lumaIndex, chromaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsHostMovieLumaCandidate(GuestDrawTexture texture)
|
||||||
|
{
|
||||||
|
if (texture.Address == 0 ||
|
||||||
|
texture.IsStorage ||
|
||||||
|
texture.IsFallback ||
|
||||||
|
texture.ArrayedView ||
|
||||||
|
texture.ArrayLayers > 1 ||
|
||||||
|
texture.Format != 1 ||
|
||||||
|
texture.NumberType != 0 ||
|
||||||
|
texture.Width < 1280 ||
|
||||||
|
texture.Height < 720)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var guestAspect = (ulong)texture.Width * _hostMovieFrameHeight;
|
||||||
|
var hostAspect = (ulong)texture.Height * _hostMovieFrameWidth;
|
||||||
|
var difference = guestAspect > hostAspect
|
||||||
|
? guestAspect - hostAspect
|
||||||
|
: hostAspect - guestAspect;
|
||||||
|
return difference * 100 <= Math.Max(guestAspect, hostAspect) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsHostMovieChromaCandidate(
|
||||||
|
GuestDrawTexture luma,
|
||||||
|
GuestDrawTexture chroma)
|
||||||
|
{
|
||||||
|
return chroma.Address != 0 &&
|
||||||
|
chroma.Address != luma.Address &&
|
||||||
|
!chroma.IsStorage &&
|
||||||
|
!chroma.IsFallback &&
|
||||||
|
!chroma.ArrayedView &&
|
||||||
|
chroma.ArrayLayers <= 1 &&
|
||||||
|
chroma.Format == 3 &&
|
||||||
|
chroma.NumberType == 0 &&
|
||||||
|
luma.Width == chroma.Width * 2 &&
|
||||||
|
luma.Height == chroma.Height * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextureResource CreateHostMovieTextureResource(
|
||||||
|
GuestDrawTexture texture,
|
||||||
|
int plane)
|
||||||
|
{
|
||||||
|
EnsureHostMovieYuvFrame();
|
||||||
|
var isLuma = plane == 0;
|
||||||
|
var pixels = isLuma ? _hostMovieLumaPixels! : _hostMovieChromaPixels!;
|
||||||
|
var width = isLuma
|
||||||
|
? _hostMovieFrameWidth
|
||||||
|
: (_hostMovieFrameWidth + 1) / 2;
|
||||||
|
var height = isLuma
|
||||||
|
? _hostMovieFrameHeight
|
||||||
|
: (_hostMovieFrameHeight + 1) / 2;
|
||||||
|
EnsureHostMovieImages(
|
||||||
|
_hostMovieFrameWidth,
|
||||||
|
_hostMovieFrameHeight,
|
||||||
|
_hostMovieLumaDstSelect,
|
||||||
|
(_hostMovieFrameWidth + 1) / 2,
|
||||||
|
(_hostMovieFrameHeight + 1) / 2,
|
||||||
|
_hostMovieChromaDstSelect);
|
||||||
|
|
||||||
|
var uploadedFrameSerial = isLuma
|
||||||
|
? _hostMovieLumaUploadedFrameSerial
|
||||||
|
: _hostMovieChromaUploadedFrameSerial;
|
||||||
|
var needsUpload = uploadedFrameSerial != _hostMovieFrameSerial;
|
||||||
|
VkBuffer stagingBuffer = default;
|
||||||
|
DeviceMemory stagingMemory = default;
|
||||||
|
if (needsUpload)
|
||||||
|
{
|
||||||
|
(stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||||
|
pixels,
|
||||||
|
$"Bink2 frame {_hostMovieFrameSerial} plane {plane} staging");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextureResource
|
||||||
|
{
|
||||||
|
Address = texture.Address,
|
||||||
|
StagingBuffer = stagingBuffer,
|
||||||
|
StagingMemory = stagingMemory,
|
||||||
|
Image = isLuma ? _hostMovieImage : _hostMovieChromaImage,
|
||||||
|
View = isLuma ? _hostMovieImageView : _hostMovieChromaImageView,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
RowLength = width,
|
||||||
|
DstSelect = texture.DstSelect,
|
||||||
|
NeedsUpload = needsUpload,
|
||||||
|
IsHostMovie = true,
|
||||||
|
HostMoviePlane = plane,
|
||||||
|
HostMovieFrameSerial = _hostMovieFrameSerial,
|
||||||
|
SamplerState = texture.Sampler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureHostMovieYuvFrame()
|
||||||
|
{
|
||||||
|
if (_hostMovieConvertedFrameSerial == _hostMovieFrameSerial)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bgra = _hostMovieFramePixels ??
|
||||||
|
throw new InvalidOperationException("Host movie frame is unavailable.");
|
||||||
|
var width = checked((int)_hostMovieFrameWidth);
|
||||||
|
var height = checked((int)_hostMovieFrameHeight);
|
||||||
|
var chromaWidth = (width + 1) / 2;
|
||||||
|
var chromaHeight = (height + 1) / 2;
|
||||||
|
if (_hostMovieLumaPixels?.Length != width * height)
|
||||||
|
{
|
||||||
|
_hostMovieLumaPixels = GC.AllocateUninitializedArray<byte>(width * height);
|
||||||
|
}
|
||||||
|
if (_hostMovieChromaPixels?.Length != chromaWidth * chromaHeight * 2)
|
||||||
|
{
|
||||||
|
_hostMovieChromaPixels =
|
||||||
|
GC.AllocateUninitializedArray<byte>(chromaWidth * chromaHeight * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConvertBgraToYuv420(
|
||||||
|
bgra,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
_hostMovieLumaPixels,
|
||||||
|
_hostMovieChromaPixels);
|
||||||
|
_hostMovieConvertedFrameSerial = _hostMovieFrameSerial;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ConvertBgraToYuv420(
|
||||||
|
ReadOnlySpan<byte> bgra,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
Span<byte> luma,
|
||||||
|
Span<byte> chroma)
|
||||||
|
{
|
||||||
|
var chromaWidth = (width + 1) / 2;
|
||||||
|
for (var y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
var source = (y * width + x) * 4;
|
||||||
|
var b = bgra[source];
|
||||||
|
var g = bgra[source + 1];
|
||||||
|
var r = bgra[source + 2];
|
||||||
|
luma[y * width + x] = ClampByte(
|
||||||
|
(54 * r + 183 * g + 19 * b + 128) >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y < height; y += 2)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < width; x += 2)
|
||||||
|
{
|
||||||
|
var red = 0;
|
||||||
|
var green = 0;
|
||||||
|
var blue = 0;
|
||||||
|
var samples = 0;
|
||||||
|
for (var sampleY = y; sampleY < Math.Min(y + 2, height); sampleY++)
|
||||||
|
{
|
||||||
|
for (var sampleX = x; sampleX < Math.Min(x + 2, width); sampleX++)
|
||||||
|
{
|
||||||
|
var source = (sampleY * width + sampleX) * 4;
|
||||||
|
blue += bgra[source];
|
||||||
|
green += bgra[source + 1];
|
||||||
|
red += bgra[source + 2];
|
||||||
|
samples++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
red /= samples;
|
||||||
|
green /= samples;
|
||||||
|
blue /= samples;
|
||||||
|
var destination = ((y / 2) * chromaWidth + x / 2) * 2;
|
||||||
|
chroma[destination] = ClampByte(
|
||||||
|
((128 * red - 116 * green - 12 * blue + 128) >> 8) + 128);
|
||||||
|
chroma[destination + 1] = ClampByte(
|
||||||
|
((-29 * red - 99 * green + 128 * blue + 128) >> 8) + 128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte ClampByte(int value) => (byte)Math.Clamp(value, 0, 255);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private TextureResource ResolveTextureResource(GuestDrawTexture texture)
|
private TextureResource ResolveTextureResource(GuestDrawTexture texture)
|
||||||
{
|
{
|
||||||
@@ -9539,6 +9900,26 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_stagingSize = size;
|
_stagingSize = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateFrameUploadBuffers(ulong size)
|
||||||
|
{
|
||||||
|
_frameUploadBuffers = new VkBuffer[MaxFramesInFlight];
|
||||||
|
_frameUploadMemory = new DeviceMemory[MaxFramesInFlight];
|
||||||
|
_frameUploadMapped = new nint[MaxFramesInFlight];
|
||||||
|
for (var slot = 0; slot < MaxFramesInFlight; slot++)
|
||||||
|
{
|
||||||
|
_frameUploadBuffers[slot] = CreateBuffer(
|
||||||
|
size,
|
||||||
|
BufferUsageFlags.TransferSrcBit,
|
||||||
|
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
|
||||||
|
out _frameUploadMemory[slot]);
|
||||||
|
void* mapped;
|
||||||
|
Check(
|
||||||
|
_vk.MapMemory(_device, _frameUploadMemory[slot], 0, size, 0, &mapped),
|
||||||
|
"vkMapMemory(frame upload)");
|
||||||
|
_frameUploadMapped[slot] = (nint)mapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
|
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
|
||||||
{
|
{
|
||||||
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
|
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
|
||||||
@@ -9590,6 +9971,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PumpHostMovieFrame();
|
||||||
|
|
||||||
if (_skipAllCompute ||
|
if (_skipAllCompute ||
|
||||||
AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress) ||
|
AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress) ||
|
||||||
(_skipTallComputeZ > 0 && work.GroupCountZ >= _skipTallComputeZ))
|
(_skipTallComputeZ > 0 && work.GroupCountZ >= _skipTallComputeZ))
|
||||||
@@ -12476,10 +12859,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
EvictDirtyCachedTextures();
|
EvictDirtyCachedTextures();
|
||||||
var completedWork = 0;
|
var completedWork = 0;
|
||||||
HashSet<string>? deferredOrderedQueues = null;
|
HashSet<string>? deferredOrderedQueues = null;
|
||||||
var renderWorkDeadline = _renderWorkBudgetTicks > 0
|
var workBudgetTicks = _renderWorkBudgetTicks;
|
||||||
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
|
var renderWorkDeadline = workBudgetTicks > 0
|
||||||
|
? System.Diagnostics.Stopwatch.GetTimestamp() + workBudgetTicks
|
||||||
: long.MaxValue;
|
: long.MaxValue;
|
||||||
while (completedWork < _maxGuestWorkPerRender)
|
var workLimit = _maxGuestWorkPerRender;
|
||||||
|
while (completedWork < workLimit)
|
||||||
{
|
{
|
||||||
// Never block the macOS main thread waiting for in-flight GPU
|
// Never block the macOS main thread waiting for in-flight GPU
|
||||||
// work to drain. If submission is at capacity (a slow-compute
|
// work to drain. If submission is at capacity (a slow-compute
|
||||||
@@ -12536,6 +12921,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// A host-decoded movie only overrides which image gets
|
||||||
|
// presented (see the presentation selection below); the
|
||||||
|
// guest's own command stream keeps draining normally.
|
||||||
|
// Silently discarding these instead of executing them
|
||||||
|
// leaves guest-visible completion state (labels, buffers,
|
||||||
|
// job results) permanently unwritten, which desyncs the
|
||||||
|
// engine's own job system and previously crashed it
|
||||||
|
// shortly after the movie finished.
|
||||||
switch (work)
|
switch (work)
|
||||||
{
|
{
|
||||||
case VulkanOffscreenGuestDraw offscreenDraw:
|
case VulkanOffscreenGuestDraw offscreenDraw:
|
||||||
@@ -12613,7 +13006,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
FlushBatchedGuestCommands();
|
FlushBatchedGuestCommands();
|
||||||
CollectAbandonedGuestImageVersions();
|
CollectAbandonedGuestImageVersions();
|
||||||
|
|
||||||
if (!TryTakePresentation(_presentedSequence, out var presentation))
|
Presentation presentation;
|
||||||
|
if (!TryTakePresentation(_presentedSequence, out presentation))
|
||||||
{
|
{
|
||||||
if (_pendingHostSplashReplay is { } splash)
|
if (_pendingHostSplashReplay is { } splash)
|
||||||
{
|
{
|
||||||
@@ -12637,7 +13031,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_hostSurface is not null)
|
if (_hostSurface is not null)
|
||||||
{
|
{
|
||||||
if (presentation.IsSplash && presentation.Pixels is not null)
|
if (presentation.IsSplash && presentation.Pixels is not null)
|
||||||
@@ -12690,6 +13083,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TranslatedDrawResources? translatedResources = null;
|
TranslatedDrawResources? translatedResources = null;
|
||||||
@@ -12817,19 +13211,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
if (pixels is not null)
|
if (pixels is not null)
|
||||||
{
|
{
|
||||||
// The staging buffer is shared across frame slots; a CPU
|
var mapped = (void*)_frameUploadMapped[frameSlot];
|
||||||
// pixel upload (splash / host frames) degrades to serial
|
|
||||||
// presentation rather than corrupting an in-flight copy.
|
|
||||||
WaitAllFrameSlots();
|
|
||||||
void* mapped;
|
|
||||||
Check(
|
|
||||||
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
|
|
||||||
"vkMapMemory");
|
|
||||||
fixed (byte* source = pixels)
|
fixed (byte* source = pixels)
|
||||||
{
|
{
|
||||||
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
||||||
}
|
}
|
||||||
_vk.UnmapMemory(_device, _stagingMemory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
|
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
|
||||||
@@ -12843,7 +13229,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
PipelineStageFlags waitStage;
|
PipelineStageFlags waitStage;
|
||||||
if (pixels is not null)
|
if (pixels is not null)
|
||||||
{
|
{
|
||||||
RecordUpload(imageIndex);
|
RecordUpload(imageIndex, frameSlot);
|
||||||
waitStage = PipelineStageFlags.TransferBit;
|
waitStage = PipelineStageFlags.TransferBit;
|
||||||
}
|
}
|
||||||
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
|
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
|
||||||
@@ -13365,11 +13751,22 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hostMovieImageInitialized = texture.HostMoviePlane switch
|
||||||
|
{
|
||||||
|
0 => _hostMovieImageInitialized,
|
||||||
|
1 => _hostMovieChromaImageInitialized,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
var toTransfer = new ImageMemoryBarrier
|
var toTransfer = new ImageMemoryBarrier
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageMemoryBarrier,
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
|
SrcAccessMask = texture.IsHostMovie && hostMovieImageInitialized
|
||||||
|
? AccessFlags.ShaderReadBit
|
||||||
|
: 0,
|
||||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||||
OldLayout = ImageLayout.Undefined,
|
OldLayout = texture.IsHostMovie && hostMovieImageInitialized
|
||||||
|
? ImageLayout.ShaderReadOnlyOptimal
|
||||||
|
: ImageLayout.Undefined,
|
||||||
NewLayout = ImageLayout.TransferDstOptimal,
|
NewLayout = ImageLayout.TransferDstOptimal,
|
||||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
@@ -13378,7 +13775,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineStageFlags.TopOfPipeBit,
|
texture.IsHostMovie && hostMovieImageInitialized
|
||||||
|
? PipelineStageFlags.AllCommandsBit
|
||||||
|
: PipelineStageFlags.TopOfPipeBit,
|
||||||
PipelineStageFlags.TransferBit,
|
PipelineStageFlags.TransferBit,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -13438,6 +13837,19 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
// reuse the image without restaging it.
|
// reuse the image without restaging it.
|
||||||
texture.NeedsUpload = false;
|
texture.NeedsUpload = false;
|
||||||
}
|
}
|
||||||
|
if (texture.IsHostMovie)
|
||||||
|
{
|
||||||
|
if (texture.HostMoviePlane == 0)
|
||||||
|
{
|
||||||
|
_hostMovieImageInitialized = true;
|
||||||
|
_hostMovieLumaUploadedFrameSerial = texture.HostMovieFrameSerial;
|
||||||
|
}
|
||||||
|
else if (texture.HostMoviePlane == 1)
|
||||||
|
{
|
||||||
|
_hostMovieChromaImageInitialized = true;
|
||||||
|
_hostMovieChromaUploadedFrameSerial = texture.HostMovieFrameSerial;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14751,7 +15163,176 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordUpload(uint imageIndex)
|
private void EnsureHostMovieImages(
|
||||||
|
uint lumaWidth,
|
||||||
|
uint lumaHeight,
|
||||||
|
uint lumaDstSelect,
|
||||||
|
uint chromaWidth,
|
||||||
|
uint chromaHeight,
|
||||||
|
uint chromaDstSelect)
|
||||||
|
{
|
||||||
|
if (_hostMovieImage.Handle != 0 &&
|
||||||
|
_hostMovieImageWidth == lumaWidth &&
|
||||||
|
_hostMovieImageHeight == lumaHeight &&
|
||||||
|
_hostMovieImageFormat == Format.R8Unorm &&
|
||||||
|
_hostMovieImageDstSelect == lumaDstSelect &&
|
||||||
|
_hostMovieChromaImage.Handle != 0 &&
|
||||||
|
_hostMovieChromaImageWidth == chromaWidth &&
|
||||||
|
_hostMovieChromaImageHeight == chromaHeight &&
|
||||||
|
_hostMovieChromaImageDstSelect == chromaDstSelect)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hostMovieImage.Handle != 0 || _hostMovieChromaImage.Handle != 0)
|
||||||
|
{
|
||||||
|
FlushBatchedGuestCommands();
|
||||||
|
WaitForAllGuestSubmissions();
|
||||||
|
DrainFrameSlots();
|
||||||
|
DestroyHostMovieImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateHostMoviePlaneImage(
|
||||||
|
lumaWidth,
|
||||||
|
lumaHeight,
|
||||||
|
Format.R8Unorm,
|
||||||
|
lumaDstSelect,
|
||||||
|
"luma",
|
||||||
|
out _hostMovieImage,
|
||||||
|
out _hostMovieImageMemory,
|
||||||
|
out _hostMovieImageView);
|
||||||
|
CreateHostMoviePlaneImage(
|
||||||
|
chromaWidth,
|
||||||
|
chromaHeight,
|
||||||
|
Format.R8G8Unorm,
|
||||||
|
chromaDstSelect,
|
||||||
|
"chroma",
|
||||||
|
out _hostMovieChromaImage,
|
||||||
|
out _hostMovieChromaImageMemory,
|
||||||
|
out _hostMovieChromaImageView);
|
||||||
|
_hostMovieImageWidth = lumaWidth;
|
||||||
|
_hostMovieImageHeight = lumaHeight;
|
||||||
|
_hostMovieImageFormat = Format.R8Unorm;
|
||||||
|
_hostMovieImageDstSelect = lumaDstSelect;
|
||||||
|
_hostMovieChromaImageWidth = chromaWidth;
|
||||||
|
_hostMovieChromaImageHeight = chromaHeight;
|
||||||
|
_hostMovieChromaImageDstSelect = chromaDstSelect;
|
||||||
|
_hostMovieImageInitialized = false;
|
||||||
|
_hostMovieChromaImageInitialized = false;
|
||||||
|
_hostMovieLumaUploadedFrameSerial = -1;
|
||||||
|
_hostMovieChromaUploadedFrameSerial = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateHostMoviePlaneImage(
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
Format format,
|
||||||
|
uint dstSelect,
|
||||||
|
string planeName,
|
||||||
|
out Image image,
|
||||||
|
out DeviceMemory memory,
|
||||||
|
out ImageView view)
|
||||||
|
{
|
||||||
|
var imageInfo = new ImageCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ImageCreateInfo,
|
||||||
|
ImageType = ImageType.Type2D,
|
||||||
|
Format = format,
|
||||||
|
Extent = new Extent3D(width, height, 1),
|
||||||
|
MipLevels = 1,
|
||||||
|
ArrayLayers = 1,
|
||||||
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
|
Tiling = ImageTiling.Optimal,
|
||||||
|
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
|
||||||
|
SharingMode = SharingMode.Exclusive,
|
||||||
|
InitialLayout = ImageLayout.Undefined,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateImage(_device, &imageInfo, null, out image),
|
||||||
|
$"vkCreateImage(host movie {planeName})");
|
||||||
|
_vk.GetImageMemoryRequirements(_device, image, out var requirements);
|
||||||
|
var allocationInfo = new MemoryAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryAllocateInfo,
|
||||||
|
AllocationSize = requirements.Size,
|
||||||
|
MemoryTypeIndex = FindMemoryType(
|
||||||
|
requirements.MemoryTypeBits,
|
||||||
|
MemoryPropertyFlags.DeviceLocalBit),
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.AllocateMemory(
|
||||||
|
_device,
|
||||||
|
&allocationInfo,
|
||||||
|
null,
|
||||||
|
out memory),
|
||||||
|
$"vkAllocateMemory(host movie {planeName})");
|
||||||
|
Check(
|
||||||
|
_vk.BindImageMemory(_device, image, memory, 0),
|
||||||
|
$"vkBindImageMemory(host movie {planeName})");
|
||||||
|
var viewInfo = new ImageViewCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ImageViewCreateInfo,
|
||||||
|
Image = image,
|
||||||
|
ViewType = ImageViewType.Type2D,
|
||||||
|
Format = format,
|
||||||
|
Components = ToVkComponentMapping(dstSelect),
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateImageView(
|
||||||
|
_device,
|
||||||
|
&viewInfo,
|
||||||
|
null,
|
||||||
|
out view),
|
||||||
|
$"vkCreateImageView(host movie {planeName})");
|
||||||
|
SetDebugName(ObjectType.Image, image.Handle, $"SharpEmu Bink2 {planeName} image");
|
||||||
|
SetDebugName(ObjectType.ImageView, view.Handle, $"SharpEmu Bink2 {planeName} view");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DestroyHostMovieImage()
|
||||||
|
{
|
||||||
|
if (_hostMovieImageView.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyImageView(_device, _hostMovieImageView, null);
|
||||||
|
_hostMovieImageView = default;
|
||||||
|
}
|
||||||
|
if (_hostMovieImage.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyImage(_device, _hostMovieImage, null);
|
||||||
|
_hostMovieImage = default;
|
||||||
|
}
|
||||||
|
if (_hostMovieImageMemory.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.FreeMemory(_device, _hostMovieImageMemory, null);
|
||||||
|
_hostMovieImageMemory = default;
|
||||||
|
}
|
||||||
|
if (_hostMovieChromaImageView.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyImageView(_device, _hostMovieChromaImageView, null);
|
||||||
|
_hostMovieChromaImageView = default;
|
||||||
|
}
|
||||||
|
if (_hostMovieChromaImage.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyImage(_device, _hostMovieChromaImage, null);
|
||||||
|
_hostMovieChromaImage = default;
|
||||||
|
}
|
||||||
|
if (_hostMovieChromaImageMemory.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.FreeMemory(_device, _hostMovieChromaImageMemory, null);
|
||||||
|
_hostMovieChromaImageMemory = default;
|
||||||
|
}
|
||||||
|
_hostMovieImageWidth = 0;
|
||||||
|
_hostMovieImageHeight = 0;
|
||||||
|
_hostMovieImageFormat = Format.Undefined;
|
||||||
|
_hostMovieChromaImageWidth = 0;
|
||||||
|
_hostMovieChromaImageHeight = 0;
|
||||||
|
_hostMovieImageInitialized = false;
|
||||||
|
_hostMovieChromaImageInitialized = false;
|
||||||
|
_hostMovieLumaUploadedFrameSerial = -1;
|
||||||
|
_hostMovieChromaUploadedFrameSerial = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordUpload(uint imageIndex, int frameSlot)
|
||||||
{
|
{
|
||||||
var oldLayout = _imageInitialized[imageIndex]
|
var oldLayout = _imageInitialized[imageIndex]
|
||||||
? ImageLayout.PresentSrcKhr
|
? ImageLayout.PresentSrcKhr
|
||||||
@@ -14793,7 +15374,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.CmdCopyBufferToImage(
|
_vk.CmdCopyBufferToImage(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
_stagingBuffer,
|
_frameUploadBuffers[frameSlot],
|
||||||
_swapchainImages[imageIndex],
|
_swapchainImages[imageIndex],
|
||||||
ImageLayout.TransferDstOptimal,
|
ImageLayout.TransferDstOptimal,
|
||||||
1,
|
1,
|
||||||
@@ -15661,7 +16242,26 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void DestroySwapchainResources()
|
private void DestroySwapchainResources()
|
||||||
{
|
{
|
||||||
|
DestroyHostMovieImage();
|
||||||
DestroyPresentEncodeImage();
|
DestroyPresentEncodeImage();
|
||||||
|
for (var slot = 0; slot < _frameUploadBuffers.Length; slot++)
|
||||||
|
{
|
||||||
|
if (_frameUploadMapped.Length > slot && _frameUploadMapped[slot] != 0)
|
||||||
|
{
|
||||||
|
_vk.UnmapMemory(_device, _frameUploadMemory[slot]);
|
||||||
|
}
|
||||||
|
if (_frameUploadBuffers[slot].Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyBuffer(_device, _frameUploadBuffers[slot], null);
|
||||||
|
}
|
||||||
|
if (_frameUploadMemory[slot].Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.FreeMemory(_device, _frameUploadMemory[slot], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_frameUploadBuffers = [];
|
||||||
|
_frameUploadMemory = [];
|
||||||
|
_frameUploadMapped = [];
|
||||||
if (_stagingBuffer.Handle != 0)
|
if (_stagingBuffer.Handle != 0)
|
||||||
{
|
{
|
||||||
_vk.DestroyBuffer(_device, _stagingBuffer, null);
|
_vk.DestroyBuffer(_device, _stagingBuffer, null);
|
||||||
|
|||||||
@@ -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,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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,8 +37,12 @@ public sealed class GnmTilingDetileTests
|
|||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void TryDetile_ExactXorMode27_MatchesReferenceAddressEquation()
|
[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 uint swizzleMode = 27; // 64 KiB RB+ R_X
|
||||||
const int bytesPerElement = 2;
|
const int bytesPerElement = 2;
|
||||||
@@ -46,8 +50,6 @@ public sealed class GnmTilingDetileTests
|
|||||||
// SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored.
|
// SquareBlockDimensions(32768 elements): 15 bits split 8/7, x favored.
|
||||||
const int blockWidth = 256;
|
const int blockWidth = 256;
|
||||||
const int blockHeight = 128;
|
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 blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
|
||||||
var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight;
|
var blocksPerColumn = (elementsHigh + blockHeight - 1) / blockHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,25 @@ public sealed class AvPlayerPathTests : IDisposable
|
|||||||
AvPlayerExports.GetFfprobePath(ffmpeg, isWindows));
|
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]
|
[Fact]
|
||||||
public void RelativeFileUriCannotEscapeApp0()
|
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,105 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
public uint FramesPerSecondNumerator => 20;
|
||||||
|
|
||||||
|
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);
|
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]
|
[Fact]
|
||||||
public void Mprotect_ZeroAddressReturnsInvalidArgument()
|
public void Mprotect_ZeroAddressReturnsInvalidArgument()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user