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 +