mirror of
https://github.com/azahar-emu/azahar.git
synced 2026-06-05 18:23:39 -04:00
Implement libretro core (#1215)
* libretro core * Bringing citra libretro implementation over * libretro: hook up vulkan renderer * libretro: github actions * libretro: gyro * libretro: core options v2 * libretro: on ios turn off shader jit if unavailable * moltenvk 1.3.0 introduces 8-bit indexes but allocates 16-bit for metal; this ends up allocating stream buffer * 2 = 132MiB. Instead, just use 16-bit indexes. (This will be necessary for standalone when bumping moltenvk version.) * libretro core: address review feedback * libretro: microphone support * cmake: Add ENABLE_ROOM_STANDALONE to list of incompatible libretro flags * libretro: proper initial geometry * libretro: fix software renderer * libretro: address review feedback * .github/libretro.yml: Pin macOS runners at macOS 26 * ci: Remove explicit selection of Xcode 16.0 * .github/libretro.yml: remove unnecessary windows builder apt commands * .github/libretro.yml: bump min macos version to 11.0 * ci: Re-enable CI jobs for all libretro cores This is under the condition that we don't introduce build cache for these builds --------- Co-authored-by: OpenSauce04 <opensauce04@gmail.com> Co-authored-by: PabloMK7 <hackyglitch2@gmail.com>
This commit is contained in:
parent
d0eaf07a40
commit
d9b77cc21e
61 changed files with 5907 additions and 93 deletions
166
.github/workflows/libretro.yml
vendored
Normal file
166
.github/workflows/libretro.yml
vendored
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
name: citra-libretro
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
tags: [ "*" ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CORE_ARGS: -DENABLE_LIBRETRO=ON
|
||||
|
||||
jobs:
|
||||
android:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
OS: android
|
||||
TARGET: arm64-v8a
|
||||
API_LEVEL: 21
|
||||
ANDROID_NDK_VERSION: 26.2.11394342
|
||||
ANDROID_ABI: arm64-v8a
|
||||
BUILD_DIR: build/android-arm64-v8a
|
||||
EXTRA_PATH: bin/Release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set tag name
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
|
||||
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||
fi
|
||||
echo $GIT_TAG_NAME
|
||||
- name: Update Android SDK CMake version
|
||||
run: |
|
||||
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "ndk;$ANDROID_NDK_VERSION"
|
||||
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
|
||||
- name: Build
|
||||
run: |
|
||||
export NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/$ANDROID_NDK_VERSION
|
||||
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI . -B $BUILD_DIR
|
||||
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro_android.so
|
||||
linux:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
OS: linux
|
||||
TARGET: x86_64
|
||||
BUILD_DIR: build/linux-x86_64
|
||||
EXTRA_PATH: bin/Release
|
||||
EXTRA_CORE_ARGS: -DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12 -DENABLE_LTO=OFF
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
run: |
|
||||
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
|
||||
cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.so
|
||||
windows:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OS: windows
|
||||
TARGET: x86_64
|
||||
BUILD_DIR: build/windows-x86_64
|
||||
EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja
|
||||
CMAKE: x86_64-w64-mingw32.static-cmake
|
||||
IMAGE: git.libretro.com:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
|
||||
EXTRA_PATH: bin/Release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build in cross-container
|
||||
run: |
|
||||
docker pull $IMAGE
|
||||
docker run --rm --user root \
|
||||
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
|
||||
-w "${GITHUB_WORKSPACE}" \
|
||||
$IMAGE \
|
||||
bash -lc "\
|
||||
${CMAKE} $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR && \
|
||||
${CMAKE} --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)"
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dll
|
||||
macos:
|
||||
runs-on: macos-26
|
||||
strategy:
|
||||
matrix:
|
||||
target: ["x86_64", "arm64"]
|
||||
env:
|
||||
OS: macos
|
||||
TARGET: ${{ matrix.target }}
|
||||
MACOSX_DEPLOYMENT_TARGET: 11.0
|
||||
BUILD_DIR: build/osx-${{ matrix.target }}
|
||||
EXTRA_PATH: bin/Release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install tools
|
||||
run: brew install spirv-tools
|
||||
- name: Build
|
||||
run: |
|
||||
cmake $CORE_ARGS -DCMAKE_OSX_ARCHITECTURES=$TARGET . -B $BUILD_DIR
|
||||
cmake --build $BUILD_DIR --target azahar_libretro --config Release
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
|
||||
ios:
|
||||
runs-on: macos-26
|
||||
env:
|
||||
OS: ios
|
||||
TARGET: arm64
|
||||
BUILD_DIR: build/ios-arm64
|
||||
EXTRA_PATH: bin/Release
|
||||
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
run: |
|
||||
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
|
||||
cmake --build $BUILD_DIR --target azahar_libretro --config Release
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
|
||||
tvos:
|
||||
runs-on: macos-26
|
||||
env:
|
||||
OS: tvos
|
||||
TARGET: arm64
|
||||
BUILD_DIR: build/tvos-arm64
|
||||
EXTRA_PATH: bin/Release
|
||||
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
run: |
|
||||
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
|
||||
cmake --build $BUILD_DIR --target azahar_libretro --config Release
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
|
||||
127
.gitlab-ci.yml
Normal file
127
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
.core-defs:
|
||||
variables:
|
||||
JNI_PATH: .
|
||||
CORENAME: azahar
|
||||
API_LEVEL: 21
|
||||
BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF
|
||||
CORE_ARGS: ${BASE_CORE_ARGS}
|
||||
EXTRA_PATH: bin/Release
|
||||
|
||||
variables:
|
||||
STATIC_RETROARCH_BRANCH: master
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Inclusion templates, required for the build to work
|
||||
include:
|
||||
################################## DESKTOPS ############################## ##
|
||||
# Windows 64-bit
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/windows-cmake-mingw.yml'
|
||||
|
||||
# Linux 64-bit
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/linux-cmake.yml'
|
||||
|
||||
# MacOS x86_64
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/osx-cmake-x86.yml'
|
||||
|
||||
# MacOS ARM64
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/osx-cmake-arm64.yml'
|
||||
|
||||
################################## CELLULAR ############################## ##
|
||||
# Android
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/android-cmake.yml'
|
||||
|
||||
# iOS
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/ios-cmake.yml'
|
||||
|
||||
# tvOS
|
||||
- project: 'libretro-infrastructure/ci-templates'
|
||||
file: '/tvos-cmake.yml'
|
||||
|
||||
################################## CONSOLES ############################## ##
|
||||
|
||||
# Stages for building
|
||||
stages:
|
||||
- build-prepare
|
||||
- build-shared
|
||||
- build-static
|
||||
|
||||
##############################################################################
|
||||
#################################### STAGES ##################################
|
||||
##############################################################################
|
||||
#
|
||||
################################### DESKTOPS #################################
|
||||
# Windows 64-bit
|
||||
libretro-build-windows-x64:
|
||||
extends:
|
||||
- .core-defs
|
||||
- .libretro-windows-cmake-x86_64
|
||||
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
|
||||
variables:
|
||||
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja
|
||||
|
||||
# Linux 64-bit
|
||||
libretro-build-linux-x64:
|
||||
extends:
|
||||
- .core-defs
|
||||
- .libretro-linux-cmake-x86_64
|
||||
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports
|
||||
variables:
|
||||
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF
|
||||
CC: /usr/bin/gcc-12
|
||||
CXX: /usr/bin/g++-12
|
||||
|
||||
# MacOS x86_64
|
||||
libretro-build-osx-x64:
|
||||
tags:
|
||||
- mac-apple-silicon
|
||||
variables:
|
||||
CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
extends:
|
||||
- .core-defs
|
||||
- .libretro-osx-cmake-x86_64
|
||||
|
||||
# MacOS ARM64
|
||||
libretro-build-osx-arm64:
|
||||
extends:
|
||||
- .core-defs
|
||||
- .libretro-osx-cmake-arm64
|
||||
|
||||
################################### CELLULAR #################################
|
||||
# Android ARMv8a
|
||||
android-arm64-v8a:
|
||||
extends:
|
||||
- .libretro-android-cmake-arm64-v8a
|
||||
- .core-defs
|
||||
variables:
|
||||
ANDROID_NDK_VERSION: 26.2.11394342
|
||||
NDK_ROOT: /android-sdk-linux/ndk/$ANDROID_NDK_VERSION
|
||||
|
||||
# iOS arm64
|
||||
libretro-build-ios-arm64:
|
||||
extends:
|
||||
- .libretro-ios-cmake-arm64
|
||||
- .core-defs
|
||||
variables:
|
||||
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
|
||||
IOS_MINVER: "14.0"
|
||||
EXTRA_PATH: bin/RelWithDebInfo
|
||||
|
||||
# tvOS arm64
|
||||
libretro-build-tvos-arm64:
|
||||
extends:
|
||||
- .libretro-tvos-cmake-arm64
|
||||
- .core-defs
|
||||
variables:
|
||||
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
|
||||
MINVER: "14.0"
|
||||
EXTRA_PATH: bin/RelWithDebInfo
|
||||
|
||||
################################### CONSOLES #################################
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -103,3 +103,6 @@
|
|||
[submodule "externals/xxHash"]
|
||||
path = externals/xxHash
|
||||
url = https://github.com/Cyan4973/xxHash.git
|
||||
[submodule "externals/libretro-common"]
|
||||
path = externals/libretro-common/libretro-common
|
||||
url = https://github.com/libretro/libretro-common.git
|
||||
|
|
|
|||
|
|
@ -17,20 +17,23 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules")
|
|||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules")
|
||||
include(DownloadExternals)
|
||||
include(CMakeDependentOption)
|
||||
include(FindPkgConfig)
|
||||
|
||||
project(citra LANGUAGES C CXX ASM)
|
||||
# must be invoked after project() command when using CMAKE_TOOLCHAIN_FILE
|
||||
include(FindPkgConfig)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
|
||||
enable_language(OBJC OBJCXX)
|
||||
endif()
|
||||
|
||||
option(ENABLE_LIBRETRO "Build as a LibRetro core" OFF)
|
||||
|
||||
# Some submodules like to pick their own default build type if not specified.
|
||||
# Make sure we default to Release build type always, unless the generator has custom types.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
if (APPLE AND NOT ENABLE_LIBRETRO)
|
||||
# Silence warnings on empty objects, for example when platform-specific code is #ifdef'd out.
|
||||
set(CMAKE_C_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
|
||||
set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
|
||||
|
|
@ -90,6 +93,17 @@ else()
|
|||
set(DEFAULT_ENABLE_OPENGL ON)
|
||||
endif()
|
||||
|
||||
# Track which options were explicitly set by the user (for libretro conflict detection)
|
||||
set(_LIBRETRO_INCOMPATIBLE_OPTIONS
|
||||
ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING
|
||||
ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB)
|
||||
set(_USER_SET_OPTIONS "")
|
||||
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
|
||||
if(DEFINED ${_opt})
|
||||
list(APPEND _USER_SET_OPTIONS ${_opt})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
option(ENABLE_SDL2 "Enable using SDL2" ON)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF)
|
||||
option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF)
|
||||
|
|
@ -130,6 +144,31 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via
|
|||
option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
|
||||
option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON)
|
||||
|
||||
# Handle incompatible options for libretro builds
|
||||
if(ENABLE_LIBRETRO)
|
||||
# Check for explicitly-set conflicting options
|
||||
set(_CONFLICTS "")
|
||||
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
|
||||
list(FIND _USER_SET_OPTIONS ${_opt} _idx)
|
||||
if(NOT _idx EQUAL -1 AND ${_opt})
|
||||
list(APPEND _CONFLICTS ${_opt})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(_CONFLICTS)
|
||||
string(REPLACE ";" ", " _CONFLICTS_STR "${_CONFLICTS}")
|
||||
message(FATAL_ERROR
|
||||
"ENABLE_LIBRETRO is incompatible with: ${_CONFLICTS_STR}\n"
|
||||
"These options were explicitly enabled but are not supported for libretro builds.\n"
|
||||
"Remove these options or set them to OFF.")
|
||||
endif()
|
||||
|
||||
# Force disable incompatible options (handles defaulted-on options)
|
||||
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
|
||||
set(${_opt} OFF CACHE BOOL "Disabled for libretro" FORCE)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Pass the following values to C++ land
|
||||
if (ENABLE_QT)
|
||||
add_definitions(-DENABLE_QT)
|
||||
|
|
@ -300,6 +339,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO)
|
|||
# set up output paths for executable binaries
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$<CONFIG>)
|
||||
|
||||
if (ENABLE_LIBRETRO)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
|
||||
# System imported libraries
|
||||
# ======================
|
||||
|
|
@ -359,7 +401,7 @@ if (APPLE)
|
|||
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
|
||||
|
||||
if (ENABLE_VULKAN)
|
||||
if (ENABLE_VULKAN AND NOT ENABLE_LIBRETRO)
|
||||
if (NOT USE_SYSTEM_MOLTENVK)
|
||||
download_moltenvk()
|
||||
endif()
|
||||
|
|
|
|||
9
externals/CMakeLists.txt
vendored
9
externals/CMakeLists.txt
vendored
|
|
@ -292,6 +292,15 @@ if (USE_DISCORD_PRESENCE)
|
|||
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
||||
endif()
|
||||
|
||||
# LibRetro
|
||||
if (ENABLE_LIBRETRO)
|
||||
add_library(libretro INTERFACE)
|
||||
target_include_directories(libretro INTERFACE ./libretro-common/libretro-common/include)
|
||||
if (ANDROID)
|
||||
add_subdirectory(libretro-common EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# JSON
|
||||
add_library(json-headers INTERFACE)
|
||||
if (USE_SYSTEM_JSON)
|
||||
|
|
|
|||
16
externals/libretro-common/CMakeLists.txt
vendored
Normal file
16
externals/libretro-common/CMakeLists.txt
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
add_library(libretro_common STATIC
|
||||
libretro-common/compat/compat_posix_string.c
|
||||
libretro-common/compat/fopen_utf8.c
|
||||
libretro-common/encodings/encoding_utf.c
|
||||
libretro-common/compat/compat_strl.c
|
||||
libretro-common/file/file_path.c
|
||||
libretro-common/streams/file_stream.c
|
||||
libretro-common/streams/file_stream_transforms.c
|
||||
libretro-common/string/stdstring.c
|
||||
libretro-common/time/rtime.c
|
||||
libretro-common/vfs/vfs_implementation.c
|
||||
)
|
||||
target_include_directories(libretro_common PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common/include
|
||||
)
|
||||
1
externals/libretro-common/libretro-common
vendored
Submodule
1
externals/libretro-common/libretro-common
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 7fc7feeddca391be65c94e6541381467684b814d
|
||||
|
|
@ -110,10 +110,14 @@ else()
|
|||
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||
-Wno-unused-command-line-argument
|
||||
# Build fortification options
|
||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||
-fstack-protector-strong
|
||||
-fstack-clash-protection
|
||||
)
|
||||
if (NOT ENABLE_LIBRETRO)
|
||||
add_compile_options(
|
||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||
-fstack-clash-protection
|
||||
)
|
||||
endif()
|
||||
|
||||
# If we define _FORTIFY_SOURCE when it is already defined, compilation will fail
|
||||
string(FIND "-D_FORTIFY_SOURCE" "${CMAKE_CXX_FLAGS} " FORTIFY_SOURCE_DEFINED)
|
||||
|
|
@ -201,6 +205,10 @@ if (ENABLE_QT OR ENABLE_SDL2_FRONTEND)
|
|||
add_subdirectory(citra_meta)
|
||||
endif()
|
||||
|
||||
if (ENABLE_LIBRETRO)
|
||||
add_subdirectory(citra_libretro)
|
||||
endif()
|
||||
|
||||
if (ENABLE_ROOM)
|
||||
add_subdirectory(citra_room)
|
||||
endif()
|
||||
|
|
@ -209,7 +217,7 @@ if (ENABLE_ROOM_STANDALONE)
|
|||
add_subdirectory(citra_room_standalone)
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
if (ANDROID AND NOT ENABLE_LIBRETRO)
|
||||
add_subdirectory(android/app/src/main/jni)
|
||||
target_include_directories(citra-android PRIVATE android/app/src/main)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ add_library(audio_core STATIC
|
|||
|
||||
$<$<BOOL:${ENABLE_SDL2}>:sdl2_sink.cpp sdl2_sink.h>
|
||||
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h>
|
||||
$<$<BOOL:${ENABLE_LIBRETRO}>:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h>
|
||||
$<$<BOOL:${ENABLE_OPENAL}>:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,11 @@ void DspInterface::OutputFrame(StereoFrame16 frame) {
|
|||
return;
|
||||
}
|
||||
|
||||
fifo.Push(frame.data(), frame.size());
|
||||
if (sink->ImmediateSubmission()) {
|
||||
sink->PushSamples(frame.data(), frame.size());
|
||||
} else {
|
||||
fifo.Push(frame.data(), frame.size());
|
||||
}
|
||||
|
||||
auto video_dumper = system.GetVideoDumper();
|
||||
if (video_dumper && video_dumper->IsDumping()) {
|
||||
|
|
@ -54,7 +58,11 @@ void DspInterface::OutputSample(std::array<s16, 2> sample) {
|
|||
return;
|
||||
}
|
||||
|
||||
fifo.Push(&sample, 1);
|
||||
if (sink->ImmediateSubmission()) {
|
||||
sink->PushSamples(&sample, 1);
|
||||
} else {
|
||||
fifo.Push(&sample, 1);
|
||||
}
|
||||
|
||||
auto video_dumper = system.GetVideoDumper();
|
||||
if (video_dumper && video_dumper->IsDumping()) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -15,6 +15,9 @@
|
|||
#ifdef HAVE_OPENAL
|
||||
#include "audio_core/openal_input.h"
|
||||
#endif
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "audio_core/libretro_input.h"
|
||||
#endif
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
|
||||
|
|
@ -22,6 +25,18 @@ namespace AudioCore {
|
|||
namespace {
|
||||
// input_details is ordered in terms of desirability, with the best choice at the top.
|
||||
constexpr std::array input_details = {
|
||||
#ifdef HAVE_LIBRETRO
|
||||
InputDetails{InputType::LibRetro, "Real Device (LibRetro)", true,
|
||||
[](Core::System& system, std::string_view device_id) -> std::unique_ptr<Input> {
|
||||
if (!system.HasMicPermission()) {
|
||||
LOG_WARNING(Audio,
|
||||
"Microphone permission denied, falling back to null input.");
|
||||
return std::make_unique<NullInput>();
|
||||
}
|
||||
return std::make_unique<LibRetroInput>();
|
||||
},
|
||||
[] { return std::vector<std::string>{"LibRetro Microphone"}; }},
|
||||
#endif
|
||||
#ifdef HAVE_CUBEB
|
||||
InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true,
|
||||
[](Core::System& system, std::string_view device_id) -> std::unique_ptr<Input> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ enum class InputType : u32 {
|
|||
Static = 2,
|
||||
Cubeb = 3,
|
||||
OpenAL = 4,
|
||||
LibRetro = 5,
|
||||
};
|
||||
|
||||
struct InputDetails {
|
||||
|
|
|
|||
327
src/audio_core/libretro_input.cpp
Normal file
327
src/audio_core/libretro_input.cpp
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "audio_core/libretro_input.h"
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/ring_buffer.h"
|
||||
#include "libretro.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
namespace {
|
||||
// Global instance pointer for access from retro_run
|
||||
LibRetroInput* g_libretro_input = nullptr;
|
||||
} // namespace
|
||||
|
||||
struct LibRetroInput::Impl {
|
||||
std::optional<retro_microphone_interface> mic_interface;
|
||||
retro_microphone_t* mic_handle = nullptr;
|
||||
bool is_sampling = false;
|
||||
u8 sample_size_in_bytes = 2;
|
||||
int warmup_frames = 0;
|
||||
|
||||
// The rate at which the frontend actually provides samples (may differ from
|
||||
// what the 3DS mic service requested). We open the mic at this rate to avoid
|
||||
// RetroArch's internal resampler path, which has a convergence bug when
|
||||
// downsampling (ratio < 1). We resample ourselves in Read() instead.
|
||||
u32 native_sample_rate = 0;
|
||||
|
||||
// Ring buffer for thread-safe sample storage
|
||||
// Capacity: 4096 samples should be plenty for buffering between frames
|
||||
// The 3DS mic service reads 16 samples at a time at ~32728 Hz
|
||||
Common::RingBuffer<s16, 4096> sample_buffer;
|
||||
|
||||
// Temporary buffer for reading from frontend
|
||||
std::vector<s16> read_buffer;
|
||||
|
||||
Impl() {
|
||||
// Try to get the microphone interface from the frontend
|
||||
retro_microphone_interface interface{};
|
||||
interface.interface_version = RETRO_MICROPHONE_INTERFACE_VERSION;
|
||||
|
||||
if (LibRetro::GetMicrophoneInterface(&interface)) {
|
||||
if (interface.interface_version == RETRO_MICROPHONE_INTERFACE_VERSION) {
|
||||
mic_interface = interface;
|
||||
LOG_INFO(Audio, "LibRetro microphone interface available (version {})",
|
||||
interface.interface_version);
|
||||
} else {
|
||||
LOG_WARNING(Audio,
|
||||
"LibRetro microphone interface version mismatch: expected {}, got {}",
|
||||
RETRO_MICROPHONE_INTERFACE_VERSION, interface.interface_version);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(Audio, "LibRetro microphone interface not available");
|
||||
}
|
||||
|
||||
// Keep this small enough that RetroArch's microphone_driver_read can
|
||||
// fill its outgoing FIFO in a single flush iteration. The CoreAudio
|
||||
// driver's internal FIFO is ~480 samples (10ms at 48kHz). If we
|
||||
// request more than that, the blocking while-loop in
|
||||
// microphone_driver_read must wait for the next hardware callback,
|
||||
// and on ARM64 without memory barriers in the FIFO, it may never
|
||||
// see the new data. 128 samples is conservative enough to succeed
|
||||
// in one pass.
|
||||
read_buffer.resize(128);
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
CloseMicrophone();
|
||||
}
|
||||
|
||||
bool EnsureMicrophoneOpen() {
|
||||
if (mic_handle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mic_interface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always open at 48000 Hz regardless of what the game requests.
|
||||
// RetroArch's microphone_driver_read has a resampler whose while-loop
|
||||
// deadlocks when the ratio is < 1 (core rate < device rate). The
|
||||
// libretro get_params API only returns the effective (requested) rate,
|
||||
// not the device's native rate, so we can't detect the mismatch.
|
||||
// Opening at 48000 Hz (the most common hardware rate) keeps the
|
||||
// frontend's internal resampling ratio at or near 1.0, avoiding the
|
||||
// bug. We resample to the game's requested rate ourselves in Read().
|
||||
static constexpr u32 kMicOpenRate = 48000;
|
||||
native_sample_rate = kMicOpenRate;
|
||||
|
||||
retro_microphone_params_t params{};
|
||||
params.rate = kMicOpenRate;
|
||||
|
||||
mic_handle = mic_interface->open_mic(¶ms);
|
||||
if (!mic_handle) {
|
||||
LOG_ERROR(Audio, "Failed to open LibRetro microphone");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frontend may start recording immediately in open_mic (e.g.
|
||||
// CoreAudio calls AudioOutputUnitStart). Pause it right away so the
|
||||
// mic is available but idle until StartSampling enables it.
|
||||
mic_interface->set_mic_state(mic_handle, false);
|
||||
|
||||
LOG_INFO(Audio, "LibRetro microphone opened at {} Hz (idle)", native_sample_rate);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CloseMicrophone() {
|
||||
if (mic_interface && mic_handle) {
|
||||
mic_interface->close_mic(mic_handle);
|
||||
mic_handle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SetMicrophoneActive(bool active) {
|
||||
if (!mic_interface || !mic_handle) {
|
||||
return false;
|
||||
}
|
||||
return mic_interface->set_mic_state(mic_handle, active);
|
||||
}
|
||||
|
||||
bool IsMicrophoneActive() const {
|
||||
if (!mic_interface || !mic_handle) {
|
||||
return false;
|
||||
}
|
||||
return mic_interface->get_mic_state(mic_handle);
|
||||
}
|
||||
};
|
||||
|
||||
LibRetroInput::LibRetroInput() : impl(std::make_unique<Impl>()) {
|
||||
g_libretro_input = this;
|
||||
}
|
||||
|
||||
LibRetroInput::~LibRetroInput() {
|
||||
StopSampling();
|
||||
if (g_libretro_input == this) {
|
||||
g_libretro_input = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void LibRetroInput::StartSampling(const InputParameters& params) {
|
||||
if (IsSampling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// LibRetro only provides signed 16-bit PCM samples
|
||||
// We'll convert to the requested format in Read()
|
||||
if (params.sign == Signedness::Unsigned) {
|
||||
LOG_DEBUG(Audio, "Application requested unsigned PCM format; will convert from signed.");
|
||||
}
|
||||
|
||||
parameters = params;
|
||||
impl->sample_size_in_bytes = params.sample_size / 8;
|
||||
|
||||
if (!impl->EnsureMicrophoneOpen()) {
|
||||
LOG_WARNING(Audio, "Cannot start sampling: microphone not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the microphone (transitions from idle to recording)
|
||||
if (!impl->SetMicrophoneActive(true)) {
|
||||
LOG_ERROR(Audio, "Failed to activate microphone");
|
||||
return;
|
||||
}
|
||||
|
||||
impl->is_sampling = true;
|
||||
// Give the audio hardware a few frames to start delivering data before
|
||||
// we attempt a (blocking) read_mic call. Without this, the very first
|
||||
// read can hang because the CoreAudio callback hasn't fired yet.
|
||||
impl->warmup_frames = 10;
|
||||
LOG_INFO(Audio, "LibRetro microphone sampling started at {} Hz, {} bit", params.sample_rate,
|
||||
params.sample_size);
|
||||
}
|
||||
|
||||
void LibRetroInput::StopSampling() {
|
||||
if (!impl->is_sampling) {
|
||||
return;
|
||||
}
|
||||
|
||||
impl->SetMicrophoneActive(false);
|
||||
impl->is_sampling = false;
|
||||
|
||||
LOG_INFO(Audio, "LibRetro microphone sampling stopped (mic remains idle)");
|
||||
}
|
||||
|
||||
bool LibRetroInput::IsSampling() {
|
||||
return impl->is_sampling;
|
||||
}
|
||||
|
||||
void LibRetroInput::AdjustSampleRate(u32 sample_rate) {
|
||||
if (!IsSampling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restart with new sample rate
|
||||
auto new_parameters = parameters;
|
||||
new_parameters.sample_rate = sample_rate;
|
||||
StopSampling();
|
||||
StartSampling(new_parameters);
|
||||
}
|
||||
|
||||
void LibRetroInput::PollMicrophone() {
|
||||
// This is called from the main thread (retro_run)
|
||||
// Read samples from the frontend and push to the ring buffer
|
||||
|
||||
if (!impl->is_sampling || !impl->mic_interface || !impl->mic_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the audio hardware to start delivering data before making
|
||||
// any blocking read_mic calls.
|
||||
if (impl->warmup_frames > 0) {
|
||||
impl->warmup_frames--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Issue a memory fence before reading. RetroArch's CoreAudio mic driver
|
||||
// fills its FIFO from a callback thread without memory barriers. On ARM64
|
||||
// (weak memory model), the main thread may not see the callback's writes
|
||||
// without an explicit barrier.
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
|
||||
int samples_read = impl->mic_interface->read_mic(impl->mic_handle, impl->read_buffer.data(),
|
||||
static_cast<size_t>(impl->read_buffer.size()));
|
||||
|
||||
if (samples_read > 0) {
|
||||
impl->sample_buffer.Push(
|
||||
std::span<const s16>(impl->read_buffer.data(), static_cast<size_t>(samples_read)));
|
||||
}
|
||||
}
|
||||
|
||||
Samples LibRetroInput::Read() {
|
||||
// This is called from the CoreTiming scheduler thread
|
||||
// Pop samples from the ring buffer (thread-safe)
|
||||
|
||||
if (!impl->is_sampling) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Pop available samples from the buffer (at native device rate)
|
||||
std::vector<s16> raw_samples = impl->sample_buffer.Pop();
|
||||
|
||||
if (raw_samples.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resample from native device rate to the rate the 3DS mic service expects
|
||||
if (impl->native_sample_rate != 0 && impl->native_sample_rate != parameters.sample_rate) {
|
||||
double ratio = static_cast<double>(parameters.sample_rate) / impl->native_sample_rate;
|
||||
auto output_count = static_cast<std::size_t>(raw_samples.size() * ratio);
|
||||
if (output_count == 0) {
|
||||
return {};
|
||||
}
|
||||
std::vector<s16> resampled(output_count);
|
||||
for (std::size_t i = 0; i < output_count; i++) {
|
||||
double src_pos = i / ratio;
|
||||
auto idx = static_cast<std::size_t>(src_pos);
|
||||
double frac = src_pos - idx;
|
||||
if (idx + 1 < raw_samples.size()) {
|
||||
resampled[i] =
|
||||
static_cast<s16>(raw_samples[idx] * (1.0 - frac) + raw_samples[idx + 1] * frac);
|
||||
} else {
|
||||
resampled[i] = raw_samples[std::min(idx, raw_samples.size() - 1)];
|
||||
}
|
||||
}
|
||||
raw_samples = std::move(resampled);
|
||||
}
|
||||
|
||||
// Convert sample format if needed
|
||||
constexpr auto convert_s16_to_u16 = [](s16 sample) -> u16 {
|
||||
return static_cast<u16>(sample) ^ 0x8000;
|
||||
};
|
||||
|
||||
constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 {
|
||||
return static_cast<s8>(sample >> 8);
|
||||
};
|
||||
|
||||
constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 {
|
||||
return static_cast<u8>((static_cast<u16>(sample) ^ 0x8000) >> 8);
|
||||
};
|
||||
|
||||
Samples output;
|
||||
output.reserve(raw_samples.size() * impl->sample_size_in_bytes);
|
||||
|
||||
if (impl->sample_size_in_bytes == 1) {
|
||||
// 8-bit output
|
||||
if (parameters.sign == Signedness::Unsigned) {
|
||||
for (s16 sample : raw_samples) {
|
||||
output.push_back(convert_s16_to_u8(sample));
|
||||
}
|
||||
} else {
|
||||
for (s16 sample : raw_samples) {
|
||||
output.push_back(static_cast<u8>(convert_s16_to_s8(sample)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 16-bit output
|
||||
if (parameters.sign == Signedness::Unsigned) {
|
||||
for (s16 sample : raw_samples) {
|
||||
u16 converted = convert_s16_to_u16(sample);
|
||||
output.push_back(static_cast<u8>(converted & 0xFF));
|
||||
output.push_back(static_cast<u8>(converted >> 8));
|
||||
}
|
||||
} else {
|
||||
// Signed 16-bit - just copy the raw bytes
|
||||
const u8* data = reinterpret_cast<const u8*>(raw_samples.data());
|
||||
output.insert(output.end(), data, data + raw_samples.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
LibRetroInput* GetLibRetroInput() {
|
||||
return g_libretro_input;
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
36
src/audio_core/libretro_input.h
Normal file
36
src/audio_core/libretro_input.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "audio_core/input.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
class LibRetroInput final : public Input {
|
||||
public:
|
||||
LibRetroInput();
|
||||
~LibRetroInput() override;
|
||||
|
||||
void StartSampling(const InputParameters& params) override;
|
||||
void StopSampling() override;
|
||||
bool IsSampling() override;
|
||||
void AdjustSampleRate(u32 sample_rate) override;
|
||||
Samples Read() override;
|
||||
|
||||
/// Called from main thread (retro_run) to read samples from the frontend
|
||||
/// and store them in the thread-safe buffer for Read() to consume.
|
||||
void PollMicrophone();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
/// Returns the global LibRetroInput instance, or nullptr if not initialized.
|
||||
/// This is used by citra_libretro.cpp to poll the microphone from the main thread.
|
||||
LibRetroInput* GetLibRetroInput();
|
||||
|
||||
} // namespace AudioCore
|
||||
27
src/audio_core/libretro_sink.cpp
Normal file
27
src/audio_core/libretro_sink.cpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "audio_core/libretro_sink.h"
|
||||
#include "citra_libretro/environment.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
LibRetroSink::LibRetroSink(std::string) {}
|
||||
|
||||
LibRetroSink::~LibRetroSink() = default;
|
||||
|
||||
unsigned int LibRetroSink::GetNativeSampleRate() const {
|
||||
return native_sample_rate;
|
||||
}
|
||||
|
||||
void LibRetroSink::PushSamples(const void* data, std::size_t num_samples) {
|
||||
// libretro calls stereo pairs "frames", Azahar calls them "samples"
|
||||
LibRetro::SubmitAudio(static_cast<const s16*>(data), num_samples);
|
||||
}
|
||||
|
||||
std::vector<std::string> ListLibretroSinkDevices() {
|
||||
return std::vector<std::string>{"LibRetro"};
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
33
src/audio_core/libretro_sink.h
Normal file
33
src/audio_core/libretro_sink.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "audio_core/sink.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
class LibRetroSink final : public Sink {
|
||||
public:
|
||||
explicit LibRetroSink(std::string target_device_name);
|
||||
~LibRetroSink() override;
|
||||
|
||||
unsigned int GetNativeSampleRate() const override;
|
||||
|
||||
// Not used for immediate submission sinks
|
||||
void SetCallback(std::function<void(s16*, std::size_t)> cb) override {};
|
||||
|
||||
bool ImmediateSubmission() override {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PushSamples(const void* data, std::size_t num_samples) override;
|
||||
};
|
||||
|
||||
std::vector<std::string> ListLibretroSinkDevices();
|
||||
|
||||
} // namespace AudioCore
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include "common/common_types.h"
|
||||
#include "audio_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
|
|
@ -30,6 +30,23 @@ public:
|
|||
* @param sample_count Number of samples.
|
||||
*/
|
||||
virtual void SetCallback(std::function<void(s16*, std::size_t)> cb) = 0;
|
||||
|
||||
/**
|
||||
* Override and set this to true if the sink wants audio data submitted
|
||||
* immediately rather than requesting audio on demand
|
||||
* @return true if audio data should be pushed to the sink
|
||||
*/
|
||||
virtual bool ImmediateSubmission() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push audio samples directly to the sink, bypassing the FIFO.
|
||||
* Only called when ImmediateSubmission() returns true.
|
||||
* @param data Pointer to stereo PCM16 samples (each sample is L+R pair)
|
||||
* @param num_samples Number of stereo samples
|
||||
*/
|
||||
virtual void PushSamples(const void* data, std::size_t num_samples) {}
|
||||
};
|
||||
|
||||
} // namespace AudioCore
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -11,6 +11,9 @@
|
|||
#ifdef HAVE_SDL2
|
||||
#include "audio_core/sdl2_sink.h"
|
||||
#endif
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "audio_core/libretro_sink.h"
|
||||
#endif
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "audio_core/cubeb_sink.h"
|
||||
#endif
|
||||
|
|
@ -23,6 +26,13 @@ namespace AudioCore {
|
|||
namespace {
|
||||
// sink_details is ordered in terms of desirability, with the best choice at the top.
|
||||
constexpr std::array sink_details = {
|
||||
#ifdef HAVE_LIBRETRO
|
||||
SinkDetails{SinkType::LibRetro, "libretro",
|
||||
[](std::string_view device_id) -> std::unique_ptr<Sink> {
|
||||
return std::make_unique<LibRetroSink>(std::string(device_id));
|
||||
},
|
||||
&ListLibretroSinkDevices},
|
||||
#endif
|
||||
#ifdef HAVE_CUBEB
|
||||
SinkDetails{SinkType::Cubeb, "Cubeb",
|
||||
[](std::string_view device_id) -> std::unique_ptr<Sink> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ enum class SinkType : u32 {
|
|||
Cubeb = 2,
|
||||
OpenAL = 3,
|
||||
SDL2 = 4,
|
||||
LibRetro = 5,
|
||||
};
|
||||
|
||||
struct SinkDetails {
|
||||
|
|
|
|||
97
src/citra_libretro/CMakeLists.txt
Normal file
97
src/citra_libretro/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$<CONFIG>)
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules)
|
||||
|
||||
# Object library for libretro code (can be linked into both shared lib and tests)
|
||||
add_library(azahar_libretro_common OBJECT
|
||||
emu_window/libretro_window.cpp
|
||||
emu_window/libretro_window.h
|
||||
input/input_factory.cpp
|
||||
input/input_factory.h
|
||||
input/mouse_tracker.cpp
|
||||
input/mouse_tracker.h
|
||||
$<$<BOOL:${ENABLE_VULKAN}>: libretro_vk.cpp libretro_vk.h>
|
||||
environment.cpp
|
||||
environment.h
|
||||
core_settings.cpp
|
||||
core_settings.h)
|
||||
|
||||
target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO)
|
||||
target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map)
|
||||
if(ENABLE_OPENGL)
|
||||
target_link_libraries(azahar_libretro_common PRIVATE glad)
|
||||
endif()
|
||||
if(ENABLE_VULKAN)
|
||||
target_link_libraries(azahar_libretro_common PRIVATE sirit vulkan-headers vma)
|
||||
endif()
|
||||
|
||||
add_library(azahar_libretro SHARED
|
||||
citra_libretro.cpp
|
||||
citra_libretro.h
|
||||
$<TARGET_OBJECTS:azahar_libretro_common>)
|
||||
|
||||
create_target_directory_groups(azahar_libretro)
|
||||
|
||||
target_link_libraries(citra_common PRIVATE libretro)
|
||||
target_link_libraries(citra_core PRIVATE libretro)
|
||||
target_link_libraries(video_core PRIVATE libretro)
|
||||
target_link_libraries(audio_core PRIVATE libretro)
|
||||
target_link_libraries(input_common PRIVATE libretro)
|
||||
target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO)
|
||||
target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO)
|
||||
target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO)
|
||||
target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO)
|
||||
target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO)
|
||||
|
||||
target_link_libraries(azahar_libretro PRIVATE citra_common citra_core)
|
||||
target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map)
|
||||
if(ENABLE_VULKAN)
|
||||
target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma)
|
||||
endif()
|
||||
if(ENABLE_OPENGL)
|
||||
target_link_libraries(azahar_libretro PRIVATE glad)
|
||||
endif()
|
||||
target_link_libraries(azahar_libretro PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
|
||||
if(DEFINED LIBRETRO_STATIC)
|
||||
target_link_libraries(azahar_libretro PRIVATE -static-libstdc++)
|
||||
endif()
|
||||
|
||||
set_target_properties(azahar_libretro PROPERTIES PREFIX "")
|
||||
target_compile_definitions(azahar_libretro PRIVATE HAVE_LIBRETRO)
|
||||
|
||||
if(ANDROID)
|
||||
target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO_VFS)
|
||||
target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO_VFS)
|
||||
target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO_VFS)
|
||||
target_compile_definitions(azahar_libretro_common PRIVATE USING_GLES HAVE_LIBRETRO_VFS)
|
||||
target_compile_definitions(azahar_libretro PRIVATE USING_GLES HAVE_LIBRETRO_VFS)
|
||||
target_link_libraries(citra_common PRIVATE libretro_common)
|
||||
target_link_libraries(citra_core PRIVATE libretro_common)
|
||||
target_link_libraries(video_core PRIVATE libretro_common)
|
||||
target_link_libraries(azahar_libretro_common PRIVATE libretro_common)
|
||||
target_link_libraries(azahar_libretro PRIVATE libretro_common)
|
||||
# Link Android log library for __android_log_print
|
||||
target_link_libraries(azahar_libretro PRIVATE log)
|
||||
set_target_properties(azahar_libretro PROPERTIES SUFFIX "_android.so")
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
target_link_libraries(azahar_libretro PRIVATE crypt32)
|
||||
endif()
|
||||
|
||||
if(IOS)
|
||||
target_compile_definitions(azahar_libretro_common PRIVATE IOS)
|
||||
target_compile_definitions(azahar_libretro PRIVATE IOS)
|
||||
target_link_libraries(azahar_libretro PRIVATE "-framework CoreFoundation" "-framework Foundation")
|
||||
endif()
|
||||
|
||||
if (SSE42_COMPILE_OPTION)
|
||||
target_compile_definitions(azahar_libretro PRIVATE CITRA_HAS_SSE42)
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
|
||||
CMAKE_SYSTEM_NAME STREQUAL "iOS" OR
|
||||
CMAKE_SYSTEM_NAME STREQUAL "tvOS")
|
||||
target_link_libraries(azahar_libretro PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/libretro.osx.def")
|
||||
else()
|
||||
target_link_libraries(azahar_libretro PRIVATE "-Wl,-Bsymbolic")
|
||||
endif()
|
||||
717
src/citra_libretro/citra_libretro.cpp
Normal file
717
src/citra_libretro/citra_libretro.cpp
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <list>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include "glad/glad.h"
|
||||
#include "video_core/renderer_opengl/gl_vars.h"
|
||||
#endif
|
||||
#include "libretro.h"
|
||||
|
||||
#include "audio_core/libretro_input.h"
|
||||
#include "audio_core/libretro_sink.h"
|
||||
#include "video_core/gpu.h"
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||
#endif
|
||||
#ifdef ENABLE_VULKAN
|
||||
#include "citra_libretro/libretro_vk.h"
|
||||
#endif
|
||||
#include "video_core/renderer_software/renderer_software.h"
|
||||
#include "video_core/video_core.h"
|
||||
|
||||
#include "citra_libretro/citra_libretro.h"
|
||||
#include "citra_libretro/core_settings.h"
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "citra_libretro/input/input_factory.h"
|
||||
|
||||
#include "common/arch.h"
|
||||
#if CITRA_ARCH(x86_64)
|
||||
#include "common/x64/cpu_detect.h"
|
||||
#endif
|
||||
#include "common/logging/backend.h"
|
||||
#include "common/logging/filter.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/core.h"
|
||||
#include "core/frontend/applets/default_applets.h"
|
||||
#include "core/frontend/image_interface.h"
|
||||
#include "core/hle/kernel/kernel.h"
|
||||
#include "core/hle/kernel/memory.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
#include <streams/file_stream_transforms.h>
|
||||
#endif
|
||||
|
||||
class CitraLibRetro {
|
||||
public:
|
||||
CitraLibRetro() : log_filter(Common::Log::Level::Debug) {}
|
||||
|
||||
Common::Log::Filter log_filter;
|
||||
std::unique_ptr<EmuWindow_LibRetro> emu_window;
|
||||
bool game_loaded = false;
|
||||
struct retro_hw_render_callback hw_render{};
|
||||
};
|
||||
|
||||
CitraLibRetro* emu_instance;
|
||||
|
||||
void retro_init() {
|
||||
emu_instance = new CitraLibRetro();
|
||||
Common::Log::LibRetroStart(LibRetro::GetLoggingBackend());
|
||||
Common::Log::SetGlobalFilter(emu_instance->log_filter);
|
||||
|
||||
LOG_DEBUG(Frontend, "Initializing core...");
|
||||
|
||||
// Set up LLE cores
|
||||
for (const auto& service_module : Service::service_module_map) {
|
||||
Settings::values.lle_modules.emplace(service_module.name, false);
|
||||
}
|
||||
|
||||
// Setup default, stub handlers for HLE applets
|
||||
Frontend::RegisterDefaultApplets(Core::System::GetInstance());
|
||||
|
||||
// Register generic image interface
|
||||
Core::System::GetInstance().RegisterImageInterface(
|
||||
std::make_shared<Frontend::ImageInterface>());
|
||||
|
||||
LibRetro::Input::Init();
|
||||
}
|
||||
|
||||
void retro_deinit() {
|
||||
LOG_DEBUG(Frontend, "Shutting down core...");
|
||||
if (Core::System::GetInstance().IsPoweredOn()) {
|
||||
Core::System::GetInstance().Shutdown();
|
||||
}
|
||||
|
||||
LibRetro::Input::Shutdown();
|
||||
|
||||
delete emu_instance;
|
||||
|
||||
Common::Log::Stop();
|
||||
}
|
||||
|
||||
unsigned retro_api_version() {
|
||||
return RETRO_API_VERSION;
|
||||
}
|
||||
|
||||
void LibRetro::OnConfigureEnvironment() {
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
struct retro_vfs_interface_info vfs_iface_info{1, nullptr};
|
||||
LibRetro::SetVFSCallback(&vfs_iface_info);
|
||||
#endif
|
||||
|
||||
LibRetro::RegisterCoreOptions();
|
||||
|
||||
static const struct retro_controller_description controllers[] = {
|
||||
{"Nintendo 3DS", RETRO_DEVICE_JOYPAD},
|
||||
};
|
||||
|
||||
static const struct retro_controller_info ports[] = {
|
||||
{controllers, 1},
|
||||
{nullptr, 0},
|
||||
};
|
||||
|
||||
LibRetro::SetControllerInfo(ports);
|
||||
}
|
||||
|
||||
uintptr_t LibRetro::GetFramebuffer() {
|
||||
return emu_instance->hw_render.get_current_framebuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Citra's settings with Libretro's.
|
||||
*/
|
||||
static void UpdateSettings() {
|
||||
LibRetro::ParseCoreOptions();
|
||||
|
||||
struct retro_input_descriptor desc[] = {
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2, "ZL"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2, "ZR"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3, "Home/Swap screens"},
|
||||
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3, "Touch Screen Touch"},
|
||||
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X,
|
||||
"Circle Pad X"},
|
||||
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y,
|
||||
"Circle Pad Y"},
|
||||
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X,
|
||||
"C-Stick / Pointer X"},
|
||||
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y,
|
||||
"C-Stick / Pointer Y"},
|
||||
{0, 0},
|
||||
};
|
||||
|
||||
LibRetro::SetInputDescriptors(desc);
|
||||
|
||||
Settings::values.current_input_profile.touch_device = "engine:emu_window";
|
||||
|
||||
// Hardcode buttons to bind to libretro - it is entirely redundant to have
|
||||
// two methods of rebinding controls.
|
||||
// Citra: A = RETRO_DEVICE_ID_JOYPAD_A (8)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::A] =
|
||||
"button:8,joystick:0,engine:libretro";
|
||||
// Citra: B = RETRO_DEVICE_ID_JOYPAD_B (0)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::B] =
|
||||
"button:0,joystick:0,engine:libretro";
|
||||
// Citra: X = RETRO_DEVICE_ID_JOYPAD_X (9)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::X] =
|
||||
"button:9,joystick:0,engine:libretro";
|
||||
// Citra: Y = RETRO_DEVICE_ID_JOYPAD_Y (1)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Y] =
|
||||
"button:1,joystick:0,engine:libretro";
|
||||
// Citra: UP = RETRO_DEVICE_ID_JOYPAD_UP (4)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Up] =
|
||||
"button:4,joystick:0,engine:libretro";
|
||||
// Citra: DOWN = RETRO_DEVICE_ID_JOYPAD_DOWN (5)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Down] =
|
||||
"button:5,joystick:0,engine:libretro";
|
||||
// Citra: LEFT = RETRO_DEVICE_ID_JOYPAD_LEFT (6)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Left] =
|
||||
"button:6,joystick:0,engine:libretro";
|
||||
// Citra: RIGHT = RETRO_DEVICE_ID_JOYPAD_RIGHT (7)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Right] =
|
||||
"button:7,joystick:0,engine:libretro";
|
||||
// Citra: L = RETRO_DEVICE_ID_JOYPAD_L (10)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::L] =
|
||||
"button:10,joystick:0,engine:libretro";
|
||||
// Citra: R = RETRO_DEVICE_ID_JOYPAD_R (11)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::R] =
|
||||
"button:11,joystick:0,engine:libretro";
|
||||
// Citra: START = RETRO_DEVICE_ID_JOYPAD_START (3)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Start] =
|
||||
"button:3,joystick:0,engine:libretro";
|
||||
// Citra: SELECT = RETRO_DEVICE_ID_JOYPAD_SELECT (2)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Select] =
|
||||
"button:2,joystick:0,engine:libretro";
|
||||
// Citra: ZL = RETRO_DEVICE_ID_JOYPAD_L2 (12)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZL] =
|
||||
"button:12,joystick:0,engine:libretro";
|
||||
// Citra: ZR = RETRO_DEVICE_ID_JOYPAD_R2 (13)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZR] =
|
||||
"button:13,joystick:0,engine:libretro";
|
||||
// Citra: HOME = RETRO_DEVICE_ID_JOYPAD_L3 (as per above bindings) (14)
|
||||
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Home] =
|
||||
"button:14,joystick:0,engine:libretro";
|
||||
|
||||
// Circle Pad
|
||||
Settings::values.current_input_profile.analogs[0] = "axis:0,joystick:0,engine:libretro";
|
||||
// C-Stick
|
||||
if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) {
|
||||
Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro";
|
||||
} else {
|
||||
Settings::values.current_input_profile.analogs[1] = "";
|
||||
}
|
||||
|
||||
if (!emu_instance->emu_window) {
|
||||
emu_instance->emu_window = std::make_unique<EmuWindow_LibRetro>();
|
||||
}
|
||||
|
||||
// Update the framebuffer sizing.
|
||||
emu_instance->emu_window->UpdateLayout();
|
||||
|
||||
Core::System::GetInstance().ApplySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* libretro callback; Called every game tick.
|
||||
*/
|
||||
void retro_run() {
|
||||
// Check to see if we actually have any config updates to process.
|
||||
if (LibRetro::HasUpdatedConfig()) {
|
||||
LibRetro::ParseCoreOptions();
|
||||
Core::System::GetInstance().ApplySettings();
|
||||
emu_instance->emu_window->UpdateLayout();
|
||||
}
|
||||
|
||||
// Poll microphone input from the frontend and buffer it for the emulator
|
||||
// This must be done from the main thread as LibRetro's mic interface is not thread-safe
|
||||
if (auto* mic_input = AudioCore::GetLibRetroInput()) {
|
||||
mic_input->PollMicrophone();
|
||||
}
|
||||
|
||||
// Check if the screen swap button is pressed
|
||||
static bool screen_swap_btn_state = false;
|
||||
static bool screen_swap_toggled = false;
|
||||
bool screen_swap_btn =
|
||||
!!LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3);
|
||||
if (screen_swap_btn != screen_swap_btn_state) {
|
||||
if (LibRetro::settings.toggle_swap_screen) {
|
||||
if (!screen_swap_btn_state)
|
||||
screen_swap_toggled = !screen_swap_toggled;
|
||||
|
||||
if (screen_swap_toggled)
|
||||
Settings::values.swap_screen =
|
||||
LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom";
|
||||
else
|
||||
Settings::values.swap_screen =
|
||||
LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom";
|
||||
} else {
|
||||
if (screen_swap_btn)
|
||||
Settings::values.swap_screen =
|
||||
LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom";
|
||||
else
|
||||
Settings::values.swap_screen =
|
||||
LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom";
|
||||
}
|
||||
|
||||
Core::System::GetInstance().ApplySettings();
|
||||
|
||||
// Update the framebuffer sizing.
|
||||
emu_instance->emu_window->UpdateLayout();
|
||||
|
||||
screen_swap_btn_state = screen_swap_btn;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
|
||||
// We can't assume that the frontend has been nice and preserved all OpenGL settings. Reset.
|
||||
auto last_state = OpenGL::OpenGLState::GetCurState();
|
||||
ResetGLState();
|
||||
last_state.Apply();
|
||||
}
|
||||
#endif
|
||||
|
||||
while (!emu_instance->emu_window->HasSubmittedFrame()) {
|
||||
auto result = Core::System::GetInstance().RunLoop();
|
||||
|
||||
if (result != Core::System::ResultStatus::Success) {
|
||||
std::string errorContent = Core::System::GetInstance().GetStatusDetails();
|
||||
std::string msg;
|
||||
|
||||
switch (result) {
|
||||
case Core::System::ResultStatus::ErrorSystemFiles:
|
||||
msg = "Azahar was unable to locate a 3DS system archive: " + errorContent;
|
||||
break;
|
||||
default:
|
||||
msg = "Fatal Error encountered (" + std::to_string(static_cast<int>(result)) +
|
||||
"): " + errorContent;
|
||||
break;
|
||||
}
|
||||
|
||||
LibRetro::DisplayMessage(msg.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void setup_memory_maps() {
|
||||
auto process = Core::System::GetInstance().Kernel().GetCurrentProcess();
|
||||
if (!process)
|
||||
return;
|
||||
|
||||
std::vector<retro_memory_descriptor> descs;
|
||||
|
||||
for (const auto& [addr, vma] : process->vm_manager.vma_map) {
|
||||
if (vma.type != Kernel::VMAType::BackingMemory)
|
||||
continue;
|
||||
if (vma.size == 0 || !vma.backing_memory)
|
||||
continue;
|
||||
|
||||
// Only expose the well-known user-accessible memory regions
|
||||
uint64_t flags = 0;
|
||||
if (vma.base >= Memory::HEAP_VADDR && vma.base < Memory::HEAP_VADDR_END) {
|
||||
flags = RETRO_MEMDESC_SYSTEM_RAM;
|
||||
} else if (vma.base >= Memory::LINEAR_HEAP_VADDR &&
|
||||
vma.base < Memory::LINEAR_HEAP_VADDR_END) {
|
||||
flags = RETRO_MEMDESC_SYSTEM_RAM;
|
||||
} else if (vma.base >= Memory::NEW_LINEAR_HEAP_VADDR &&
|
||||
vma.base < Memory::NEW_LINEAR_HEAP_VADDR_END) {
|
||||
flags = RETRO_MEMDESC_SYSTEM_RAM;
|
||||
} else if (vma.base >= Memory::VRAM_VADDR && vma.base < Memory::VRAM_VADDR_END) {
|
||||
flags = RETRO_MEMDESC_VIDEO_RAM;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
retro_memory_descriptor desc = {};
|
||||
desc.flags = flags;
|
||||
desc.ptr = const_cast<u8*>(vma.backing_memory.GetPtr());
|
||||
desc.start = vma.base;
|
||||
desc.len = vma.size;
|
||||
|
||||
// select=0 requires power-of-2 len AND start aligned to len.
|
||||
// When that doesn't hold, compute a select mask instead.
|
||||
bool need_select = (vma.size & (vma.size - 1)) != 0;
|
||||
if (!need_select && (vma.base & (vma.size - 1)) != 0)
|
||||
need_select = true;
|
||||
|
||||
if (need_select) {
|
||||
uint64_t np2 = 1;
|
||||
while (np2 < vma.size)
|
||||
np2 <<= 1;
|
||||
if (vma.base & (np2 - 1)) {
|
||||
LOG_WARNING(Frontend, "VMA at 0x{:08X} size 0x{:X} not aligned, skipping", vma.base,
|
||||
vma.size);
|
||||
continue;
|
||||
}
|
||||
desc.select = ~(np2 - 1);
|
||||
}
|
||||
|
||||
descs.push_back(desc);
|
||||
}
|
||||
|
||||
if (!descs.empty()) {
|
||||
retro_memory_map map = {descs.data(), static_cast<unsigned>(descs.size())};
|
||||
LibRetro::SetMemoryMaps(&map);
|
||||
}
|
||||
}
|
||||
|
||||
static bool do_load_game() {
|
||||
const Core::System::ResultStatus load_result{
|
||||
Core::System::GetInstance().Load(*emu_instance->emu_window, LibRetro::settings.file_path)};
|
||||
|
||||
switch (load_result) {
|
||||
case Core::System::ResultStatus::Success:
|
||||
break; // Expected case
|
||||
case Core::System::ResultStatus::ErrorGetLoader:
|
||||
LibRetro::DisplayMessage("Failed to obtain loader for specified ROM!");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorLoader:
|
||||
LibRetro::DisplayMessage("Failed to load ROM!");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted:
|
||||
LibRetro::DisplayMessage("The game that you are trying to load must be decrypted before "
|
||||
"being used with Azahar.");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
|
||||
LibRetro::DisplayMessage("Error while loading ROM: The ROM format is not supported.");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
|
||||
LibRetro::DisplayMessage(
|
||||
"Error loading the specified application as it is GBA Virtual Console");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorNotInitialized:
|
||||
LibRetro::DisplayMessage("CPUCore not initialized");
|
||||
return false;
|
||||
case Core::System::ResultStatus::ErrorSystemMode:
|
||||
LibRetro::DisplayMessage("Failed to determine system mode!");
|
||||
return false;
|
||||
default:
|
||||
LibRetro::DisplayMessage(
|
||||
("Unknown error: " + std::to_string(static_cast<int>(load_result))).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 program_id{};
|
||||
Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
|
||||
Core::System::GetInstance().GPU().ApplyPerProgramSettings(program_id);
|
||||
|
||||
if (Settings::values.use_disk_shader_cache) {
|
||||
Core::System::GetInstance().GPU().Renderer().Rasterizer()->LoadDefaultDiskResources(
|
||||
false, nullptr);
|
||||
}
|
||||
|
||||
setup_memory_maps();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
static void* load_opengl_func(const char* name) {
|
||||
return (void*)emu_instance->hw_render.get_proc_address(name);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void context_reset() {
|
||||
LOG_DEBUG(Frontend, "context_reset");
|
||||
|
||||
switch (Settings::values.graphics_api.GetValue()) {
|
||||
#ifdef ENABLE_OPENGL
|
||||
case Settings::GraphicsAPI::OpenGL:
|
||||
#if defined(USING_GLES)
|
||||
Settings::values.use_gles = true;
|
||||
// Set the global GLES flag immediately to ensure any shader compilation
|
||||
// that happens before the Driver is created uses the correct version
|
||||
OpenGL::GLES = true;
|
||||
#else
|
||||
Settings::values.use_gles = false;
|
||||
OpenGL::GLES = false;
|
||||
#endif
|
||||
// Check to see if the frontend provides us with OpenGL symbols
|
||||
if (emu_instance->hw_render.get_proc_address != nullptr) {
|
||||
bool loaded = Settings::values.use_gles
|
||||
? gladLoadGLES2Loader((GLADloadproc)load_opengl_func)
|
||||
: gladLoadGLLoader((GLADloadproc)load_opengl_func);
|
||||
|
||||
if (!loaded) {
|
||||
LOG_CRITICAL(Frontend, "Glad failed to load (frontend-provided symbols)!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Else, try to load them on our own
|
||||
if (!gladLoadGL()) {
|
||||
LOG_CRITICAL(Frontend, "Glad failed to load (internal symbols)!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef ENABLE_VULKAN
|
||||
case Settings::GraphicsAPI::Vulkan:
|
||||
LibRetro::VulkanResetContext();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
// software renderer never gets here
|
||||
break;
|
||||
}
|
||||
|
||||
emu_instance->emu_window->CreateContext();
|
||||
|
||||
if (!emu_instance->game_loaded) {
|
||||
emu_instance->game_loaded = do_load_game();
|
||||
} else {
|
||||
// Game is already loaded, just recreate the renderer for the new GL context
|
||||
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
|
||||
Core::System::GetInstance().GPU().RecreateRenderer(*emu_instance->emu_window, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void context_destroy() {
|
||||
LOG_DEBUG(Frontend, "context_destroy");
|
||||
if (emu_instance->game_loaded &&
|
||||
Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
|
||||
// Release the renderer's OpenGL resources
|
||||
Core::System::GetInstance().GPU().ReleaseRenderer();
|
||||
}
|
||||
emu_instance->emu_window->DestroyContext();
|
||||
}
|
||||
|
||||
void retro_reset() {
|
||||
LOG_DEBUG(Frontend, "retro_reset");
|
||||
Core::System::GetInstance().Shutdown();
|
||||
emu_instance->game_loaded = do_load_game();
|
||||
}
|
||||
|
||||
/**
|
||||
* libretro callback; Called when a game is to be loaded.
|
||||
*/
|
||||
bool retro_load_game(const struct retro_game_info* info) {
|
||||
LOG_INFO(Frontend, "Starting Azahar RetroArch game...");
|
||||
|
||||
#if CITRA_ARCH(x86_64) && CITRA_HAS_SSE42
|
||||
if (!Common::GetCPUCaps().sse4_2) {
|
||||
LOG_CRITICAL(Frontend, "This CPU does not support SSE4.2, which is required by this build");
|
||||
LibRetro::DisplayMessage(
|
||||
"This CPU does not support SSE4.2, which is required by this build");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
UpdateSettings();
|
||||
|
||||
// If using HW rendering, don't actually load the game here. azahar wants
|
||||
// the graphics context ready and available before calling System::Load.
|
||||
LibRetro::settings.file_path = info->path;
|
||||
|
||||
if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) {
|
||||
LibRetro::DisplayMessage("XRGB8888 is not supported.");
|
||||
return false;
|
||||
}
|
||||
|
||||
emu_instance->emu_window->UpdateLayout();
|
||||
|
||||
switch (Settings::values.graphics_api.GetValue()) {
|
||||
case Settings::GraphicsAPI::OpenGL:
|
||||
#ifdef ENABLE_OPENGL
|
||||
LOG_INFO(Frontend, "Using OpenGL hw renderer");
|
||||
LibRetro::SetHWSharedContext();
|
||||
#if defined(USING_GLES)
|
||||
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGLES3;
|
||||
emu_instance->hw_render.version_major = 3;
|
||||
emu_instance->hw_render.version_minor = 2;
|
||||
#else
|
||||
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGL_CORE;
|
||||
emu_instance->hw_render.version_major = 4;
|
||||
emu_instance->hw_render.version_minor = 3;
|
||||
#endif
|
||||
emu_instance->hw_render.context_reset = context_reset;
|
||||
emu_instance->hw_render.context_destroy = context_destroy;
|
||||
emu_instance->hw_render.cache_context = false;
|
||||
emu_instance->hw_render.bottom_left_origin = true;
|
||||
if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) {
|
||||
LibRetro::DisplayMessage("Failed to set HW renderer");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case Settings::GraphicsAPI::Vulkan:
|
||||
#ifdef ENABLE_VULKAN
|
||||
LOG_INFO(Frontend, "Using Vulkan hw renderer");
|
||||
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_VULKAN;
|
||||
emu_instance->hw_render.version_major = VK_MAKE_VERSION(1, 1, 0);
|
||||
emu_instance->hw_render.version_minor = 0;
|
||||
emu_instance->hw_render.context_reset = context_reset;
|
||||
emu_instance->hw_render.context_destroy = context_destroy;
|
||||
emu_instance->hw_render.cache_context = true;
|
||||
if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) {
|
||||
LibRetro::DisplayMessage("Failed to set HW renderer");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up Vulkan context negotiation interface
|
||||
static const struct retro_hw_render_context_negotiation_interface_vulkan vk_negotiation = {
|
||||
RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN,
|
||||
RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION,
|
||||
LibRetro::GetVulkanApplicationInfo,
|
||||
LibRetro::CreateVulkanDevice,
|
||||
nullptr, // destroy_device - not needed (frontend owns the device)
|
||||
};
|
||||
LibRetro::SetHWRenderContextNegotiationInterface((void**)&vk_negotiation);
|
||||
#endif
|
||||
break;
|
||||
case Settings::GraphicsAPI::Software:
|
||||
emu_instance->game_loaded = do_load_game();
|
||||
if (!emu_instance->game_loaded)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
uint64_t quirks =
|
||||
RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE | RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE;
|
||||
LibRetro::SetSerializationQuirks(quirks);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void retro_unload_game() {
|
||||
LOG_DEBUG(Frontend, "Unloading game...");
|
||||
Core::System::GetInstance().Shutdown();
|
||||
}
|
||||
|
||||
unsigned retro_get_region() {
|
||||
return RETRO_REGION_NTSC;
|
||||
}
|
||||
|
||||
bool retro_load_game_special(unsigned game_type, const struct retro_game_info* info,
|
||||
size_t num_info) {
|
||||
return retro_load_game(info);
|
||||
}
|
||||
|
||||
/// Drain any pending async kernel operations by running the emulation loop.
|
||||
///
|
||||
/// Savestates are unsafe to create while RunAsync operations (file I/O, network, etc.)
|
||||
/// are in flight. The Qt frontend handles this by deferring serialization inside
|
||||
/// System::RunLoop(): it sets a request flag via SendSignal(Signal::Save), and RunLoop
|
||||
/// only performs the save when !kernel->AreAsyncOperationsPending() (see core.cpp).
|
||||
///
|
||||
/// The Qt frontend needs that indirection because its UI and emulation run on separate
|
||||
/// threads. In libretro, the frontend calls API entry points (retro_run, retro_serialize,
|
||||
/// etc.) sequentially, so we can call RunLoop() directly from here to drain pending ops,
|
||||
/// then call SaveStateBuffer()/LoadStateBuffer() ourselves.
|
||||
///
|
||||
/// Note: RunLoop() can itself start new async operations (CPU executes HLE service calls),
|
||||
/// so the pending count may not decrease monotonically. In practice games reach quiescent
|
||||
/// points between frames; the 5-second timeout (matching RunLoop's existing handler)
|
||||
/// covers the pathological case.
|
||||
static bool DrainAsyncOperations(Core::System& system) {
|
||||
if (!system.KernelRunning() || !system.Kernel().AreAsyncOperationsPending()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
emu_instance->emu_window->suppressPresentation = true;
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
while (system.Kernel().AreAsyncOperationsPending()) {
|
||||
if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
|
||||
LOG_ERROR(Frontend, "Timed out waiting for async operations to complete");
|
||||
emu_instance->emu_window->suppressPresentation = false;
|
||||
return false;
|
||||
}
|
||||
auto result = system.RunLoop();
|
||||
if (result != Core::System::ResultStatus::Success) {
|
||||
emu_instance->emu_window->suppressPresentation = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emu_instance->emu_window->suppressPresentation = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::vector<u8>> savestate = {};
|
||||
|
||||
size_t retro_serialize_size() {
|
||||
auto& system = Core::System::GetInstance();
|
||||
if (!system.IsPoweredOn())
|
||||
return 0;
|
||||
|
||||
if (!DrainAsyncOperations(system)) {
|
||||
savestate.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
savestate = system.SaveStateBuffer();
|
||||
return savestate->size();
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Frontend, "Error saving state: {}", e.what());
|
||||
savestate.reset();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool retro_serialize(void* data, size_t size) {
|
||||
if (!savestate.has_value())
|
||||
return false;
|
||||
if (size < savestate->size())
|
||||
return false;
|
||||
memcpy(data, savestate->data(), savestate->size());
|
||||
savestate.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool retro_unserialize(const void* data, size_t size) {
|
||||
auto& system = Core::System::GetInstance();
|
||||
if (!system.IsPoweredOn())
|
||||
return false;
|
||||
|
||||
if (!DrainAsyncOperations(system)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u8> buffer(static_cast<const u8*>(data), static_cast<const u8*>(data) + size);
|
||||
try {
|
||||
return system.LoadStateBuffer(std::move(buffer));
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Frontend, "Error loading state: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void* retro_get_memory_data(unsigned id) {
|
||||
// Memory is exposed via RETRO_ENVIRONMENT_SET_MEMORY_MAPS instead,
|
||||
// using virtual addresses for stable cheat/achievement support.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t retro_get_memory_size(unsigned id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void retro_cheat_reset() {}
|
||||
|
||||
void retro_cheat_set(unsigned index, bool enabled, const char* code) {}
|
||||
10
src/citra_libretro/citra_libretro.h
Normal file
10
src/citra_libretro/citra_libretro.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/core.h"
|
||||
#include "emu_window/libretro_window.h"
|
||||
|
||||
namespace LibRetro {} // namespace LibRetro
|
||||
1014
src/citra_libretro/core_settings.cpp
Normal file
1014
src/citra_libretro/core_settings.cpp
Normal file
File diff suppressed because it is too large
Load diff
41
src/citra_libretro/core_settings.h
Normal file
41
src/citra_libretro/core_settings.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "core/hle/service/cfg/cfg.h"
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
enum CStickFunction { Both, CStick, Touchscreen };
|
||||
|
||||
struct CoreSettings {
|
||||
|
||||
std::string file_path;
|
||||
|
||||
float deadzone = 1.f;
|
||||
|
||||
LibRetro::CStickFunction analog_function;
|
||||
|
||||
bool mouse_touchscreen;
|
||||
|
||||
Service::CFG::SystemLanguage language_value;
|
||||
|
||||
bool touch_touchscreen;
|
||||
|
||||
bool render_touchscreen;
|
||||
|
||||
bool toggle_swap_screen;
|
||||
|
||||
bool motion_enabled;
|
||||
|
||||
float motion_sensitivity;
|
||||
|
||||
} extern settings;
|
||||
|
||||
void RegisterCoreOptions(void);
|
||||
void ParseCoreOptions(void);
|
||||
|
||||
} // namespace LibRetro
|
||||
342
src/citra_libretro/emu_window/libretro_window.cpp
Normal file
342
src/citra_libretro/emu_window/libretro_window.cpp
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include <glad/glad.h>
|
||||
#endif
|
||||
#include <libretro.h>
|
||||
|
||||
#include "audio_core/audio_types.h"
|
||||
#include "citra_libretro/citra_libretro.h"
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "citra_libretro/input/input_factory.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/3ds.h"
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include "video_core/renderer_opengl/gl_state.h"
|
||||
#endif
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/renderer_software/renderer_software.h"
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
/// LibRetro expects a "default" GL state.
|
||||
void ResetGLState() {
|
||||
// Reset internal state.
|
||||
OpenGL::OpenGLState state{};
|
||||
state.Apply();
|
||||
|
||||
// Clean up global state.
|
||||
if (!Settings::values.use_gles) {
|
||||
glLogicOp(GL_COPY);
|
||||
}
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LESS);
|
||||
glDepthMask(GL_TRUE);
|
||||
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFF);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
glBlendEquation(GL_FUNC_ADD);
|
||||
glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
|
||||
glBlendColor(0, 0, 0, 0);
|
||||
|
||||
glDisable(GL_COLOR_LOGIC_OP);
|
||||
|
||||
glDisable(GL_DITHER);
|
||||
|
||||
glDisable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
#endif
|
||||
|
||||
EmuWindow_LibRetro::EmuWindow_LibRetro() {
|
||||
strict_context_required = true;
|
||||
window_info.type = Frontend::WindowSystemType::LibRetro;
|
||||
}
|
||||
|
||||
EmuWindow_LibRetro::~EmuWindow_LibRetro() {}
|
||||
|
||||
void EmuWindow_LibRetro::SwapBuffers() {
|
||||
if (suppressPresentation)
|
||||
return;
|
||||
submittedFrame = true;
|
||||
|
||||
switch (Settings::values.graphics_api.GetValue()) {
|
||||
case Settings::GraphicsAPI::OpenGL: {
|
||||
#ifdef ENABLE_OPENGL
|
||||
auto current_state = OpenGL::OpenGLState::GetCurState();
|
||||
ResetGLState();
|
||||
if (enableEmulatedPointer && tracker) {
|
||||
tracker->Render(width, height);
|
||||
}
|
||||
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
|
||||
static_cast<unsigned>(height), 0);
|
||||
current_state.Apply();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case Settings::GraphicsAPI::Vulkan: {
|
||||
#ifdef ENABLE_VULKAN
|
||||
if (enableEmulatedPointer && tracker) {
|
||||
tracker->Render(width, height);
|
||||
}
|
||||
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
|
||||
static_cast<unsigned>(height), 0);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case Settings::GraphicsAPI::Software: {
|
||||
retro_framebuffer fb;
|
||||
u8* data;
|
||||
size_t pitch;
|
||||
bool did_malloc = false;
|
||||
if (LibRetro::GetSoftwareFramebuffer(&fb, width, height)) {
|
||||
data = static_cast<u8*>(fb.data);
|
||||
pitch = fb.pitch;
|
||||
} else {
|
||||
pitch = static_cast<size_t>(width) * 4;
|
||||
data = static_cast<u8*>(calloc(1, pitch * height));
|
||||
did_malloc = true;
|
||||
}
|
||||
|
||||
std::memset(data, 0, pitch * height);
|
||||
|
||||
auto& system = Core::System::GetInstance();
|
||||
const auto& renderer = static_cast<SwRenderer::RendererSoftware&>(system.GPU().Renderer());
|
||||
const auto& layout = GetFramebufferLayout();
|
||||
|
||||
// Blit a single screen from ScreenInfo (column-major RGBA) to the
|
||||
// output buffer (row-major XRGB8888), rotating and scaling as needed.
|
||||
// The 3DS framebuffer is portrait-oriented; ScreenInfo stores pixels
|
||||
// column-major so the transpose gives us the landscape orientation:
|
||||
// display (dx, dy) -> ScreenInfo (x=dy, y=dx)
|
||||
auto blit_screen = [&](VideoCore::ScreenId screen_id, const Common::Rectangle<u32>& rect) {
|
||||
const auto& info = renderer.Screen(screen_id);
|
||||
if (info.pixels.empty())
|
||||
return;
|
||||
|
||||
const u32 rect_w = rect.GetWidth();
|
||||
const u32 rect_h = rect.GetHeight();
|
||||
if (rect_w == 0 || rect_h == 0)
|
||||
return;
|
||||
|
||||
// Landscape display dimensions (transposed from portrait storage)
|
||||
const u32 native_w = info.height;
|
||||
const u32 native_h = info.width;
|
||||
|
||||
for (u32 oy = 0; oy < rect_h; oy++) {
|
||||
for (u32 ox = 0; ox < rect_w; ox++) {
|
||||
const u32 dx = ox * native_w / rect_w;
|
||||
const u32 dy = oy * native_h / rect_h;
|
||||
|
||||
const u32 src_off = (dy * info.height + dx) * 4;
|
||||
if (src_off + 3 >= info.pixels.size())
|
||||
continue;
|
||||
|
||||
const u8* src = info.pixels.data() + src_off;
|
||||
const size_t dst_off = static_cast<size_t>(rect.top + oy) * pitch +
|
||||
static_cast<size_t>(rect.left + ox) * 4;
|
||||
|
||||
// RGBA -> XRGB8888 (little-endian: B, G, R, 0)
|
||||
data[dst_off + 0] = src[2];
|
||||
data[dst_off + 1] = src[1];
|
||||
data[dst_off + 2] = src[0];
|
||||
data[dst_off + 3] = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (layout.top_screen_enabled) {
|
||||
blit_screen(VideoCore::ScreenId::TopLeft, layout.top_screen);
|
||||
}
|
||||
if (layout.bottom_screen_enabled) {
|
||||
blit_screen(VideoCore::ScreenId::Bottom, layout.bottom_screen);
|
||||
}
|
||||
|
||||
// Software cursor rendering with framebuffer access
|
||||
if (enableEmulatedPointer && tracker) {
|
||||
tracker->Render(width, height, data);
|
||||
}
|
||||
|
||||
LibRetro::UploadVideoFrame(data, static_cast<unsigned>(width),
|
||||
static_cast<unsigned>(height), pitch);
|
||||
if (did_malloc)
|
||||
free(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::SetupFramebuffer() {
|
||||
if (Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::OpenGL)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
// TODO: Expose interface in renderer_opengl to configure this in it's internal state
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(LibRetro::GetFramebuffer()));
|
||||
|
||||
// glClear can be a slow path - skip clearing if we don't need to.
|
||||
if (doCleanFrame) {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
doCleanFrame = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::PollEvents() {
|
||||
// The software renderer doesn't call render_window.SwapBuffers() — standalone
|
||||
// frontends (Qt/SDL) use separate presentation threads that pull from screen_infos
|
||||
// instead. In libretro there's no such thread, so we present here: PollEvents is
|
||||
// called from EndFrame() during each VBlank, right after PrepareRenderTarget has
|
||||
// filled the screen pixel buffers.
|
||||
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::Software) {
|
||||
SwapBuffers();
|
||||
}
|
||||
|
||||
LibRetro::PollInput();
|
||||
|
||||
// TODO: Poll for right click for motion emu
|
||||
|
||||
if (enableEmulatedPointer && tracker) {
|
||||
tracker->Update(width, height, GetFramebufferLayout());
|
||||
|
||||
if (tracker->IsPressed()) {
|
||||
auto mousePos = tracker->GetPressedPosition();
|
||||
|
||||
if (hasTouched) {
|
||||
TouchMoved(mousePos.first, mousePos.second);
|
||||
} else {
|
||||
TouchPressed(mousePos.first, mousePos.second);
|
||||
hasTouched = true;
|
||||
}
|
||||
} else if (hasTouched) {
|
||||
hasTouched = false;
|
||||
TouchReleased();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::MakeCurrent() {
|
||||
// They don't get any say in the matter - GL context is always current!
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::DoneCurrent() {
|
||||
// They don't get any say in the matter - GL context is always current!
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> _minimal_size) {}
|
||||
|
||||
LayoutGeometry ComputeLayoutGeometry() {
|
||||
unsigned baseX;
|
||||
unsigned baseY;
|
||||
bool emulated_pointer = true;
|
||||
|
||||
float scaling = Settings::values.resolution_factor.GetValue();
|
||||
bool swapped = Settings::values.swap_screen.GetValue();
|
||||
|
||||
switch (Settings::values.layout_option.GetValue()) {
|
||||
case Settings::LayoutOption::SingleScreen:
|
||||
if (swapped) { // Bottom screen visible
|
||||
baseX = Core::kScreenBottomWidth;
|
||||
baseY = Core::kScreenBottomHeight;
|
||||
} else { // Top screen visible
|
||||
baseX = Core::kScreenTopWidth;
|
||||
baseY = Core::kScreenTopHeight;
|
||||
emulated_pointer = false;
|
||||
}
|
||||
baseX *= scaling;
|
||||
baseY *= scaling;
|
||||
break;
|
||||
case Settings::LayoutOption::LargeScreen:
|
||||
if (swapped) { // Bottom screen biggest
|
||||
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth / 4;
|
||||
baseY = Core::kScreenBottomHeight;
|
||||
} else { // Top screen biggest
|
||||
baseX = Core::kScreenTopWidth + Core::kScreenBottomWidth / 4;
|
||||
baseY = Core::kScreenTopHeight;
|
||||
}
|
||||
|
||||
if (scaling < 4) {
|
||||
// Unfortunately, to get this aspect ratio correct (and have non-blurry 1x scaling),
|
||||
// we have to have a pretty large buffer for the minimum ratio.
|
||||
baseX *= 4;
|
||||
baseY *= 4;
|
||||
} else {
|
||||
baseX *= scaling;
|
||||
baseY *= scaling;
|
||||
}
|
||||
break;
|
||||
case Settings::LayoutOption::SideScreen:
|
||||
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth;
|
||||
baseY = Core::kScreenTopHeight;
|
||||
baseX *= scaling;
|
||||
baseY *= scaling;
|
||||
break;
|
||||
case Settings::LayoutOption::Default:
|
||||
default:
|
||||
baseX = Core::kScreenTopWidth;
|
||||
baseY = Core::kScreenTopHeight + Core::kScreenBottomHeight;
|
||||
baseX *= scaling;
|
||||
baseY *= scaling;
|
||||
break;
|
||||
}
|
||||
|
||||
return {baseX, baseY, emulated_pointer};
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::UpdateLayout() {
|
||||
auto geom = ComputeLayoutGeometry();
|
||||
unsigned baseX = geom.width;
|
||||
unsigned baseY = geom.height;
|
||||
enableEmulatedPointer = geom.emulated_pointer;
|
||||
|
||||
// Update Libretro with our status
|
||||
struct retro_system_av_info info{};
|
||||
info.timing.fps = 60.0;
|
||||
info.timing.sample_rate = AudioCore::native_sample_rate;
|
||||
info.geometry.aspect_ratio = (float)baseX / (float)baseY;
|
||||
info.geometry.base_width = baseX;
|
||||
info.geometry.base_height = baseY;
|
||||
info.geometry.max_width = baseX;
|
||||
info.geometry.max_height = baseY;
|
||||
if (!LibRetro::SetGeometry(&info)) {
|
||||
LOG_CRITICAL(Frontend, "Failed to update 3DS layout in frontend!");
|
||||
}
|
||||
|
||||
width = baseX;
|
||||
height = baseY;
|
||||
|
||||
UpdateCurrentFramebufferLayout(baseX, baseY);
|
||||
|
||||
doCleanFrame = true;
|
||||
}
|
||||
|
||||
bool EmuWindow_LibRetro::NeedsClearing() const {
|
||||
// We manage this ourselves.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EmuWindow_LibRetro::HasSubmittedFrame() {
|
||||
bool state = submittedFrame;
|
||||
submittedFrame = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::CreateContext() {
|
||||
tracker = std::make_unique<LibRetro::Input::MouseTracker>();
|
||||
|
||||
doCleanFrame = true;
|
||||
}
|
||||
|
||||
void EmuWindow_LibRetro::DestroyContext() {
|
||||
tracker = nullptr;
|
||||
}
|
||||
79
src/citra_libretro/emu_window/libretro_window.h
Normal file
79
src/citra_libretro/emu_window/libretro_window.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include "citra_libretro/input/mouse_tracker.h"
|
||||
#include "core/frontend/emu_window.h"
|
||||
|
||||
struct LayoutGeometry {
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
bool emulated_pointer;
|
||||
};
|
||||
|
||||
/// Compute framebuffer dimensions from current layout/scaling/swap settings.
|
||||
LayoutGeometry ComputeLayoutGeometry();
|
||||
|
||||
void ResetGLState();
|
||||
|
||||
class EmuWindow_LibRetro : public Frontend::EmuWindow {
|
||||
public:
|
||||
EmuWindow_LibRetro();
|
||||
~EmuWindow_LibRetro();
|
||||
|
||||
/// Swap buffers to display the next frame
|
||||
void SwapBuffers() override;
|
||||
|
||||
/// Polls window events
|
||||
void PollEvents() override;
|
||||
|
||||
/// Makes the graphics context current for the caller thread
|
||||
void MakeCurrent() override;
|
||||
|
||||
/// Releases the GL context from the caller thread
|
||||
void DoneCurrent() override;
|
||||
|
||||
void SetupFramebuffer() override;
|
||||
|
||||
/// Prepares the window for rendering
|
||||
void UpdateLayout();
|
||||
|
||||
/// States whether a frame has been submitted. Resets after call.
|
||||
bool HasSubmittedFrame();
|
||||
|
||||
/// Flags that the framebuffer should be cleared.
|
||||
bool NeedsClearing() const override;
|
||||
|
||||
/// Creates state for a currently running OpenGL context.
|
||||
void CreateContext();
|
||||
|
||||
/// Destroys a currently running OpenGL context.
|
||||
void DestroyContext();
|
||||
|
||||
/// When true, SwapBuffers() is suppressed (used during savestate drain loops)
|
||||
bool suppressPresentation = false;
|
||||
|
||||
private:
|
||||
/// Called when a configuration change affects the minimal size of the window
|
||||
void OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal_size) override;
|
||||
|
||||
int width;
|
||||
int height;
|
||||
|
||||
bool submittedFrame = false;
|
||||
|
||||
// Hack to ensure stuff runs on the main thread
|
||||
bool doCleanFrame = false;
|
||||
|
||||
// For tracking LibRetro state
|
||||
bool hasTouched = false;
|
||||
|
||||
// For tracking mouse cursor
|
||||
std::unique_ptr<LibRetro::Input::MouseTracker> tracker = nullptr;
|
||||
|
||||
bool enableEmulatedPointer = false;
|
||||
};
|
||||
281
src/citra_libretro/environment.cpp
Normal file
281
src/citra_libretro/environment.cpp
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "audio_core/audio_types.h"
|
||||
#include "audio_core/libretro_sink.h"
|
||||
#include "common/scm_rev.h"
|
||||
#include "core/3ds.h"
|
||||
#include "emu_window/libretro_window.h"
|
||||
#include "environment.h"
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
#include "streams/file_stream.h"
|
||||
#endif
|
||||
|
||||
using namespace LibRetro;
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
namespace {
|
||||
|
||||
static retro_video_refresh_t video_cb;
|
||||
static retro_audio_sample_batch_t audio_batch_cb;
|
||||
static retro_environment_t environ_cb;
|
||||
static retro_input_poll_t input_poll_cb;
|
||||
static retro_input_state_t input_state_cb;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height) {
|
||||
fb->data = nullptr;
|
||||
fb->width = width;
|
||||
fb->height = height;
|
||||
fb->pitch = 0;
|
||||
fb->format = RETRO_PIXEL_FORMAT_XRGB8888;
|
||||
fb->access_flags = RETRO_MEMORY_ACCESS_WRITE;
|
||||
fb->memory_flags = 0;
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER, fb);
|
||||
}
|
||||
|
||||
void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch) {
|
||||
return video_cb(data, width, height, pitch);
|
||||
}
|
||||
|
||||
bool SetHWSharedContext() {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT, NULL);
|
||||
}
|
||||
|
||||
void PollInput() {
|
||||
return input_poll_cb();
|
||||
}
|
||||
|
||||
bool GetSensorInterface(struct retro_sensor_interface* sensor_interface) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE, sensor_interface);
|
||||
}
|
||||
|
||||
bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE, mic_interface);
|
||||
}
|
||||
|
||||
Settings::GraphicsAPI GetPreferredRenderer() {
|
||||
// try and maintain the current driver
|
||||
retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL;
|
||||
environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type);
|
||||
switch (context_type) {
|
||||
#ifdef ENABLE_OPENGL
|
||||
case RETRO_HW_CONTEXT_OPENGL:
|
||||
case RETRO_HW_CONTEXT_OPENGL_CORE:
|
||||
case RETRO_HW_CONTEXT_OPENGLES2:
|
||||
case RETRO_HW_CONTEXT_OPENGLES3:
|
||||
case RETRO_HW_CONTEXT_OPENGLES_VERSION:
|
||||
return Settings::GraphicsAPI::OpenGL;
|
||||
#endif
|
||||
#ifdef ENABLE_VULKAN
|
||||
case RETRO_HW_CONTEXT_VULKAN:
|
||||
return Settings::GraphicsAPI::Vulkan;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// we can't maintain the current driver, need to switch
|
||||
#if defined(ENABLE_VULKAN)
|
||||
return Settings::GraphicsAPI::Vulkan;
|
||||
#elif defined(ENABLE_OPENGL)
|
||||
return Settings::GraphicsAPI::OpenGL;
|
||||
#else
|
||||
return Settings::GraphicsAPI::Software;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SetVariables(const retro_variable vars[]) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)vars);
|
||||
}
|
||||
|
||||
bool SetCoreOptionsV2(const retro_core_options_v2* options) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, (void*)options);
|
||||
}
|
||||
|
||||
bool SetCoreOptionsV1(const retro_core_option_definition* options) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, (void*)options);
|
||||
}
|
||||
|
||||
bool GetCoreOptionsVersion(unsigned* version) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, version);
|
||||
}
|
||||
|
||||
bool SetMemoryMaps(const retro_memory_map* map) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, (void*)map);
|
||||
}
|
||||
|
||||
bool SetControllerInfo(const retro_controller_info info[]) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)info);
|
||||
}
|
||||
|
||||
bool SetPixelFormat(const retro_pixel_format fmt) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, (void*)&fmt);
|
||||
}
|
||||
|
||||
bool SetHWRenderer(retro_hw_render_callback* cb) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, cb);
|
||||
}
|
||||
|
||||
bool GetHWRenderInterface(void** interface) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, interface) && !!*interface;
|
||||
}
|
||||
|
||||
bool SetHWRenderContextNegotiationInterface(void** interface) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, interface) &&
|
||||
!!*interface;
|
||||
}
|
||||
|
||||
bool SetAudioCallback(retro_audio_callback* cb) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, cb);
|
||||
}
|
||||
|
||||
bool SetFrameTimeCallback(retro_frame_time_callback* cb) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, cb);
|
||||
}
|
||||
|
||||
bool SetGeometry(retro_system_av_info* cb) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, cb);
|
||||
}
|
||||
|
||||
bool SetInputDescriptors(const retro_input_descriptor desc[]) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, (void*)desc);
|
||||
}
|
||||
|
||||
bool HasUpdatedConfig() {
|
||||
bool updated = false;
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated;
|
||||
}
|
||||
|
||||
bool Shutdown() {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, NULL);
|
||||
}
|
||||
|
||||
/// Displays the specified message to the screen.
|
||||
bool DisplayMessage(const char* sg) {
|
||||
retro_message msg;
|
||||
msg.msg = sg;
|
||||
msg.frames = 60 * 10;
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
|
||||
}
|
||||
|
||||
bool SetSerializationQuirks(uint64_t quirks) {
|
||||
return environ_cb(RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS, &quirks);
|
||||
}
|
||||
|
||||
std::string FetchVariable(std::string key, std::string def) {
|
||||
struct retro_variable var = {nullptr};
|
||||
var.key = key.c_str();
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) {
|
||||
// Fetching variable failed.
|
||||
LOG_ERROR(Frontend, "Fetching variable {} failed.", key);
|
||||
return def;
|
||||
}
|
||||
return std::string(var.value);
|
||||
}
|
||||
|
||||
std::string GetSaveDir() {
|
||||
char* var = nullptr;
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &var) || var == nullptr) {
|
||||
// Fetching variable failed.
|
||||
LOG_ERROR(Frontend, "No save directory provided by LibRetro.");
|
||||
return std::string();
|
||||
}
|
||||
return std::string(var);
|
||||
}
|
||||
|
||||
std::string GetSystemDir() {
|
||||
char* var = nullptr;
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &var) || var == nullptr) {
|
||||
// Fetching variable failed.
|
||||
LOG_ERROR(Frontend, "No system directory provided by LibRetro.");
|
||||
return std::string();
|
||||
}
|
||||
return std::string(var);
|
||||
}
|
||||
|
||||
retro_log_printf_t GetLoggingBackend() {
|
||||
retro_log_callback callback{};
|
||||
if (!environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &callback)) {
|
||||
return nullptr;
|
||||
}
|
||||
return callback.log;
|
||||
}
|
||||
|
||||
int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id) {
|
||||
return input_state_cb(port, device, index, id);
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info) {
|
||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, vfs_iface_info))
|
||||
filestream_vfs_init(vfs_iface_info);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IOS
|
||||
bool CanUseJIT() {
|
||||
bool can_jit = false;
|
||||
return environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && can_jit;
|
||||
}
|
||||
#endif
|
||||
|
||||
}; // namespace LibRetro
|
||||
|
||||
void retro_get_system_info(struct retro_system_info* info) {
|
||||
memset(info, 0, sizeof(*info));
|
||||
info->library_name = "Azahar";
|
||||
info->library_version = Common::g_build_fullname;
|
||||
info->need_fullpath = true;
|
||||
info->valid_extensions = "3ds|3dsx|z3dsx|elf|axf|cci|zcci|cxi|zcxi|app";
|
||||
}
|
||||
|
||||
void LibRetro::SubmitAudio(const int16_t* data, size_t frames) {
|
||||
audio_batch_cb(data, frames);
|
||||
}
|
||||
|
||||
void retro_set_audio_sample(retro_audio_sample_t cb) {
|
||||
// We don't need single audio sample callbacks.
|
||||
}
|
||||
|
||||
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) {
|
||||
LibRetro::audio_batch_cb = cb;
|
||||
}
|
||||
|
||||
void retro_set_input_poll(retro_input_poll_t cb) {
|
||||
LibRetro::input_poll_cb = cb;
|
||||
}
|
||||
|
||||
void retro_set_video_refresh(retro_video_refresh_t cb) {
|
||||
LibRetro::video_cb = cb;
|
||||
}
|
||||
void retro_set_environment(retro_environment_t cb) {
|
||||
LibRetro::environ_cb = cb;
|
||||
LibRetro::OnConfigureEnvironment();
|
||||
}
|
||||
|
||||
void retro_set_controller_port_device(unsigned port, unsigned device) {}
|
||||
|
||||
void retro_set_input_state(retro_input_state_t cb) {
|
||||
input_state_cb = cb;
|
||||
}
|
||||
|
||||
void retro_get_system_av_info(struct retro_system_av_info* info) {
|
||||
info->timing.fps = 60.0;
|
||||
info->timing.sample_rate = AudioCore::native_sample_rate;
|
||||
|
||||
// Compute geometry from current settings so the frontend allocates the
|
||||
// correct framebuffer on first use.
|
||||
auto geom = ComputeLayoutGeometry();
|
||||
info->geometry.base_width = geom.width;
|
||||
info->geometry.base_height = geom.height;
|
||||
// Max must cover the largest possible layout (SideScreen at 10x = 7200).
|
||||
info->geometry.max_width = (Core::kScreenBottomWidth + Core::kScreenTopWidth) * 10;
|
||||
info->geometry.max_height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * 10;
|
||||
info->geometry.aspect_ratio = (float)geom.width / (float)geom.height;
|
||||
}
|
||||
129
src/citra_libretro/environment.h
Normal file
129
src/citra_libretro/environment.h
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "common/logging/backend.h"
|
||||
#include "common/logging/filter.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "libretro.h"
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
/// May fetch a framebuffer that can be rendered into for software rendering
|
||||
/// @see RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER
|
||||
/// @see retro_framebuffer
|
||||
/// @see retro_video_refresh_t
|
||||
bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height);
|
||||
|
||||
/// Calls back to LibRetro to upload a particular video frame.
|
||||
/// @see retro_video_refresh_t
|
||||
void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch);
|
||||
|
||||
/// Calls back to LibRetro to poll input.
|
||||
/// @see retro_input_poll_t
|
||||
void PollInput();
|
||||
|
||||
/// Gets the sensor interface for motion input
|
||||
bool GetSensorInterface(struct retro_sensor_interface* sensor_interface);
|
||||
|
||||
/// Gets the microphone interface for audio input
|
||||
bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface);
|
||||
|
||||
/// Sets the environmental variables used for settings.
|
||||
bool SetVariables(const retro_variable vars[]);
|
||||
|
||||
/// Sets the core options using the v2 interface with categories.
|
||||
bool SetCoreOptionsV2(const retro_core_options_v2* options);
|
||||
|
||||
/// Sets the core options using the v1 interface.
|
||||
bool SetCoreOptionsV1(const retro_core_option_definition* options);
|
||||
|
||||
/// Gets the core options version supported by the frontend.
|
||||
bool GetCoreOptionsVersion(unsigned* version);
|
||||
|
||||
bool SetHWSharedContext(void);
|
||||
|
||||
/// Returns the LibRetro save directory, or a empty string if one doesn't exist.
|
||||
std::string GetSaveDir();
|
||||
|
||||
/// Returns the LibRetro system directory, or a empty string if one doesn't exist.
|
||||
std::string GetSystemDir();
|
||||
|
||||
/// Fetches a variable by key name.
|
||||
std::string FetchVariable(std::string key, std::string def);
|
||||
|
||||
/// Returns a logging backend, or null if the frontend refuses to provide one.
|
||||
retro_log_printf_t GetLoggingBackend();
|
||||
|
||||
/// Returns graphics api based on global frontend setting
|
||||
Settings::GraphicsAPI GetPreferredRenderer();
|
||||
|
||||
/// Displays information about the kinds of controllers that this Citra recreates.
|
||||
bool SetControllerInfo(const retro_controller_info info[]);
|
||||
|
||||
/// Sets the memory maps for the core.
|
||||
bool SetMemoryMaps(const retro_memory_map* map);
|
||||
|
||||
/// Sets the framebuffer pixel format.
|
||||
bool SetPixelFormat(const retro_pixel_format fmt);
|
||||
|
||||
/// Sets the H/W rendering context.
|
||||
bool SetHWRenderer(retro_hw_render_callback* cb);
|
||||
|
||||
/// Gets the H/W rendering interface.
|
||||
bool GetHWRenderInterface(void** interface);
|
||||
|
||||
/// Sets the H/W rendering context negotiation interface.
|
||||
bool SetHWRenderContextNegotiationInterface(void** interface);
|
||||
|
||||
/// Sets the async audio callback.
|
||||
bool SetAudioCallback(retro_audio_callback* cb);
|
||||
|
||||
/// Sets the frame time callback.
|
||||
bool SetFrameTimeCallback(retro_frame_time_callback* cb);
|
||||
|
||||
/// Set the size of the new screen buffer.
|
||||
bool SetGeometry(retro_system_av_info* cb);
|
||||
|
||||
/// Tells LibRetro what input buttons are labelled on the 3DS.
|
||||
bool SetInputDescriptors(const retro_input_descriptor desc[]);
|
||||
|
||||
/// Returns the current status of a input.
|
||||
int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
|
||||
/// Called when the emulator environment is ready to be configured.
|
||||
void OnConfigureEnvironment();
|
||||
|
||||
/// Submits audio frames to LibRetro.
|
||||
/// @see retro_audio_sample_batch_t
|
||||
void SubmitAudio(const int16_t* data, size_t frames);
|
||||
|
||||
/// Checks to see if the frontend configuration has been updated.
|
||||
bool HasUpdatedConfig();
|
||||
|
||||
/// Returns the current framebuffer.
|
||||
uintptr_t GetFramebuffer();
|
||||
|
||||
/// Tells the frontend that we are done.
|
||||
bool Shutdown();
|
||||
|
||||
/// Displays the specified message to the screen.
|
||||
bool DisplayMessage(const char* sg);
|
||||
|
||||
/// Sets serialization quirks for the core.
|
||||
bool SetSerializationQuirks(uint64_t quirks);
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info);
|
||||
#endif
|
||||
|
||||
#ifdef IOS
|
||||
bool CanUseJIT();
|
||||
#endif
|
||||
|
||||
} // namespace LibRetro
|
||||
216
src/citra_libretro/input/input_factory.cpp
Normal file
216
src/citra_libretro/input/input_factory.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <libretro.h>
|
||||
|
||||
#include "common/math_util.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "core/frontend/input.h"
|
||||
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "citra_libretro/input/input_factory.h"
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
namespace Input {
|
||||
|
||||
class LibRetroButtonFactory;
|
||||
class LibRetroAxisFactory;
|
||||
class LibRetroMotionFactory;
|
||||
|
||||
class LibRetroButton final : public ::Input::ButtonDevice {
|
||||
public:
|
||||
explicit LibRetroButton(int joystick_, int button_) : joystick(joystick_), button(button_) {}
|
||||
|
||||
bool GetStatus() const override {
|
||||
return CheckInput((unsigned int)joystick, RETRO_DEVICE_JOYPAD, 0, (unsigned int)button) > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
int joystick;
|
||||
int button;
|
||||
};
|
||||
|
||||
/// A button device factory that creates button devices from LibRetro joystick
|
||||
class LibRetroButtonFactory final : public ::Input::Factory<::Input::ButtonDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates a button device from a joystick button
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "joystick": the index of the joystick to bind
|
||||
* - "button": the index of the button to bind
|
||||
*/
|
||||
std::unique_ptr<::Input::ButtonDevice> Create(const Common::ParamPackage& params) override {
|
||||
const int joystick_index = params.Get("joystick", 0);
|
||||
|
||||
const int button = params.Get("button", 0);
|
||||
return std::make_unique<LibRetroButton>(joystick_index, button);
|
||||
}
|
||||
};
|
||||
|
||||
/// A axis device factory that creates axis devices from LibRetro joystick
|
||||
class LibRetroAxis final : public ::Input::AnalogDevice {
|
||||
public:
|
||||
explicit LibRetroAxis(int joystick_, int button_) : joystick(joystick_), button(button_) {}
|
||||
|
||||
std::tuple<float, float> GetStatus() const override {
|
||||
auto axis_x =
|
||||
(float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 0);
|
||||
auto axis_y =
|
||||
(float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 1);
|
||||
return std::make_tuple(axis_x / INT16_MAX, -axis_y / INT16_MAX);
|
||||
}
|
||||
|
||||
private:
|
||||
int joystick;
|
||||
int button;
|
||||
};
|
||||
|
||||
/// A axis device factory that creates axis devices from SDL joystick
|
||||
class LibRetroAxisFactory final : public ::Input::Factory<::Input::AnalogDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates a button device from a joystick button
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "joystick": the index of the joystick to bind
|
||||
* - "button"(optional): the index of the button to bind
|
||||
* - "hat"(optional): the index of the hat to bind as direction buttons
|
||||
* - "axis"(optional): the index of the axis to bind
|
||||
* - "direction"(only used for hat): the direction name of the hat to bind. Can be "up",
|
||||
* "down", "left" or "right"
|
||||
* - "threshould"(only used for axis): a float value in (-1.0, 1.0) which the button is
|
||||
* triggered if the axis value crosses
|
||||
* - "direction"(only used for axis): "+" means the button is triggered when the axis value
|
||||
* is greater than the threshold; "-" means the button is triggered when the axis value
|
||||
* is smaller than the threshold
|
||||
*/
|
||||
std::unique_ptr<::Input::AnalogDevice> Create(const Common::ParamPackage& params) override {
|
||||
const int joystick_index = params.Get("joystick", 0);
|
||||
|
||||
const int button = params.Get("axis", 0);
|
||||
return std::make_unique<LibRetroAxis>(joystick_index, button);
|
||||
}
|
||||
};
|
||||
|
||||
/// Static sensor interface callbacks for LibRetro motion input
|
||||
static retro_sensor_get_input_t sensor_get_input_callback = nullptr;
|
||||
static retro_set_sensor_state_t sensor_set_state_callback = nullptr;
|
||||
static bool gyro_enabled = false;
|
||||
static bool accel_enabled = false;
|
||||
|
||||
/// LibRetro motion device that implements 3DS gyroscope and accelerometer input
|
||||
class LibRetroMotion final : public ::Input::MotionDevice {
|
||||
public:
|
||||
explicit LibRetroMotion(int port_, float sensitivity_)
|
||||
: port(port_), sensitivity(sensitivity_) {
|
||||
InitSensors();
|
||||
}
|
||||
|
||||
std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const override {
|
||||
Common::Vec3<float> accel = {0.0f, 0.0f, -1.0f}; // Default gravity pointing down
|
||||
Common::Vec3<float> gyro = {0.0f, 0.0f, 0.0f}; // Default no rotation
|
||||
|
||||
if (sensor_get_input_callback) {
|
||||
if (accel_enabled) {
|
||||
// Get accelerometer data (in g units)
|
||||
// LibRetro coordinate system matches 3DS: X=LEFT, Y=OUT, Z=UP
|
||||
accel.x =
|
||||
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_X) * sensitivity;
|
||||
accel.y =
|
||||
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Y) * sensitivity;
|
||||
accel.z =
|
||||
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Z) * sensitivity;
|
||||
}
|
||||
|
||||
if (gyro_enabled) {
|
||||
// Get gyroscope data (convert to degrees/sec)
|
||||
// LibRetro gives radians/sec, 3DS expects degrees/sec
|
||||
constexpr float RAD_TO_DEG = 180.0f / 3.14159265f;
|
||||
gyro.x = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_X) * RAD_TO_DEG *
|
||||
sensitivity;
|
||||
gyro.y = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Y) * RAD_TO_DEG *
|
||||
sensitivity;
|
||||
gyro.z = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Z) * RAD_TO_DEG *
|
||||
sensitivity;
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(accel, gyro);
|
||||
}
|
||||
|
||||
private:
|
||||
int port;
|
||||
float sensitivity;
|
||||
|
||||
void InitSensors() const {
|
||||
// Initialize sensors if not already done
|
||||
if (!sensor_get_input_callback || !sensor_set_state_callback) {
|
||||
struct retro_sensor_interface sensor_interface;
|
||||
if (LibRetro::GetSensorInterface(&sensor_interface)) {
|
||||
sensor_get_input_callback = sensor_interface.get_sensor_input;
|
||||
sensor_set_state_callback = sensor_interface.set_sensor_state;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable sensors at 60Hz rate (matching 3DS update frequency)
|
||||
const unsigned int event_rate = 60;
|
||||
|
||||
if (sensor_set_state_callback) {
|
||||
if (!accel_enabled &&
|
||||
sensor_set_state_callback(port, RETRO_SENSOR_ACCELEROMETER_ENABLE, event_rate)) {
|
||||
accel_enabled = true;
|
||||
}
|
||||
if (!gyro_enabled &&
|
||||
sensor_set_state_callback(port, RETRO_SENSOR_GYROSCOPE_ENABLE, event_rate)) {
|
||||
gyro_enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Motion device factory that creates motion devices from LibRetro sensor interface
|
||||
class LibRetroMotionFactory final : public ::Input::Factory<::Input::MotionDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates a motion device from LibRetro sensor interface
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "port": the controller port to read motion from (default 0)
|
||||
* - "sensitivity": motion sensitivity multiplier (default 1.0)
|
||||
*/
|
||||
std::unique_ptr<::Input::MotionDevice> Create(const Common::ParamPackage& params) override {
|
||||
const int port = params.Get("port", 0);
|
||||
const float sensitivity = params.Get("sensitivity", 1.0f);
|
||||
return std::make_unique<LibRetroMotion>(port, sensitivity);
|
||||
}
|
||||
};
|
||||
|
||||
void Init() {
|
||||
using namespace ::Input;
|
||||
RegisterFactory<ButtonDevice>("libretro", std::make_shared<LibRetroButtonFactory>());
|
||||
RegisterFactory<AnalogDevice>("libretro", std::make_shared<LibRetroAxisFactory>());
|
||||
RegisterFactory<MotionDevice>("libretro", std::make_shared<LibRetroMotionFactory>());
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
using namespace ::Input;
|
||||
UnregisterFactory<ButtonDevice>("libretro");
|
||||
UnregisterFactory<AnalogDevice>("libretro");
|
||||
UnregisterFactory<MotionDevice>("libretro");
|
||||
|
||||
// Disable sensors on shutdown
|
||||
if (sensor_set_state_callback) {
|
||||
sensor_set_state_callback(0, RETRO_SENSOR_ACCELEROMETER_DISABLE, 60);
|
||||
sensor_set_state_callback(0, RETRO_SENSOR_GYROSCOPE_DISABLE, 60);
|
||||
sensor_get_input_callback = nullptr;
|
||||
sensor_set_state_callback = nullptr;
|
||||
accel_enabled = false;
|
||||
gyro_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Input
|
||||
} // namespace LibRetro
|
||||
20
src/citra_libretro/input/input_factory.h
Normal file
20
src/citra_libretro/input/input_factory.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/frontend/input.h"
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
namespace Input {
|
||||
|
||||
/// Initializes and registers LibRetro device factories
|
||||
void Init();
|
||||
|
||||
/// Unresisters LibRetro device factories and shut them down.
|
||||
void Shutdown();
|
||||
|
||||
} // namespace Input
|
||||
} // namespace LibRetro
|
||||
440
src/citra_libretro/input/mouse_tracker.cpp
Normal file
440
src/citra_libretro/input/mouse_tracker.cpp
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
#include "citra_libretro/core_settings.h"
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "citra_libretro/input/mouse_tracker.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/frontend/framebuffer_layout.h"
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include <glad/glad.h>
|
||||
#include "video_core/shader/generator/glsl_shader_gen.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VULKAN
|
||||
#include "core/core.h"
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||
#endif
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
namespace Input {
|
||||
|
||||
/// Shared cursor coordinate calculation
|
||||
struct CursorCoordinates {
|
||||
float centerX, centerY;
|
||||
float renderWidth, renderHeight;
|
||||
float boundingLeft, boundingTop, boundingRight, boundingBottom;
|
||||
float verticalLeft, verticalRight, verticalTop, verticalBottom;
|
||||
float horizontalLeft, horizontalRight, horizontalTop, horizontalBottom;
|
||||
|
||||
CursorCoordinates(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
|
||||
float renderRatio, const Layout::FramebufferLayout& layout) {
|
||||
// Convert to normalized device coordinates
|
||||
centerX = (projectedX / bufferWidth) * 2 - 1;
|
||||
centerY = (projectedY / bufferHeight) * 2 - 1;
|
||||
|
||||
renderWidth = renderRatio / bufferWidth;
|
||||
renderHeight = renderRatio / bufferHeight;
|
||||
|
||||
boundingLeft = (layout.bottom_screen.left / (float)bufferWidth) * 2 - 1;
|
||||
boundingTop = (layout.bottom_screen.top / (float)bufferHeight) * 2 - 1;
|
||||
boundingRight = (layout.bottom_screen.right / (float)bufferWidth) * 2 - 1;
|
||||
boundingBottom = (layout.bottom_screen.bottom / (float)bufferHeight) * 2 - 1;
|
||||
|
||||
// Calculate cursor dimensions
|
||||
verticalLeft = std::fmax(centerX - renderWidth / 5, boundingLeft);
|
||||
verticalRight = std::fmin(centerX + renderWidth / 5, boundingRight);
|
||||
verticalTop = -std::fmax(centerY - renderHeight, boundingTop);
|
||||
verticalBottom = -std::fmin(centerY + renderHeight, boundingBottom);
|
||||
|
||||
horizontalLeft = std::fmax(centerX - renderWidth, boundingLeft);
|
||||
horizontalRight = std::fmin(centerX + renderWidth, boundingRight);
|
||||
horizontalTop = -std::fmax(centerY - renderHeight / 5, boundingTop);
|
||||
horizontalBottom = -std::fmin(centerY + renderHeight / 5, boundingBottom);
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper function to check if coordinates are within the touchscreen area
|
||||
/// (uses the same logic as EmuWindow::IsWithinTouchscreen)
|
||||
static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x,
|
||||
unsigned framebuffer_y) {
|
||||
// Note: LibRetro doesn't support SeparateWindows, so we can skip that check
|
||||
|
||||
Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue();
|
||||
|
||||
if (render_3d_mode == Settings::StereoRenderOption::SideBySide ||
|
||||
render_3d_mode == Settings::StereoRenderOption::SideBySideFull) {
|
||||
return (framebuffer_y >= layout.bottom_screen.top &&
|
||||
framebuffer_y < layout.bottom_screen.bottom &&
|
||||
((framebuffer_x >= layout.bottom_screen.left / 2 &&
|
||||
framebuffer_x < layout.bottom_screen.right / 2) ||
|
||||
(framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) &&
|
||||
framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2))));
|
||||
} else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) {
|
||||
return (framebuffer_y >= layout.bottom_screen.top &&
|
||||
framebuffer_y < layout.bottom_screen.bottom &&
|
||||
((framebuffer_x >= layout.bottom_screen.left &&
|
||||
framebuffer_x < layout.bottom_screen.right) ||
|
||||
(framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) &&
|
||||
framebuffer_x < layout.cardboard.bottom_screen_right_eye +
|
||||
layout.bottom_screen.GetWidth() + (layout.width / 2))));
|
||||
} else {
|
||||
return (framebuffer_y >= layout.bottom_screen.top &&
|
||||
framebuffer_y < layout.bottom_screen.bottom &&
|
||||
framebuffer_x >= layout.bottom_screen.left &&
|
||||
framebuffer_x < layout.bottom_screen.right);
|
||||
}
|
||||
}
|
||||
|
||||
MouseTracker::MouseTracker() {
|
||||
// Create renderer-specific cursor renderer based on current graphics API
|
||||
cursor_renderer = nullptr;
|
||||
switch (Settings::values.graphics_api.GetValue()) {
|
||||
case Settings::GraphicsAPI::OpenGL:
|
||||
#ifdef ENABLE_OPENGL
|
||||
cursor_renderer = std::make_unique<OpenGLCursorRenderer>();
|
||||
#endif
|
||||
break;
|
||||
case Settings::GraphicsAPI::Vulkan:
|
||||
#ifdef ENABLE_VULKAN
|
||||
cursor_renderer = std::make_unique<VulkanCursorRenderer>();
|
||||
#endif
|
||||
break;
|
||||
case Settings::GraphicsAPI::Software:
|
||||
cursor_renderer = std::make_unique<SoftwareCursorRenderer>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MouseTracker::~MouseTracker() = default;
|
||||
|
||||
void MouseTracker::OnMouseMove(int deltaX, int deltaY) {
|
||||
x += deltaX;
|
||||
y += deltaY;
|
||||
}
|
||||
|
||||
void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) {
|
||||
x = std::clamp(x, minX, maxX);
|
||||
y = std::clamp(y, minY, maxY);
|
||||
}
|
||||
|
||||
void MouseTracker::Update(int bufferWidth, int bufferHeight,
|
||||
const Layout::FramebufferLayout& layout) {
|
||||
bool state = false;
|
||||
|
||||
if (LibRetro::settings.mouse_touchscreen) {
|
||||
// Check mouse input
|
||||
state |= LibRetro::CheckInput(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT);
|
||||
|
||||
// Read in and convert pointer values to absolute values on the canvas
|
||||
auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X);
|
||||
auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y);
|
||||
auto newX = static_cast<int>((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth);
|
||||
auto newY = static_cast<int>((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight);
|
||||
|
||||
// Use mouse pointer movement
|
||||
if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) {
|
||||
lastMouseX = newX;
|
||||
lastMouseY = newY;
|
||||
|
||||
// Use layout system to validate and map coordinates
|
||||
if (IsWithinTouchscreen(layout, newX, newY)) {
|
||||
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
|
||||
static_cast<int>(layout.bottom_screen.right)) -
|
||||
layout.bottom_screen.left;
|
||||
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
|
||||
static_cast<int>(layout.bottom_screen.bottom)) -
|
||||
layout.bottom_screen.top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LibRetro::settings.touch_touchscreen) {
|
||||
// Check touchscreen input
|
||||
state |= LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED);
|
||||
|
||||
// Read in and convert pointer values to absolute values on the canvas
|
||||
auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X);
|
||||
auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y);
|
||||
auto newX = static_cast<int>((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth);
|
||||
auto newY = static_cast<int>((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight);
|
||||
|
||||
// Use mouse pointer movement
|
||||
if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) {
|
||||
lastMouseX = newX;
|
||||
lastMouseY = newY;
|
||||
|
||||
// Use layout system to validate and map coordinates
|
||||
if (IsWithinTouchscreen(layout, newX, newY)) {
|
||||
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
|
||||
static_cast<int>(layout.bottom_screen.right)) -
|
||||
layout.bottom_screen.left;
|
||||
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
|
||||
static_cast<int>(layout.bottom_screen.bottom)) -
|
||||
layout.bottom_screen.top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LibRetro::settings.analog_function != LibRetro::CStickFunction::CStick) {
|
||||
// Check right analog input
|
||||
state |= LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3);
|
||||
|
||||
// TODO: Provide config option for ratios here
|
||||
auto widthSpeed = (layout.bottom_screen.GetWidth() / 20.0);
|
||||
auto heightSpeed = (layout.bottom_screen.GetHeight() / 20.0);
|
||||
|
||||
// Use controller movement
|
||||
float controllerX =
|
||||
((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT,
|
||||
RETRO_DEVICE_ID_ANALOG_X) /
|
||||
INT16_MAX);
|
||||
float controllerY =
|
||||
((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT,
|
||||
RETRO_DEVICE_ID_ANALOG_Y) /
|
||||
INT16_MAX);
|
||||
|
||||
// Deadzone the controller inputs
|
||||
float smoothedX = std::abs(controllerX);
|
||||
float smoothedY = std::abs(controllerY);
|
||||
|
||||
if (smoothedX < LibRetro::settings.deadzone) {
|
||||
controllerX = 0;
|
||||
}
|
||||
if (smoothedY < LibRetro::settings.deadzone) {
|
||||
controllerY = 0;
|
||||
}
|
||||
|
||||
OnMouseMove(static_cast<int>(controllerX * widthSpeed),
|
||||
static_cast<int>(controllerY * heightSpeed));
|
||||
}
|
||||
|
||||
Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight());
|
||||
|
||||
// Make the coordinates 0 -> 1
|
||||
projectedX = (float)x / layout.bottom_screen.GetWidth();
|
||||
projectedY = (float)y / layout.bottom_screen.GetHeight();
|
||||
|
||||
// Ensure that the projected position doesn't overlap outside the bottom screen framebuffer.
|
||||
// TODO: Provide config option
|
||||
renderRatio = (float)layout.bottom_screen.GetHeight() / 30;
|
||||
|
||||
// Map the mouse coord to the bottom screen's position
|
||||
projectedX = layout.bottom_screen.left + projectedX * layout.bottom_screen.GetWidth();
|
||||
projectedY = layout.bottom_screen.top + projectedY * layout.bottom_screen.GetHeight();
|
||||
|
||||
isPressed = state;
|
||||
|
||||
this->framebuffer_layout = layout;
|
||||
}
|
||||
|
||||
void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) {
|
||||
if (!LibRetro::settings.render_touchscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to renderer-specific implementation
|
||||
if (cursor_renderer) {
|
||||
cursor_renderer->Render(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
|
||||
framebuffer_layout, framebuffer_data);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
// OpenGL-specific cursor renderer implementation
|
||||
OpenGLCursorRenderer::OpenGLCursorRenderer() {
|
||||
// Could potentially also use Citra's built-in shaders, if they can be
|
||||
// wrangled to cooperate.
|
||||
|
||||
std::string vertex;
|
||||
if (Settings::values.use_gles) {
|
||||
vertex += fragment_shader_precision_OES;
|
||||
}
|
||||
|
||||
vertex += R"(
|
||||
in vec2 position;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
std::string fragment;
|
||||
if (Settings::values.use_gles) {
|
||||
fragment += fragment_shader_precision_OES;
|
||||
}
|
||||
fragment += R"(
|
||||
out vec4 color;
|
||||
|
||||
void main()
|
||||
{
|
||||
color = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
vao.Create();
|
||||
vbo.Create();
|
||||
|
||||
glBindVertexArray(vao.handle);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo.handle);
|
||||
|
||||
shader.Create(vertex.c_str(), fragment.c_str());
|
||||
|
||||
auto positionVariable = (GLuint)glGetAttribLocation(shader.handle, "position");
|
||||
glEnableVertexAttribArray(positionVariable);
|
||||
|
||||
glVertexAttribPointer(positionVariable, 2, GL_FLOAT, GL_FALSE, 0, 0);
|
||||
}
|
||||
|
||||
OpenGLCursorRenderer::~OpenGLCursorRenderer() {
|
||||
shader.Release();
|
||||
vao.Release();
|
||||
vbo.Release();
|
||||
}
|
||||
|
||||
void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
|
||||
float projectedY, float renderRatio,
|
||||
const Layout::FramebufferLayout& layout, void* framebuffer_data) {
|
||||
// Use shared coordinate calculation
|
||||
CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
|
||||
layout);
|
||||
|
||||
glUseProgram(shader.handle);
|
||||
|
||||
glBindVertexArray(vao.handle);
|
||||
|
||||
// clang-format off
|
||||
GLfloat cursor[] = {
|
||||
// | in the cursor
|
||||
coords.verticalLeft, coords.verticalTop,
|
||||
coords.verticalRight, coords.verticalTop,
|
||||
coords.verticalRight, coords.verticalBottom,
|
||||
|
||||
coords.verticalLeft, coords.verticalTop,
|
||||
coords.verticalRight, coords.verticalBottom,
|
||||
coords.verticalLeft, coords.verticalBottom,
|
||||
|
||||
// - in the cursor
|
||||
coords.horizontalLeft, coords.horizontalTop,
|
||||
coords.horizontalRight, coords.horizontalTop,
|
||||
coords.horizontalRight, coords.horizontalBottom,
|
||||
|
||||
coords.horizontalLeft, coords.horizontalTop,
|
||||
coords.horizontalRight, coords.horizontalBottom,
|
||||
coords.horizontalLeft, coords.horizontalBottom
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo.handle);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(cursor), cursor, GL_STATIC_DRAW);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 12);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VULKAN
|
||||
// Vulkan-specific cursor renderer implementation
|
||||
VulkanCursorRenderer::VulkanCursorRenderer() {
|
||||
// Vulkan cursor rendering will be integrated into the main rendering pipeline
|
||||
}
|
||||
|
||||
VulkanCursorRenderer::~VulkanCursorRenderer() = default;
|
||||
|
||||
void VulkanCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
|
||||
float projectedY, float renderRatio,
|
||||
const Layout::FramebufferLayout& layout, void* framebuffer_data) {
|
||||
// Use shared coordinate calculation
|
||||
CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
|
||||
layout);
|
||||
|
||||
// TODO: Implement actual Vulkan cursor drawing using the renderer's command buffer
|
||||
// This would involve:
|
||||
// 1. Creating a simple vertex buffer with cursor geometry using coords
|
||||
// 2. Using a basic shader pipeline
|
||||
// 3. Recording draw commands into the current command buffer
|
||||
// 4. Using blend mode similar to OpenGL (ONE_MINUS_DST_COLOR, ONE_MINUS_SRC_COLOR)
|
||||
|
||||
// For now, this is a placeholder - the cursor won't be visible in Vulkan mode
|
||||
// but the touchscreen input will still work
|
||||
}
|
||||
#endif
|
||||
|
||||
// Software-specific cursor renderer implementation
|
||||
SoftwareCursorRenderer::SoftwareCursorRenderer() {
|
||||
// Software renderer initialization
|
||||
}
|
||||
|
||||
SoftwareCursorRenderer::~SoftwareCursorRenderer() = default;
|
||||
|
||||
void SoftwareCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
|
||||
float projectedY, float renderRatio,
|
||||
const Layout::FramebufferLayout& layout,
|
||||
void* framebuffer_data) {
|
||||
if (!framebuffer_data) {
|
||||
return; // No framebuffer data available
|
||||
}
|
||||
|
||||
// Convert coordinates to screen space
|
||||
int centerX = static_cast<int>(projectedX);
|
||||
int centerY = static_cast<int>(projectedY);
|
||||
int radius = static_cast<int>(renderRatio);
|
||||
|
||||
// Calculate cursor dimensions within bounds
|
||||
int verticalLeft = std::max(centerX - radius / 5, static_cast<int>(layout.bottom_screen.left));
|
||||
int verticalRight =
|
||||
std::min(centerX + radius / 5, static_cast<int>(layout.bottom_screen.right));
|
||||
int verticalTop = std::max(centerY - radius, static_cast<int>(layout.bottom_screen.top));
|
||||
int verticalBottom = std::min(centerY + radius, static_cast<int>(layout.bottom_screen.bottom));
|
||||
|
||||
int horizontalLeft = std::max(centerX - radius, static_cast<int>(layout.bottom_screen.left));
|
||||
int horizontalRight = std::min(centerX + radius, static_cast<int>(layout.bottom_screen.right));
|
||||
int horizontalTop = std::max(centerY - radius / 5, static_cast<int>(layout.bottom_screen.top));
|
||||
int horizontalBottom =
|
||||
std::min(centerY + radius / 5, static_cast<int>(layout.bottom_screen.bottom));
|
||||
|
||||
// Draw cursor directly to framebuffer (assuming RGBA8888 format)
|
||||
uint32_t* pixels = static_cast<uint32_t*>(framebuffer_data);
|
||||
const uint32_t cursorColor = 0xFFFFFFFF; // White cursor
|
||||
|
||||
// Draw vertical line of cursor
|
||||
for (int y = verticalTop; y < verticalBottom; ++y) {
|
||||
for (int x = verticalLeft; x < verticalRight; ++x) {
|
||||
if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) {
|
||||
int pixelIndex = y * bufferWidth + x;
|
||||
// XOR blend for visibility on any background
|
||||
pixels[pixelIndex] ^= cursorColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw horizontal line of cursor
|
||||
for (int y = horizontalTop; y < horizontalBottom; ++y) {
|
||||
for (int x = horizontalLeft; x < horizontalRight; ++x) {
|
||||
if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) {
|
||||
int pixelIndex = y * bufferWidth + x;
|
||||
// XOR blend for visibility on any background
|
||||
pixels[pixelIndex] ^= cursorColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Input
|
||||
|
||||
} // namespace LibRetro
|
||||
111
src/citra_libretro/input/mouse_tracker.h
Normal file
111
src/citra_libretro/input/mouse_tracker.h
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/math_util.h"
|
||||
#include "core/frontend/framebuffer_layout.h"
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||
#endif
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
namespace Input {
|
||||
|
||||
class CursorRenderer {
|
||||
public:
|
||||
virtual ~CursorRenderer() = default;
|
||||
virtual void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
|
||||
float renderRatio, const Layout::FramebufferLayout& layout,
|
||||
void* framebuffer_data = nullptr) = 0;
|
||||
};
|
||||
|
||||
/// The mouse tracker provides a mechanism to handle relative mouse/joypad input
|
||||
/// for a touch-screen device.
|
||||
class MouseTracker {
|
||||
public:
|
||||
MouseTracker();
|
||||
~MouseTracker();
|
||||
|
||||
/// Called whenever a mouse moves.
|
||||
void OnMouseMove(int xDelta, int yDelta);
|
||||
|
||||
/// Restricts the mouse cursor to a specified rectangle.
|
||||
void Restrict(int minX, int minY, int maxX, int maxY);
|
||||
|
||||
/// Updates the tracker.
|
||||
void Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout);
|
||||
|
||||
/// Renders the cursor to the screen (delegates to renderer-specific implementation).
|
||||
void Render(int bufferWidth, int bufferHeight, void* framebuffer_data = nullptr);
|
||||
|
||||
/// If the touchscreen is being pressed.
|
||||
bool IsPressed() {
|
||||
return isPressed;
|
||||
}
|
||||
|
||||
/// Get the pressed position, relative to the framebuffer.
|
||||
std::pair<unsigned, unsigned> GetPressedPosition() {
|
||||
return {static_cast<const unsigned int&>(projectedX),
|
||||
static_cast<const unsigned int&>(projectedY)};
|
||||
}
|
||||
|
||||
private:
|
||||
int x;
|
||||
int y;
|
||||
|
||||
float lastMouseX;
|
||||
float lastMouseY;
|
||||
|
||||
float projectedX;
|
||||
float projectedY;
|
||||
float renderRatio;
|
||||
|
||||
bool isPressed;
|
||||
|
||||
Layout::FramebufferLayout framebuffer_layout;
|
||||
std::unique_ptr<CursorRenderer> cursor_renderer;
|
||||
};
|
||||
|
||||
#ifdef ENABLE_OPENGL
|
||||
class OpenGLCursorRenderer : public CursorRenderer {
|
||||
public:
|
||||
OpenGLCursorRenderer();
|
||||
~OpenGLCursorRenderer();
|
||||
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
|
||||
float renderRatio, const Layout::FramebufferLayout& layout,
|
||||
void* framebuffer_data = nullptr) override;
|
||||
|
||||
private:
|
||||
OpenGL::OGLProgram shader;
|
||||
OpenGL::OGLVertexArray vao;
|
||||
OpenGL::OGLBuffer vbo;
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VULKAN
|
||||
class VulkanCursorRenderer : public CursorRenderer {
|
||||
public:
|
||||
VulkanCursorRenderer();
|
||||
~VulkanCursorRenderer();
|
||||
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
|
||||
float renderRatio, const Layout::FramebufferLayout& layout,
|
||||
void* framebuffer_data = nullptr) override;
|
||||
};
|
||||
#endif
|
||||
|
||||
class SoftwareCursorRenderer : public CursorRenderer {
|
||||
public:
|
||||
SoftwareCursorRenderer();
|
||||
~SoftwareCursorRenderer();
|
||||
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
|
||||
float renderRatio, const Layout::FramebufferLayout& layout,
|
||||
void* framebuffer_data = nullptr) override;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
|
||||
} // namespace LibRetro
|
||||
27
src/citra_libretro/libretro.osx.def
Normal file
27
src/citra_libretro/libretro.osx.def
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#LIBRARY "libretro"
|
||||
#EXPORTS
|
||||
_retro_set_environment
|
||||
_retro_set_video_refresh
|
||||
_retro_set_audio_sample
|
||||
_retro_set_audio_sample_batch
|
||||
_retro_set_input_poll
|
||||
_retro_set_input_state
|
||||
_retro_init
|
||||
_retro_deinit
|
||||
_retro_api_version
|
||||
_retro_get_system_info
|
||||
_retro_get_system_av_info
|
||||
_retro_set_controller_port_device
|
||||
_retro_reset
|
||||
_retro_run
|
||||
_retro_serialize_size
|
||||
_retro_serialize
|
||||
_retro_unserialize
|
||||
_retro_cheat_reset
|
||||
_retro_cheat_set
|
||||
_retro_load_game
|
||||
_retro_load_game_special
|
||||
_retro_unload_game
|
||||
_retro_get_region
|
||||
_retro_get_memory_data
|
||||
_retro_get_memory_size
|
||||
860
src/citra_libretro/libretro_vk.cpp
Normal file
860
src/citra_libretro/libretro_vk.cpp
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "citra_libretro/environment.h"
|
||||
#include "citra_libretro/libretro_vk.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/frontend/emu_window.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
static const struct retro_hw_render_interface_vulkan* vulkan_intf;
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
const VkApplicationInfo* GetVulkanApplicationInfo() {
|
||||
static VkApplicationInfo app_info{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app_info.pApplicationName = "Azahar";
|
||||
app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
|
||||
app_info.pEngineName = "Azahar";
|
||||
app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0);
|
||||
// Request Vulkan 1.1 for better compatibility (especially on Android)
|
||||
// Extensions can be used for features beyond 1.1
|
||||
app_info.apiVersion = VK_API_VERSION_1_1;
|
||||
return &app_info;
|
||||
}
|
||||
|
||||
void AddExtensionIfAvailable(std::vector<const char*>& enabled_exts,
|
||||
const std::vector<VkExtensionProperties>& available_exts,
|
||||
const char* ext_name) {
|
||||
// Check if already in the list
|
||||
for (const char* ext : enabled_exts) {
|
||||
if (ext && !strcmp(ext, ext_name)) {
|
||||
return; // Already enabled
|
||||
}
|
||||
}
|
||||
|
||||
// Check if available
|
||||
for (const auto& ext : available_exts) {
|
||||
if (!strcmp(ext.extensionName, ext_name)) {
|
||||
enabled_exts.push_back(ext_name);
|
||||
LOG_INFO(Render_Vulkan, "Enabling Vulkan extension: {}", ext_name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name);
|
||||
}
|
||||
|
||||
bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance,
|
||||
VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const char** required_device_extensions,
|
||||
unsigned num_required_device_extensions,
|
||||
const char** required_device_layers, unsigned num_required_device_layers,
|
||||
const VkPhysicalDeviceFeatures* required_features) {
|
||||
|
||||
LOG_INFO(Render_Vulkan, "CreateDevice callback invoked - negotiating Vulkan device creation");
|
||||
|
||||
// Get available extensions for this physical device
|
||||
uint32_t ext_count = 0;
|
||||
PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties =
|
||||
(PFN_vkEnumerateDeviceExtensionProperties)get_instance_proc_addr(
|
||||
instance, "vkEnumerateDeviceExtensionProperties");
|
||||
|
||||
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, nullptr);
|
||||
std::vector<VkExtensionProperties> available_exts(ext_count);
|
||||
if (ext_count > 0) {
|
||||
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data());
|
||||
}
|
||||
|
||||
// Start with frontend's required extensions
|
||||
std::vector<const char*> enabled_exts;
|
||||
enabled_exts.reserve(num_required_device_extensions + 10);
|
||||
for (unsigned i = 0; i < num_required_device_extensions; i++) {
|
||||
if (required_device_extensions[i]) {
|
||||
enabled_exts.push_back(required_device_extensions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add extensions we want (if available)
|
||||
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_SWAPCHAIN_EXTENSION_NAME);
|
||||
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
||||
AddExtensionIfAvailable(enabled_exts, available_exts,
|
||||
VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
|
||||
AddExtensionIfAvailable(enabled_exts, available_exts,
|
||||
VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
|
||||
AddExtensionIfAvailable(enabled_exts, available_exts, VK_EXT_TOOLING_INFO_EXTENSION_NAME);
|
||||
|
||||
// These are beneficial but blacklisted on some platforms due to driver bugs
|
||||
// For now, let the Instance class handle these decisions
|
||||
// AddExtensionIfAvailable(enabled_exts, available_exts,
|
||||
// VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME);
|
||||
// AddExtensionIfAvailable(enabled_exts, available_exts,
|
||||
// VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
|
||||
|
||||
// Merge frontend's required features with our baseline
|
||||
VkPhysicalDeviceFeatures merged_features{};
|
||||
if (required_features) {
|
||||
// Copy all frontend requirements
|
||||
for (unsigned i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) {
|
||||
if (reinterpret_cast<const VkBool32*>(required_features)[i]) {
|
||||
reinterpret_cast<VkBool32*>(&merged_features)[i] = VK_TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request features we need (these will be OR'd with frontend requirements)
|
||||
// The Instance class will validate these against actual device capabilities
|
||||
merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects
|
||||
merged_features.logicOp = VK_TRUE; // Used for blending modes
|
||||
merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering
|
||||
|
||||
// Find queue family with graphics support
|
||||
PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties =
|
||||
(PFN_vkGetPhysicalDeviceQueueFamilyProperties)get_instance_proc_addr(
|
||||
instance, "vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
|
||||
uint32_t queue_family_count = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, queue_families.data());
|
||||
|
||||
uint32_t graphics_queue_family = VK_QUEUE_FAMILY_IGNORED;
|
||||
for (uint32_t i = 0; i < queue_family_count; i++) {
|
||||
if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||
graphics_queue_family = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (graphics_queue_family == VK_QUEUE_FAMILY_IGNORED) {
|
||||
LOG_CRITICAL(Render_Vulkan, "No graphics queue family found!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create device
|
||||
const float queue_priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
queue_info.queueFamilyIndex = graphics_queue_family;
|
||||
queue_info.queueCount = 1;
|
||||
queue_info.pQueuePriorities = &queue_priority;
|
||||
|
||||
VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
device_info.queueCreateInfoCount = 1;
|
||||
device_info.pQueueCreateInfos = &queue_info;
|
||||
device_info.enabledExtensionCount = static_cast<uint32_t>(enabled_exts.size());
|
||||
device_info.ppEnabledExtensionNames = enabled_exts.data();
|
||||
device_info.enabledLayerCount = num_required_device_layers;
|
||||
device_info.ppEnabledLayerNames = required_device_layers;
|
||||
device_info.pEnabledFeatures = &merged_features;
|
||||
|
||||
PFN_vkCreateDevice vkCreateDevice =
|
||||
(PFN_vkCreateDevice)get_instance_proc_addr(instance, "vkCreateDevice");
|
||||
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkResult result = vkCreateDevice(gpu, &device_info, nullptr, &device);
|
||||
if (result != VK_SUCCESS) {
|
||||
LOG_CRITICAL(Render_Vulkan, "vkCreateDevice failed: {}", static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the queue
|
||||
PFN_vkGetDeviceQueue vkGetDeviceQueue =
|
||||
(PFN_vkGetDeviceQueue)get_instance_proc_addr(instance, "vkGetDeviceQueue");
|
||||
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
vkGetDeviceQueue(device, graphics_queue_family, 0, &queue);
|
||||
|
||||
// Fill in the context for the frontend
|
||||
context->gpu = gpu;
|
||||
context->device = device;
|
||||
context->queue = queue;
|
||||
context->queue_family_index = graphics_queue_family;
|
||||
context->presentation_queue = queue; // Same queue for LibRetro
|
||||
context->presentation_queue_family_index = graphics_queue_family;
|
||||
|
||||
LOG_INFO(Render_Vulkan,
|
||||
"Vulkan device created successfully via negotiation interface (GPU: {}, Queue "
|
||||
"Family: {})",
|
||||
static_cast<void*>(gpu), graphics_queue_family);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanResetContext() {
|
||||
LibRetro::GetHWRenderInterface((void**)&vulkan_intf);
|
||||
|
||||
// Initialize dispatcher with LibRetro's function pointers
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER.init(vulkan_intf->get_instance_proc_addr);
|
||||
|
||||
vk::Instance vk_instance{vulkan_intf->instance};
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance);
|
||||
}
|
||||
|
||||
} // namespace LibRetro
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
std::shared_ptr<Common::DynamicLibrary> OpenLibrary(
|
||||
[[maybe_unused]] Frontend::GraphicsContext* context) {
|
||||
// the frontend takes care of this, we'll get the instance later
|
||||
return std::make_shared<Common::DynamicLibrary>();
|
||||
}
|
||||
|
||||
vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) {
|
||||
// LibRetro cores don't use surfaces - we render to our own output texture
|
||||
// This function should not be called in LibRetro mode
|
||||
LOG_WARNING(Render_Vulkan, "CreateSurface called in LibRetro mode - this should not happen");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
vk::UniqueInstance CreateInstance([[maybe_unused]] const Common::DynamicLibrary& library,
|
||||
[[maybe_unused]] Frontend::WindowSystemType window_type,
|
||||
[[maybe_unused]] bool enable_validation,
|
||||
[[maybe_unused]] bool dump_command_buffers) {
|
||||
// LibRetro cores don't create instances - frontend handles this
|
||||
LOG_WARNING(Render_Vulkan, "CreateInstance called in LibRetro mode - this should not happen");
|
||||
return vk::UniqueInstance{};
|
||||
}
|
||||
|
||||
DebugCallback CreateDebugCallback(vk::Instance instance, bool& debug_utils_supported) {
|
||||
// LibRetro handles debugging, return empty callback
|
||||
debug_utils_supported = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window,
|
||||
[[maybe_unused]] u32 physical_device_index)
|
||||
: Instance(Instance::NoInit{}) {
|
||||
// Ensure LibRetro interface is available
|
||||
if (!vulkan_intf) {
|
||||
LOG_CRITICAL(Render_Vulkan, "LibRetro Vulkan interface not initialized!");
|
||||
throw std::runtime_error("LibRetro Vulkan interface not available");
|
||||
}
|
||||
|
||||
// Initialize basic Vulkan objects from LibRetro
|
||||
physical_device = vulkan_intf->gpu;
|
||||
if (!physical_device) {
|
||||
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid physical device!");
|
||||
throw std::runtime_error("Invalid physical device from LibRetro");
|
||||
}
|
||||
|
||||
// Get device properties and features
|
||||
properties = physical_device.getProperties();
|
||||
|
||||
const std::vector extensions = physical_device.enumerateDeviceExtensionProperties();
|
||||
available_extensions.reserve(extensions.size());
|
||||
for (const auto& extension : extensions) {
|
||||
available_extensions.emplace_back(extension.extensionName.data());
|
||||
}
|
||||
|
||||
// Get queues from LibRetro
|
||||
graphics_queue = vulkan_intf->queue;
|
||||
queue_family_index = vulkan_intf->queue_index;
|
||||
present_queue = graphics_queue; // Same queue for LibRetro
|
||||
|
||||
if (!graphics_queue) {
|
||||
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid graphics queue!");
|
||||
throw std::runtime_error("Invalid graphics queue from LibRetro");
|
||||
}
|
||||
|
||||
// Initialize Vulkan HPP dispatcher with LibRetro's device
|
||||
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device});
|
||||
|
||||
// Now run device capability detection with dispatcher initialized
|
||||
CreateDevice();
|
||||
|
||||
// LibRetro-specific: Validate function pointers are actually available
|
||||
// LibRetro's device may not have loaded all extension functions even if extensions are
|
||||
// available
|
||||
if (extended_dynamic_state) {
|
||||
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetCullModeEXT ||
|
||||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthTestEnableEXT ||
|
||||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthWriteEnableEXT ||
|
||||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetFrontFaceEXT) {
|
||||
LOG_WARNING(Render_Vulkan, "Extended dynamic state function pointers not available in "
|
||||
"LibRetro context, disabling");
|
||||
extended_dynamic_state = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (timeline_semaphores) {
|
||||
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkGetSemaphoreCounterValueKHR) {
|
||||
LOG_WARNING(Render_Vulkan, "Timeline semaphore function pointers not available in "
|
||||
"LibRetro context, disabling");
|
||||
timeline_semaphores = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize subsystems
|
||||
CreateAllocator();
|
||||
CreateFormatTable();
|
||||
CollectToolingInfo();
|
||||
CreateCustomFormatTable();
|
||||
CreateAttribTable();
|
||||
|
||||
LOG_INFO(Render_Vulkan, "LibRetro Vulkan Instance initialized successfully");
|
||||
LOG_INFO(Render_Vulkan, "Device: {} ({})", properties.deviceName.data(), GetVendorName());
|
||||
LOG_INFO(Render_Vulkan, "Driver: {}", GetDriverVersionName());
|
||||
}
|
||||
|
||||
vk::Instance LibRetroVKInstance::GetInstance() const {
|
||||
return vk::Instance{vulkan_intf->instance};
|
||||
}
|
||||
|
||||
vk::Device LibRetroVKInstance::GetDevice() const {
|
||||
return vk::Device{vulkan_intf->device};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PresentWindow Implementation (LibRetro version)
|
||||
// ============================================================================
|
||||
|
||||
PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_,
|
||||
Scheduler& scheduler_, [[maybe_unused]] bool low_refresh_rate)
|
||||
: emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_},
|
||||
graphics_queue{instance.GetGraphicsQueue()} {
|
||||
const vk::Device device = instance.GetDevice();
|
||||
|
||||
LOG_INFO(Render_Vulkan, "Initializing LibRetro PresentWindow");
|
||||
|
||||
// Create command pool for frame operations
|
||||
const vk::CommandPoolCreateInfo pool_info = {
|
||||
.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer |
|
||||
vk::CommandPoolCreateFlagBits::eTransient,
|
||||
.queueFamilyIndex = instance.GetGraphicsQueueFamilyIndex(),
|
||||
};
|
||||
command_pool = device.createCommandPool(pool_info);
|
||||
|
||||
// Create render pass for LibRetro output
|
||||
present_renderpass = CreateRenderpass();
|
||||
|
||||
// Start with initial dimensions from layout
|
||||
const auto& layout = emu_window.GetFramebufferLayout();
|
||||
CreateOutputTexture(layout.width, layout.height);
|
||||
CreateFrameResources();
|
||||
|
||||
LOG_INFO(Render_Vulkan, "LibRetro PresentWindow initialized with {}x{}", layout.width,
|
||||
layout.height);
|
||||
}
|
||||
|
||||
PresentWindow::~PresentWindow() {
|
||||
const vk::Device device = instance.GetDevice();
|
||||
|
||||
LOG_DEBUG(Render_Vulkan, "Destroying LibRetro PresentWindow");
|
||||
|
||||
// Wait for any pending operations
|
||||
WaitPresent();
|
||||
device.waitIdle();
|
||||
|
||||
// Destroy frame resources
|
||||
DestroyFrameResources();
|
||||
|
||||
// Destroy output texture
|
||||
DestroyOutputTexture();
|
||||
|
||||
// Destroy Vulkan objects
|
||||
if (command_pool) {
|
||||
device.destroyCommandPool(command_pool);
|
||||
}
|
||||
if (present_renderpass) {
|
||||
device.destroyRenderPass(present_renderpass);
|
||||
}
|
||||
}
|
||||
|
||||
void PresentWindow::CreateOutputTexture(u32 width, u32 height) {
|
||||
if (width == 0 || height == 0) {
|
||||
LOG_ERROR(Render_Vulkan, "Invalid output texture dimensions: {}x{}", width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy existing texture if dimensions changed
|
||||
if (output_image && (output_width != width || output_height != height)) {
|
||||
DestroyOutputTexture();
|
||||
}
|
||||
|
||||
// Skip if already created with correct dimensions
|
||||
if (output_image && output_width == width && output_height == height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vk::Device device = instance.GetDevice();
|
||||
output_width = width;
|
||||
output_height = height;
|
||||
|
||||
// Create output image with LibRetro requirements
|
||||
const vk::ImageCreateInfo image_info = {
|
||||
.imageType = vk::ImageType::e2D,
|
||||
.format = output_format,
|
||||
.extent = {width, height, 1},
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = 1,
|
||||
.samples = vk::SampleCountFlagBits::e1,
|
||||
.tiling = vk::ImageTiling::eOptimal,
|
||||
.usage = vk::ImageUsageFlagBits::eColorAttachment | // For rendering
|
||||
vk::ImageUsageFlagBits::eTransferSrc | // Required by LibRetro
|
||||
vk::ImageUsageFlagBits::eSampled | // Required by LibRetro
|
||||
vk::ImageUsageFlagBits::eTransferDst, // For clearing
|
||||
.sharingMode = vk::SharingMode::eExclusive,
|
||||
.initialLayout = vk::ImageLayout::eUndefined,
|
||||
};
|
||||
|
||||
// Create image with VMA - using budget-aware allocation like standalone version
|
||||
VmaAllocationCreateInfo alloc_info = {};
|
||||
alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
alloc_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
||||
|
||||
VkImage vk_image;
|
||||
const VkResult result = vmaCreateImage(instance.GetAllocator(),
|
||||
reinterpret_cast<const VkImageCreateInfo*>(&image_info),
|
||||
&alloc_info, &vk_image, &output_allocation, nullptr);
|
||||
|
||||
if (result != VK_SUCCESS) {
|
||||
LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast<int>(result));
|
||||
throw std::runtime_error("Failed to create LibRetro output texture");
|
||||
}
|
||||
|
||||
output_image = vk::Image{vk_image};
|
||||
|
||||
// Create image view
|
||||
output_view_create_info = {
|
||||
.image = output_image,
|
||||
.viewType = vk::ImageViewType::e2D,
|
||||
.format = output_format,
|
||||
.components =
|
||||
{
|
||||
.r = vk::ComponentSwizzle::eIdentity,
|
||||
.g = vk::ComponentSwizzle::eIdentity,
|
||||
.b = vk::ComponentSwizzle::eIdentity,
|
||||
.a = vk::ComponentSwizzle::eIdentity,
|
||||
},
|
||||
.subresourceRange =
|
||||
{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
};
|
||||
output_image_view = device.createImageView(output_view_create_info);
|
||||
|
||||
LOG_DEBUG(Render_Vulkan, "Created LibRetro output texture: {}x{}", width, height);
|
||||
}
|
||||
|
||||
void PresentWindow::DestroyOutputTexture() {
|
||||
if (!output_image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vk::Device device = instance.GetDevice();
|
||||
|
||||
if (output_image_view) {
|
||||
device.destroyImageView(output_image_view);
|
||||
output_image_view = nullptr;
|
||||
}
|
||||
|
||||
if (output_allocation) {
|
||||
vmaDestroyImage(instance.GetAllocator(), static_cast<VkImage>(output_image),
|
||||
output_allocation);
|
||||
output_allocation = {};
|
||||
}
|
||||
|
||||
output_image = nullptr;
|
||||
output_width = 0;
|
||||
output_height = 0;
|
||||
}
|
||||
|
||||
vk::RenderPass PresentWindow::CreateRenderpass() {
|
||||
const vk::AttachmentDescription color_attachment = {
|
||||
.format = output_format,
|
||||
.samples = vk::SampleCountFlagBits::e1,
|
||||
.loadOp = vk::AttachmentLoadOp::eClear,
|
||||
.storeOp = vk::AttachmentStoreOp::eStore,
|
||||
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
|
||||
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
|
||||
.initialLayout = vk::ImageLayout::eUndefined,
|
||||
.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal, // Ready for LibRetro
|
||||
};
|
||||
|
||||
const vk::AttachmentReference color_ref = {
|
||||
.attachment = 0,
|
||||
.layout = vk::ImageLayout::eColorAttachmentOptimal,
|
||||
};
|
||||
|
||||
const vk::SubpassDescription subpass = {
|
||||
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &color_ref,
|
||||
};
|
||||
|
||||
const vk::SubpassDependency dependency = {
|
||||
.srcSubpass = VK_SUBPASS_EXTERNAL,
|
||||
.dstSubpass = 0,
|
||||
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
||||
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
||||
.srcAccessMask = {},
|
||||
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
|
||||
};
|
||||
|
||||
const vk::RenderPassCreateInfo renderpass_info = {
|
||||
.attachmentCount = 1,
|
||||
.pAttachments = &color_attachment,
|
||||
.subpassCount = 1,
|
||||
.pSubpasses = &subpass,
|
||||
.dependencyCount = 1,
|
||||
.pDependencies = &dependency,
|
||||
};
|
||||
|
||||
return instance.GetDevice().createRenderPass(renderpass_info);
|
||||
}
|
||||
|
||||
void PresentWindow::CreateFrameResources() {
|
||||
const vk::Device device = instance.GetDevice();
|
||||
const u32 frame_count = 2; // Double buffering for LibRetro
|
||||
|
||||
// Destroy existing frames
|
||||
DestroyFrameResources();
|
||||
|
||||
// Create frame pool
|
||||
frame_pool.resize(frame_count);
|
||||
|
||||
// Allocate command buffers
|
||||
const vk::CommandBufferAllocateInfo alloc_info = {
|
||||
.commandPool = command_pool,
|
||||
.level = vk::CommandBufferLevel::ePrimary,
|
||||
.commandBufferCount = frame_count,
|
||||
};
|
||||
const std::vector command_buffers = device.allocateCommandBuffers(alloc_info);
|
||||
|
||||
// Initialize frames
|
||||
for (u32 i = 0; i < frame_count; i++) {
|
||||
Frame& frame = frame_pool[i];
|
||||
frame.width = output_width;
|
||||
frame.height = output_height;
|
||||
frame.image = output_image; // All frames use the same output texture
|
||||
frame.image_view = output_image_view;
|
||||
frame.allocation = {}; // VMA allocation handled separately
|
||||
frame.cmdbuf = command_buffers[i];
|
||||
frame.render_ready = device.createSemaphore({});
|
||||
frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled});
|
||||
|
||||
// Create framebuffer for this frame
|
||||
const vk::FramebufferCreateInfo fb_info = {
|
||||
.renderPass = present_renderpass,
|
||||
.attachmentCount = 1,
|
||||
.pAttachments = &output_image_view,
|
||||
.width = output_width,
|
||||
.height = output_height,
|
||||
.layers = 1,
|
||||
};
|
||||
frame.framebuffer = device.createFramebuffer(fb_info);
|
||||
}
|
||||
|
||||
LOG_DEBUG(Render_Vulkan, "Created {} frame resources for LibRetro", frame_count);
|
||||
}
|
||||
|
||||
void PresentWindow::DestroyFrameResources() {
|
||||
if (frame_pool.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vk::Device device = instance.GetDevice();
|
||||
|
||||
for (auto& frame : frame_pool) {
|
||||
if (frame.framebuffer) {
|
||||
device.destroyFramebuffer(frame.framebuffer);
|
||||
}
|
||||
if (frame.render_ready) {
|
||||
device.destroySemaphore(frame.render_ready);
|
||||
}
|
||||
if (frame.present_done) {
|
||||
device.destroyFence(frame.present_done);
|
||||
}
|
||||
}
|
||||
|
||||
frame_pool.clear();
|
||||
current_frame_index = 0;
|
||||
}
|
||||
|
||||
Frame* PresentWindow::GetRenderFrame() {
|
||||
if (frame_pool.empty()) {
|
||||
LOG_ERROR(Render_Vulkan, "No frames available in LibRetro PresentWindow");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// RetroArch may not call context_reset during fullscreen toggle, leaving us
|
||||
// with a stale interface pointer that can crash
|
||||
const struct retro_hw_render_interface_vulkan* current_intf = nullptr;
|
||||
if (!LibRetro::GetHWRenderInterface((void**)¤t_intf) || !current_intf) {
|
||||
LOG_ERROR(Render_Vulkan, "Failed to get current Vulkan interface");
|
||||
return &frame_pool[current_frame_index];
|
||||
}
|
||||
|
||||
// Update global interface if it changed
|
||||
if (current_intf != vulkan_intf) {
|
||||
LOG_INFO(Render_Vulkan, "Vulkan interface changed during runtime from {} to {}",
|
||||
static_cast<const void*>(vulkan_intf), static_cast<const void*>(current_intf));
|
||||
vulkan_intf = current_intf;
|
||||
}
|
||||
|
||||
// LibRetro synchronization: Use LibRetro's wait mechanism instead of fences
|
||||
if (vulkan_intf && vulkan_intf->wait_sync_index && vulkan_intf->handle) {
|
||||
vulkan_intf->wait_sync_index(vulkan_intf->handle);
|
||||
}
|
||||
|
||||
// Use LibRetro's sync index for frame selection if available
|
||||
u32 frame_index = current_frame_index;
|
||||
if (vulkan_intf && vulkan_intf->get_sync_index && vulkan_intf->handle) {
|
||||
LOG_TRACE(Render_Vulkan, "Calling get_sync_index with handle: {}",
|
||||
static_cast<void*>(vulkan_intf->handle));
|
||||
|
||||
const u32 sync_index = vulkan_intf->get_sync_index(vulkan_intf->handle);
|
||||
frame_index = sync_index % frame_pool.size();
|
||||
LOG_TRACE(Render_Vulkan, "LibRetro sync index: {}, using frame: {}", sync_index,
|
||||
frame_index);
|
||||
}
|
||||
|
||||
return &frame_pool[frame_index];
|
||||
}
|
||||
|
||||
void PresentWindow::RecreateFrame(Frame* frame, u32 width, u32 height) {
|
||||
if (!frame) {
|
||||
LOG_ERROR(Render_Vulkan, "Invalid frame for recreation");
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame->width == width && frame->height == height) {
|
||||
return; // No change needed
|
||||
}
|
||||
|
||||
LOG_DEBUG(Render_Vulkan, "Recreating LibRetro frame: {}x{} -> {}x{}", frame->width,
|
||||
frame->height, width, height);
|
||||
|
||||
// Wait for frame to be idle
|
||||
const vk::Device device = instance.GetDevice();
|
||||
[[maybe_unused]] const vk::Result wait_result =
|
||||
device.waitForFences(frame->present_done, VK_TRUE, UINT64_MAX);
|
||||
|
||||
// Recreate output texture with new dimensions
|
||||
CreateOutputTexture(width, height);
|
||||
|
||||
// Recreate frame resources
|
||||
CreateFrameResources();
|
||||
|
||||
LOG_INFO(Render_Vulkan, "LibRetro frame recreated for {}x{}", width, height);
|
||||
}
|
||||
|
||||
void PresentWindow::Present(Frame* frame) {
|
||||
if (!frame) {
|
||||
LOG_ERROR(Render_Vulkan, "Cannot present null frame");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vulkan_intf) {
|
||||
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for presentation");
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Use persistent struct to avoid stack lifetime issues!
|
||||
// RetroArch may cache this pointer for frame duping during pause
|
||||
persistent_libretro_image.image_view = static_cast<VkImageView>(frame->image_view);
|
||||
persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
persistent_libretro_image.create_info =
|
||||
static_cast<VkImageViewCreateInfo>(output_view_create_info);
|
||||
|
||||
vulkan_intf->set_image(vulkan_intf->handle, &persistent_libretro_image, 0, nullptr,
|
||||
instance.GetGraphicsQueueFamilyIndex());
|
||||
|
||||
// Call EmuWindow SwapBuffers to trigger LibRetro video frame submission
|
||||
emu_window.SwapBuffers();
|
||||
|
||||
// LibRetro manages frame indices via sync_index, so we don't manually increment
|
||||
// current_frame_index = (current_frame_index + 1) % frame_pool.size();
|
||||
|
||||
LOG_TRACE(Render_Vulkan, "Frame presented to LibRetro: {}x{}", frame->width, frame->height);
|
||||
}
|
||||
|
||||
void PresentWindow::WaitPresent() {
|
||||
if (frame_pool.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vk::Device device = instance.GetDevice();
|
||||
|
||||
// Wait for all frames to complete
|
||||
std::vector<vk::Fence> fences;
|
||||
fences.reserve(frame_pool.size());
|
||||
|
||||
for (const auto& frame : frame_pool) {
|
||||
fences.push_back(frame.present_done);
|
||||
}
|
||||
|
||||
if (!fences.empty()) {
|
||||
[[maybe_unused]] const vk::Result wait_result =
|
||||
device.waitForFences(fences, VK_TRUE, UINT64_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
void PresentWindow::NotifySurfaceChanged() {
|
||||
// LibRetro doesn't use surfaces, so this is a no-op
|
||||
LOG_DEBUG(Render_Vulkan, "Surface change notification ignored in LibRetro mode");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MasterSemaphoreLibRetro Implementation
|
||||
// ============================================================================
|
||||
|
||||
constexpr u64 FENCE_RESERVE = 8;
|
||||
|
||||
MasterSemaphoreLibRetro::MasterSemaphoreLibRetro(const Instance& instance_) : instance{instance_} {
|
||||
const vk::Device device{instance.GetDevice()};
|
||||
// Pre-allocate fence pool
|
||||
for (u64 i = 0; i < FENCE_RESERVE; i++) {
|
||||
free_queue.push_back(device.createFence({}));
|
||||
}
|
||||
// Start background wait thread
|
||||
wait_thread = std::jthread([this](std::stop_token token) { WaitThread(token); });
|
||||
}
|
||||
|
||||
MasterSemaphoreLibRetro::~MasterSemaphoreLibRetro() {
|
||||
// wait_thread will be automatically stopped by jthread destructor
|
||||
// Clean up remaining fences
|
||||
const vk::Device device{instance.GetDevice()};
|
||||
for (const auto& fence : free_queue) {
|
||||
device.destroyFence(fence);
|
||||
}
|
||||
}
|
||||
|
||||
void MasterSemaphoreLibRetro::Refresh() {}
|
||||
|
||||
void MasterSemaphoreLibRetro::Wait(u64 tick) {
|
||||
std::unique_lock lock{free_mutex};
|
||||
free_cv.wait(lock, [this, tick] { return gpu_tick.load(std::memory_order_relaxed) >= tick; });
|
||||
}
|
||||
|
||||
void MasterSemaphoreLibRetro::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait,
|
||||
vk::Semaphore signal, u64 signal_value) {
|
||||
if (!vulkan_intf) {
|
||||
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for command submission");
|
||||
return;
|
||||
}
|
||||
|
||||
cmdbuf.end();
|
||||
|
||||
// Get a fence from the pool
|
||||
const vk::Fence fence = GetFreeFence();
|
||||
|
||||
// Strip semaphores - RetroArch handles frame sync, we track resources internally
|
||||
const vk::SubmitInfo submit_info = {
|
||||
.waitSemaphoreCount = 0,
|
||||
.pWaitSemaphores = nullptr,
|
||||
.pWaitDstStageMask = nullptr,
|
||||
.commandBufferCount = 1u,
|
||||
.pCommandBuffers = &cmdbuf,
|
||||
.signalSemaphoreCount = 0,
|
||||
.pSignalSemaphores = nullptr,
|
||||
};
|
||||
|
||||
// Use LibRetro's queue coordination
|
||||
if (vulkan_intf->lock_queue) {
|
||||
vulkan_intf->lock_queue(vulkan_intf->handle);
|
||||
}
|
||||
|
||||
try {
|
||||
// Submit with fence for internal resource tracking
|
||||
vk::Queue queue{vulkan_intf->queue};
|
||||
queue.submit(submit_info, fence);
|
||||
|
||||
if (vulkan_intf->unlock_queue) {
|
||||
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
||||
}
|
||||
} catch (vk::DeviceLostError& err) {
|
||||
if (vulkan_intf->unlock_queue) {
|
||||
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
||||
}
|
||||
UNREACHABLE_MSG("Device lost during submit: {}", err.what());
|
||||
} catch (...) {
|
||||
if (vulkan_intf->unlock_queue) {
|
||||
vulkan_intf->unlock_queue(vulkan_intf->handle);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
// Enqueue fence for wait thread to process
|
||||
{
|
||||
std::scoped_lock lock{wait_mutex};
|
||||
wait_queue.emplace(fence, signal_value);
|
||||
wait_cv.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
void MasterSemaphoreLibRetro::WaitThread(std::stop_token token) {
|
||||
const vk::Device device{instance.GetDevice()};
|
||||
|
||||
while (!token.stop_requested()) {
|
||||
vk::Fence fence;
|
||||
u64 signal_value;
|
||||
|
||||
// Wait for work
|
||||
{
|
||||
std::unique_lock lock{wait_mutex};
|
||||
Common::CondvarWait(wait_cv, lock, token, [this] { return !wait_queue.empty(); });
|
||||
if (token.stop_requested()) {
|
||||
return;
|
||||
}
|
||||
std::tie(fence, signal_value) = wait_queue.front();
|
||||
wait_queue.pop();
|
||||
}
|
||||
|
||||
// Wait for fence (blocks only this background thread)
|
||||
const vk::Result result = device.waitForFences(fence, true, UINT64_MAX);
|
||||
if (result != vk::Result::eSuccess) {
|
||||
LOG_ERROR(Render_Vulkan, "Fence wait failed: {}", vk::to_string(result));
|
||||
}
|
||||
|
||||
// Reset fence and return to pool
|
||||
device.resetFences(fence);
|
||||
|
||||
// Update GPU tick - signals main thread's Wait()
|
||||
gpu_tick.store(signal_value, std::memory_order_release);
|
||||
|
||||
// Return fence to pool
|
||||
{
|
||||
std::scoped_lock lock{free_mutex};
|
||||
free_queue.push_back(fence);
|
||||
free_cv.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vk::Fence MasterSemaphoreLibRetro::GetFreeFence() {
|
||||
std::scoped_lock lock{free_mutex};
|
||||
if (free_queue.empty()) {
|
||||
// Pool exhausted - create new fence
|
||||
return instance.GetDevice().createFence({});
|
||||
}
|
||||
|
||||
const vk::Fence fence = free_queue.front();
|
||||
free_queue.pop_front();
|
||||
return fence;
|
||||
}
|
||||
|
||||
// Factory function for scheduler to create LibRetro MasterSemaphore
|
||||
std::unique_ptr<MasterSemaphore> CreateLibRetroMasterSemaphore(const Instance& instance) {
|
||||
return std::make_unique<MasterSemaphoreLibRetro>(instance);
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
175
src/citra_libretro/libretro_vk.h
Normal file
175
src/citra_libretro/libretro_vk.h
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/dynamic_library/dynamic_library.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
|
||||
#include "libretro_vulkan.h"
|
||||
|
||||
VK_DEFINE_HANDLE(VmaAllocation)
|
||||
|
||||
namespace LibRetro {
|
||||
|
||||
extern void VulkanResetContext();
|
||||
|
||||
/// Returns VkApplicationInfo for negotiation interface
|
||||
const VkApplicationInfo* GetVulkanApplicationInfo();
|
||||
|
||||
/// CreateDevice callback for negotiation interface
|
||||
bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance,
|
||||
VkPhysicalDevice gpu, VkSurfaceKHR surface,
|
||||
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
|
||||
const char** required_device_extensions,
|
||||
unsigned num_required_device_extensions,
|
||||
const char** required_device_layers, unsigned num_required_device_layers,
|
||||
const VkPhysicalDeviceFeatures* required_features);
|
||||
|
||||
} // namespace LibRetro
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class LibRetroVKInstance : public Instance {
|
||||
public:
|
||||
explicit LibRetroVKInstance(Frontend::EmuWindow& window, u32 physical_device_index);
|
||||
|
||||
/// Returns the Vulkan instance
|
||||
vk::Instance GetInstance() const override;
|
||||
|
||||
/// Returns the Vulkan device
|
||||
vk::Device GetDevice() const override;
|
||||
};
|
||||
|
||||
class Scheduler;
|
||||
class RenderManager;
|
||||
class MasterSemaphore;
|
||||
|
||||
/// LibRetro-specific MasterSemaphore implementation
|
||||
class MasterSemaphoreLibRetro : public MasterSemaphore {
|
||||
using Waitable = std::pair<vk::Fence, u64>;
|
||||
|
||||
public:
|
||||
explicit MasterSemaphoreLibRetro(const Instance& instance);
|
||||
~MasterSemaphoreLibRetro() override;
|
||||
|
||||
void Refresh() override;
|
||||
void Wait(u64 tick) override;
|
||||
void SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, vk::Semaphore signal,
|
||||
u64 signal_value) override;
|
||||
|
||||
private:
|
||||
void WaitThread(std::stop_token token);
|
||||
vk::Fence GetFreeFence();
|
||||
|
||||
const Instance& instance;
|
||||
std::deque<vk::Fence> free_queue;
|
||||
std::queue<Waitable> wait_queue;
|
||||
std::mutex free_mutex;
|
||||
std::mutex wait_mutex;
|
||||
std::condition_variable free_cv;
|
||||
std::condition_variable_any wait_cv;
|
||||
std::jthread wait_thread;
|
||||
};
|
||||
|
||||
/// Factory function for scheduler to create LibRetro MasterSemaphore
|
||||
std::unique_ptr<MasterSemaphore> CreateLibRetroMasterSemaphore(const Instance& instance);
|
||||
|
||||
struct Frame {
|
||||
u32 width;
|
||||
u32 height;
|
||||
VmaAllocation allocation;
|
||||
vk::Framebuffer framebuffer;
|
||||
vk::Image image;
|
||||
vk::ImageView image_view;
|
||||
vk::Semaphore render_ready;
|
||||
vk::Fence present_done;
|
||||
vk::CommandBuffer cmdbuf;
|
||||
};
|
||||
|
||||
/// LibRetro-specific PresentWindow implementation (same interface as desktop version)
|
||||
class PresentWindow final {
|
||||
public:
|
||||
explicit PresentWindow(Frontend::EmuWindow& emu_window, const Instance& instance,
|
||||
Scheduler& scheduler, bool low_refresh_rate);
|
||||
~PresentWindow();
|
||||
|
||||
/// Waits for all queued frames to finish presenting.
|
||||
void WaitPresent();
|
||||
|
||||
/// Returns the last used render frame.
|
||||
Frame* GetRenderFrame();
|
||||
|
||||
/// Recreates the render frame to match provided parameters.
|
||||
void RecreateFrame(Frame* frame, u32 width, u32 height);
|
||||
|
||||
/// Queues the provided frame for presentation.
|
||||
void Present(Frame* frame);
|
||||
|
||||
/// This is called to notify the rendering backend of a surface change
|
||||
void NotifySurfaceChanged();
|
||||
|
||||
[[nodiscard]] vk::RenderPass Renderpass() const noexcept {
|
||||
return present_renderpass;
|
||||
}
|
||||
|
||||
u32 ImageCount() const noexcept {
|
||||
return static_cast<u32>(frame_pool.size());
|
||||
}
|
||||
|
||||
private:
|
||||
/// Creates the render pass for LibRetro output
|
||||
vk::RenderPass CreateRenderpass();
|
||||
|
||||
/// Creates output texture for LibRetro submission
|
||||
void CreateOutputTexture(u32 width, u32 height);
|
||||
|
||||
/// Destroys current output texture
|
||||
void DestroyOutputTexture();
|
||||
|
||||
/// Creates frame resources
|
||||
void CreateFrameResources();
|
||||
|
||||
/// Destroys frame resources
|
||||
void DestroyFrameResources();
|
||||
|
||||
private:
|
||||
Frontend::EmuWindow& emu_window;
|
||||
const Instance& instance;
|
||||
[[maybe_unused]] Scheduler& scheduler;
|
||||
|
||||
// LibRetro output texture (replaces swapchain)
|
||||
vk::Image output_image{};
|
||||
vk::ImageView output_image_view{};
|
||||
VmaAllocation output_allocation{};
|
||||
vk::Format output_format{vk::Format::eR8G8B8A8Unorm};
|
||||
vk::ImageViewCreateInfo output_view_create_info{};
|
||||
|
||||
// Frame management
|
||||
vk::RenderPass present_renderpass{};
|
||||
vk::CommandPool command_pool{};
|
||||
std::vector<Frame> frame_pool;
|
||||
u32 current_frame_index{0};
|
||||
|
||||
// Current output dimensions
|
||||
u32 output_width{0};
|
||||
u32 output_height{0};
|
||||
|
||||
// Vulkan objects
|
||||
vk::Queue graphics_queue{};
|
||||
|
||||
// Persistent LibRetro image descriptor, must persist across frames for RetroArch frame duping
|
||||
// during pause
|
||||
retro_vulkan_image persistent_libretro_image{};
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
|
|
@ -63,8 +63,6 @@ add_library(citra_common STATIC
|
|||
aarch64/oaknut_abi.h
|
||||
aarch64/oaknut_util.h
|
||||
alignment.h
|
||||
android_storage.h
|
||||
android_storage.cpp
|
||||
announce_multiplayer_room.h
|
||||
arch.h
|
||||
archives.h
|
||||
|
|
@ -172,7 +170,7 @@ if (UNIX AND NOT APPLE)
|
|||
target_link_libraries(citra_common PRIVATE gamemode)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
if (APPLE AND NOT ENABLE_LIBRETRO)
|
||||
target_sources(citra_common PUBLIC
|
||||
apple_authorization.h
|
||||
apple_authorization.cpp
|
||||
|
|
@ -181,6 +179,14 @@ if (APPLE)
|
|||
)
|
||||
endif()
|
||||
|
||||
# Android storage is only used for non-libretro Android builds
|
||||
if (ANDROID AND NOT ENABLE_LIBRETRO)
|
||||
target_sources(citra_common PRIVATE
|
||||
android_storage.cpp
|
||||
android_storage.h
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MSVC)
|
||||
target_compile_options(citra_common PRIVATE
|
||||
/W4
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// Copyright 2013 Dolphin Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -31,7 +35,7 @@ std::string NativeErrorToString(int e) {
|
|||
#else
|
||||
char err_str[255];
|
||||
#if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) || \
|
||||
defined(ANDROID)
|
||||
(defined(ANDROID) && !defined(HAVE_LIBRETRO))
|
||||
// Thread safe (GNU-specific)
|
||||
const char* str = strerror_r(e, err_str, sizeof(err_str));
|
||||
return std::string(str);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@
|
|||
#define fseeko _fseeki64
|
||||
#define ftello _ftelli64
|
||||
#define fileno _fileno
|
||||
typedef struct _stat64 file_stat_t;
|
||||
#define fstat _fstat64
|
||||
#elif defined(HAVE_LIBRETRO)
|
||||
typedef struct _stat64 file_stat_t;
|
||||
#else
|
||||
typedef struct stat file_stat_t;
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
|
@ -56,6 +62,7 @@
|
|||
#include <dirent.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
typedef struct stat file_stat_t;
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
|
@ -75,7 +82,7 @@
|
|||
|
||||
#endif
|
||||
|
||||
#ifdef ANDROID
|
||||
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
#include "common/android_storage.h"
|
||||
#include "common/string_util.h"
|
||||
#endif
|
||||
|
|
@ -87,6 +94,36 @@
|
|||
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
#define SKIP_STDIO_REDEFINES
|
||||
#include <streams/file_stream.h>
|
||||
#include <streams/file_stream_transforms.h>
|
||||
|
||||
#define FILE RFILE
|
||||
#define FTELL rftell
|
||||
#define FOPEN rfopen
|
||||
#define FCLOSE rfclose
|
||||
#define FSEEK rfseek
|
||||
#define FREAD rfread
|
||||
#define FWRITE rfwrite
|
||||
#define FEOF rfeof
|
||||
#define FERROR rferror
|
||||
#define FFLUSH rfflush
|
||||
|
||||
#else
|
||||
|
||||
#define FTELL ftello
|
||||
#define FOPEN fopen
|
||||
#define FCLOSE std::fclose
|
||||
#define FSEEK fseeko
|
||||
#define FREAD std::fread
|
||||
#define FWRITE std::fwrite
|
||||
#define FEOF feof
|
||||
#define FERROR ferror
|
||||
#define FFLUSH std::fflush
|
||||
|
||||
#endif
|
||||
|
||||
// This namespace has various generic functions related to files and paths.
|
||||
// The code still needs a ton of cleanup.
|
||||
// REMEMBER: strdup considered harmful!
|
||||
|
|
@ -119,7 +156,7 @@ bool Exists(const std::string& filename) {
|
|||
copy += DIR_SEP_CHR;
|
||||
|
||||
int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
int result = AndroidStorage::FileExists(filename) ? 0 : -1;
|
||||
#else
|
||||
struct stat file_info;
|
||||
|
|
@ -130,7 +167,7 @@ bool Exists(const std::string& filename) {
|
|||
}
|
||||
|
||||
bool IsDirectory(const std::string& filename) {
|
||||
#ifdef ANDROID
|
||||
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
return AndroidStorage::IsDirectory(filename);
|
||||
#endif
|
||||
|
||||
|
|
@ -178,7 +215,7 @@ bool Delete(const std::string& filename) {
|
|||
LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg());
|
||||
return false;
|
||||
}
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
if (!AndroidStorage::DeleteDocument(filename)) {
|
||||
LOG_ERROR(Common_Filesystem, "unlink failed on {}", filename);
|
||||
return false;
|
||||
|
|
@ -205,7 +242,7 @@ bool CreateDir(const std::string& path) {
|
|||
}
|
||||
LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error);
|
||||
return false;
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
std::string directory = path;
|
||||
std::string filename = path;
|
||||
if (Common::EndsWith(path, "/")) {
|
||||
|
|
@ -292,7 +329,7 @@ bool DeleteDir(const std::string& filename) {
|
|||
#ifdef _WIN32
|
||||
if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str()))
|
||||
return true;
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
if (AndroidStorage::DeleteDocument(filename))
|
||||
return true;
|
||||
#else
|
||||
|
|
@ -310,7 +347,7 @@ bool Rename(const std::string& srcFullPath, const std::string& destFullPath) {
|
|||
if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(),
|
||||
Common::UTF8ToUTF16W(destFullPath).c_str()) == 0)
|
||||
return true;
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
// srcFullPath and destFullPath are relative to the user directory
|
||||
if (AndroidStorage::GetBuildFlavor() == AndroidStorage::AndroidBuildFlavors::GOOGLEPLAY) {
|
||||
if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath))
|
||||
|
|
@ -343,36 +380,36 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) {
|
|||
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
|
||||
GetLastErrorMsg());
|
||||
return false;
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
return AndroidStorage::CopyFile(srcFilename, std::string(GetParentPath(destFilename)),
|
||||
std::string(GetFilename(destFilename)));
|
||||
#else
|
||||
|
||||
// Open input file
|
||||
FILE* input = fopen(srcFilename.c_str(), "rb");
|
||||
FILE* input = FOPEN(srcFilename.c_str(), "rb");
|
||||
if (!input) {
|
||||
LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename,
|
||||
destFilename, GetLastErrorMsg());
|
||||
return false;
|
||||
}
|
||||
SCOPE_EXIT({ fclose(input); });
|
||||
SCOPE_EXIT({ FCLOSE(input); });
|
||||
|
||||
// open output file
|
||||
FILE* output = fopen(destFilename.c_str(), "wb");
|
||||
FILE* output = FOPEN(destFilename.c_str(), "wb");
|
||||
if (!output) {
|
||||
LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename,
|
||||
destFilename, GetLastErrorMsg());
|
||||
return false;
|
||||
}
|
||||
SCOPE_EXIT({ fclose(output); });
|
||||
SCOPE_EXIT({ FCLOSE(output); });
|
||||
|
||||
// copy loop
|
||||
std::array<char, 1024> buffer;
|
||||
while (!feof(input)) {
|
||||
while (!FEOF(input)) {
|
||||
// read input
|
||||
std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input);
|
||||
std::size_t rnum = FREAD(buffer.data(), sizeof(char), buffer.size(), input);
|
||||
if (rnum != buffer.size()) {
|
||||
if (ferror(input) != 0) {
|
||||
if (FERROR(input) != 0) {
|
||||
LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}",
|
||||
srcFilename, destFilename, GetLastErrorMsg());
|
||||
return false;
|
||||
|
|
@ -380,7 +417,7 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) {
|
|||
}
|
||||
|
||||
// write output
|
||||
std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output);
|
||||
std::size_t wnum = FWRITE(buffer.data(), sizeof(char), rnum, output);
|
||||
if (wnum != rnum) {
|
||||
LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename,
|
||||
destFilename, GetLastErrorMsg());
|
||||
|
|
@ -408,7 +445,7 @@ u64 GetSize(const std::string& filename) {
|
|||
#ifdef _WIN32
|
||||
struct _stat64 buf;
|
||||
if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0)
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
u64 result = AndroidStorage::GetSize(filename);
|
||||
LOG_TRACE(Common_Filesystem, "{}: {}", filename, result);
|
||||
return result;
|
||||
|
|
@ -425,7 +462,7 @@ u64 GetSize(const std::string& filename) {
|
|||
}
|
||||
|
||||
u64 GetSize(const int fd) {
|
||||
struct stat buf;
|
||||
file_stat_t buf;
|
||||
if (fstat(fd, &buf) != 0) {
|
||||
LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg());
|
||||
return 0;
|
||||
|
|
@ -435,13 +472,13 @@ u64 GetSize(const int fd) {
|
|||
|
||||
u64 GetSize(FILE* f) {
|
||||
// can't use off_t here because it can be 32-bit
|
||||
u64 pos = ftello(f);
|
||||
if (fseeko(f, 0, SEEK_END) != 0) {
|
||||
u64 pos = FTELL(f);
|
||||
if (FSEEK(f, 0, SEEK_END) != 0) {
|
||||
LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
|
||||
return 0;
|
||||
}
|
||||
u64 size = ftello(f);
|
||||
if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) {
|
||||
u64 size = FTELL(f);
|
||||
if ((size != pos) && (FSEEK(f, pos, SEEK_SET) != 0)) {
|
||||
LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -481,7 +518,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
|
|||
// windows loop
|
||||
do {
|
||||
const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName));
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
// android loop
|
||||
auto result = AndroidStorage::GetFilesName(directory);
|
||||
for (auto virtual_name : result) {
|
||||
|
|
@ -508,7 +545,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
|
|||
#ifdef _WIN32
|
||||
} while (FindNextFileW(handle_find, &ffd) != 0);
|
||||
FindClose(handle_find);
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
}
|
||||
#else
|
||||
}
|
||||
|
|
@ -605,7 +642,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path,
|
|||
if (!FileUtil::Exists(dest_path))
|
||||
FileUtil::CreateFullPath(dest_path);
|
||||
|
||||
#ifdef ANDROID
|
||||
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
auto result = AndroidStorage::GetFilesName(source_path);
|
||||
for (auto virtualName : result) {
|
||||
#else
|
||||
|
|
@ -635,7 +672,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path,
|
|||
FileUtil::Copy(source, dest);
|
||||
}
|
||||
|
||||
#ifndef ANDROID
|
||||
#if !(defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS))
|
||||
closedir(dirp);
|
||||
#endif // ANDROID
|
||||
#endif // _WIN32
|
||||
|
|
@ -811,7 +848,7 @@ void SetUserPath(const std::string& path) {
|
|||
|
||||
g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
|
||||
g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
user_path = "/";
|
||||
g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
|
||||
g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
|
||||
|
|
@ -1070,7 +1107,7 @@ std::string_view RemoveTrailingSlash(std::string_view path) {
|
|||
|
||||
std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
|
||||
std::string path(path_);
|
||||
#ifdef ANDROID
|
||||
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
return std::string(RemoveTrailingSlash(path));
|
||||
#endif
|
||||
char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
|
||||
|
|
@ -1137,7 +1174,7 @@ bool IOFile::Open() {
|
|||
Common::UTF8ToUTF16W(openmode).c_str(), flags);
|
||||
m_good = m_file != nullptr;
|
||||
|
||||
#elif ANDROID
|
||||
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
|
||||
// Check whether filepath is startsWith content
|
||||
AndroidStorage::AndroidOpenMode android_open_mode = AndroidStorage::ParseOpenmode(openmode);
|
||||
if (android_open_mode == AndroidStorage::AndroidOpenMode::WRITE ||
|
||||
|
|
@ -1168,7 +1205,7 @@ bool IOFile::Open() {
|
|||
|
||||
m_good = m_file != nullptr;
|
||||
#else
|
||||
m_file = std::fopen(filename.c_str(), openmode.c_str());
|
||||
m_file = FOPEN(filename.c_str(), openmode.c_str());
|
||||
m_good = m_file != nullptr;
|
||||
#endif
|
||||
|
||||
|
|
@ -1176,7 +1213,7 @@ bool IOFile::Open() {
|
|||
}
|
||||
|
||||
bool IOFile::Close() {
|
||||
if (!IsOpen() || 0 != std::fclose(m_file))
|
||||
if (!IsOpen() || 0 != FCLOSE(m_file))
|
||||
m_good = false;
|
||||
|
||||
m_file = nullptr;
|
||||
|
|
@ -1191,7 +1228,7 @@ u64 IOFile::GetSize() const {
|
|||
}
|
||||
|
||||
bool IOFile::SeekImpl(s64 off, int origin) {
|
||||
if (!IsOpen() || 0 != fseeko(m_file, off, origin))
|
||||
if (!IsOpen() || 0 != FSEEK(m_file, off, origin))
|
||||
m_good = false;
|
||||
|
||||
return m_good;
|
||||
|
|
@ -1199,13 +1236,13 @@ bool IOFile::SeekImpl(s64 off, int origin) {
|
|||
|
||||
u64 IOFile::TellImpl() const {
|
||||
if (IsOpen())
|
||||
return ftello(m_file);
|
||||
return FTELL(m_file);
|
||||
|
||||
return std::numeric_limits<u64>::max();
|
||||
}
|
||||
|
||||
bool IOFile::Flush() {
|
||||
if (!IsOpen() || 0 != std::fflush(m_file))
|
||||
if (!IsOpen() || 0 != FFLUSH(m_file))
|
||||
m_good = false;
|
||||
|
||||
return m_good;
|
||||
|
|
@ -1223,7 +1260,7 @@ std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_si
|
|||
|
||||
DEBUG_ASSERT(data != nullptr);
|
||||
|
||||
return std::fread(data, data_size, length, m_file);
|
||||
return FREAD(data, data_size, length, m_file);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
|
@ -1266,7 +1303,16 @@ std::size_t IOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t o
|
|||
|
||||
DEBUG_ASSERT(data != nullptr);
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
std::scoped_lock lock(m_file_pos_mutex);
|
||||
int64_t pos = filestream_tell(m_file);
|
||||
FSEEK(m_file, offset, RETRO_VFS_SEEK_POSITION_START);
|
||||
int64_t rv = FREAD(data, 1, byte_count, m_file);
|
||||
FSEEK(m_file, pos, RETRO_VFS_SEEK_POSITION_START);
|
||||
return rv;
|
||||
#else
|
||||
return pread(fileno(m_file), data, byte_count, offset);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) {
|
||||
|
|
@ -1281,12 +1327,18 @@ std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t
|
|||
|
||||
DEBUG_ASSERT(data != nullptr);
|
||||
|
||||
#if defined(HAVE_LIBRETRO_VFS)
|
||||
return rfwrite(data, data_size, length, m_file) / data_size;
|
||||
#else
|
||||
return std::fwrite(data, data_size, length, m_file);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IOFile::Resize(u64 size) {
|
||||
if (!IsOpen() || 0 !=
|
||||
#ifdef _WIN32
|
||||
#if defined(HAVE_LIBRETRO_VFS)
|
||||
filestream_truncate(m_file, size)
|
||||
#elif defined(_WIN32)
|
||||
// ector: _chsize sucks, not 64-bit safe
|
||||
// F|RES: changed to _chsize_s. i think it is 64-bit safe
|
||||
_chsize_s(_fileno(m_file), size)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <ios>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
|
@ -32,6 +33,14 @@
|
|||
#include "common/string_util.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
#define SKIP_STDIO_REDEFINES
|
||||
#include <streams/file_stream_transforms.h>
|
||||
#define CORE_FILE RFILE
|
||||
#else
|
||||
#define CORE_FILE std::FILE
|
||||
#endif
|
||||
|
||||
namespace FileUtil {
|
||||
|
||||
// User paths for GetUserPath
|
||||
|
|
@ -120,7 +129,7 @@ private:
|
|||
[[nodiscard]] u64 GetSize(int fd);
|
||||
|
||||
// Overloaded GetSize, accepts FILE*
|
||||
[[nodiscard]] u64 GetSize(FILE* f);
|
||||
[[nodiscard]] u64 GetSize(CORE_FILE* f);
|
||||
|
||||
// Returns true if successful, or path already exists.
|
||||
bool CreateDir(const std::string& filename);
|
||||
|
|
@ -423,7 +432,11 @@ public:
|
|||
return m_good;
|
||||
}
|
||||
[[nodiscard]] virtual int GetFd() const {
|
||||
#ifdef ANDROID
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
if (m_file == nullptr)
|
||||
return -1;
|
||||
return fileno(filestream_get_vfs_handle(m_file)->fp);
|
||||
#elif defined(ANDROID)
|
||||
return m_fd;
|
||||
#else
|
||||
if (m_file == nullptr)
|
||||
|
|
@ -448,7 +461,12 @@ public:
|
|||
// clear error state
|
||||
virtual void Clear() {
|
||||
m_good = true;
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
filestream_rewind(m_file);
|
||||
#else
|
||||
std::clearerr(m_file);
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual bool IsCrypto() {
|
||||
|
|
@ -476,9 +494,16 @@ protected:
|
|||
virtual u64 TellImpl() const;
|
||||
|
||||
private:
|
||||
std::FILE* m_file = nullptr;
|
||||
CORE_FILE* m_file = nullptr;
|
||||
int m_fd = -1;
|
||||
bool m_good = true;
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
// pread() doesn't touch the file position, so it's safe alongside
|
||||
// concurrent fread/fwrite. Libretro VFS has no pread equivalent, so
|
||||
// ReadAtImpl emulates it with seek+read+seek, which would corrupt the
|
||||
// file position for concurrent Read/Write operations.
|
||||
mutable std::mutex m_file_pos_mutex;
|
||||
#endif
|
||||
|
||||
std::string filename;
|
||||
std::string openmode;
|
||||
|
|
@ -547,4 +572,4 @@ void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmod
|
|||
}
|
||||
|
||||
BOOST_CLASS_EXPORT_KEY(FileUtil::IOFile)
|
||||
BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile)
|
||||
BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,61 @@ public:
|
|||
virtual void Close() = 0;
|
||||
};
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
/**
|
||||
* LibRetro backend
|
||||
*/
|
||||
class LibRetroBackend : public Backend {
|
||||
public:
|
||||
explicit LibRetroBackend() {}
|
||||
explicit LibRetroBackend(retro_log_printf_t callback) : callback(callback) {}
|
||||
|
||||
~LibRetroBackend() override = default;
|
||||
|
||||
void Write(const Entry& entry) override {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
retro_log_level log_level;
|
||||
|
||||
switch (entry.log_level) {
|
||||
case Common::Log::Level::Trace:
|
||||
log_level = retro_log_level::RETRO_LOG_DEBUG;
|
||||
break;
|
||||
case Common::Log::Level::Debug:
|
||||
log_level = retro_log_level::RETRO_LOG_DEBUG;
|
||||
break;
|
||||
case Common::Log::Level::Info:
|
||||
log_level = retro_log_level::RETRO_LOG_INFO;
|
||||
break;
|
||||
case Common::Log::Level::Warning:
|
||||
log_level = retro_log_level::RETRO_LOG_WARN;
|
||||
break;
|
||||
case Common::Log::Level::Error:
|
||||
log_level = retro_log_level::RETRO_LOG_ERROR;
|
||||
break;
|
||||
case Common::Log::Level::Critical:
|
||||
log_level = retro_log_level::RETRO_LOG_ERROR;
|
||||
break;
|
||||
default:
|
||||
log_level = retro_log_level::RETRO_LOG_DUMMY;
|
||||
}
|
||||
|
||||
auto str = FormatLogMessage(entry).append(1, '\n');
|
||||
callback(log_level, str.c_str());
|
||||
}
|
||||
|
||||
void Flush() override {}
|
||||
|
||||
void Close() override {}
|
||||
|
||||
void EnableForStacktrace() override {}
|
||||
|
||||
private:
|
||||
retro_log_printf_t callback = nullptr;
|
||||
};
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Backend that writes to stderr and with color
|
||||
*/
|
||||
|
|
@ -218,7 +273,19 @@ public:
|
|||
}
|
||||
return *instance;
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
static void Initialize(retro_log_printf_t callback) {
|
||||
if (instance) {
|
||||
LOG_WARNING(Log, "Reinitializing logging backend");
|
||||
return;
|
||||
}
|
||||
initialization_in_progress_suppress_logging = true;
|
||||
Filter filter;
|
||||
filter.ParseFilterString(Settings::values.log_filter.GetValue());
|
||||
instance = std::unique_ptr<Impl, decltype(&Deleter)>(new Impl(callback, filter), Deleter);
|
||||
initialization_in_progress_suppress_logging = false;
|
||||
}
|
||||
#endif
|
||||
static void Initialize(std::string_view log_file) {
|
||||
if (instance) {
|
||||
LOG_WARNING(Log, "Reinitializing logging backend");
|
||||
|
|
@ -310,6 +377,10 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
#ifdef HAVE_LIBRETRO
|
||||
Impl(retro_log_printf_t callback, const Filter& filter_)
|
||||
: filter{filter_}, file_backend{""}, libretro_backend{callback} {}
|
||||
#endif
|
||||
Impl(const std::string& file_backend_filename, const Filter& filter_)
|
||||
: filter{filter_}, file_backend{file_backend_filename} {
|
||||
#ifdef CITRA_LINUX_GCC_BACKTRACE
|
||||
|
|
@ -412,12 +483,16 @@ private:
|
|||
}
|
||||
|
||||
void ForEachBackend(auto lambda) {
|
||||
#ifdef HAVE_LIBRETRO
|
||||
lambda(static_cast<Backend&>(libretro_backend));
|
||||
#else
|
||||
lambda(static_cast<Backend&>(debugger_backend));
|
||||
lambda(static_cast<Backend&>(color_console_backend));
|
||||
lambda(static_cast<Backend&>(file_backend));
|
||||
#ifdef ANDROID
|
||||
lambda(static_cast<Backend&>(lc_backend));
|
||||
#endif
|
||||
#endif // ANDROID
|
||||
#endif // HAVE_LIBRETRO
|
||||
}
|
||||
|
||||
static void Deleter(Impl* ptr) {
|
||||
|
|
@ -464,6 +539,9 @@ private:
|
|||
#ifdef ANDROID
|
||||
LogcatBackend lc_backend{};
|
||||
#endif
|
||||
#ifdef HAVE_LIBRETRO
|
||||
LibRetroBackend libretro_backend;
|
||||
#endif
|
||||
|
||||
MPSCQueue<Entry> message_queue{};
|
||||
std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()};
|
||||
|
|
@ -478,6 +556,13 @@ private:
|
|||
};
|
||||
} // namespace
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
void LibRetroStart(retro_log_printf_t callback) {
|
||||
Impl::Initialize(callback);
|
||||
Impl::Start();
|
||||
}
|
||||
#endif
|
||||
|
||||
void Initialize(std::string_view log_file) {
|
||||
Impl::Initialize(log_file.empty() ? LOG_FILE : log_file);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2014 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
#include <string_view>
|
||||
#include "common/logging/filter.h"
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "libretro.h"
|
||||
#endif
|
||||
|
||||
namespace Common::Log {
|
||||
|
||||
|
|
@ -13,6 +16,9 @@ class Filter;
|
|||
|
||||
/// Initializes the logging system. This should be the first thing called in main.
|
||||
void Initialize(std::string_view log_file = "");
|
||||
#ifdef HAVE_LIBRETRO
|
||||
void LibRetroStart(retro_log_printf_t callback);
|
||||
#endif
|
||||
|
||||
void Start();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <format>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <zstd.h>
|
||||
|
|
|
|||
|
|
@ -362,6 +362,10 @@ public:
|
|||
|
||||
void LoadState(u32 slot);
|
||||
|
||||
std::vector<u8> SaveStateBuffer() const;
|
||||
|
||||
bool LoadStateBuffer(std::vector<u8> buffer);
|
||||
|
||||
/// Self delete ncch
|
||||
bool SetSelfDelete(const std::string& file) {
|
||||
if (m_filepath == file) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ enum class WindowSystemType : u8 {
|
|||
MacOS,
|
||||
X11,
|
||||
Wayland,
|
||||
LibRetro,
|
||||
};
|
||||
|
||||
struct Frame;
|
||||
|
|
@ -258,6 +259,16 @@ public:
|
|||
return is_secondary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests for a frontend to setup a framebuffer.
|
||||
*/
|
||||
virtual void SetupFramebuffer() {}
|
||||
|
||||
/// Flags that the framebuffer should be cleared.
|
||||
virtual bool NeedsClearing() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
Settings::StereoRenderOption get3DMode() const;
|
||||
|
||||
protected:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@
|
|||
#include "core/hle/service/cfg/cfg_u.h"
|
||||
#include "core/hw/unique_data.h"
|
||||
#include "core/loader/loader.h"
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "citra_libretro/core_settings.h"
|
||||
#endif
|
||||
|
||||
SERVICE_CONSTRUCT_IMPL(Service::CFG::Module)
|
||||
SERIALIZE_EXPORT_IMPL(Service::CFG::Module)
|
||||
|
|
@ -1058,6 +1061,11 @@ void Module::UpdatePreferredRegionCode() {
|
|||
if (preferred_region_chosen || !system.IsPoweredOn()) {
|
||||
return;
|
||||
}
|
||||
#ifdef HAVE_LIBRETRO
|
||||
// Apply language set in core options first
|
||||
SetSystemLanguage(LibRetro::settings.language_value);
|
||||
#endif
|
||||
|
||||
preferred_region_chosen = true;
|
||||
|
||||
const auto preferred_regions = system.GetAppLoader().GetPreferredRegions();
|
||||
|
|
|
|||
|
|
@ -214,7 +214,6 @@ struct MIC_U::Impl {
|
|||
LOG_CRITICAL(Service_MIC,
|
||||
"Application started sampling again before stopping sampling");
|
||||
mic->StopSampling();
|
||||
mic.reset();
|
||||
}
|
||||
|
||||
u8 sample_size = encoding == Encoding::PCM8Signed || encoding == Encoding::PCM8 ? 8 : 16;
|
||||
|
|
@ -225,7 +224,9 @@ struct MIC_U::Impl {
|
|||
state.looped_buffer = audio_buffer_loop;
|
||||
state.size = audio_buffer_size;
|
||||
|
||||
CreateMic();
|
||||
if (!mic) {
|
||||
CreateMic();
|
||||
}
|
||||
StartSampling();
|
||||
|
||||
timing.ScheduleEvent(GetBufferUpdatePeriod(state.sample_rate), buffer_write_event);
|
||||
|
|
@ -259,7 +260,6 @@ struct MIC_U::Impl {
|
|||
timing.RemoveEvent(buffer_write_event);
|
||||
if (mic) {
|
||||
mic->StopSampling();
|
||||
mic.reset();
|
||||
}
|
||||
LOG_TRACE(Service_MIC, "called");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2308,7 +2308,9 @@ std::optional<SOC_U::InterfaceInfo> SOC_U::GetDefaultInterfaceInfo() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
#elif !(defined(ANDROID) && defined(HAVE_LIBRETRO))
|
||||
// Libretro Android builds target API 21, but getifaddrs() requires API 24+.
|
||||
// Standalone Android (minSdk 29) and other platforms have getifaddrs().
|
||||
struct ifaddrs* ifaddr;
|
||||
struct ifaddrs* ifa;
|
||||
if (getifaddrs(&ifaddr) == -1) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -217,4 +217,77 @@ void System::LoadState(u32 slot) {
|
|||
ia&* this;
|
||||
}
|
||||
|
||||
std::vector<u8> System::SaveStateBuffer() const {
|
||||
std::ostringstream sstream{std::ios_base::binary};
|
||||
// Serialize
|
||||
oarchive oa{sstream};
|
||||
oa&* this;
|
||||
|
||||
const std::string& str{sstream.str()};
|
||||
const auto data = std::span<const u8>{reinterpret_cast<const u8*>(str.data()), str.size()};
|
||||
auto buffer = Common::Compression::CompressDataZSTDDefault(data);
|
||||
|
||||
CSTHeader header{};
|
||||
header.filetype = header_magic_bytes;
|
||||
header.program_id = title_id;
|
||||
std::string rev_bytes;
|
||||
CryptoPP::StringSource ss(Common::g_scm_rev, true,
|
||||
new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes)));
|
||||
std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision));
|
||||
header.time = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const std::string build_fullname = Common::g_build_fullname;
|
||||
std::memset(header.build_name.data(), 0, sizeof(header.build_name));
|
||||
std::memcpy(header.build_name.data(), build_fullname.c_str(),
|
||||
std::min(build_fullname.length(), sizeof(header.build_name) - 1));
|
||||
|
||||
std::vector<u8> result((u8*)&header, (u8*)&header + sizeof(header));
|
||||
std::copy(buffer.begin(), buffer.end(), std::back_inserter(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool System::LoadStateBuffer(std::vector<u8> buffer) {
|
||||
CSTHeader header;
|
||||
|
||||
if (buffer.size() < sizeof(header)) {
|
||||
LOG_ERROR(Core, "Save state too small");
|
||||
return false;
|
||||
}
|
||||
|
||||
header = *((CSTHeader*)buffer.data());
|
||||
|
||||
if (header.filetype != header_magic_bytes) {
|
||||
LOG_ERROR(Core, "Invalid save state");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.program_id != title_id) {
|
||||
LOG_ERROR(Core, "Save state isn't for the current game");
|
||||
return false;
|
||||
}
|
||||
std::string revision = fmt::format("{:02x}", fmt::join(header.revision, ""));
|
||||
if (revision != Common::g_scm_rev) {
|
||||
LOG_ERROR(Core,
|
||||
"Save state file created from a different revision (core: {}, savestate: {})",
|
||||
Common::g_scm_rev, revision);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u8> state(buffer.begin() + sizeof(CSTHeader), buffer.end());
|
||||
auto decompressed = Common::Compression::DecompressDataZSTD(state);
|
||||
|
||||
std::istringstream sstream{
|
||||
std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()},
|
||||
std::ios_base::binary};
|
||||
decompressed.clear();
|
||||
|
||||
// Deserialize
|
||||
iarchive ia{sstream};
|
||||
ia&* this;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ create_target_directory_groups(tests)
|
|||
target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core)
|
||||
target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads)
|
||||
|
||||
if (ENABLE_LIBRETRO)
|
||||
target_link_libraries(tests PRIVATE $<TARGET_OBJECTS:azahar_libretro_common>)
|
||||
endif()
|
||||
|
||||
add_test(NAME tests COMMAND tests)
|
||||
|
||||
if (CITRA_USE_PRECOMPILED_HEADERS)
|
||||
|
|
|
|||
|
|
@ -183,10 +183,10 @@ if (ENABLE_VULKAN)
|
|||
renderer_vulkan/vk_instance.h
|
||||
renderer_vulkan/vk_pipeline_cache.cpp
|
||||
renderer_vulkan/vk_pipeline_cache.h
|
||||
renderer_vulkan/vk_platform.cpp
|
||||
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_platform.cpp>
|
||||
renderer_vulkan/vk_platform.h
|
||||
renderer_vulkan/vk_present_window.cpp
|
||||
renderer_vulkan/vk_present_window.h
|
||||
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_present_window.cpp>
|
||||
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_present_window.h>
|
||||
renderer_vulkan/vk_render_manager.cpp
|
||||
renderer_vulkan/vk_render_manager.h
|
||||
renderer_vulkan/vk_shader_disk_cache.cpp
|
||||
|
|
@ -195,8 +195,8 @@ if (ENABLE_VULKAN)
|
|||
renderer_vulkan/vk_shader_util.h
|
||||
renderer_vulkan/vk_stream_buffer.cpp
|
||||
renderer_vulkan/vk_stream_buffer.h
|
||||
renderer_vulkan/vk_swapchain.cpp
|
||||
renderer_vulkan/vk_swapchain.h
|
||||
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_swapchain.cpp>
|
||||
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_swapchain.h>
|
||||
renderer_vulkan/vk_texture_runtime.cpp
|
||||
renderer_vulkan/vk_texture_runtime.h
|
||||
shader/generator/spv_fs_shader_gen.cpp
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include "core/core_timing.h"
|
||||
#include "core/hle/service/gsp/gsp_gpu.h"
|
||||
#include "core/hle/service/plgldr/plgldr.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "video_core/debug_utils/debug_utils.h"
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/gpu_debugger.h"
|
||||
|
|
@ -421,6 +422,53 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) {
|
|||
impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event);
|
||||
}
|
||||
|
||||
void GPU::RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window) {
|
||||
// Reset the renderer (this will destroy OpenGL resources)
|
||||
impl->renderer.reset();
|
||||
|
||||
// Create a new renderer
|
||||
impl->renderer =
|
||||
VideoCore::CreateRenderer(emu_window, secondary_window, impl->pica, impl->system);
|
||||
impl->rasterizer = impl->renderer->Rasterizer();
|
||||
|
||||
// Rebind the rasterizer to the PICA GPU
|
||||
impl->pica.BindRasterizer(impl->rasterizer);
|
||||
|
||||
// Update the sw_blitter with the new rasterizer
|
||||
impl->sw_blitter = std::make_unique<SwRenderer::SwBlitter>(impl->memory, impl->rasterizer);
|
||||
|
||||
// Re-apply per-game configuration and reload disk shader cache
|
||||
u64 program_id{};
|
||||
impl->system.GetAppLoader().ReadProgramId(program_id);
|
||||
ApplyPerProgramSettings(program_id);
|
||||
if (Settings::values.use_disk_shader_cache) {
|
||||
impl->renderer->Rasterizer()->LoadDefaultDiskResources(false, nullptr);
|
||||
}
|
||||
|
||||
// Mark ALL GPU registers as dirty so current state gets uploaded to new renderer
|
||||
impl->pica.dirty_regs.SetAllDirty();
|
||||
|
||||
// Also mark shader setups as dirty so uniforms get re-uploaded and
|
||||
// stale pointers to the old rasterizer's JIT cache are cleared.
|
||||
impl->pica.vs_setup.uniforms_dirty = true;
|
||||
impl->pica.vs_setup.cached_shader = nullptr;
|
||||
impl->pica.gs_setup.uniforms_dirty = true;
|
||||
impl->pica.gs_setup.cached_shader = nullptr;
|
||||
|
||||
// Mark all cached LUT/table state in pica as dirty
|
||||
impl->pica.lighting.lut_dirty = impl->pica.lighting.LutAllDirty;
|
||||
impl->pica.fog.lut_dirty = true;
|
||||
impl->pica.proctex.table_dirty = impl->pica.proctex.TableAllDirty;
|
||||
}
|
||||
|
||||
void GPU::ReleaseRenderer() {
|
||||
// Just reset the renderer to release OpenGL resources
|
||||
// Don't null out rasterizer pointer as it will become dangling
|
||||
impl->renderer.reset();
|
||||
impl->sw_blitter.reset();
|
||||
LOG_INFO(HW_GPU, "Renderer released for context destroy");
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void GPU::serialize(Archive& ar, const u32 file_version) {
|
||||
ar & impl->pica;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ public:
|
|||
|
||||
void ApplyPerProgramSettings(u64 program_ID);
|
||||
|
||||
/// Recreates the renderer (for GL context reset in libretro)
|
||||
void RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window);
|
||||
|
||||
/// Releases the renderer (for GL context destroy in libretro)
|
||||
void ReleaseRenderer();
|
||||
|
||||
private:
|
||||
void SubmitCmdList(u32 index);
|
||||
|
||||
|
|
|
|||
|
|
@ -457,6 +457,14 @@ FileUtil::IOFile ShaderDiskCache::AppendTransferableFile() {
|
|||
const auto transferable_path{GetTransferablePath()};
|
||||
const bool existed = FileUtil::Exists(transferable_path);
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
// LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which
|
||||
// uses "r+b" internally and fails if the file doesn't exist. Pre-create it.
|
||||
if (!existed) {
|
||||
FileUtil::CreateEmptyFile(transferable_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
FileUtil::IOFile file(transferable_path, "ab+");
|
||||
if (!file.IsOpen()) {
|
||||
LOG_ERROR(Render_OpenGL, "Failed to open transferable cache in path={}", transferable_path);
|
||||
|
|
@ -480,6 +488,14 @@ FileUtil::IOFile ShaderDiskCache::AppendPrecompiledFile(bool write_header) {
|
|||
const auto precompiled_path{GetPrecompiledPath()};
|
||||
const bool existed = FileUtil::Exists(precompiled_path);
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
// LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which
|
||||
// uses "r+b" internally and fails if the file doesn't exist. Pre-create it.
|
||||
if (!existed) {
|
||||
FileUtil::CreateEmptyFile(precompiled_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
FileUtil::IOFile file(precompiled_path, "ab+");
|
||||
if (!file.IsOpen()) {
|
||||
LOG_ERROR(Render_OpenGL, "Failed to open precompiled cache in path={}", precompiled_path);
|
||||
|
|
@ -514,6 +530,13 @@ void ShaderDiskCache::SaveVirtualPrecompiledFile() {
|
|||
|
||||
const auto precompiled_path{GetPrecompiledPath()};
|
||||
|
||||
#ifdef HAVE_LIBRETRO_VFS
|
||||
const bool existed = FileUtil::Exists(precompiled_path);
|
||||
if (!existed) {
|
||||
FileUtil::CreateEmptyFile(precompiled_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
precompiled_file.Close();
|
||||
if (!FileUtil::Delete(GetPrecompiledPath())) {
|
||||
LOG_ERROR(Render_OpenGL, "Failed to invalidate precompiled file={}", GetPrecompiledPath());
|
||||
|
|
|
|||
|
|
@ -94,9 +94,14 @@ void RendererOpenGL::SwapBuffers() {
|
|||
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||
state.Apply();
|
||||
|
||||
render_window.SetupFramebuffer();
|
||||
|
||||
PrepareRendertarget();
|
||||
RenderScreenshot();
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
DrawScreens(render_window.GetFramebufferLayout(), false);
|
||||
render_window.SwapBuffers();
|
||||
#else
|
||||
const auto& main_layout = render_window.GetFramebufferLayout();
|
||||
RenderToMailbox(main_layout, render_window.mailbox, false);
|
||||
|
||||
|
|
@ -124,6 +129,7 @@ void RendererOpenGL::SwapBuffers() {
|
|||
LOG_DEBUG(Render_OpenGL, "Frame dumper exception caught: {}", exception.what());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
system.perf_stats->EndSwap();
|
||||
EndFrame();
|
||||
|
|
@ -663,7 +669,9 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f
|
|||
const auto& bottom_screen = layout.bottom_screen;
|
||||
|
||||
glViewport(0, 0, layout.width, layout.height);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
if (render_window.NeedsClearing()) {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
// Set projection matrix
|
||||
std::array<GLfloat, 3 * 2> ortho_matrix =
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) && !defined(HAVE_LIBRETRO)
|
||||
#include "common/apple_utils.h"
|
||||
#endif
|
||||
|
||||
|
|
@ -60,11 +60,11 @@ constexpr static std::array<vk::DescriptorSetLayoutBinding, 1> PRESENT_BINDINGS
|
|||
|
||||
namespace {
|
||||
static bool IsLowRefreshRate() {
|
||||
#if (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO)
|
||||
if (!Settings::values.use_display_refresh_rate_detection) {
|
||||
LOG_INFO(Render_Vulkan, "Refresh rate detection is currently disabled via settings");
|
||||
return false;
|
||||
}
|
||||
#if defined(__APPLE__) || defined(ENABLE_SDL2)
|
||||
#ifdef __APPLE__
|
||||
// Apple's low power mode sometimes limits applications to 30fps without changing the refresh
|
||||
// rate, meaning the above code doesn't catch it.
|
||||
|
|
@ -98,7 +98,7 @@ static bool IsLowRefreshRate() {
|
|||
LOG_INFO(Render_Vulkan, "Refresh rate is above emulated 3DS screen: {}hz. Good.",
|
||||
cur_refresh_rate);
|
||||
}
|
||||
#endif // defined(__APPLE__) || defined(ENABLE_SDL2)
|
||||
#endif // (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO)
|
||||
|
||||
// We have no available method of checking refresh rate. Just assume that everything is fine :)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@
|
|||
#include "common/common_types.h"
|
||||
#include "common/math_util.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "citra_libretro/libretro_vk.h"
|
||||
#else
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_present_window.h"
|
||||
#endif
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
#include "video_core/renderer_vulkan/vk_render_manager.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
|
|
@ -116,7 +120,11 @@ private:
|
|||
Memory::MemorySystem& memory;
|
||||
Pica::PicaCore& pica;
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
LibRetroVKInstance instance;
|
||||
#else
|
||||
Instance instance;
|
||||
#endif
|
||||
Scheduler scheduler;
|
||||
RenderManager renderpass_cache;
|
||||
PresentWindow main_present_window;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -464,7 +464,9 @@ bool Instance::CreateDevice() {
|
|||
const bool has_custom_border_color =
|
||||
add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, is_qualcomm,
|
||||
"it is broken on most Qualcomm driver versions");
|
||||
const bool has_index_type_uint8 = add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME);
|
||||
const bool has_index_type_uint8 =
|
||||
add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME, is_moltenvk,
|
||||
"uint8 index conversion causes memory leaks in MoltenVK");
|
||||
const bool has_fragment_shader_interlock =
|
||||
add_extension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, is_nvidia,
|
||||
"it is broken on Nvidia drivers");
|
||||
|
|
@ -481,6 +483,9 @@ bool Instance::CreateDevice() {
|
|||
return false;
|
||||
}
|
||||
|
||||
#ifndef HAVE_LIBRETRO
|
||||
// Find graphics queue family. LibRetro builds skip this since queue_family_index
|
||||
// is already set by LibRetroVKInstance from the frontend-provided context.
|
||||
bool graphics_queue_found = false;
|
||||
for (std::size_t i = 0; i < family_properties.size(); i++) {
|
||||
const u32 index = static_cast<u32>(i);
|
||||
|
|
@ -494,6 +499,7 @@ bool Instance::CreateDevice() {
|
|||
LOG_CRITICAL(Render_Vulkan, "Unable to find graphics and/or present queues.");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
static constexpr std::array<f32, 1> queue_priorities = {1.0f};
|
||||
|
||||
|
|
@ -612,6 +618,10 @@ bool Instance::CreateDevice() {
|
|||
#undef PROP_GET
|
||||
#undef FEAT_SET
|
||||
|
||||
#ifdef HAVE_LIBRETRO
|
||||
// LibRetro builds: device already created by frontend, just return after feature detection
|
||||
return true;
|
||||
#else
|
||||
try {
|
||||
device = physical_device.createDeviceUnique(device_chain.get());
|
||||
} catch (vk::ExtensionNotPresentError& err) {
|
||||
|
|
@ -626,6 +636,7 @@ bool Instance::CreateDevice() {
|
|||
|
||||
CreateAllocator();
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Instance::CreateAllocator() {
|
||||
|
|
@ -636,9 +647,9 @@ void Instance::CreateAllocator() {
|
|||
|
||||
const VmaAllocatorCreateInfo allocator_info = {
|
||||
.physicalDevice = physical_device,
|
||||
.device = *device,
|
||||
.device = GetDevice(),
|
||||
.pVulkanFunctions = &functions,
|
||||
.instance = *instance,
|
||||
.instance = GetInstance(),
|
||||
.vulkanApiVersion = TargetVulkanApiVersion,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ struct FormatTraits {
|
|||
|
||||
class Instance {
|
||||
public:
|
||||
struct NoInit {};
|
||||
explicit Instance(bool validation = false, bool dump_command_buffers = false);
|
||||
explicit Instance(Frontend::EmuWindow& window, u32 physical_device_index);
|
||||
~Instance();
|
||||
explicit Instance(NoInit) {} // For LibRetro inheritance - does minimal setup
|
||||
virtual ~Instance();
|
||||
|
||||
/// Returns the FormatTraits struct for the provided pixel format
|
||||
const FormatTraits& GetTraits(VideoCore::PixelFormat pixel_format) const;
|
||||
|
|
@ -58,7 +60,7 @@ public:
|
|||
std::string GetDriverVersionName();
|
||||
|
||||
/// Returns the Vulkan instance
|
||||
vk::Instance GetInstance() const {
|
||||
virtual vk::Instance GetInstance() const {
|
||||
return *instance;
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +70,7 @@ public:
|
|||
}
|
||||
|
||||
/// Returns the Vulkan device
|
||||
vk::Device GetDevice() const {
|
||||
virtual vk::Device GetDevice() const {
|
||||
return *device;
|
||||
}
|
||||
|
||||
|
|
@ -254,6 +256,11 @@ public:
|
|||
return features.shaderSampledImageArrayDynamicIndexing;
|
||||
}
|
||||
|
||||
/// Returns true if layered rendering (array attachments) is supported
|
||||
bool IsLayeredRenderingSupported() const {
|
||||
return layered_rendering_supported;
|
||||
}
|
||||
|
||||
/// Returns the minimum vertex stride alignment
|
||||
u32 GetMinVertexStrideAlignment() const {
|
||||
return min_vertex_stride_alignment;
|
||||
|
|
@ -270,7 +277,7 @@ public:
|
|||
driver_id == vk::DriverIdKHR::eQualcommProprietary;
|
||||
}
|
||||
|
||||
private:
|
||||
protected:
|
||||
/// Returns the optimal supported usage for the requested format
|
||||
[[nodiscard]] FormatTraits DetermineTraits(VideoCore::PixelFormat pixel_format,
|
||||
vk::Format format);
|
||||
|
|
@ -294,7 +301,7 @@ private:
|
|||
// Collects logging gpu info
|
||||
void CollectToolingInfo();
|
||||
|
||||
private:
|
||||
protected:
|
||||
std::shared_ptr<Common::DynamicLibrary> library;
|
||||
vk::UniqueInstance instance;
|
||||
vk::PhysicalDevice physical_device;
|
||||
|
|
@ -328,10 +335,11 @@ private:
|
|||
bool shader_stencil_export{};
|
||||
bool external_memory_host{};
|
||||
u64 min_imported_host_pointer_alignment{};
|
||||
bool layered_rendering_supported{true};
|
||||
bool tooling_info{};
|
||||
bool debug_utils_supported{};
|
||||
bool has_nsight_graphics{};
|
||||
bool has_renderdoc{};
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
} // namespace Vulkan
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// Copyright 2020 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
|
@ -159,7 +163,10 @@ void DescriptorHeap::Allocate(std::size_t begin, std::size_t end) {
|
|||
if (result == vk::Result::eSuccess) {
|
||||
break;
|
||||
}
|
||||
if (result == vk::Result::eErrorOutOfPoolMemory) {
|
||||
// eErrorFragmentedPool: pool has space but is too fragmented to allocate.
|
||||
// MoltenVK on iOS/tvOS returns this more frequently than native Vulkan drivers.
|
||||
if (result == vk::Result::eErrorOutOfPoolMemory ||
|
||||
result == vk::Result::eErrorFragmentedPool) {
|
||||
current_pool++;
|
||||
if (current_pool == pools.size()) {
|
||||
LOG_INFO(Render_Vulkan, "Run out of pools, creating new one!");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019 yuzu Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
#include "common/thread.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#ifdef HAVE_LIBRETRO
|
||||
#include "citra_libretro/libretro_vk.h"
|
||||
#endif
|
||||
|
||||
MICROPROFILE_DEFINE(Vulkan_WaitForWorker, "Vulkan", "Wait for worker", MP_RGB(255, 192, 192));
|
||||
MICROPROFILE_DEFINE(Vulkan_Submit, "Vulkan", "Submit Exectution", MP_RGB(255, 192, 255));
|
||||
|
|
@ -17,11 +20,15 @@ namespace Vulkan {
|
|||
namespace {
|
||||
|
||||
std::unique_ptr<MasterSemaphore> MakeMasterSemaphore(const Instance& instance) {
|
||||
#ifdef HAVE_LIBRETRO
|
||||
return CreateLibRetroMasterSemaphore(instance);
|
||||
#else
|
||||
if (instance.IsTimelineSemaphoreSupported()) {
|
||||
return std::make_unique<MasterSemaphoreTimeline>(instance);
|
||||
} else {
|
||||
return std::make_unique<MasterSemaphoreFence>(instance);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
|
@ -146,7 +146,10 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T
|
|||
vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags,
|
||||
vk::ImageAspectFlags aspect, bool need_format_list,
|
||||
std::string_view debug_name = {}) {
|
||||
const u32 layers = type == TextureType::CubeMap ? 6 : 1;
|
||||
// On tvOS/iOS, fall back to 2D textures when layered rendering isn't supported
|
||||
const bool is_cube_map =
|
||||
type == TextureType::CubeMap && instance->IsLayeredRenderingSupported();
|
||||
const u32 layers = is_cube_map ? 6 : 1;
|
||||
|
||||
const std::array format_list = {
|
||||
vk::Format::eR8G8B8A8Unorm,
|
||||
|
|
@ -192,8 +195,7 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T
|
|||
const vk::Image image{unsafe_image};
|
||||
const vk::ImageViewCreateInfo view_info = {
|
||||
.image = image,
|
||||
.viewType =
|
||||
type == TextureType::CubeMap ? vk::ImageViewType::eCube : vk::ImageViewType::e2D,
|
||||
.viewType = is_cube_map ? vk::ImageViewType::eCube : vk::ImageViewType::e2D,
|
||||
.format = format,
|
||||
.subresourceRange{
|
||||
.aspectMask = aspect,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue