diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml new file mode 100644 index 000000000..5225a43fa --- /dev/null +++ b/.github/workflows/libretro.yml @@ -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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..e8f4f141b --- /dev/null +++ b/.gitlab-ci.yml @@ -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 ################################# + diff --git a/.gitmodules b/.gitmodules index cb600a64c..18c5cc79a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a3635988..df86bf28c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 " Scr ") set(CMAKE_CXX_ARCHIVE_CREATE " Scr ") @@ -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/$) +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() diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 01e76a095..d7cf35f3b 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -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) diff --git a/externals/libretro-common/CMakeLists.txt b/externals/libretro-common/CMakeLists.txt new file mode 100644 index 000000000..0bb4e6b5e --- /dev/null +++ b/externals/libretro-common/CMakeLists.txt @@ -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 +) diff --git a/externals/libretro-common/libretro-common b/externals/libretro-common/libretro-common new file mode 160000 index 000000000..7fc7feedd --- /dev/null +++ b/externals/libretro-common/libretro-common @@ -0,0 +1 @@ +Subproject commit 7fc7feeddca391be65c94e6541381467684b814d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69d17bac2..b18925867 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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() diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index a000e1fc1..6ea16672e 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(audio_core STATIC $<$:sdl2_sink.cpp sdl2_sink.h> $<$:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h> + $<$:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h> $<$:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h> ) diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index 259459a5b..52e781ca4 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -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 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()) { diff --git a/src/audio_core/input_details.cpp b/src/audio_core/input_details.cpp index e98310d31..f0a3b80b4 100644 --- a/src/audio_core/input_details.cpp +++ b/src/audio_core/input_details.cpp @@ -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 { + if (!system.HasMicPermission()) { + LOG_WARNING(Audio, + "Microphone permission denied, falling back to null input."); + return std::make_unique(); + } + return std::make_unique(); + }, + [] { return std::vector{"LibRetro Microphone"}; }}, +#endif #ifdef HAVE_CUBEB InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true, [](Core::System& system, std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/input_details.h b/src/audio_core/input_details.h index a091dfe6c..709453eac 100644 --- a/src/audio_core/input_details.h +++ b/src/audio_core/input_details.h @@ -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 { diff --git a/src/audio_core/libretro_input.cpp b/src/audio_core/libretro_input.cpp new file mode 100644 index 000000000..49354a65d --- /dev/null +++ b/src/audio_core/libretro_input.cpp @@ -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 +#include +#include +#include +#include +#include +#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 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 sample_buffer; + + // Temporary buffer for reading from frontend + std::vector 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()) { + 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(impl->read_buffer.size())); + + if (samples_read > 0) { + impl->sample_buffer.Push( + std::span(impl->read_buffer.data(), static_cast(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 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(parameters.sample_rate) / impl->native_sample_rate; + auto output_count = static_cast(raw_samples.size() * ratio); + if (output_count == 0) { + return {}; + } + std::vector resampled(output_count); + for (std::size_t i = 0; i < output_count; i++) { + double src_pos = i / ratio; + auto idx = static_cast(src_pos); + double frac = src_pos - idx; + if (idx + 1 < raw_samples.size()) { + resampled[i] = + static_cast(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(sample) ^ 0x8000; + }; + + constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 { + return static_cast(sample >> 8); + }; + + constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 { + return static_cast((static_cast(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(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(converted & 0xFF)); + output.push_back(static_cast(converted >> 8)); + } + } else { + // Signed 16-bit - just copy the raw bytes + const u8* data = reinterpret_cast(raw_samples.data()); + output.insert(output.end(), data, data + raw_samples.size() * 2); + } + } + + return output; +} + +LibRetroInput* GetLibRetroInput() { + return g_libretro_input; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_input.h b/src/audio_core/libretro_input.h new file mode 100644 index 000000000..2320e4ef2 --- /dev/null +++ b/src/audio_core/libretro_input.h @@ -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 +#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; +}; + +/// 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 diff --git a/src/audio_core/libretro_sink.cpp b/src/audio_core/libretro_sink.cpp new file mode 100644 index 000000000..d3bcf0ca5 --- /dev/null +++ b/src/audio_core/libretro_sink.cpp @@ -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(data), num_samples); +} + +std::vector ListLibretroSinkDevices() { + return std::vector{"LibRetro"}; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_sink.h b/src/audio_core/libretro_sink.h new file mode 100644 index 000000000..b9685fb80 --- /dev/null +++ b/src/audio_core/libretro_sink.h @@ -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 +#include +#include +#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 cb) override {}; + + bool ImmediateSubmission() override { + return true; + } + + void PushSamples(const void* data, std::size_t num_samples) override; +}; + +std::vector ListLibretroSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h index 73a10f567..d0d0ad02a 100644 --- a/src/audio_core/sink.h +++ b/src/audio_core/sink.h @@ -5,7 +5,7 @@ #pragma once #include -#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 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 diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 961e040b3..ca48b1f23 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -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 { + return std::make_unique(std::string(device_id)); + }, + &ListLibretroSinkDevices}, +#endif #ifdef HAVE_CUBEB SinkDetails{SinkType::Cubeb, "Cubeb", [](std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index 2a0d202a7..a3e7a4edb 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -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 { diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt new file mode 100644 index 000000000..8ea9b7b10 --- /dev/null +++ b/src/citra_libretro/CMakeLists.txt @@ -0,0 +1,97 @@ +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$) +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 + $<$: 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 + $) + +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() diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp new file mode 100644 index 000000000..1fb73cb23 --- /dev/null +++ b/src/citra_libretro/citra_libretro.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#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 +#endif + +class CitraLibRetro { +public: + CitraLibRetro() : log_filter(Common::Log::Level::Debug) {} + + Common::Log::Filter log_filter; + std::unique_ptr 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()); + + 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(); + } + + // 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(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 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(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(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(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> 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 buffer(static_cast(data), static_cast(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) {} diff --git a/src/citra_libretro/citra_libretro.h b/src/citra_libretro/citra_libretro.h new file mode 100644 index 000000000..0d8a08f89 --- /dev/null +++ b/src/citra_libretro/citra_libretro.h @@ -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 diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp new file mode 100644 index 000000000..86e7098f6 --- /dev/null +++ b/src/citra_libretro/core_settings.cpp @@ -0,0 +1,1014 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" + +#include "common/file_util.h" +#include "common/settings.h" +#include "core/hle/service/cfg/cfg.h" + +namespace LibRetro { + +CoreSettings settings = {}; + +namespace config { + +static constexpr const char* enabled = "enabled"; +static constexpr const char* disabled = "disabled"; + +namespace category { +static constexpr const char* cpu = "cpu"; +static constexpr const char* system = "system"; +static constexpr const char* audio = "audio"; +static constexpr const char* graphics = "graphics"; +static constexpr const char* layout = "layout"; +static constexpr const char* storage = "storage"; +static constexpr const char* input = "input"; +} // namespace category + +namespace cpu { +static constexpr const char* use_cpu_jit = "citra_use_cpu_jit"; +static constexpr const char* cpu_clock_percentage = "citra_cpu_scale"; +} // namespace cpu + +namespace system { +static constexpr const char* is_new_3ds = "citra_is_new_3ds"; +static constexpr const char* region = "citra_region_value"; +static constexpr const char* language = "citra_language"; +} // namespace system + +namespace audio { +static constexpr const char* audio_emulation = "citra_audio_emulation"; +static constexpr const char* input_type = "citra_input_type"; +} // namespace audio + +namespace graphics { +static constexpr const char* graphics_api = "citra_graphics_api"; +static constexpr const char* use_hw_shader = "citra_use_hw_shaders"; +static constexpr const char* use_shader_jit = "citra_use_shader_jit"; +static constexpr const char* shaders_accurate_mul = "citra_use_acc_mul"; +static constexpr const char* use_disk_shader_cache = "citra_use_hw_shader_cache"; +static constexpr const char* resolution_factor = "citra_resolution_factor"; +static constexpr const char* texture_filter = "citra_texture_filter"; +static constexpr const char* texture_sampling = "citra_texture_sampling"; +static constexpr const char* custom_textures = "citra_custom_textures"; +static constexpr const char* dump_textures = "citra_dump_textures"; +} // namespace graphics + +namespace layout { +static constexpr const char* layout_option = "citra_layout_option"; +static constexpr const char* swap_screen = "citra_swap_screen"; +static constexpr const char* toggle_swap_screen = "citra_swap_screen_mode"; +} // namespace layout + +namespace storage { +static constexpr const char* use_virtual_sd = "citra_use_virtual_sd"; +static constexpr const char* use_libretro_save_path = "citra_use_libretro_save_path"; +} // namespace storage + +namespace input { +static constexpr const char* analog_function = "citra_analog_function"; +static constexpr const char* deadzone = "citra_deadzone"; +static constexpr const char* mouse_touchscreen = "citra_mouse_touchscreen"; +static constexpr const char* touch_touchscreen = "citra_touch_touchscreen"; +static constexpr const char* render_touchscreen = "citra_render_touchscreen"; +static constexpr const char* motion_enabled = "citra_motion_enabled"; +static constexpr const char* motion_sensitivity = "citra_motion_sensitivity"; +} // namespace input + +} // namespace config + +// clang-format off +static constexpr retro_core_option_v2_category option_categories[] = { + { + config::category::cpu, + "CPU", + "Settings related to CPU emulation performance and accuracy." + }, + { + config::category::system, + "System", + "Nintendo 3DS system configuration and region settings." + }, + { + config::category::audio, + "Audio", + "Audio emulation and microphone settings." + }, + { + config::category::graphics, + "Graphics", + "Graphics API, rendering, and visual enhancement settings." + }, + { + config::category::layout, + "Layout", + "Screen layout and display positioning options." + }, + { + config::category::storage, + "Storage", + "Save data and virtual SD card settings." + }, + { + config::category::input, + "Input", + "Controller and touchscreen input configuration." + }, + { nullptr, nullptr, nullptr } +}; + +// ============================================================================ +// Option Definitions +// ============================================================================ + +static constexpr retro_core_option_v2_definition option_definitions[] = { + // CPU Category + { + config::cpu::use_cpu_jit, + "Enable CPU JIT", + "CPU JIT", + "Enable Just-In-Time compilation for ARM CPU emulation. " + "Significantly improves performance but may reduce accuracy. " + "Restart required.", + nullptr, + config::category::cpu, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::cpu::cpu_clock_percentage, + "CPU Clock Speed", + "CPU Clock Speed", + "Adjust the emulated 3DS CPU clock speed as a percentage of normal speed. " + "Higher values may improve performance in some games but can cause issues. " + "Lower values can help with games that run too fast.", + nullptr, + config::category::cpu, + { + { "25", "25%" }, { "50", "50%" }, { "75", "75%" }, { "100", "100% (Default)" }, + { "125", "125%" }, { "150", "150%" }, { "175", "175%" }, { "200", "200%" }, + { "225", "225%" }, { "250", "250%" }, { "275", "275%" }, { "300", "300%" }, + { "325", "325%" }, { "350", "350%" }, { "375", "375%" }, { "400", "400%" }, + { nullptr, nullptr } + }, + "100" + }, + + // System Category + { + config::system::is_new_3ds, + "3DS System Model", + "System Model", + "Select whether to emulate the original 3DS or New 3DS. " + "New 3DS has additional CPU power and memory, required for some games. " + "Restart required.", + nullptr, + config::category::system, + { + { "Old 3DS", "Original 3DS" }, + { "New 3DS", "New 3DS" }, + { nullptr, nullptr } + }, + "Old 3DS" + }, + { + config::system::region, + "3DS System Region", + "System Region", + "Set the 3DS system region. Auto-select will choose based on the game. " + "Some games are region-locked and require matching regions.", + nullptr, + config::category::system, + { + { "Auto", "Auto" }, + { "Japan", "Japan" }, + { "USA", "USA" }, + { "Europe", "Europe" }, + { "Australia", "Australia" }, + { "China", "China" }, + { "Korea", "Korea" }, + { "Taiwan", "Taiwan" }, + { nullptr, nullptr } + }, + "Auto" + }, + { + config::system::language, + "3DS System Language", + "System Language", + "Set the system language for the emulated 3DS. " + "This affects in-game text language when supported.", + nullptr, + config::category::system, + { + { "English", "English" }, + { "Japanese", "Japanese" }, + { "French", "French" }, + { "Spanish", "Spanish" }, + { "German", "German" }, + { "Italian", "Italian" }, + { "Dutch", "Dutch" }, + { "Portuguese", "Portuguese" }, + { "Russian", "Russian" }, + { "Korean", "Korean" }, + { "Traditional Chinese", "Traditional Chinese" }, + { "Simplified Chinese", "Simplified Chinese" }, + { nullptr, nullptr } + }, + "english" + }, + + // Audio Category + { + config::audio::audio_emulation, + "Audio Emulation", + "Audio Emulation", + "Select audio emulation method. HLE is faster, LLE is more accurate.", + nullptr, + config::category::audio, + { + { "hle", "HLE (Fast)" }, + { "lle", "LLE (Accurate)" }, + { "lle_multithread", "LLE Multithreaded" }, + { nullptr, nullptr } + }, + "hle" + }, + { + config::audio::input_type, + "Microphone Input Type", + "Microphone Input", + "Select how microphone input is handled for games that support it.", + nullptr, + config::category::audio, + { + { "auto", "Auto" }, + { "none", "None" }, + { "static_noise", "Static Noise" }, + { "frontend", "Frontend" }, + { nullptr, nullptr } + }, + "auto" + }, + + // Graphics Category + { + config::graphics::graphics_api, + "Graphics API", + "Graphics API", + "Select the graphics rendering API. Auto will choose the best available option. " + "Restart required.", + nullptr, + config::category::graphics, + { + { "Auto", "Auto" }, +#ifdef ENABLE_VULKAN + { "Vulkan", "Vulkan" }, +#endif +#ifdef ENABLE_OPENGL + { "OpenGL", "OpenGL" }, +#endif + { "Software", "Software" }, + { nullptr, nullptr } + }, + "auto" + }, + { + config::graphics::use_hw_shader, + "Enable Hardware Shaders", + "Hardware Shaders", + "Use GPU hardware to accelerate shader processing. " + "Significantly improves performance but may reduce accuracy.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_shader_jit, + "Enable Shader JIT", + "Shader JIT", + "Use Just-In-Time compilation for shaders. " + "Improves performance but may cause graphical issues in some games.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::shaders_accurate_mul, + "Accurate Shader Multiplication", + "Accurate Multiplication", + "Use accurate multiplication in shaders. " + "More accurate but can reduce performance. Only works with hardware shaders.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_disk_shader_cache, + "Hardware Shader Cache", + "Shader Cache", + "Save compiled shaders to disk to reduce loading times on subsequent runs.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::resolution_factor, + "Internal Resolution", + "Internal Resolution", + "Render the 3DS screens at a higher resolution. " + "Higher values improve visual quality but significantly impact performance.", + nullptr, + config::category::graphics, + { + { "1", "1x (Native 400x240)" }, + { "2", "2x (800x480)" }, + { "3", "3x (1200x720)" }, + { "4", "4x (1600x960)" }, + { "5", "5x (2000x1200)" }, + { "6", "6x (2400x1440)" }, + { "7", "7x (2800x1680)" }, + { "8", "8x (3200x1920)" }, + { "9", "9x (3600x2160)" }, + { "10", "10x (4000x2400)" }, + { nullptr, nullptr } + }, + "1" + }, + { + config::graphics::texture_filter, + "Texture Filter", + "Texture Filter", + "Apply texture filtering to enhance visual quality. " + "Some filters may significantly impact performance.", + nullptr, + config::category::graphics, + { + { "none", "None" }, + { "Anime4K Ultrafast", "Anime4K Ultrafast" }, + { "Bicubic", "Bicubic" }, + { "ScaleForce", "ScaleForce" }, + { "xBRZ", "xBRZ" }, + { "MMPX", "MMPX" }, + { nullptr, nullptr } + }, + "none" + }, + { + config::graphics::texture_sampling, + "Texture Sampling", + "Texture Sampling", + "Control how textures are sampled and filtered.", + nullptr, + config::category::graphics, + { + { "GameControlled", "Game Controlled" }, + { "NearestNeighbor", "Nearest Neighbor" }, + { "Linear", "Linear" }, + { nullptr, nullptr } + }, + "GameControlled" + }, + { + config::graphics::custom_textures, + "Custom Textures", + "Custom Textures", + "Enable loading of custom texture packs to replace original game textures.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::graphics::dump_textures, + "Dump Game Textures", + "Dump Textures", + "Save original game textures to disk for creating custom texture packs. " + "May impact performance.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + + // Layout Category + { + config::layout::layout_option, + "Screen Layout", + "Screen Layout", + "Choose how the 3DS screens are arranged in the display.", + nullptr, + config::category::layout, + { + { "default", "Default Top-Bottom" }, + { "single_screen", "Single Screen Only" }, + { "large_screen", "Large Screen, Small Screen" }, + { "side_by_side", "Side by Side" }, + { nullptr, nullptr } + }, + "default" + }, + { + config::layout::swap_screen, + "Prominent 3DS Screen", + "Prominent Screen", + "Choose which screen is displayed prominently in single screen or large screen layouts.", + nullptr, + config::category::layout, + { + { "Top", "Top Screen" }, + { "Bottom", "Bottom Screen" }, + { nullptr, nullptr } + }, + "Top" + }, + { + config::layout::toggle_swap_screen, + "Screen Swap Mode", + "Swap Mode", + "How screen swapping behaves when using the screen swap hotkey.", + nullptr, + config::category::layout, + { + { "Toggle", "Toggle" }, + { "Hold", "Hold" }, + { nullptr, nullptr } + }, + "Toggle" + }, + + // Storage Category + { + config::storage::use_virtual_sd, + "Enable Virtual SD Card", + "Virtual SD Card", + "Enable virtual SD card support for homebrew and some commercial games.", + nullptr, + config::category::storage, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::storage::use_libretro_save_path, + "Save Data Location", + "Save Location", + "Choose where save data and system files are stored.", + nullptr, + config::category::storage, + { + { "LibRetro Default", "LibRetro Default" }, + { "Azahar Default", "Azahar Default" }, + { nullptr, nullptr } + }, + "LibRetro Default" + }, + + // Input Category + { + config::input::analog_function, + "Right Analog Function", + "Right Analog Function", + "Configure what the right analog stick controls.", + nullptr, + config::category::input, + { + { "c_stick_and_touchscreen", "C-Stick and Touchscreen Pointer" }, + { "touchscreen_pointer", "Touchscreen Pointer" }, + { "c_stick", "C-Stick" }, + { nullptr, nullptr } + }, + "c_stick_and_touchscreen" + }, + { + config::input::deadzone, + "Analog Deadzone", + "Analog Deadzone", + "Set the deadzone percentage for analog input to reduce drift.", + nullptr, + config::category::input, + { + { "0", "0%" }, { "5", "5%" }, { "10", "10%" }, { "15", "15%" }, + { "20", "20%" }, { "25", "25%" }, { "30", "30%" }, { "35", "35%" }, + { nullptr, nullptr } + }, + "15" + }, + { + config::input::mouse_touchscreen, + "Mouse Touchscreen Support", + "Mouse Touchscreen", + "Enable mouse input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::touch_touchscreen, + "Touch Device Support", + "Touch Support", + "Enable touch device input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::render_touchscreen, + "Show Touch Interactions", + "Show Touch", + "Visually indicate touchscreen interactions on screen.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::motion_enabled, + "Gyroscope/Accelerometer Support", + "Motion Support", + "Enable gyroscope and accelerometer input for games that support motion controls.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::motion_sensitivity, + "Motion Sensitivity", + "Motion Sensitivity", + "Adjust sensitivity of motion controls (gyroscope/accelerometer).", + nullptr, + config::category::input, + { + { "0.1", "10%" }, + { "0.25", "25%" }, + { "0.5", "50%" }, + { "0.75", "75%" }, + { "1.0", "100%" }, + { "1.25", "125%" }, + { "1.5", "150%" }, + { "2.0", "200%" }, + { nullptr, nullptr } + }, + "1.0" + }, + + // Terminator + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, { { nullptr, nullptr } }, nullptr } +}; +// clang-format on + +static const retro_core_options_v2 options_v2 = { + const_cast(option_categories), + const_cast(option_definitions)}; + +void RegisterCoreOptions(void) { + // Try v2 first, then fallback to v1 and v0 if needed + unsigned version = 0; + if (!LibRetro::GetCoreOptionsVersion(&version)) { + version = 0; + } + + LOG_INFO(Frontend, "Frontend reports core options version: {}", version); + + if (version >= 2) { + if (LibRetro::SetCoreOptionsV2(&options_v2)) { + LOG_INFO(Frontend, "V2 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V2 core options not supported, trying V1"); + + // Count number of options + unsigned num_options = 0; + while (option_definitions[num_options].key != nullptr) { + num_options++; + } + + if (version >= 1) { + // Create V1 options array + std::vector options_v1(num_options + 1); + + // Copy parameters from V2 to V1 + for (unsigned i = 0; i < num_options; i++) { + const auto& v2_option = option_definitions[i]; + auto& v1_option = options_v1[i]; + + v1_option.key = v2_option.key; + v1_option.desc = v2_option.desc; + v1_option.info = v2_option.info; + v1_option.default_value = v2_option.default_value; + std::memcpy(v1_option.values, v2_option.values, sizeof(v1_option.values)); + } + + // Null terminator + std::memset(&options_v1.back(), 0, sizeof(retro_core_option_definition)); + + if (LibRetro::SetCoreOptionsV1(options_v1.data())) { + LOG_INFO(Frontend, "V1 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V1 core options not supported, trying V0"); + + // Create V0 variables array + std::vector variables(num_options + 1); + std::vector values_buffer(num_options); + + for (unsigned i = 0; i < num_options; i++) { + const auto& option = option_definitions[i]; + std::string desc = option.desc ? option.desc : ""; + std::string default_value = option.default_value ? option.default_value : ""; + + values_buffer[i] = ""; + + if (!desc.empty()) { + // Count number of values + size_t num_values = 0; + size_t default_index = 0; + + while (option.values[num_values].value != nullptr) { + if (!default_value.empty() && + std::string(option.values[num_values].value) == default_value) { + default_index = num_values; + } + num_values++; + } + + // Build values string: "Description; default_value|other_value1|other_value2" + if (num_values > 0) { + values_buffer[i] = desc + "; " + option.values[default_index].value; + + // Add remaining values + for (size_t j = 0; j < num_values; j++) { + if (j != default_index) { + values_buffer[i] += "|" + std::string(option.values[j].value); + } + } + } + } + + variables[i].key = option.key; + variables[i].value = values_buffer[i].c_str(); + } + + // Null terminator + std::memset(&variables.back(), 0, sizeof(retro_variable)); + + // Set V0 variables + if (LibRetro::SetVariables(variables.data())) { + LOG_INFO(Frontend, "V0 core options set successfully"); + } else { + LOG_ERROR(Frontend, "Failed to set core options with any version"); + } +} + +static void ParseCpuOptions(void) { + Settings::values.use_cpu_jit = + LibRetro::FetchVariable(config::cpu::use_cpu_jit, config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_cpu_jit = false; +#endif + + auto cpu_clock = LibRetro::FetchVariable(config::cpu::cpu_clock_percentage, "100"); + Settings::values.cpu_clock_percentage = std::stoi(cpu_clock); +} + +static int GetRegionValue(const std::string& name) { + if (name == "Japan") + return 0; + if (name == "USA") + return 1; + if (name == "Europe") + return 2; + if (name == "Australia") + return 3; + if (name == "China") + return 4; + if (name == "Korea") + return 5; + if (name == "Taiwan") + return 6; + return -1; // Auto +} + +static Service::CFG::SystemLanguage GetLanguageValue(const std::string& name) { + if (name == "Japanese") + return Service::CFG::LANGUAGE_JP; + if (name == "French") + return Service::CFG::LANGUAGE_FR; + if (name == "Spanish") + return Service::CFG::LANGUAGE_ES; + if (name == "German") + return Service::CFG::LANGUAGE_DE; + if (name == "Italian") + return Service::CFG::LANGUAGE_IT; + if (name == "Dutch") + return Service::CFG::LANGUAGE_NL; + if (name == "Portuguese") + return Service::CFG::LANGUAGE_PT; + if (name == "Russian") + return Service::CFG::LANGUAGE_RU; + if (name == "Korean") + return Service::CFG::LANGUAGE_KO; + if (name == "Traditional Chinese") + return Service::CFG::LANGUAGE_TW; + if (name == "Simplified Chinese") + return Service::CFG::LANGUAGE_ZH; + return Service::CFG::LANGUAGE_EN; // English default +} + +static void ParseSystemOptions(void) { + Settings::values.is_new_3ds = + LibRetro::FetchVariable(config::system::is_new_3ds, "Old 3DS") == "New 3DS"; + + Settings::values.region_value = + GetRegionValue(LibRetro::FetchVariable("citra_region_value", "Auto")); + + LibRetro::settings.language_value = + GetLanguageValue(LibRetro::FetchVariable(config::system::language, "English")); +} + +static Settings::AudioEmulation GetAudioEmulation(const std::string& name) { + if (name == "lle") + return Settings::AudioEmulation::LLE; + if (name == "lle_multithread") + return Settings::AudioEmulation::LLEMultithreaded; + return Settings::AudioEmulation::HLE; // Default +} + +static void ParseAudioOptions(void) { + Settings::values.audio_emulation = + GetAudioEmulation(LibRetro::FetchVariable(config::audio::audio_emulation, "hle")); + + auto input_type = LibRetro::FetchVariable(config::audio::input_type, "auto"); + if (input_type == "none") { + Settings::values.input_type = AudioCore::InputType::Null; + } else if (input_type == "static_noise") { + Settings::values.input_type = AudioCore::InputType::Static; + } else if (input_type == "frontend") { + Settings::values.input_type = AudioCore::InputType::LibRetro; + } else { + Settings::values.input_type = AudioCore::InputType::Auto; + } +} + +static Settings::TextureFilter GetTextureFilter(const std::string& name) { + if (name == "Anime4K Ultrafast") + return Settings::TextureFilter::Anime4K; + if (name == "Bicubic") + return Settings::TextureFilter::Bicubic; + if (name == "ScaleForce") + return Settings::TextureFilter::ScaleForce; + if (name == "xBRZ freescale") + return Settings::TextureFilter::xBRZ; + if (name == "MMPX") + return Settings::TextureFilter::MMPX; + + return Settings::TextureFilter::NoFilter; +} + +static Settings::TextureSampling GetTextureSampling(const std::string& name) { + if (name == "NearestNeighbor") + return Settings::TextureSampling::NearestNeighbor; + if (name == "Linear") + return Settings::TextureSampling::Linear; + + return Settings::TextureSampling::GameControlled; +} + +static Settings::GraphicsAPI GetGraphicsAPI(const std::string& name) { + if (name == "Software") + return Settings::GraphicsAPI::Software; +#ifdef ENABLE_VULKAN + if (name == "Vulkan") + return Settings::GraphicsAPI::Vulkan; +#endif +#ifdef ENABLE_OPENGL + if (name == "OpenGL") + return Settings::GraphicsAPI::OpenGL; +#endif + // Auto selection + return LibRetro::GetPreferredRenderer(); +} + +static void ParseGraphicsOptions(void) { + Settings::values.graphics_api = + GetGraphicsAPI(LibRetro::FetchVariable(config::graphics::graphics_api, "auto")); + + Settings::values.use_hw_shader = LibRetro::FetchVariable(config::graphics::use_hw_shader, + config::enabled) == config::enabled; + + Settings::values.use_shader_jit = LibRetro::FetchVariable(config::graphics::use_shader_jit, + config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_shader_jit = false; +#endif + + Settings::values.shaders_accurate_mul = + LibRetro::FetchVariable(config::graphics::shaders_accurate_mul, config::enabled) == + config::enabled; + + Settings::values.use_disk_shader_cache = + LibRetro::FetchVariable(config::graphics::use_disk_shader_cache, config::enabled) == + config::enabled; + + auto resolution = LibRetro::FetchVariable(config::graphics::resolution_factor, "1"); + Settings::values.resolution_factor = std::stoi(resolution); + + Settings::values.texture_filter = + GetTextureFilter(LibRetro::FetchVariable(config::graphics::texture_filter, "none")); + + Settings::values.texture_sampling = GetTextureSampling( + LibRetro::FetchVariable(config::graphics::texture_sampling, "GameControlled")); + + Settings::values.custom_textures = LibRetro::FetchVariable(config::graphics::custom_textures, + config::disabled) == config::enabled; + + Settings::values.dump_textures = LibRetro::FetchVariable(config::graphics::dump_textures, + config::disabled) == config::enabled; +} + +static Settings::LayoutOption GetLayoutOption(const std::string& name) { + if (name == "single_screen" || name == "Single Screen Only") + return Settings::LayoutOption::SingleScreen; + if (name == "large_screen" || name == "Large Screen, Small Screen") + return Settings::LayoutOption::LargeScreen; + if (name == "side_by_side" || name == "Side by Side") + return Settings::LayoutOption::SideScreen; + return Settings::LayoutOption::Default; +} + +static void ParseLayoutOptions(void) { + Settings::values.layout_option = + GetLayoutOption(LibRetro::FetchVariable(config::layout::layout_option, "default")); + + Settings::values.swap_screen = + LibRetro::FetchVariable(config::layout::swap_screen, "Top") == "Bottom"; + + LibRetro::settings.toggle_swap_screen = + LibRetro::FetchVariable(config::layout::toggle_swap_screen, "Toggle") == "Toggle"; +} + +static void ParseStorageOptions(void) { + Settings::values.use_virtual_sd = LibRetro::FetchVariable(config::storage::use_virtual_sd, + config::enabled) == config::enabled; + + // Configure the file storage location + auto use_libretro_saves = LibRetro::FetchVariable(config::storage::use_libretro_save_path, + "LibRetro Default") == "LibRetro Default"; + + if (use_libretro_saves) { + auto target_dir = LibRetro::GetSaveDir(); + if (target_dir.empty()) { + LOG_INFO(Frontend, "No save dir provided; trying system dir..."); + target_dir = LibRetro::GetSystemDir(); + } + + if (!target_dir.empty()) { + if (!target_dir.ends_with("/")) + target_dir += "/"; + + target_dir += "Azahar/"; + + // Ensure that this new dir exists + if (!FileUtil::CreateDir(target_dir)) { + LOG_ERROR(Frontend, "Failed to create \"{}\". Using Azahar's default paths.", + target_dir); + } else { + FileUtil::SetUserPath(target_dir); + const auto& target_dir_result = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + LOG_INFO(Frontend, "User dir set to \"{}\".", target_dir_result); + } + } + } +} + +static LibRetro::CStickFunction GetAnalogFunction(const std::string& name) { + if (name == "c_stick" || name == "C-Stick") + return LibRetro::CStickFunction::CStick; + if (name == "touchscreen_pointer" || name == "Touchscreen Pointer") + return LibRetro::CStickFunction::Touchscreen; + return LibRetro::CStickFunction::Both; // Default +} + +static void ParseInputOptions(void) { + LibRetro::settings.analog_function = GetAnalogFunction( + LibRetro::FetchVariable(config::input::analog_function, "c_stick_and_touchscreen")); + + 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] = ""; + } + + auto deadzone = LibRetro::FetchVariable(config::input::deadzone, "15"); + LibRetro::settings.deadzone = static_cast(std::stoi(deadzone)) / 100.0f; + + LibRetro::settings.mouse_touchscreen = + LibRetro::FetchVariable(config::input::mouse_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.touch_touchscreen = + LibRetro::FetchVariable(config::input::touch_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.render_touchscreen = + LibRetro::FetchVariable(config::input::render_touchscreen, config::disabled) == + config::enabled; + LibRetro::settings.motion_enabled = + LibRetro::FetchVariable(config::input::motion_enabled, config::enabled) == config::enabled; + auto motion_sens = LibRetro::FetchVariable(config::input::motion_sensitivity, "1.0"); + LibRetro::settings.motion_sensitivity = std::stof(motion_sens); + + // Configure motion device based on user settings + if (LibRetro::settings.motion_enabled) { + Settings::values.current_input_profile.motion_device = + "port:0,sensitivity:" + std::to_string(LibRetro::settings.motion_sensitivity) + + ",engine:libretro"; + } else { + Settings::values.current_input_profile.motion_device = "engine:motion_emu"; + } +} + +void ParseCoreOptions(void) { + // Override default values that aren't user-selectable and aren't correct for the core + Settings::values.enable_audio_stretching = false; + Settings::values.frame_limit = 0; +#if defined(USING_GLES) + Settings::values.use_gles = true; +#else + Settings::values.use_gles = false; +#endif + Settings::values.filter_mode = false; + + ParseCpuOptions(); + ParseSystemOptions(); + ParseAudioOptions(); + ParseGraphicsOptions(); + ParseLayoutOptions(); + ParseStorageOptions(); + ParseInputOptions(); +} + +} // namespace LibRetro diff --git a/src/citra_libretro/core_settings.h b/src/citra_libretro/core_settings.h new file mode 100644 index 000000000..78bcff236 --- /dev/null +++ b/src/citra_libretro/core_settings.h @@ -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 +#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 diff --git a/src/citra_libretro/emu_window/libretro_window.cpp b/src/citra_libretro/emu_window/libretro_window.cpp new file mode 100644 index 000000000..10d49d883 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.cpp @@ -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 +#endif +#include + +#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(width), + static_cast(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(width), + static_cast(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(fb.data); + pitch = fb.pitch; + } else { + pitch = static_cast(width) * 4; + data = static_cast(calloc(1, pitch * height)); + did_malloc = true; + } + + std::memset(data, 0, pitch * height); + + auto& system = Core::System::GetInstance(); + const auto& renderer = static_cast(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& 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(rect.top + oy) * pitch + + static_cast(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(width), + static_cast(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(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 _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(); + + doCleanFrame = true; +} + +void EmuWindow_LibRetro::DestroyContext() { + tracker = nullptr; +} diff --git a/src/citra_libretro/emu_window/libretro_window.h b/src/citra_libretro/emu_window/libretro_window.h new file mode 100644 index 000000000..235d14319 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.h @@ -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 +#include +#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 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 tracker = nullptr; + + bool enableEmulatedPointer = false; +}; diff --git a/src/citra_libretro/environment.cpp b/src/citra_libretro/environment.cpp new file mode 100644 index 000000000..04c78356f --- /dev/null +++ b/src/citra_libretro/environment.cpp @@ -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 + +#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; +} diff --git a/src/citra_libretro/environment.h b/src/citra_libretro/environment.h new file mode 100644 index 000000000..2b84d8b85 --- /dev/null +++ b/src/citra_libretro/environment.h @@ -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 +#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 diff --git a/src/citra_libretro/input/input_factory.cpp b/src/citra_libretro/input/input_factory.cpp new file mode 100644 index 000000000..3a5ae9665 --- /dev/null +++ b/src/citra_libretro/input/input_factory.cpp @@ -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 +#include +#include +#include + +#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(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 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(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> GetStatus() const override { + Common::Vec3 accel = {0.0f, 0.0f, -1.0f}; // Default gravity pointing down + Common::Vec3 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(port, sensitivity); + } +}; + +void Init() { + using namespace ::Input; + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); +} + +void Shutdown() { + using namespace ::Input; + UnregisterFactory("libretro"); + UnregisterFactory("libretro"); + UnregisterFactory("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 diff --git a/src/citra_libretro/input/input_factory.h b/src/citra_libretro/input/input_factory.h new file mode 100644 index 000000000..74ec5fb74 --- /dev/null +++ b/src/citra_libretro/input/input_factory.h @@ -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 diff --git a/src/citra_libretro/input/mouse_tracker.cpp b/src/citra_libretro/input/mouse_tracker.cpp new file mode 100644 index 000000000..6829aad2d --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.cpp @@ -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 +#include +#include + +#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 +#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(); +#endif + break; + case Settings::GraphicsAPI::Vulkan: +#ifdef ENABLE_VULKAN + cursor_renderer = std::make_unique(); +#endif + break; + case Settings::GraphicsAPI::Software: + cursor_renderer = std::make_unique(); + 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((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((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(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(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((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((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(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(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(controllerX * widthSpeed), + static_cast(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(projectedX); + int centerY = static_cast(projectedY); + int radius = static_cast(renderRatio); + + // Calculate cursor dimensions within bounds + int verticalLeft = std::max(centerX - radius / 5, static_cast(layout.bottom_screen.left)); + int verticalRight = + std::min(centerX + radius / 5, static_cast(layout.bottom_screen.right)); + int verticalTop = std::max(centerY - radius, static_cast(layout.bottom_screen.top)); + int verticalBottom = std::min(centerY + radius, static_cast(layout.bottom_screen.bottom)); + + int horizontalLeft = std::max(centerX - radius, static_cast(layout.bottom_screen.left)); + int horizontalRight = std::min(centerX + radius, static_cast(layout.bottom_screen.right)); + int horizontalTop = std::max(centerY - radius / 5, static_cast(layout.bottom_screen.top)); + int horizontalBottom = + std::min(centerY + radius / 5, static_cast(layout.bottom_screen.bottom)); + + // Draw cursor directly to framebuffer (assuming RGBA8888 format) + uint32_t* pixels = static_cast(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 diff --git a/src/citra_libretro/input/mouse_tracker.h b/src/citra_libretro/input/mouse_tracker.h new file mode 100644 index 000000000..3f4435792 --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.h @@ -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 GetPressedPosition() { + return {static_cast(projectedX), + static_cast(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 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 diff --git a/src/citra_libretro/libretro.osx.def b/src/citra_libretro/libretro.osx.def new file mode 100644 index 000000000..53a556a60 --- /dev/null +++ b/src/citra_libretro/libretro.osx.def @@ -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 diff --git a/src/citra_libretro/libretro_vk.cpp b/src/citra_libretro/libretro_vk.cpp new file mode 100644 index 000000000..eed09d5bc --- /dev/null +++ b/src/citra_libretro/libretro_vk.cpp @@ -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 +#include +#include +#include +#include +#include + +#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 + +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& enabled_exts, + const std::vector& 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 available_exts(ext_count); + if (ext_count > 0) { + vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data()); + } + + // Start with frontend's required extensions + std::vector 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(required_features)[i]) { + reinterpret_cast(&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 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(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(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(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 OpenLibrary( + [[maybe_unused]] Frontend::GraphicsContext* context) { + // the frontend takes care of this, we'll get the instance later + return std::make_shared(); +} + +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(&image_info), + &alloc_info, &vk_image, &output_allocation, nullptr); + + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast(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(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(vulkan_intf), static_cast(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(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(frame->image_view); + persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + persistent_libretro_image.create_info = + static_cast(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 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 CreateLibRetroMasterSemaphore(const Instance& instance) { + return std::make_unique(instance); +} + +} // namespace Vulkan diff --git a/src/citra_libretro/libretro_vk.h b/src/citra_libretro/libretro_vk.h new file mode 100644 index 000000000..c51075e21 --- /dev/null +++ b/src/citra_libretro/libretro_vk.h @@ -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 +#include +#include + +#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; + +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 free_queue; + std::queue 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 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(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_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 diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6bb14fc9e..4d7981d82 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -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 diff --git a/src/common/error.cpp b/src/common/error.cpp index 1d7467f68..def285115 100644 --- a/src/common/error.cpp +++ b/src/common/error.cpp @@ -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); diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 79c0afaca..e42fcd785 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -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 #include #include +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 +#include + +#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 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::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) diff --git a/src/common/file_util.h b/src/common/file_util.h index 98d232dcf..58cc4f7aa 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,14 @@ #include "common/string_util.h" #endif +#ifdef HAVE_LIBRETRO_VFS +#define SKIP_STDIO_REDEFINES +#include +#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) \ No newline at end of file +BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 36fcfeccf..e1b8621bd 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -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(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(libretro_backend)); +#else lambda(static_cast(debugger_backend)); lambda(static_cast(color_console_backend)); lambda(static_cast(file_backend)); #ifdef ANDROID lambda(static_cast(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 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); } diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index e16a129b1..c87bff2dc 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -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 #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(); diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index 3257fa2d8..20840e3ac 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include diff --git a/src/core/core.h b/src/core/core.h index 085a7b3b0..d493e8491 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -362,6 +362,10 @@ public: void LoadState(u32 slot); + std::vector SaveStateBuffer() const; + + bool LoadStateBuffer(std::vector buffer); + /// Self delete ncch bool SetSelfDelete(const std::string& file) { if (m_filepath == file) { diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index d5b7803cc..1f0fec7b7 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -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: diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 070612250..8a34237c6 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -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(); diff --git a/src/core/hle/service/mic/mic_u.cpp b/src/core/hle/service/mic/mic_u.cpp index d15c077bd..f6fee7784 100644 --- a/src/core/hle/service/mic/mic_u.cpp +++ b/src/core/hle/service/mic/mic_u.cpp @@ -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"); } diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index 23bfb5aec..d4ae261bf 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -2308,7 +2308,9 @@ std::optional 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) { diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index 3390af273..573862d81 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -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 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{reinterpret_cast(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::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 result((u8*)&header, (u8*)&header + sizeof(header)); + std::copy(buffer.begin(), buffer.end(), std::back_inserter(result)); + + return result; +} + +bool System::LoadStateBuffer(std::vector 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 state(buffer.begin() + sizeof(CSTHeader), buffer.end()); + auto decompressed = Common::Compression::DecompressDataZSTD(state); + + std::istringstream sstream{ + std::string{reinterpret_cast(decompressed.data()), decompressed.size()}, + std::ios_base::binary}; + decompressed.clear(); + + // Deserialize + iarchive ia{sstream}; + ia&* this; + + return true; +} + } // namespace Core diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 4ac9368b1..f248ece4b 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -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 $) +endif() + add_test(NAME tests COMMAND tests) if (CITRA_USE_PRECOMPILED_HEADERS) diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 3da82159e..a8ade344d 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -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 + $<$>:renderer_vulkan/vk_platform.cpp> renderer_vulkan/vk_platform.h - renderer_vulkan/vk_present_window.cpp - renderer_vulkan/vk_present_window.h + $<$>:renderer_vulkan/vk_present_window.cpp> + $<$>: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 + $<$>:renderer_vulkan/vk_swapchain.cpp> + $<$>:renderer_vulkan/vk_swapchain.h> renderer_vulkan/vk_texture_runtime.cpp renderer_vulkan/vk_texture_runtime.h shader/generator/spv_fs_shader_gen.cpp diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 37272d4ad..9a76f4859 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.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(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 void GPU::serialize(Archive& ar, const u32 file_version) { ar & impl->pica; diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 1b326a067..4e32b6d2e 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -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); diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp index 1f9a16793..6969faa88 100644 --- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp @@ -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()); diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 967fd21fa..c8cb6c000 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -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 ortho_matrix = diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 59a5a1c94..0b04b0ba7 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -22,7 +22,7 @@ #include -#ifdef __APPLE__ +#if defined(__APPLE__) && !defined(HAVE_LIBRETRO) #include "common/apple_utils.h" #endif @@ -60,11 +60,11 @@ constexpr static std::array 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; diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 29ed8a66e..b275f3189 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -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; diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d41cdfb9e..3f968e4c9 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -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(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 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, }; diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 09984b53f..f6fbc28fb 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -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 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 \ No newline at end of file +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 0021167e4..03b644ea2 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -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!"); diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 0099b0ca3..5d50b6c97 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -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 MakeMasterSemaphore(const Instance& instance) { +#ifdef HAVE_LIBRETRO + return CreateLibRetroMasterSemaphore(instance); +#else if (instance.IsTimelineSemaphoreSupported()) { return std::make_unique(instance); } else { return std::make_unique(instance); } +#endif } } // Anonymous namespace diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index fdaa4ef2e..cfcb199a1 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -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,