mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +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>
|
||||
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux</_HostRidOSPrefix>
|
||||
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx</_HostRidOSPrefix>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64</_HostRidArch>
|
||||
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64</_HostRidArch>
|
||||
<RuntimeIdentifier Condition="'$(_HostRidOSPrefix)' != ''">$(_HostRidOSPrefix)-$(_HostRidArch)</RuntimeIdentifier>
|
||||
|
||||
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+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
|
||||
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 <rid>` (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.
|
||||
|
||||
@@ -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);
|
||||
PreloadGlfw();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
@@ -213,6 +214,27 @@ internal static partial class Program
|
||||
"as libvulkan.1.dylib.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharpEmu.CLI.csproj publishes glfw into a "plugins" subfolder rather
|
||||
/// than flat next to the executable, which falls outside the default OS
|
||||
/// DLL/dlopen search path. Preloading it here by full path first means
|
||||
/// any later bare-name lookup (however Silk.NET/GLFW itself resolves the
|
||||
/// library) finds it already loaded in the process and reuses it -- the
|
||||
/// same technique <see cref="PreloadMacVulkanLoader"/> already relies on
|
||||
/// for the Vulkan loader.
|
||||
/// </summary>
|
||||
private static void PreloadGlfw()
|
||||
{
|
||||
var fileName = OperatingSystem.IsWindows() ? "glfw3.dll"
|
||||
: OperatingSystem.IsMacOS() ? "libglfw.3.dylib"
|
||||
: "libglfw.so.3";
|
||||
var candidate = Path.Combine(AppContext.BaseDirectory, "plugins", fileName);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
NativeLibrary.TryLoad(candidate, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
||||
{
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
@@ -20,6 +20,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
|
||||
natively, so on Apple Silicon it runs under Rosetta 2. -->
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<!-- A plain "dotnet publish" with no -r defaults $(RuntimeIdentifier) to
|
||||
the host's own RID; see Directory.Build.props, which is where that
|
||||
default actually has to live (PublishDir's RID suffix is decided
|
||||
there, evaluated before this file, so a default set only here would
|
||||
be too late for it). -->
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
@@ -60,7 +65,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -74,56 +79,75 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
<!-- Native libraries (glfw, FFmpeg) publish into a subfolder next to the
|
||||
executable instead of sitting loose beside it, so the publish
|
||||
directory stays uncluttered as more native deps get added. The folder
|
||||
name is a fixed constant, not derived from the RID/architecture: each
|
||||
publish output only ever holds one architecture's binaries anyway, so
|
||||
varying the name added a class of bugs (RID resolution timing, host-OS
|
||||
vs. target-RID mixups) for no benefit. Runtime code (Program.cs's
|
||||
PreloadGlfw, FfmpegNativeBinkFrameSource's RootPath) uses the same
|
||||
literal "plugins" folder name. -->
|
||||
<PropertyGroup>
|
||||
<NativeLibraryFolderName>plugins</NativeLibraryFolderName>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file in the native subfolder; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw')) Or $([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('libglfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<RelativePath>$(NativeLibraryFolderName)/%(Filename)%(Extension)</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</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>
|
||||
<Bink2BridgeBuildDir>$(BaseIntermediateOutputPath)bink2-bridge/$(RuntimeIdentifier)</Bink2BridgeBuildDir>
|
||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('Windows'))">sharpemu_bink2_bridge.dll</Bink2BridgeFileName>
|
||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('Linux'))">libsharpemu_bink2_bridge.so</Bink2BridgeFileName>
|
||||
<Bink2BridgeFileName Condition="$([MSBuild]::IsOSPlatform('OSX'))">libsharpemu_bink2_bridge.dylib</Bink2BridgeFileName>
|
||||
<Bink2BridgeOutput>$(Bink2BridgeBuildDir)/out/$(Bink2BridgeFileName)</Bink2BridgeOutput>
|
||||
<FfmpegRuntimeTag>2c92585</FfmpegRuntimeTag>
|
||||
<FfmpegRuntimeDir>
|
||||
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'linux-x64'">ffmpeg-linux-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-x64'">ffmpeg-macos-x64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">ffmpeg-macos-arm64.zip</FfmpegRuntimePackage>
|
||||
<FfmpegRuntimeArchive>$(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)</FfmpegRuntimeArchive>
|
||||
<FfmpegRuntimeExtractDir>$(FfmpegRuntimeDir)/extracted</FfmpegRuntimeExtractDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="BuildBink2Bridge"
|
||||
BeforeTargets="ComputeResolvedFilesToPublishList"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(Bink2BridgeFileName)' != ''">
|
||||
<Exec Condition="$([MSBuild]::IsOSPlatform('Windows'))"
|
||||
Command="cmake -S "$(RepoRoot)native/bink2-bridge" -B "$(Bink2BridgeBuildDir)" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
|
||||
<Exec Condition="!$([MSBuild]::IsOSPlatform('Windows'))"
|
||||
Command="cmake -S "$(RepoRoot)native/bink2-bridge" -B "$(Bink2BridgeBuildDir)" -DCMAKE_BUILD_TYPE=Release -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
|
||||
<Exec Command="cmake --build "$(Bink2BridgeBuildDir)" --config Release" />
|
||||
<Target Name="FetchFfmpegRuntime"
|
||||
BeforeTargets="Publish"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||
<Unzip
|
||||
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||
</Target>
|
||||
|
||||
<!-- Embeds the bridge into the single-file bundle (self-extracted at first
|
||||
load, see Bink2MovieBridge.NativeAdapter) instead of leaving it as a
|
||||
loose file next to the executable, so a downloaded release needs no
|
||||
setup: the DLL is already inside SharpEmu.exe. -->
|
||||
<Target Name="EmbedBink2Bridge"
|
||||
AfterTargets="ComputeResolvedFilesToPublishList"
|
||||
DependsOnTargets="BuildBink2Bridge"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And Exists('$(Bink2BridgeOutput)')">
|
||||
<Target Name="PublishFfmpegRuntime"
|
||||
AfterTargets="Publish"
|
||||
DependsOnTargets="FetchFfmpegRuntime"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
|
||||
e.g. linux-x64 from a Windows machine is a supported cross-publish,
|
||||
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
|
||||
depends on which platform's ffmpeg-core package was fetched. -->
|
||||
<ItemGroup>
|
||||
<ResolvedFileToPublish Include="$(Bink2BridgeOutput)">
|
||||
<RelativePath>$(Bink2BridgeFileName)</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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<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()
|
||||
{
|
||||
@@ -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<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 HashSet<string> PendingMoviePathSet =
|
||||
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>
|
||||
<PackageReference Include="FFmpeg.AutoGen" />
|
||||
<PackageReference Include="Silk.NET.Input" />
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
|
||||
Reference in New Issue
Block a user