mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f37dd85e0 | |||
| 38621e7be9 | |||
| c9c0793059 |
Binary file not shown.
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 82 KiB |
@@ -26,25 +26,6 @@ Before opening a pull request, please keep the following in mind:
|
||||
|
||||
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
Pull requests should provide real, observable emulator behavior rather than only suppressing errors or unresolved imports.
|
||||
|
||||
Changes that only return success, zero, or fabricated handles without implementing the expected state, output, or side effects will generally not be accepted. Functions that create resources, write output structures, register callbacks, or expose runtime state should model the behavior required by the guest.
|
||||
|
||||
When applicable, PRs should include:
|
||||
|
||||
- The affected game or application.
|
||||
- Relevant logs or failing imports.
|
||||
- Behavior before and after the change.
|
||||
- Real game testing and known limitations.
|
||||
|
||||
Avoid submitting large collections of speculative NIDs or unrelated exports. Keep each PR focused on one problem or a closely related set of changes.
|
||||
|
||||
Large architectural changes should be discussed with the maintainers before implementation. Contributors are encouraged to ask first when they are uncertain whether a proposed direction fits the project.
|
||||
|
||||
Opening a PR does not guarantee that it will be merged. Maintainers evaluate changes based on correctness, evidence, testing, scope, maintenance cost, and the long-term direction of the project.
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
|
||||
|
||||
@@ -9,18 +9,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.2-beta.5</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.2-beta.4</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<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,7 +11,6 @@ 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,10 +108,17 @@ release includes the MoltenVK Vulkan implementation.
|
||||
## Build
|
||||
|
||||
1. Install the .NET SDK version specified in [`global.json`](./global.json).
|
||||
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.
|
||||
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.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
+36
-47
@@ -9,24 +9,17 @@ 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 decoder is
|
||||
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge 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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -34,42 +27,38 @@ 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 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
|
||||
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override forces a host FFmpeg
|
||||
source. SharpEmu searches
|
||||
`SHARPEMU_FFMPEG_PATH`, the executable directory, its `ffmpeg` subdirectory,
|
||||
and then `PATH` (plus a couple of common Homebrew paths on macOS). That
|
||||
`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
|
||||
only recognizes the Bink container is not sufficient. Most users want the
|
||||
default `native` mode instead, which always has Bink 2 support since it's
|
||||
built against `ffmpeg-core` specifically.
|
||||
and then `PATH`. The FFmpeg build must contain the Bink 2 decoder; stock FFmpeg
|
||||
builds that only recognize the Bink container are not sufficient.
|
||||
|
||||
## Supplying the FFmpeg libraries
|
||||
## Supplying the adapter
|
||||
|
||||
`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.
|
||||
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.
|
||||
|
||||
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.
|
||||
`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`.
|
||||
|
||||
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.
|
||||
To use a different build of the adapter, point to it explicitly:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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()
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* 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,7 +64,6 @@ internal static partial class Program
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
PreloadGlfw();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
@@ -214,27 +213,6 @@ 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");
|
||||
@@ -573,7 +551,12 @@ internal static partial class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] childArgs = [MitigatedChildFlag, .. args];
|
||||
var childArgs = new string[args.Length + 1];
|
||||
childArgs[0] = MitigatedChildFlag;
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
childArgs[i + 1] = args[i];
|
||||
}
|
||||
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
|
||||
@@ -20,11 +20,6 @@ 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>
|
||||
@@ -65,7 +60,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -79,75 +74,56 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 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
|
||||
<!-- Keep glfw as a loose file next to the executable; 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>
|
||||
<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>
|
||||
<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>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="FetchFfmpegRuntime"
|
||||
BeforeTargets="Publish"
|
||||
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
|
||||
<DownloadFile
|
||||
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
|
||||
DestinationFolder="$(FfmpegRuntimeDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
|
||||
<Unzip
|
||||
SourceFiles="$(FfmpegRuntimeArchive)"
|
||||
DestinationFolder="$(FfmpegRuntimeExtractDir)"
|
||||
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
|
||||
<Target 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>
|
||||
|
||||
<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. -->
|
||||
<!-- 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)')">
|
||||
<ItemGroup>
|
||||
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
|
||||
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
|
||||
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
|
||||
<ResolvedFileToPublish Include="$(Bink2BridgeOutput)">
|
||||
<RelativePath>$(Bink2BridgeFileName)</RelativePath>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
|
||||
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -965,20 +965,6 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// RenderThread/Subrender probe this before writing a NOP. Unresolved
|
||||
// GetSize returns NOT_FOUND and leaves command-buffer sizing broken.
|
||||
// CbNop rejects dwordCount < 2, so report that floor.
|
||||
[SysAbiExport(
|
||||
Nid = "t7PlZ9nt5Lc",
|
||||
ExportName = "sceAgcCbNopGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int CbNopGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 2u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "k3GhuSNmBLU",
|
||||
ExportName = "sceAgcCbDispatch",
|
||||
@@ -1176,18 +1162,6 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Matches the fixed 8-dword ACQUIRE_MEM packet AcbAcquireMem writes above.
|
||||
[SysAbiExport(
|
||||
Nid = "ewobAQeMo5k",
|
||||
ExportName = "sceAgcAcbAcquireMemGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int AcbAcquireMemGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "htn36gPnBk4",
|
||||
ExportName = "sceAgcAcbWaitRegMem",
|
||||
@@ -1763,18 +1737,6 @@ public static partial class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Matches the fixed 8-dword ACQUIRE_MEM packet DcbAcquireMem writes above.
|
||||
[SysAbiExport(
|
||||
Nid = "-vnlTPPXPrw",
|
||||
ExportName = "sceAgcDcbAcquireMemGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbAcquireMemGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i1jyy49AjXU",
|
||||
ExportName = "sceAgcDcbWriteData",
|
||||
@@ -2428,20 +2390,6 @@ public static partial class AgcExports
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// PatchAddress/PatchData touch UInt64 fields at +12/+20 of an RReleaseMem
|
||||
// packet, so the packet is at least 7 dwords; use the 8-dword RELEASE_MEM
|
||||
// family size already used elsewhere in this file.
|
||||
[SysAbiExport(
|
||||
Nid = "hL7C0IRpWZI",
|
||||
ExportName = "sceAgcCbQueueEndOfPipeActionGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int CbQueueEndOfPipeActionGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0fWWK5uG9rQ",
|
||||
ExportName = "sceAgcQueueEndOfPipeActionPatchAddress",
|
||||
@@ -11521,34 +11469,6 @@ public static partial class AgcExports
|
||||
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
||||
}
|
||||
|
||||
// Hardware REWIND is a fixed 2-dword header + valid-bit packet (same floor
|
||||
// as CbNopGetSize). No Rewind writer is implemented yet; size-only is enough
|
||||
// for callers that allocate the packet before filling it.
|
||||
[SysAbiExport(
|
||||
Nid = "QIXCsbipds0",
|
||||
ExportName = "sceAgcDcbRewindGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbRewindGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 2u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
// Matches the 4-dword INDIRECT_BUFFER packet DcbJump writes below.
|
||||
// Returning NOT_FOUND here left callers with a null packet pointer and an
|
||||
// immediate write AV on RenderThread.
|
||||
[SysAbiExport(
|
||||
Nid = "VEGu4dixjUg",
|
||||
ExportName = "sceAgcDcbJumpGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbJumpGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 4u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xSAR0LTcRKM",
|
||||
ExportName = "sceAgcDcbJump",
|
||||
|
||||
@@ -21,7 +21,6 @@ public static class AjmExports
|
||||
private const int MaxInstanceIndex = 0x2FFF;
|
||||
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
|
||||
private static int _nextContextId;
|
||||
private static int _nextBatchId;
|
||||
|
||||
private sealed class AjmContextState
|
||||
{
|
||||
@@ -228,250 +227,10 @@ public static class AjmExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a decode job on a batch. Titles call this on the Bink/AJM hot
|
||||
/// path; leaving it unresolved floods Import WARN spam. This is a silence
|
||||
/// stub, not a codec: advance the batch cursor and report the input as
|
||||
/// consumed with silence produced so the title does not spin on the same
|
||||
/// packet.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "39WxhR-ePew",
|
||||
ExportName = "sceAjmBatchJobDecode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchJobDecode(CpuContext ctx)
|
||||
{
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
var instanceId = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var inputAddress = ctx[CpuRegister.Rdx];
|
||||
var inputSize = ctx[CpuRegister.Rcx];
|
||||
var outputAddress = ctx[CpuRegister.R8];
|
||||
var outputSize = ctx[CpuRegister.R9];
|
||||
var resultAddress = ReadStackArg64(ctx, 0);
|
||||
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
// Best-effort: bump the batch cursor when the guest filled AjmBatchInfo.
|
||||
// Still succeed without it — the unresolved stub returned 0 and titles
|
||||
// keep calling; failing here would reintroduce hot-path spam via retries.
|
||||
_ = TryAppendBatchJob(ctx, infoAddress, AjmJobRunSize);
|
||||
|
||||
// Silence: clear PCM out and claim full input consumed so the guest
|
||||
// advances its bitstream cursor instead of re-submitting forever.
|
||||
if (outputAddress != 0 && outputSize != 0 && outputSize <= MaxSilentPcmBytes)
|
||||
{
|
||||
ClearGuestMemory(ctx, outputAddress, outputSize);
|
||||
}
|
||||
|
||||
WriteDecodeStreamResult(
|
||||
ctx,
|
||||
resultAddress,
|
||||
inputConsumed: inputSize > int.MaxValue ? int.MaxValue : (int)inputSize,
|
||||
outputWritten: 0,
|
||||
totalDecodedSamples: 0,
|
||||
frames: inputSize != 0 || outputSize != 0 ? 1u : 0u);
|
||||
|
||||
Trace(
|
||||
$"batch_job_decode info=0x{infoAddress:X16} instance=0x{instanceId:X8} " +
|
||||
$"in=0x{inputAddress:X16}+0x{inputSize:X} out=0x{outputAddress:X16}+0x{outputSize:X} " +
|
||||
$"result=0x{resultAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a built batch. Instant-complete silence stub: publish a batch id
|
||||
/// and clear any error out. Decode sidebands were already filled at
|
||||
/// job-enqueue time.
|
||||
/// </summary>
|
||||
[SysAbiExport(
|
||||
Nid = "5tOfnaClcqM",
|
||||
ExportName = "sceAjmBatchStart",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchStart(CpuContext ctx)
|
||||
{
|
||||
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
var priority = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var errorAddress = ctx[CpuRegister.Rcx];
|
||||
var batchOutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
if (infoAddress == 0 || batchOutAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
ClearAjmBatchError(ctx, errorAddress);
|
||||
|
||||
var batchId = unchecked((uint)Interlocked.Increment(ref _nextBatchId));
|
||||
Span<byte> batchValue = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(batchValue, batchId);
|
||||
if (!ctx.Memory.TryWrite(batchOutAddress, batchValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
|
||||
}
|
||||
|
||||
Trace(
|
||||
$"batch_start context={contextId} info=0x{infoAddress:X16} " +
|
||||
$"priority={priority} batch={batchId} error=0x{errorAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-qLsfDAywIY",
|
||||
ExportName = "sceAjmBatchWait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchWait(CpuContext ctx)
|
||||
{
|
||||
// Batches complete synchronously in Start; Wait is a no-op success.
|
||||
var errorAddress = ctx[CpuRegister.Rcx];
|
||||
ClearAjmBatchError(ctx, errorAddress);
|
||||
Trace(
|
||||
$"batch_wait context={unchecked((uint)ctx[CpuRegister.Rdi])} " +
|
||||
$"batch={unchecked((uint)ctx[CpuRegister.Rsi])} " +
|
||||
$"timeout={unchecked((uint)ctx[CpuRegister.Rdx])}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NVDXiUesSbA",
|
||||
ExportName = "sceAjmBatchCancel",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAjm")]
|
||||
public static int AjmBatchCancel(CpuContext ctx)
|
||||
{
|
||||
Trace(
|
||||
$"batch_cancel context={unchecked((uint)ctx[CpuRegister.Rdi])} " +
|
||||
$"batch={unchecked((uint)ctx[CpuRegister.Rsi])}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Contexts.Clear();
|
||||
Interlocked.Exchange(ref _nextContextId, 0);
|
||||
Interlocked.Exchange(ref _nextBatchId, 0);
|
||||
}
|
||||
|
||||
// AjmBatchInfo: buffer, offset, size, last_good_job, last_good_job_ra (5× u64).
|
||||
private const ulong AjmBatchInfoOffsetField = 8;
|
||||
private const ulong AjmBatchInfoSizeField = 16;
|
||||
private const ulong AjmBatchInfoLastGoodJobField = 24;
|
||||
private const ulong AjmJobRunSize = 64;
|
||||
private const ulong MaxSilentPcmBytes = 1 << 20;
|
||||
// AjmSidebandResult (8) + AjmSidebandStream (16) + AjmSidebandMFrame (8).
|
||||
private const int DecodeSidebandBytes = 32;
|
||||
|
||||
private static bool TryAppendBatchJob(CpuContext ctx, ulong infoAddress, ulong jobSize)
|
||||
{
|
||||
if (!TryReadUInt64(ctx, infoAddress, out var buffer) ||
|
||||
!TryReadUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, out var offset) ||
|
||||
!TryReadUInt64(ctx, infoAddress + AjmBatchInfoSizeField, out var size))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer == 0 || jobSize == 0 || offset > size || size - offset < jobSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var jobAddress = buffer + offset;
|
||||
ClearGuestMemory(ctx, jobAddress, jobSize);
|
||||
return TryWriteUInt64(ctx, infoAddress + AjmBatchInfoLastGoodJobField, jobAddress) &&
|
||||
TryWriteUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, offset + jobSize);
|
||||
}
|
||||
|
||||
// AjmBatchError: int error_code; const void* job_addr; uint32_t cmd_offset; const void* job_ra;
|
||||
private const int AjmBatchErrorBytes = 24;
|
||||
|
||||
private static void ClearAjmBatchError(CpuContext ctx, ulong errorAddress)
|
||||
{
|
||||
if (errorAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> error = stackalloc byte[AjmBatchErrorBytes];
|
||||
error.Clear();
|
||||
_ = ctx.Memory.TryWrite(errorAddress, error);
|
||||
}
|
||||
|
||||
private static void WriteDecodeStreamResult(
|
||||
CpuContext ctx,
|
||||
ulong resultAddress,
|
||||
int inputConsumed,
|
||||
int outputWritten,
|
||||
ulong totalDecodedSamples,
|
||||
uint frames)
|
||||
{
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> sideband = stackalloc byte[DecodeSidebandBytes];
|
||||
sideband.Clear();
|
||||
// AjmSidebandResult.result / internal_result = 0 (OK)
|
||||
BinaryPrimitives.WriteInt32LittleEndian(sideband.Slice(8, 4), inputConsumed);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(sideband.Slice(12, 4), outputWritten);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(sideband.Slice(16, 8), totalDecodedSamples);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(sideband.Slice(24, 4), frames);
|
||||
_ = ctx.Memory.TryWrite(resultAddress, sideband);
|
||||
}
|
||||
|
||||
private static void ClearGuestMemory(CpuContext ctx, ulong address, ulong byteCount)
|
||||
{
|
||||
if (address == 0 || byteCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var remaining = byteCount;
|
||||
var cursor = address;
|
||||
Span<byte> zero = stackalloc byte[256];
|
||||
while (remaining > 0)
|
||||
{
|
||||
var chunk = (int)Math.Min(remaining, (ulong)zero.Length);
|
||||
if (!ctx.Memory.TryWrite(cursor, zero[..chunk]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cursor += (ulong)chunk;
|
||||
remaining -= (ulong)chunk;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ReadStackArg64(CpuContext ctx, int index)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rsp] + sizeof(ulong) + ((ulong)index * sizeof(ulong));
|
||||
return TryReadUInt64(ctx, address, out var value) ? value : 0;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
|
||||
@@ -22,12 +22,15 @@ 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;
|
||||
|
||||
@@ -195,9 +198,19 @@ internal static class Bink2MovieBridge
|
||||
|
||||
private static void AttachNativeMovieLocked(string hostPath)
|
||||
{
|
||||
if (!FfmpegNativeBinkFrameSource.TryOpen(
|
||||
hostPath, _presentationWidth, _presentationHeight, out var source) ||
|
||||
source is null)
|
||||
var adapter = GetAdapterLocked();
|
||||
if (adapter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseActiveLocked();
|
||||
if (!adapter.TryOpen(
|
||||
hostPath,
|
||||
_presentationWidth,
|
||||
_presentationHeight,
|
||||
out var movie,
|
||||
out var info))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge could not open movie '" +
|
||||
@@ -205,18 +218,19 @@ internal static class Bink2MovieBridge
|
||||
return;
|
||||
}
|
||||
|
||||
var info = new Bink2MovieInfo(
|
||||
source.Width, source.Height, source.FramesPerSecondNumerator, source.FramesPerSecondDenominator);
|
||||
if (!IsValid(info))
|
||||
{
|
||||
source.Dispose();
|
||||
adapter.Close(movie);
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
|
||||
Path.GetFileName(hostPath) + "'.");
|
||||
return;
|
||||
}
|
||||
|
||||
AttachPlaybackLocked(hostPath, info, source);
|
||||
AttachPlaybackLocked(
|
||||
hostPath,
|
||||
info,
|
||||
new NativeFrameDecoder(adapter, movie, info));
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
|
||||
info.Width + "x" + info.Height + " @ " +
|
||||
@@ -251,11 +265,12 @@ internal static class Bink2MovieBridge
|
||||
return MovieMode.Ffmpeg;
|
||||
}
|
||||
|
||||
// Native is the default: FfmpegNativeBinkFrameSource.TryOpen degrades
|
||||
// gracefully (falls back to the guest's own decode, logging one
|
||||
// informational line) if the FFmpeg libraries SharpEmu.CLI.csproj
|
||||
// downloads next to the executable are genuinely unavailable, so
|
||||
// defaulting to Native unconditionally is safe.
|
||||
// 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.
|
||||
return MovieMode.Native;
|
||||
}
|
||||
|
||||
@@ -362,6 +377,83 @@ 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()
|
||||
{
|
||||
@@ -407,6 +499,147 @@ 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);
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
// 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,7 +25,6 @@ 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" />
|
||||
|
||||
@@ -45,25 +45,6 @@ public static class SystemServiceExports
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Lo6Zv94aho",
|
||||
ExportName = "sceSystemServiceDisableNoticeScreenSkipFlagAutoSet",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceDisableNoticeScreenSkipFlagAutoSet(CpuContext ctx) =>
|
||||
ctx.SetReturn(0);
|
||||
|
||||
// Settings entry calls this immediately before spawning SaveModTime/Load
|
||||
// threads. An unresolved stub returns NOT_FOUND and the title can stall in
|
||||
// that path; accept the write and report success.
|
||||
[SysAbiExport(
|
||||
Nid = "Q3utJvma4Mo",
|
||||
ExportName = "sceSystemServiceSetNoticeScreenSkipFlag",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceSetNoticeScreenSkipFlag(CpuContext ctx) =>
|
||||
ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4veE0XiIugA",
|
||||
ExportName = "sceSystemServiceGetMainAppTitleId",
|
||||
|
||||
@@ -16,37 +16,4 @@ public static class VoiceQoSExports
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Trpt2QBZHCI",
|
||||
ExportName = "sceVoiceQoSGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSGetStatus(CpuContext ctx)
|
||||
{
|
||||
// Returns 0 to indicate connected state (voice available)
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FuXenJLkk-c",
|
||||
ExportName = "sceVoiceQoSTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSTerminate(CpuContext ctx)
|
||||
{
|
||||
// No-op: cleanup is handled by emulator shutdown
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+0lOiPZjnBI",
|
||||
ExportName = "sceVoiceQoSSetMode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVoiceQoS")]
|
||||
public static int VoiceQoSSetMode(CpuContext ctx)
|
||||
{
|
||||
// No-op: mode configuration is not emulated
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user