mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-27 13:11:04 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77fe07efc0 |
@@ -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" />
|
||||||
|
|||||||
@@ -108,17 +108,10 @@ release includes the MoltenVK Vulkan implementation.
|
|||||||
## Build
|
## Build
|
||||||
|
|
||||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||||
2. `dotnet publish` also builds the Bink 2 bridge
|
2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
|
||||||
(`native/bink2-bridge/sharpemu_bink2_bridge.c`) from source, so also
|
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
||||||
install:
|
4. Build the project: `dotnet build` or `dotnet publish`
|
||||||
* **Windows:** [CMake](https://cmake.org/download/), [Ninja](https://github.com/ninja-build/ninja/releases), and [LLVM](https://github.com/llvm/llvm-project/releases) (for `clang-cl`)
|
5. Build artifacts will be located in the `artifacts` directory.
|
||||||
* **Linux/macOS:** CMake and a C compiler toolchain (e.g. `build-essential` on Linux, Xcode Command Line Tools on macOS)
|
|
||||||
|
|
||||||
`dotnet build` alone doesn't need these; it skips the bridge.
|
|
||||||
3. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
|
|
||||||
4. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
|
|
||||||
5. Build the project: `dotnet build` or `dotnet publish`
|
|
||||||
6. Build artifacts will be located in the `artifacts` directory.
|
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
+47
-36
@@ -9,17 +9,24 @@ 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.
|
||||||
|
|
||||||
The default path decodes through the bundled FFmpeg-backed native bridge
|
The default path decodes by calling FFmpeg's own C API directly from managed
|
||||||
(`native/bink2-bridge/sharpemu_bink2_bridge.c`); see "Supplying the adapter"
|
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
|
||||||
below for where that binary comes from. Set `SHARPEMU_BINK_MODE=guest` to
|
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
|
||||||
leave decoding to the Bink implementation statically linked into the game
|
bindings) against a custom FFmpeg build
|
||||||
instead. Set `skip` only when explicitly testing a title whose cinematics are
|
(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
|
||||||
optional.
|
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
|
||||||
@@ -27,38 +34,42 @@ only; it does not decode the movie or alter its game logic.
|
|||||||
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
|
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
|
||||||
being explicit about it.
|
being explicit about it.
|
||||||
|
|
||||||
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override forces a host FFmpeg
|
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override is unrelated to the
|
||||||
source. SharpEmu searches
|
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,
|
`SHARPEMU_FFMPEG_PATH`, the executable directory, its `ffmpeg` subdirectory,
|
||||||
and then `PATH`. The FFmpeg build must contain the Bink 2 decoder; stock FFmpeg
|
and then `PATH` (plus a couple of common Homebrew paths on macOS). That
|
||||||
builds that only recognize the Bink container are not sufficient.
|
`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
|
||||||
|
only recognizes the Bink container is not sufficient. Most users want the
|
||||||
|
default `native` mode instead, which always has Bink 2 support since it's
|
||||||
|
built against `ffmpeg-core` specifically.
|
||||||
|
|
||||||
## Supplying the adapter
|
## Supplying the FFmpeg libraries
|
||||||
|
|
||||||
The adapter (`native/bink2-bridge/sharpemu_bink2_bridge.c`) links against a
|
`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
|
||||||
custom FFmpeg build (`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a
|
(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
|
||||||
Bink 2 decoder to FFmpeg 7.1.2; no proprietary RAD SDK is needed to build or
|
the `FFmpeg.AutoGen` package version in `Directory.Packages.props` -- both
|
||||||
run SharpEmu.
|
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.
|
||||||
|
|
||||||
`dotnet publish` builds it from source with CMake + Ninja + clang-cl (Windows)
|
A plain `dotnet publish` with no `-r` still works: it defaults to the host
|
||||||
or the platform's default C compiler (Linux/macOS), targeting win-x64,
|
machine's own RID (see `Directory.Build.props`), so it fetches the matching
|
||||||
linux-x64, osx-x64, or osx-arm64, then embeds the result in the published
|
`ffmpeg-core` archive and populates `plugins` without any extra flags.
|
||||||
single-file executable. Publishing SharpEmu therefore requires that
|
Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
|
||||||
toolchain locally (the same one the CI runners already ship with); there is
|
Windows) still overrides that default normally.
|
||||||
no prebuilt/download fallback. A downloaded release needs no such setup: the
|
|
||||||
compiled adapter is already inside `SharpEmu.exe`.
|
|
||||||
|
|
||||||
To use a different build of the adapter, point to it explicitly:
|
To use a different set of FFmpeg libraries, drop them into the published
|
||||||
|
`plugins` folder yourself (matching FFmpeg's own file-naming and versioning
|
||||||
|
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
|
||||||
|
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
|
||||||
|
folder and does not otherwise care where the files came from.
|
||||||
|
|
||||||
SHARPEMU_BINK2_BRIDGE=/absolute/path/sharpemu_bink2_bridge.dll \
|
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
|
||||||
./SharpEmu /path/to/eboot.bin
|
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
|
||||||
|
could not open movie ...") and leaves the guest's own rendering path
|
||||||
The expected exports are sharpemu_bink2_open_utf8,
|
untouched, rather than crashing.
|
||||||
sharpemu_bink2_open_scaled_utf8, sharpemu_bink2_decode_next_bgra, and
|
|
||||||
sharpemu_bink2_close. The adapter opens one movie, optionally scaling it down
|
|
||||||
to a maximum size, exposes BGRA pixels, and advances after each decoded
|
|
||||||
frame. The managed side validates dimensions and retains ownership of the
|
|
||||||
destination buffer.
|
|
||||||
|
|
||||||
If the bridge is absent in native mode, SharpEmu logs one informational line
|
|
||||||
and retains the existing guest rendering path.
|
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
# Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.21)
|
|
||||||
|
|
||||||
if(NOT DEFINED SHARPEMU_TARGET_RID)
|
|
||||||
message(FATAL_ERROR "SHARPEMU_TARGET_RID is required")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(SHARPEMU_TARGET_RID STREQUAL "osx-x64")
|
|
||||||
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "" FORCE)
|
|
||||||
elseif(SHARPEMU_TARGET_RID STREQUAL "osx-arm64")
|
|
||||||
set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "" FORCE)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
project(sharpemu_bink2_bridge LANGUAGES C)
|
|
||||||
|
|
||||||
set(SHARPEMU_FFMPEG_TAG "6a6861e")
|
|
||||||
set(SHARPEMU_FFMPEG_COMMIT "6a6861e357a263edee51d2f4894941f50aed59f5")
|
|
||||||
# Keyed by tag so bumping SHARPEMU_FFMPEG_TAG always fetches fresh instead of
|
|
||||||
# silently reusing a previous tag's cached download.
|
|
||||||
set(SHARPEMU_FFMPEG_ROOT "${CMAKE_BINARY_DIR}/ffmpeg-core-${SHARPEMU_FFMPEG_TAG}")
|
|
||||||
|
|
||||||
if(SHARPEMU_TARGET_RID STREQUAL "win-x64")
|
|
||||||
set(SHARPEMU_FFMPEG_PACKAGE "ffmpeg-windows-x64.zip")
|
|
||||||
elseif(SHARPEMU_TARGET_RID STREQUAL "linux-x64")
|
|
||||||
set(SHARPEMU_FFMPEG_PACKAGE "ffmpeg-linux-x64.zip")
|
|
||||||
elseif(SHARPEMU_TARGET_RID STREQUAL "osx-x64")
|
|
||||||
set(SHARPEMU_FFMPEG_PACKAGE "ffmpeg-macos-x64.zip")
|
|
||||||
elseif(SHARPEMU_TARGET_RID STREQUAL "osx-arm64")
|
|
||||||
set(SHARPEMU_FFMPEG_PACKAGE "ffmpeg-macos-arm64.zip")
|
|
||||||
else()
|
|
||||||
message(FATAL_ERROR "Unsupported Bink2 bridge RID: ${SHARPEMU_TARGET_RID}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(SHARPEMU_FFMPEG_ARCHIVE "${SHARPEMU_FFMPEG_ROOT}/${SHARPEMU_FFMPEG_PACKAGE}")
|
|
||||||
set(SHARPEMU_FFMPEG_LIB_DIR "${SHARPEMU_FFMPEG_ROOT}/lib")
|
|
||||||
set(SHARPEMU_FFMPEG_SOURCE_ARCHIVE "${SHARPEMU_FFMPEG_ROOT}/source.tar.gz")
|
|
||||||
set(SHARPEMU_FFMPEG_SOURCE_DIR
|
|
||||||
"${SHARPEMU_FFMPEG_ROOT}/source/ffmpeg-core-${SHARPEMU_FFMPEG_COMMIT}")
|
|
||||||
|
|
||||||
if(NOT EXISTS "${SHARPEMU_FFMPEG_ARCHIVE}")
|
|
||||||
file(MAKE_DIRECTORY "${SHARPEMU_FFMPEG_ROOT}")
|
|
||||||
file(DOWNLOAD
|
|
||||||
"https://github.com/sharpemu/ffmpeg-core/releases/download/${SHARPEMU_FFMPEG_TAG}/${SHARPEMU_FFMPEG_PACKAGE}"
|
|
||||||
"${SHARPEMU_FFMPEG_ARCHIVE}"
|
|
||||||
SHOW_PROGRESS
|
|
||||||
STATUS SHARPEMU_DOWNLOAD_STATUS)
|
|
||||||
list(GET SHARPEMU_DOWNLOAD_STATUS 0 SHARPEMU_DOWNLOAD_CODE)
|
|
||||||
if(NOT SHARPEMU_DOWNLOAD_CODE EQUAL 0)
|
|
||||||
message(FATAL_ERROR "Failed to download ${SHARPEMU_FFMPEG_PACKAGE}: ${SHARPEMU_DOWNLOAD_STATUS}")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${SHARPEMU_FFMPEG_LIB_DIR}")
|
|
||||||
file(MAKE_DIRECTORY "${SHARPEMU_FFMPEG_LIB_DIR}")
|
|
||||||
file(ARCHIVE_EXTRACT
|
|
||||||
INPUT "${SHARPEMU_FFMPEG_ARCHIVE}"
|
|
||||||
DESTINATION "${SHARPEMU_FFMPEG_LIB_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${SHARPEMU_FFMPEG_SOURCE_DIR}/include/libavcodec/avcodec.h")
|
|
||||||
file(MAKE_DIRECTORY "${SHARPEMU_FFMPEG_ROOT}/source")
|
|
||||||
file(DOWNLOAD
|
|
||||||
"https://github.com/sharpemu/ffmpeg-core/archive/${SHARPEMU_FFMPEG_COMMIT}.tar.gz"
|
|
||||||
"${SHARPEMU_FFMPEG_SOURCE_ARCHIVE}"
|
|
||||||
SHOW_PROGRESS
|
|
||||||
STATUS SHARPEMU_SOURCE_STATUS)
|
|
||||||
list(GET SHARPEMU_SOURCE_STATUS 0 SHARPEMU_SOURCE_CODE)
|
|
||||||
if(NOT SHARPEMU_SOURCE_CODE EQUAL 0)
|
|
||||||
message(FATAL_ERROR "Failed to download FFmpeg headers: ${SHARPEMU_SOURCE_STATUS}")
|
|
||||||
endif()
|
|
||||||
file(ARCHIVE_EXTRACT
|
|
||||||
INPUT "${SHARPEMU_FFMPEG_SOURCE_ARCHIVE}"
|
|
||||||
DESTINATION "${SHARPEMU_FFMPEG_ROOT}/source")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(sharpemu_bink2_bridge SHARED sharpemu_bink2_bridge.c)
|
|
||||||
target_compile_features(sharpemu_bink2_bridge PRIVATE c_std_11)
|
|
||||||
target_include_directories(sharpemu_bink2_bridge PRIVATE
|
|
||||||
"${SHARPEMU_FFMPEG_SOURCE_DIR}/include")
|
|
||||||
|
|
||||||
function(sharpemu_link_ffmpeg_library target library_name)
|
|
||||||
find_library(SHARPEMU_${library_name}_LIBRARY
|
|
||||||
NAMES "${library_name}" "lib${library_name}"
|
|
||||||
PATHS "${SHARPEMU_FFMPEG_LIB_DIR}"
|
|
||||||
NO_DEFAULT_PATH
|
|
||||||
REQUIRED)
|
|
||||||
target_link_libraries(${target} PRIVATE "${SHARPEMU_${library_name}_LIBRARY}")
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
sharpemu_link_ffmpeg_library(sharpemu_bink2_bridge avformat)
|
|
||||||
sharpemu_link_ffmpeg_library(sharpemu_bink2_bridge avcodec)
|
|
||||||
sharpemu_link_ffmpeg_library(sharpemu_bink2_bridge swscale)
|
|
||||||
sharpemu_link_ffmpeg_library(sharpemu_bink2_bridge avutil)
|
|
||||||
|
|
||||||
if(WIN32)
|
|
||||||
set_property(TARGET sharpemu_bink2_bridge PROPERTY
|
|
||||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
|
||||||
target_link_libraries(sharpemu_bink2_bridge PRIVATE
|
|
||||||
bcrypt ole32 oleaut32 psapi shlwapi strmiids user32 uuid ws2_32)
|
|
||||||
elseif(APPLE)
|
|
||||||
target_link_libraries(sharpemu_bink2_bridge PRIVATE
|
|
||||||
"-framework AppKit"
|
|
||||||
"-framework AudioToolbox"
|
|
||||||
"-framework CoreAudio"
|
|
||||||
"-framework CoreFoundation"
|
|
||||||
"-framework CoreMedia"
|
|
||||||
"-framework CoreServices"
|
|
||||||
"-framework CoreVideo"
|
|
||||||
"-framework Security"
|
|
||||||
"-framework VideoToolbox")
|
|
||||||
else()
|
|
||||||
target_link_libraries(sharpemu_bink2_bridge PRIVATE dl m pthread)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set_target_properties(sharpemu_bink2_bridge PROPERTIES
|
|
||||||
C_VISIBILITY_PRESET hidden
|
|
||||||
VISIBILITY_INLINES_HIDDEN YES
|
|
||||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/out"
|
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/out")
|
|
||||||
|
|
||||||
foreach(configuration Debug Release RelWithDebInfo MinSizeRel)
|
|
||||||
string(TOUPPER "${configuration}" configuration_upper)
|
|
||||||
set_target_properties(sharpemu_bink2_bridge PROPERTIES
|
|
||||||
LIBRARY_OUTPUT_DIRECTORY_${configuration_upper} "${CMAKE_BINARY_DIR}/out"
|
|
||||||
RUNTIME_OUTPUT_DIRECTORY_${configuration_upper} "${CMAKE_BINARY_DIR}/out")
|
|
||||||
endforeach()
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
*/
|
|
||||||
#include <errno.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
#include <libavutil/error.h>
|
|
||||||
#include <libswscale/swscale.h>
|
|
||||||
|
|
||||||
#if defined(_WIN32)
|
|
||||||
#define SHARPEMU_EXPORT __declspec(dllexport)
|
|
||||||
#else
|
|
||||||
#define SHARPEMU_EXPORT __attribute__((visibility("default")))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
typedef struct sharpemu_bink2_movie {
|
|
||||||
AVFormatContext *format;
|
|
||||||
AVCodecContext *codec;
|
|
||||||
struct SwsContext *converter;
|
|
||||||
AVFrame *frame;
|
|
||||||
AVPacket *packet;
|
|
||||||
int video_stream;
|
|
||||||
uint32_t output_width;
|
|
||||||
uint32_t output_height;
|
|
||||||
int draining;
|
|
||||||
} sharpemu_bink2_movie;
|
|
||||||
|
|
||||||
static void sharpemu_bink2_log_error(const char *operation, int error) {
|
|
||||||
char message[AV_ERROR_MAX_STRING_SIZE];
|
|
||||||
if (av_strerror(error, message, sizeof(message)) < 0) {
|
|
||||||
snprintf(message, sizeof(message), "FFmpeg error %d", error);
|
|
||||||
}
|
|
||||||
fprintf(stderr, "[BINK2][ERROR] %s: %s\n", operation, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sharpemu_bink2_destroy(sharpemu_bink2_movie *movie) {
|
|
||||||
if (!movie) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sws_freeContext(movie->converter);
|
|
||||||
av_packet_free(&movie->packet);
|
|
||||||
av_frame_free(&movie->frame);
|
|
||||||
avcodec_free_context(&movie->codec);
|
|
||||||
avformat_close_input(&movie->format);
|
|
||||||
free(movie);
|
|
||||||
}
|
|
||||||
|
|
||||||
static AVRational sharpemu_bink2_frame_rate(AVFormatContext *format, AVStream *stream) {
|
|
||||||
AVRational rate = av_guess_frame_rate(format, stream, NULL);
|
|
||||||
if (rate.num <= 0 || rate.den <= 0) {
|
|
||||||
rate = stream->avg_frame_rate;
|
|
||||||
}
|
|
||||||
if (rate.num <= 0 || rate.den <= 0) {
|
|
||||||
rate = stream->r_frame_rate;
|
|
||||||
}
|
|
||||||
if (rate.num <= 0 || rate.den <= 0) {
|
|
||||||
rate = (AVRational){30, 1};
|
|
||||||
}
|
|
||||||
return rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int sharpemu_bink2_open_internal(
|
|
||||||
const char *path,
|
|
||||||
uint32_t maximum_width,
|
|
||||||
uint32_t maximum_height,
|
|
||||||
void **movie_out,
|
|
||||||
sharpemu_bink2_info *info) {
|
|
||||||
sharpemu_bink2_movie *movie;
|
|
||||||
const AVCodec *decoder = NULL;
|
|
||||||
AVStream *stream;
|
|
||||||
AVRational frame_rate;
|
|
||||||
int result;
|
|
||||||
|
|
||||||
if (!path || !movie_out || !info) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*movie_out = NULL;
|
|
||||||
movie = (sharpemu_bink2_movie *)calloc(1, sizeof(*movie));
|
|
||||||
if (!movie) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = avformat_open_input(&movie->format, path, NULL, NULL);
|
|
||||||
if (result < 0) {
|
|
||||||
sharpemu_bink2_log_error("open", result);
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = avformat_find_stream_info(movie->format, NULL);
|
|
||||||
if (result < 0) {
|
|
||||||
sharpemu_bink2_log_error("stream info", result);
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = av_find_best_stream(
|
|
||||||
movie->format, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
|
|
||||||
if (result < 0 || !decoder) {
|
|
||||||
sharpemu_bink2_log_error("video stream", result);
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
movie->video_stream = result;
|
|
||||||
stream = movie->format->streams[movie->video_stream];
|
|
||||||
movie->codec = avcodec_alloc_context3(decoder);
|
|
||||||
if (!movie->codec) {
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = avcodec_parameters_to_context(movie->codec, stream->codecpar);
|
|
||||||
if (result < 0) {
|
|
||||||
sharpemu_bink2_log_error("codec parameters", result);
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
movie->codec->thread_count = 0;
|
|
||||||
movie->codec->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
|
|
||||||
result = avcodec_open2(movie->codec, decoder, NULL);
|
|
||||||
if (result < 0) {
|
|
||||||
sharpemu_bink2_log_error("codec open", result);
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
movie->frame = av_frame_alloc();
|
|
||||||
movie->packet = av_packet_alloc();
|
|
||||||
if (!movie->frame || !movie->packet ||
|
|
||||||
movie->codec->width <= 0 || movie->codec->height <= 0) {
|
|
||||||
sharpemu_bink2_destroy(movie);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame_rate = sharpemu_bink2_frame_rate(movie->format, stream);
|
|
||||||
movie->output_width = (uint32_t)movie->codec->width;
|
|
||||||
movie->output_height = (uint32_t)movie->codec->height;
|
|
||||||
if (maximum_width > 0 && maximum_height > 0 &&
|
|
||||||
(movie->output_width > maximum_width ||
|
|
||||||
movie->output_height > maximum_height)) {
|
|
||||||
if ((uint64_t)movie->output_width * maximum_height >
|
|
||||||
(uint64_t)movie->output_height * maximum_width) {
|
|
||||||
movie->output_height = (uint32_t)((uint64_t)movie->output_height *
|
|
||||||
maximum_width /
|
|
||||||
movie->output_width);
|
|
||||||
movie->output_width = maximum_width;
|
|
||||||
} else {
|
|
||||||
movie->output_width = (uint32_t)((uint64_t)movie->output_width *
|
|
||||||
maximum_height /
|
|
||||||
movie->output_height);
|
|
||||||
movie->output_height = maximum_height;
|
|
||||||
}
|
|
||||||
if (movie->output_width == 0) {
|
|
||||||
movie->output_width = 1;
|
|
||||||
}
|
|
||||||
if (movie->output_height == 0) {
|
|
||||||
movie->output_height = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info->width = movie->output_width;
|
|
||||||
info->height = movie->output_height;
|
|
||||||
info->frames_per_second_numerator = (uint32_t)frame_rate.num;
|
|
||||||
info->frames_per_second_denominator = (uint32_t)frame_rate.den;
|
|
||||||
*movie_out = movie;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
SHARPEMU_EXPORT int sharpemu_bink2_open_utf8(
|
|
||||||
const char *path,
|
|
||||||
void **movie_out,
|
|
||||||
sharpemu_bink2_info *info) {
|
|
||||||
return sharpemu_bink2_open_internal(path, 0, 0, movie_out, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
SHARPEMU_EXPORT int sharpemu_bink2_open_scaled_utf8(
|
|
||||||
const char *path,
|
|
||||||
uint32_t maximum_width,
|
|
||||||
uint32_t maximum_height,
|
|
||||||
void **movie_out,
|
|
||||||
sharpemu_bink2_info *info) {
|
|
||||||
return sharpemu_bink2_open_internal(
|
|
||||||
path, maximum_width, maximum_height, movie_out, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int sharpemu_bink2_receive_frame(sharpemu_bink2_movie *movie) {
|
|
||||||
int result;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
result = avcodec_receive_frame(movie->codec, movie->frame);
|
|
||||||
if (result >= 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (result == AVERROR_EOF) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (result != AVERROR(EAGAIN)) {
|
|
||||||
sharpemu_bink2_log_error("decode", result);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (movie->draining) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
result = av_read_frame(movie->format, movie->packet);
|
|
||||||
if (result < 0) {
|
|
||||||
movie->draining = 1;
|
|
||||||
result = avcodec_send_packet(movie->codec, NULL);
|
|
||||||
if (result < 0 && result != AVERROR_EOF) {
|
|
||||||
sharpemu_bink2_log_error("decoder drain", result);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movie->packet->stream_index != movie->video_stream) {
|
|
||||||
av_packet_unref(movie->packet);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = avcodec_send_packet(movie->codec, movie->packet);
|
|
||||||
av_packet_unref(movie->packet);
|
|
||||||
if (result < 0 && result != AVERROR(EAGAIN)) {
|
|
||||||
sharpemu_bink2_log_error("packet submit", result);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SHARPEMU_EXPORT int sharpemu_bink2_decode_next_bgra(
|
|
||||||
void *handle,
|
|
||||||
uint8_t *destination,
|
|
||||||
uint32_t stride,
|
|
||||||
uint32_t destination_bytes) {
|
|
||||||
sharpemu_bink2_movie *movie = (sharpemu_bink2_movie *)handle;
|
|
||||||
uint8_t *destination_planes[4] = {destination, NULL, NULL, NULL};
|
|
||||||
int destination_strides[4] = {(int)stride, 0, 0, 0};
|
|
||||||
uint64_t required_bytes;
|
|
||||||
int converted_rows;
|
|
||||||
|
|
||||||
if (!movie || !destination || stride < movie->output_width * 4) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
required_bytes = (uint64_t)stride * movie->output_height;
|
|
||||||
if (required_bytes > destination_bytes || !sharpemu_bink2_receive_frame(movie)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
movie->converter = sws_getCachedContext(
|
|
||||||
movie->converter,
|
|
||||||
movie->frame->width,
|
|
||||||
movie->frame->height,
|
|
||||||
(enum AVPixelFormat)movie->frame->format,
|
|
||||||
(int)movie->output_width,
|
|
||||||
(int)movie->output_height,
|
|
||||||
AV_PIX_FMT_BGRA,
|
|
||||||
SWS_FAST_BILINEAR,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL);
|
|
||||||
if (!movie->converter) {
|
|
||||||
av_frame_unref(movie->frame);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
converted_rows = sws_scale(
|
|
||||||
movie->converter,
|
|
||||||
(const uint8_t *const *)movie->frame->data,
|
|
||||||
movie->frame->linesize,
|
|
||||||
0,
|
|
||||||
movie->frame->height,
|
|
||||||
destination_planes,
|
|
||||||
destination_strides);
|
|
||||||
av_frame_unref(movie->frame);
|
|
||||||
return converted_rows == (int)movie->output_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
SHARPEMU_EXPORT void sharpemu_bink2_close(void *movie) {
|
|
||||||
sharpemu_bink2_destroy((sharpemu_bink2_movie *)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>
|
||||||
@@ -74,7 +79,20 @@ 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>
|
||||||
@@ -83,47 +101,53 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- Building the Bink2 bridge (native/bink2-bridge/sharpemu_bink2_bridge.c)
|
|
||||||
requires cmake, ninja, and clang-cl (Windows) or a C compiler
|
|
||||||
(Linux/macOS); the same tools the CI runners already ship with. No
|
|
||||||
prebuilt/download fallback: anyone publishing SharpEmu is expected to
|
|
||||||
have the same toolchain, matching every other native dependency in
|
|
||||||
this repo. -->
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Bink2BridgeBuildDir>$(BaseIntermediateOutputPath)bink2-bridge/$(RuntimeIdentifier)</Bink2BridgeBuildDir>
|
<FfmpegRuntimeTag>2c92585</FfmpegRuntimeTag>
|
||||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('Windows'))">sharpemu_bink2_bridge.dll</Bink2BridgeFileName>
|
<FfmpegRuntimeDir>
|
||||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('Linux'))">libsharpemu_bink2_bridge.so</Bink2BridgeFileName>
|
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
|
||||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('OSX'))">libsharpemu_bink2_bridge.dylib</Bink2BridgeFileName>
|
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
|
||||||
<Bink2BridgeOutput>$(Bink2BridgeBuildDir)/out/$(Bink2BridgeFileName)</Bink2BridgeOutput>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="BuildBink2Bridge"
|
<Target Name="FetchFfmpegRuntime"
|
||||||
BeforeTargets="ComputeResolvedFilesToPublishList"
|
BeforeTargets="Publish"
|
||||||
Condition="'$(RuntimeIdentifier)' != '' And '$(Bink2BridgeFileName)' != ''">
|
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||||
<Exec Condition="$([MSBuild]::IsOSPlatform('Windows'))"
|
<DownloadFile
|
||||||
Command="cmake -S "$(RepoRoot)native/bink2-bridge" -B "$(Bink2BridgeBuildDir)" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
|
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||||
<Exec Condition="!$([MSBuild]::IsOSPlatform('Windows'))"
|
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||||
Command="cmake -S "$(RepoRoot)native/bink2-bridge" -B "$(Bink2BridgeBuildDir)" -DCMAKE_BUILD_TYPE=Release -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
|
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||||
<Exec Command="cmake --build "$(Bink2BridgeBuildDir)" --config Release" />
|
<Unzip
|
||||||
|
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||||
|
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||||
|
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<!-- Embeds the bridge into the single-file bundle (self-extracted at first
|
<Target Name="PublishFfmpegRuntime"
|
||||||
load, see Bink2MovieBridge.NativeAdapter) instead of leaving it as a
|
AfterTargets="Publish"
|
||||||
loose file next to the executable, so a downloaded release needs no
|
DependsOnTargets="FetchFfmpegRuntime"
|
||||||
setup: the DLL is already inside SharpEmu.exe. -->
|
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||||
<Target Name="EmbedBink2Bridge"
|
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
|
||||||
AfterTargets="ComputeResolvedFilesToPublishList"
|
e.g. linux-x64 from a Windows machine is a supported cross-publish,
|
||||||
DependsOnTargets="BuildBink2Bridge"
|
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
|
||||||
Condition="'$(RuntimeIdentifier)' != '' And Exists('$(Bink2BridgeOutput)')">
|
depends on which platform's ffmpeg-core package was fetched. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ResolvedFileToPublish Include="$(Bink2BridgeOutput)">
|
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||||
<RelativePath>$(Bink2BridgeFileName)</RelativePath>
|
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||||
</ResolvedFileToPublish>
|
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||||
|
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||||
|
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -22,15 +22,12 @@ internal static class Bink2MovieBridge
|
|||||||
private const uint MaxHostVideoHeight = 1080;
|
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 Bink2MovieInfo _activeInfo;
|
private static Bink2MovieInfo _activeInfo;
|
||||||
private static byte[]? _frameBuffer;
|
private static byte[]? _frameBuffer;
|
||||||
private static bool _frameBufferPresented;
|
private static bool _frameBufferPresented;
|
||||||
private static BinkFramePlayback? _playback;
|
private static BinkFramePlayback? _playback;
|
||||||
private static long _frameSerial;
|
private static long _frameSerial;
|
||||||
private static bool _loadAttempted;
|
|
||||||
private static bool _availabilityReported;
|
|
||||||
private static uint _presentationWidth = MaxHostVideoWidth;
|
private static uint _presentationWidth = MaxHostVideoWidth;
|
||||||
private static uint _presentationHeight = MaxHostVideoHeight;
|
private static uint _presentationHeight = MaxHostVideoHeight;
|
||||||
|
|
||||||
@@ -198,19 +195,9 @@ internal static class Bink2MovieBridge
|
|||||||
|
|
||||||
private static void AttachNativeMovieLocked(string hostPath)
|
private static void AttachNativeMovieLocked(string hostPath)
|
||||||
{
|
{
|
||||||
var adapter = GetAdapterLocked();
|
if (!FfmpegNativeBinkFrameSource.TryOpen(
|
||||||
if (adapter is null)
|
hostPath, _presentationWidth, _presentationHeight, out var source) ||
|
||||||
{
|
source is null)
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseActiveLocked();
|
|
||||||
if (!adapter.TryOpen(
|
|
||||||
hostPath,
|
|
||||||
_presentationWidth,
|
|
||||||
_presentationHeight,
|
|
||||||
out var movie,
|
|
||||||
out var info))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||||
@@ -218,19 +205,18 @@ internal static class Bink2MovieBridge
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var info = new Bink2MovieInfo(
|
||||||
|
source.Width, source.Height, source.FramesPerSecondNumerator, source.FramesPerSecondDenominator);
|
||||||
if (!IsValid(info))
|
if (!IsValid(info))
|
||||||
{
|
{
|
||||||
adapter.Close(movie);
|
source.Dispose();
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||||
Path.GetFileName(hostPath) + "'.");
|
Path.GetFileName(hostPath) + "'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachPlaybackLocked(
|
AttachPlaybackLocked(hostPath, info, source);
|
||||||
hostPath,
|
|
||||||
info,
|
|
||||||
new NativeFrameDecoder(adapter, movie, info));
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||||
info.Width + "x" + info.Height + " @ " +
|
info.Width + "x" + info.Height + " @ " +
|
||||||
@@ -265,12 +251,11 @@ internal static class Bink2MovieBridge
|
|||||||
return MovieMode.Ffmpeg;
|
return MovieMode.Ffmpeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native is the default: the bridge ships embedded in the published
|
// Native is the default: FfmpegNativeBinkFrameSource.TryOpen degrades
|
||||||
// single-file executable (see SharpEmu.CLI.csproj), so it isn't a
|
// gracefully (falls back to the guest's own decode, logging one
|
||||||
// loose file next to the exe to probe for with File.Exists here.
|
// informational line) if the FFmpeg libraries SharpEmu.CLI.csproj
|
||||||
// GetAdapterLocked() degrades gracefully (falls back to the guest's
|
// downloads next to the executable are genuinely unavailable, so
|
||||||
// own decode, logging one informational line) if it's genuinely
|
// defaulting to Native unconditionally is safe.
|
||||||
// unavailable, so defaulting to Native unconditionally is safe.
|
|
||||||
return MovieMode.Native;
|
return MovieMode.Native;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,83 +362,6 @@ internal static class Bink2MovieBridge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NativeAdapter? GetAdapterLocked()
|
|
||||||
{
|
|
||||||
if (_loadAttempted)
|
|
||||||
{
|
|
||||||
return _adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadAttempted = true;
|
|
||||||
|
|
||||||
// Assembly-relative resolution participates in the single-file
|
|
||||||
// bundle's native-library extraction, so it finds the bridge whether
|
|
||||||
// it was embedded in the publish or sits as a loose file next to the
|
|
||||||
// executable, without us needing to know which. Skipped when the env
|
|
||||||
// override is set so that override still takes priority below.
|
|
||||||
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) &&
|
|
||||||
NativeLibrary.TryLoad(
|
|
||||||
"sharpemu_bink2_bridge", typeof(Bink2MovieBridge).Assembly, null, out var bundledLibrary))
|
|
||||||
{
|
|
||||||
if (NativeAdapter.TryCreate(bundledLibrary, out var bundledAdapter))
|
|
||||||
{
|
|
||||||
_adapter = bundledAdapter;
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded (bundled).");
|
|
||||||
return bundledAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.Free(bundledLibrary);
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
||||||
{
|
{
|
||||||
@@ -499,147 +407,6 @@ internal static class Bink2MovieBridge
|
|||||||
Ffmpeg,
|
Ffmpeg,
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class NativeFrameDecoder : IBinkFrameDecoder
|
|
||||||
{
|
|
||||||
private readonly NativeAdapter _adapter;
|
|
||||||
private readonly IntPtr _movie;
|
|
||||||
private int _disposed;
|
|
||||||
|
|
||||||
internal NativeFrameDecoder(NativeAdapter adapter, IntPtr movie, Bink2MovieInfo info)
|
|
||||||
{
|
|
||||||
_adapter = adapter;
|
|
||||||
_movie = movie;
|
|
||||||
Width = info.Width;
|
|
||||||
Height = info.Height;
|
|
||||||
FramesPerSecondNumerator = info.FramesPerSecondNumerator;
|
|
||||||
FramesPerSecondDenominator = info.FramesPerSecondDenominator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public uint Width { get; }
|
|
||||||
|
|
||||||
public uint Height { get; }
|
|
||||||
|
|
||||||
public uint FramesPerSecondNumerator { get; }
|
|
||||||
|
|
||||||
public uint FramesPerSecondDenominator { get; }
|
|
||||||
|
|
||||||
public unsafe bool TryDecodeNextFrame(Span<byte> destination)
|
|
||||||
{
|
|
||||||
fixed (byte* pointer = destination)
|
|
||||||
{
|
|
||||||
return _adapter.DecodeNextBgra(
|
|
||||||
_movie,
|
|
||||||
(IntPtr)pointer,
|
|
||||||
Width * 4,
|
|
||||||
checked((uint)destination.Length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
|
||||||
{
|
|
||||||
_adapter.Close(_movie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private sealed class NativeAdapter
|
|
||||||
{
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
|
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
||||||
private delegate int OpenScaledUtf8Delegate(
|
|
||||||
IntPtr pathUtf8,
|
|
||||||
uint maximumWidth,
|
|
||||||
uint maximumHeight,
|
|
||||||
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 OpenScaledUtf8Delegate? _openScaledUtf8;
|
|
||||||
private readonly DecodeNextBgraDelegate _decodeNextBgra;
|
|
||||||
private readonly CloseDelegate _close;
|
|
||||||
|
|
||||||
private NativeAdapter(
|
|
||||||
OpenUtf8Delegate openUtf8,
|
|
||||||
OpenScaledUtf8Delegate? openScaledUtf8,
|
|
||||||
DecodeNextBgraDelegate decodeNextBgra,
|
|
||||||
CloseDelegate close)
|
|
||||||
{
|
|
||||||
_openUtf8 = openUtf8;
|
|
||||||
_openScaledUtf8 = openScaledUtf8;
|
|
||||||
_decodeNextBgra = decodeNextBgra;
|
|
||||||
_close = close;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
|
|
||||||
{
|
|
||||||
adapter = null;
|
|
||||||
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
|
|
||||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
|
|
||||||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenScaledUtf8Delegate? openScaled = null;
|
|
||||||
if (NativeLibrary.TryGetExport(
|
|
||||||
library,
|
|
||||||
"sharpemu_bink2_open_scaled_utf8",
|
|
||||||
out var scaledOpen))
|
|
||||||
{
|
|
||||||
openScaled = Marshal.GetDelegateForFunctionPointer<OpenScaledUtf8Delegate>(scaledOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = new NativeAdapter(
|
|
||||||
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
|
|
||||||
openScaled,
|
|
||||||
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
|
|
||||||
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool TryOpen(
|
|
||||||
string path,
|
|
||||||
uint maximumWidth,
|
|
||||||
uint maximumHeight,
|
|
||||||
out IntPtr movie,
|
|
||||||
out Bink2MovieInfo info)
|
|
||||||
{
|
|
||||||
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _openScaledUtf8 is not null
|
|
||||||
? _openScaledUtf8(
|
|
||||||
utf8,
|
|
||||||
maximumWidth,
|
|
||||||
maximumHeight,
|
|
||||||
out movie,
|
|
||||||
out info)
|
|
||||||
: _openUtf8(utf8, out movie, out info);
|
|
||||||
return result != 0 && movie != IntPtr.Zero;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(utf8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
|
|
||||||
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
|
|
||||||
|
|
||||||
internal void Close(IntPtr movie) => _close(movie);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Queue<string> PendingMoviePaths = new();
|
private static readonly Queue<string> PendingMoviePaths = new();
|
||||||
private static readonly HashSet<string> PendingMoviePathSet =
|
private static readonly HashSet<string> PendingMoviePathSet =
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user