diff --git a/.github/images/demons-souls.jpg b/.github/images/demons-souls.jpg
index cbf2954e..9717e2f8 100644
Binary files a/.github/images/demons-souls.jpg and b/.github/images/demons-souls.jpg differ
diff --git a/Directory.Build.props b/Directory.Build.props
index 04c8a010..6387c545 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,6 +14,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))
+ <_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win
+ <_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux
+ <_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx
+ <_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64
+ <_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64
+ $(_HostRidOSPrefix)-$(_HostRidArch)
+
$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/
$(RepoRoot)artifacts/bin/
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5473ea5e..5839733f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
diff --git a/README.md b/README.md
index 96d6f1f2..bec4c8f4 100644
--- a/README.md
+++ b/README.md
@@ -108,17 +108,10 @@ release includes the MoltenVK Vulkan implementation.
## Build
1. Install the .NET SDK version specified in [`global.json`](./global.json).
-2. `dotnet publish` also builds the Bink 2 bridge
- (`native/bink2-bridge/sharpemu_bink2_bridge.c`) from source, so also
- install:
- * **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`)
- * **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.
+2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
+3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
+4. Build the project: `dotnet build` or `dotnet publish`
+5. Build artifacts will be located in the `artifacts` directory.
## Disclaimer
diff --git a/docs/bink2-bridge.md b/docs/bink2-bridge.md
index 5dbf25f2..d32fdcda 100644
--- a/docs/bink2-bridge.md
+++ b/docs/bink2-bridge.md
@@ -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
decoder cannot observe or replace those frames.
-SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
+SharpEmu observes successful guest .bk2 opens and, when a Bink decoder is
available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path.
-The default path decodes through the bundled FFmpeg-backed native bridge
-(`native/bink2-bridge/sharpemu_bink2_bridge.c`); see "Supplying the adapter"
-below for where that binary comes from. 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.
+The default path decodes by calling FFmpeg's own C API directly from managed
+code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
+[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
+bindings) against a custom FFmpeg build
+(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
+FFmpeg 7.1.2; see "Supplying the FFmpeg libraries" below for where those
+libraries come from. No proprietary RAD SDK is needed to build or run
+SharpEmu, and there is no C/C++ code of SharpEmu's own involved in decoding
+-- SharpEmu.CLI.csproj only downloads a prebuilt release archive.
+
+Set `SHARPEMU_BINK_MODE=guest` to leave decoding to the Bink implementation
+statically linked into the game instead. Set `skip` only when explicitly
+testing a title whose cinematics are optional.
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
@@ -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
being explicit about it.
-The experimental `SHARPEMU_BINK_MODE=ffmpeg` override forces a host FFmpeg
-source. SharpEmu searches
+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`. The FFmpeg build must contain the Bink 2 decoder; stock FFmpeg
-builds that only recognize the Bink container are not sufficient.
+and then `PATH` (plus a couple of common Homebrew paths on macOS). That
+`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
+only recognizes the Bink container is not sufficient. Most users want the
+default `native` mode instead, which always has Bink 2 support since it's
+built against `ffmpeg-core` specifically.
-## Supplying the adapter
+## Supplying the FFmpeg libraries
-The adapter (`native/bink2-bridge/sharpemu_bink2_bridge.c`) links against a
-custom FFmpeg build (`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a
-Bink 2 decoder to FFmpeg 7.1.2; no proprietary RAD SDK is needed to build or
-run SharpEmu.
+`dotnet publish` fetches a prebuilt release of `github.com/sharpemu/ffmpeg-core`
+(the tag is pinned in `SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to
+the `FFmpeg.AutoGen` package version in `Directory.Packages.props` -- both
+need to agree on the same FFmpeg ABI) and copies its dynamically linked
+libraries into a `plugins` folder next to the published executable. No C
+toolchain is required to build SharpEmu; publishing just downloads a zip.
+`plugins` is a loose, unpacked folder rather than something embedded in the
+single-file bundle, so the OS loader can resolve the libraries' own
+inter-dependencies (`avcodec` depends on `avutil`, etc.) itself.
-`dotnet publish` builds it from source with CMake + Ninja + clang-cl (Windows)
-or the platform's default C compiler (Linux/macOS), targeting win-x64,
-linux-x64, osx-x64, or osx-arm64, then embeds the result in the published
-single-file executable. Publishing SharpEmu therefore requires that
-toolchain locally (the same one the CI runners already ship with); there is
-no prebuilt/download fallback. A downloaded release needs no such setup: the
-compiled adapter is already inside `SharpEmu.exe`.
+A plain `dotnet publish` with no `-r` still works: it defaults to the host
+machine's own RID (see `Directory.Build.props`), so it fetches the matching
+`ffmpeg-core` archive and populates `plugins` without any extra flags.
+Passing an explicit `-r ` (e.g. to cross-publish `linux-x64` from
+Windows) still overrides that default normally.
-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 \
- ./SharpEmu /path/to/eboot.bin
-
-The expected exports are sharpemu_bink2_open_utf8,
-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.
+If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
+degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
+could not open movie ...") and leaves the guest's own rendering path
+untouched, rather than crashing.
diff --git a/native/bink2-bridge/CMakeLists.txt b/native/bink2-bridge/CMakeLists.txt
deleted file mode 100644
index d8e596dd..00000000
--- a/native/bink2-bridge/CMakeLists.txt
+++ /dev/null
@@ -1,131 +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 caches absolute paths; discard paths from an older FFmpeg
- # tag when a build directory is reused after a package update.
- unset(SHARPEMU_${library_name}_LIBRARY CACHE)
- 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$<$: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()
diff --git a/native/bink2-bridge/sharpemu_bink2_bridge.c b/native/bink2-bridge/sharpemu_bink2_bridge.c
deleted file mode 100644
index 1c503600..00000000
--- a/native/bink2-bridge/sharpemu_bink2_bridge.c
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright (C) 2026 SharpEmu Emulator Project
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-
-#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);
-}
diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs
index a1440715..868a7cb3 100644
--- a/src/SharpEmu.CLI/Program.cs
+++ b/src/SharpEmu.CLI/Program.cs
@@ -64,6 +64,7 @@ internal static partial class Program
}
args = NormalizeInternalArguments(args, out var isMitigatedChild);
+ PreloadGlfw();
if (args.Length == 0)
{
@@ -213,6 +214,27 @@ internal static partial class Program
"as libvulkan.1.dylib.");
}
+ ///
+ /// 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 already relies on
+ /// for the Vulkan loader.
+ ///
+ private static void PreloadGlfw()
+ {
+ var fileName = OperatingSystem.IsWindows() ? "glfw3.dll"
+ : OperatingSystem.IsMacOS() ? "libglfw.3.dylib"
+ : "libglfw.so.3";
+ var candidate = Path.Combine(AppContext.BaseDirectory, "plugins", fileName);
+ if (File.Exists(candidate))
+ {
+ NativeLibrary.TryLoad(candidate, out _);
+ }
+ }
+
private static int RunEmulator(string[] args, bool isMitigatedChild)
{
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
diff --git a/src/SharpEmu.CLI/SharpEmu.CLI.csproj b/src/SharpEmu.CLI/SharpEmu.CLI.csproj
index 8a6165fe..d4ef9e8b 100644
--- a/src/SharpEmu.CLI/SharpEmu.CLI.csproj
+++ b/src/SharpEmu.CLI/SharpEmu.CLI.csproj
@@ -20,6 +20,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
win-x64;linux-x64;osx-x64;osx-arm64
+
true
true
true
@@ -60,7 +65,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
$(NoWarn);1591
-
+
Always
@@ -74,56 +79,75 @@ SPDX-License-Identifier: GPL-2.0-or-later
-
+
+ plugins
+
+
+
<_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'))" />
true
+ $(NativeLibraryFolderName)/%(Filename)%(Extension)
-
- $(BaseIntermediateOutputPath)bink2-bridge/$(RuntimeIdentifier)
- sharpemu_bink2_bridge.dll
- libsharpemu_bink2_bridge.so
- libsharpemu_bink2_bridge.dylib
- $(Bink2BridgeBuildDir)/out/$(Bink2BridgeFileName)
+ 2c92585
+
+ $(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)
+ ffmpeg-windows-x64.zip
+ ffmpeg-linux-x64.zip
+ ffmpeg-macos-x64.zip
+ ffmpeg-macos-arm64.zip
+ $(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)
+ $(FfmpegRuntimeDir)/extracted
-
-
-
-
+
+
+
-
-
+
+
-
- $(Bink2BridgeFileName)
-
+ <_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
+ Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
+ <_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
+ Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
+
diff --git a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs
index c185af2b..36124b25 100644
--- a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs
+++ b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs
@@ -22,15 +22,12 @@ internal static class Bink2MovieBridge
private const uint MaxHostVideoHeight = 1080;
private static readonly object Gate = new();
- private static NativeAdapter? _adapter;
private static string? _activePath;
private static Bink2MovieInfo _activeInfo;
private static byte[]? _frameBuffer;
private static bool _frameBufferPresented;
private static BinkFramePlayback? _playback;
private static long _frameSerial;
- private static bool _loadAttempted;
- private static bool _availabilityReported;
private static uint _presentationWidth = MaxHostVideoWidth;
private static uint _presentationHeight = MaxHostVideoHeight;
@@ -198,19 +195,9 @@ internal static class Bink2MovieBridge
private static void AttachNativeMovieLocked(string hostPath)
{
- var adapter = GetAdapterLocked();
- if (adapter is null)
- {
- return;
- }
-
- CloseActiveLocked();
- if (!adapter.TryOpen(
- hostPath,
- _presentationWidth,
- _presentationHeight,
- out var movie,
- out var info))
+ if (!FfmpegNativeBinkFrameSource.TryOpen(
+ hostPath, _presentationWidth, _presentationHeight, out var source) ||
+ source is null)
{
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge could not open movie '" +
@@ -218,19 +205,18 @@ internal static class Bink2MovieBridge
return;
}
+ var info = new Bink2MovieInfo(
+ source.Width, source.Height, source.FramesPerSecondNumerator, source.FramesPerSecondDenominator);
if (!IsValid(info))
{
- adapter.Close(movie);
+ source.Dispose();
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
Path.GetFileName(hostPath) + "'.");
return;
}
- AttachPlaybackLocked(
- hostPath,
- info,
- new NativeFrameDecoder(adapter, movie, info));
+ AttachPlaybackLocked(hostPath, info, source);
Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + " @ " +
@@ -265,12 +251,11 @@ internal static class Bink2MovieBridge
return MovieMode.Ffmpeg;
}
- // Native is the default: the bridge ships embedded in the published
- // single-file executable (see SharpEmu.CLI.csproj), so it isn't a
- // loose file next to the exe to probe for with File.Exists here.
- // GetAdapterLocked() degrades gracefully (falls back to the guest's
- // own decode, logging one informational line) if it's genuinely
- // unavailable, so defaulting to Native unconditionally is safe.
+ // 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;
}
@@ -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 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()
{
@@ -499,147 +407,6 @@ internal static class Bink2MovieBridge
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 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(scaledOpen);
- }
-
- adapter = new NativeAdapter(
- Marshal.GetDelegateForFunctionPointer(open),
- openScaled,
- Marshal.GetDelegateForFunctionPointer(decode),
- Marshal.GetDelegateForFunctionPointer(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 PendingMoviePaths = new();
private static readonly HashSet PendingMoviePathSet =
new(StringComparer.OrdinalIgnoreCase);
diff --git a/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs b/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs
new file mode 100644
index 00000000..ab0bd581
--- /dev/null
+++ b/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs
@@ -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;
+
+///
+/// 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.
+///
+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;
+
+ ///
+ /// 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.
+ ///
+ 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 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;
+ }
+ }
+}
diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
index 76566512..f0ac613c 100644
--- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj
+++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj
@@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
+