Compare commits

..

10 Commits

Author SHA1 Message Date
ParantezTech c6430bc51d [readme] update DeS screenshot 2026-07-23 03:05:41 +03:00
ParantezTech c66d7a1e05 [Bink2] rework bridge to use FFmpeg's native Bink2 decoder instead of a C bridge 2026-07-23 03:01:10 +03:00
Mariano Zambelli 559b7f0a84 feat(voice): add QoS stubs (GetStatus, Terminate, SetMode) (#541)
* feat(voice): add QoS stubs (GetStatus, Terminate, SetMode)

Titles call these functions during voice/multiplayer setup to check
network availability and configure modes. Unresolved imports caused
WARN floods in the loader logs. Reporting initialized + disconnected
lets callers take their normal offline path.

* fix(voice): return success (0) from sceVoiceQoSGetStatus instead of disconnected state
2026-07-23 01:48:55 +03:00
MarcelMediaDev 2272b9b576 fix(ajm): silence BatchJobDecode/Start/Wait/Cancel hot-path stubs (#547)
Unresolved batch NIDs flooded Import WARNs on Bink/AJM. Claim input
consumed with silence produced; this is not a real codec.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:44:07 +03:00
MarcelMediaDev 8dd3172c0f fix(systemservice): stub notice-screen skip flag setters (#549)
Settings probes Set/DisableNoticeScreenSkipFlagAutoSet; unresolved
NOT_FOUND can stall the SaveModTime/Load path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:41:08 +03:00
Berk e7ea186ea8 Enhance contribution guidelines with PR expectations
Added expectations for pull requests regarding observable behavior and testing requirements. Clarified guidelines for AI-assisted contributions.
2026-07-23 01:40:00 +03:00
MarcelMediaDev 74a519875b fix(agc): add missing Cb/Dcb GetSize stubs for packet sizing probes (#535)
Unresolved GetSize NIDs returned NOT_FOUND during RenderThread startup,
leaving null packet pointers and an immediate write AV. Return fixed
packet byte sizes in rax only — no guest memory writes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 00:18:47 +03:00
Andrey Modnov 4682e64e81 [CLI] Simplify mitigated child arguments (#529) 2026-07-23 00:18:11 +03:00
Kurt Himebauch 912883de05 fix(cmake): invalidate stale FFmpeg library cache (#543) 2026-07-22 23:56:37 +03:00
Berk f704586a8d [VideoOut] Add Bink2 support via FFMPEG bridge (#527)
* [VideoOut] Add Bink2 support via FFMPEG bridge

* [CMake] update commit

* [CMake] update commit
2026-07-22 21:41:41 +03:00
17 changed files with 899 additions and 759 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 229 KiB

+19
View File
@@ -26,6 +26,25 @@ 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.
+7
View File
@@ -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>
+1
View File
@@ -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" />
+4 -11
View File
@@ -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
View File
@@ -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.
-128
View File
@@ -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()
-300
View File
@@ -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);
}
+23 -6
View File
@@ -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");
@@ -551,12 +573,7 @@ internal static partial class Program
return false;
}
var childArgs = new string[args.Length + 1];
childArgs[0] = MitigatedChildFlag;
for (var i = 0; i < args.Length; i++)
{
childArgs[i + 1] = args[i];
}
string[] childArgs = [MitigatedChildFlag, .. args];
var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX();
+57 -33
View File
@@ -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 &quot;$(RepoRoot)native/bink2-bridge&quot; -B &quot;$(Bink2BridgeBuildDir)&quot; -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
<Exec Condition="!$([MSBuild]::IsOSPlatform('Windows'))"
Command="cmake -S &quot;$(RepoRoot)native/bink2-bridge&quot; -B &quot;$(Bink2BridgeBuildDir)&quot; -DCMAKE_BUILD_TYPE=Release -DSHARPEMU_TARGET_RID=$(RuntimeIdentifier)" />
<Exec Command="cmake --build &quot;$(Bink2BridgeBuildDir)&quot; --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>
+80
View File
@@ -965,6 +965,20 @@ 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",
@@ -1162,6 +1176,18 @@ 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",
@@ -1737,6 +1763,18 @@ 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",
@@ -2390,6 +2428,20 @@ 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",
@@ -11469,6 +11521,34 @@ 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",
+241
View File
@@ -21,6 +21,7 @@ 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
{
@@ -227,10 +228,250 @@ 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)
+12 -245
View File
@@ -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;
}
}
}
+1
View File
@@ -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" />
@@ -45,6 +45,25 @@ 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,4 +16,37 @@ 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);
}
}