Implement libretro core (#1215)

* libretro core

* Bringing citra libretro implementation over
* libretro: hook up vulkan renderer
* libretro: github actions
* libretro: gyro
* libretro: core options v2
* libretro: on ios turn off shader jit if unavailable
* moltenvk 1.3.0 introduces 8-bit indexes but allocates 16-bit for metal; this ends up allocating stream buffer * 2 = 132MiB. Instead, just use 16-bit indexes. (This will be necessary for standalone when bumping moltenvk version.)

* libretro core: address review feedback

* libretro: microphone support

* cmake: Add ENABLE_ROOM_STANDALONE to list of incompatible libretro flags

* libretro: proper initial geometry

* libretro: fix software renderer

* libretro: address review feedback

* .github/libretro.yml: Pin macOS runners at macOS 26

* ci: Remove explicit selection of Xcode 16.0

* .github/libretro.yml: remove unnecessary windows builder apt commands

* .github/libretro.yml: bump min macos version to 11.0

* ci: Re-enable CI jobs for all libretro cores

This is under the condition that we don't introduce build cache for these builds

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
Co-authored-by: PabloMK7 <hackyglitch2@gmail.com>
This commit is contained in:
Eric Warmenhoven 2026-02-19 17:30:25 -05:00 committed by GitHub
parent d0eaf07a40
commit d9b77cc21e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 5907 additions and 93 deletions

166
.github/workflows/libretro.yml vendored Normal file
View file

@ -0,0 +1,166 @@
name: citra-libretro
on:
push:
branches: [ "*" ]
tags: [ "*" ]
pull_request:
branches: [ master ]
workflow_dispatch:
env:
CORE_ARGS: -DENABLE_LIBRETRO=ON
jobs:
android:
runs-on: ubuntu-22.04
env:
OS: android
TARGET: arm64-v8a
API_LEVEL: 21
ANDROID_NDK_VERSION: 26.2.11394342
ANDROID_ABI: arm64-v8a
BUILD_DIR: build/android-arm64-v8a
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set tag name
run: |
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
fi
echo $GIT_TAG_NAME
- name: Update Android SDK CMake version
run: |
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "ndk;$ANDROID_NDK_VERSION"
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
- name: Build
run: |
export NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/$ANDROID_NDK_VERSION
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI . -B $BUILD_DIR
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro_android.so
linux:
runs-on: ubuntu-22.04
env:
OS: linux
TARGET: x86_64
BUILD_DIR: build/linux-x86_64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12 -DENABLE_LTO=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.so
windows:
runs-on: ubuntu-latest
env:
OS: windows
TARGET: x86_64
BUILD_DIR: build/windows-x86_64
EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja
CMAKE: x86_64-w64-mingw32.static-cmake
IMAGE: git.libretro.com:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build in cross-container
run: |
docker pull $IMAGE
docker run --rm --user root \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-w "${GITHUB_WORKSPACE}" \
$IMAGE \
bash -lc "\
${CMAKE} $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR && \
${CMAKE} --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)"
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dll
macos:
runs-on: macos-26
strategy:
matrix:
target: ["x86_64", "arm64"]
env:
OS: macos
TARGET: ${{ matrix.target }}
MACOSX_DEPLOYMENT_TARGET: 11.0
BUILD_DIR: build/osx-${{ matrix.target }}
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install tools
run: brew install spirv-tools
- name: Build
run: |
cmake $CORE_ARGS -DCMAKE_OSX_ARCHITECTURES=$TARGET . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
ios:
runs-on: macos-26
env:
OS: ios
TARGET: arm64
BUILD_DIR: build/ios-arm64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
tvos:
runs-on: macos-26
env:
OS: tvos
TARGET: arm64
BUILD_DIR: build/tvos-arm64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib

127
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,127 @@
.core-defs:
variables:
JNI_PATH: .
CORENAME: azahar
API_LEVEL: 21
BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF
CORE_ARGS: ${BASE_CORE_ARGS}
EXTRA_PATH: bin/Release
variables:
STATIC_RETROARCH_BRANCH: master
GIT_SUBMODULE_STRATEGY: recursive
# Inclusion templates, required for the build to work
include:
################################## DESKTOPS ############################## ##
# Windows 64-bit
- project: 'libretro-infrastructure/ci-templates'
file: '/windows-cmake-mingw.yml'
# Linux 64-bit
- project: 'libretro-infrastructure/ci-templates'
file: '/linux-cmake.yml'
# MacOS x86_64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-x86.yml'
# MacOS ARM64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-arm64.yml'
################################## CELLULAR ############################## ##
# Android
- project: 'libretro-infrastructure/ci-templates'
file: '/android-cmake.yml'
# iOS
- project: 'libretro-infrastructure/ci-templates'
file: '/ios-cmake.yml'
# tvOS
- project: 'libretro-infrastructure/ci-templates'
file: '/tvos-cmake.yml'
################################## CONSOLES ############################## ##
# Stages for building
stages:
- build-prepare
- build-shared
- build-static
##############################################################################
#################################### STAGES ##################################
##############################################################################
#
################################### DESKTOPS #################################
# Windows 64-bit
libretro-build-windows-x64:
extends:
- .core-defs
- .libretro-windows-cmake-x86_64
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja
# Linux 64-bit
libretro-build-linux-x64:
extends:
- .core-defs
- .libretro-linux-cmake-x86_64
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF
CC: /usr/bin/gcc-12
CXX: /usr/bin/g++-12
# MacOS x86_64
libretro-build-osx-x64:
tags:
- mac-apple-silicon
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64
MACOSX_DEPLOYMENT_TARGET: "10.15"
extends:
- .core-defs
- .libretro-osx-cmake-x86_64
# MacOS ARM64
libretro-build-osx-arm64:
extends:
- .core-defs
- .libretro-osx-cmake-arm64
################################### CELLULAR #################################
# Android ARMv8a
android-arm64-v8a:
extends:
- .libretro-android-cmake-arm64-v8a
- .core-defs
variables:
ANDROID_NDK_VERSION: 26.2.11394342
NDK_ROOT: /android-sdk-linux/ndk/$ANDROID_NDK_VERSION
# iOS arm64
libretro-build-ios-arm64:
extends:
- .libretro-ios-cmake-arm64
- .core-defs
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
IOS_MINVER: "14.0"
EXTRA_PATH: bin/RelWithDebInfo
# tvOS arm64
libretro-build-tvos-arm64:
extends:
- .libretro-tvos-cmake-arm64
- .core-defs
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
MINVER: "14.0"
EXTRA_PATH: bin/RelWithDebInfo
################################### CONSOLES #################################

3
.gitmodules vendored
View file

@ -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

View file

@ -17,20 +17,23 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules")
include(DownloadExternals)
include(CMakeDependentOption)
include(FindPkgConfig)
project(citra LANGUAGES C CXX ASM)
# must be invoked after project() command when using CMAKE_TOOLCHAIN_FILE
include(FindPkgConfig)
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
enable_language(OBJC OBJCXX)
endif()
option(ENABLE_LIBRETRO "Build as a LibRetro core" OFF)
# Some submodules like to pick their own default build type if not specified.
# Make sure we default to Release build type always, unless the generator has custom types.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
endif()
if (APPLE)
if (APPLE AND NOT ENABLE_LIBRETRO)
# Silence warnings on empty objects, for example when platform-specific code is #ifdef'd out.
set(CMAKE_C_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
@ -90,6 +93,17 @@ else()
set(DEFAULT_ENABLE_OPENGL ON)
endif()
# Track which options were explicitly set by the user (for libretro conflict detection)
set(_LIBRETRO_INCOMPATIBLE_OPTIONS
ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING
ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB)
set(_USER_SET_OPTIONS "")
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
if(DEFINED ${_opt})
list(APPEND _USER_SET_OPTIONS ${_opt})
endif()
endforeach()
option(ENABLE_SDL2 "Enable using SDL2" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF)
option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF)
@ -130,6 +144,31 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via
option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON)
# Handle incompatible options for libretro builds
if(ENABLE_LIBRETRO)
# Check for explicitly-set conflicting options
set(_CONFLICTS "")
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
list(FIND _USER_SET_OPTIONS ${_opt} _idx)
if(NOT _idx EQUAL -1 AND ${_opt})
list(APPEND _CONFLICTS ${_opt})
endif()
endforeach()
if(_CONFLICTS)
string(REPLACE ";" ", " _CONFLICTS_STR "${_CONFLICTS}")
message(FATAL_ERROR
"ENABLE_LIBRETRO is incompatible with: ${_CONFLICTS_STR}\n"
"These options were explicitly enabled but are not supported for libretro builds.\n"
"Remove these options or set them to OFF.")
endif()
# Force disable incompatible options (handles defaulted-on options)
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
set(${_opt} OFF CACHE BOOL "Disabled for libretro" FORCE)
endforeach()
endif()
# Pass the following values to C++ land
if (ENABLE_QT)
add_definitions(-DENABLE_QT)
@ -300,6 +339,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO)
# set up output paths for executable binaries
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$<CONFIG>)
if (ENABLE_LIBRETRO)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
# System imported libraries
# ======================
@ -359,7 +401,7 @@ if (APPLE)
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
if (ENABLE_VULKAN)
if (ENABLE_VULKAN AND NOT ENABLE_LIBRETRO)
if (NOT USE_SYSTEM_MOLTENVK)
download_moltenvk()
endif()

View file

@ -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)

View file

@ -0,0 +1,16 @@
add_library(libretro_common STATIC
libretro-common/compat/compat_posix_string.c
libretro-common/compat/fopen_utf8.c
libretro-common/encodings/encoding_utf.c
libretro-common/compat/compat_strl.c
libretro-common/file/file_path.c
libretro-common/streams/file_stream.c
libretro-common/streams/file_stream_transforms.c
libretro-common/string/stdstring.c
libretro-common/time/rtime.c
libretro-common/vfs/vfs_implementation.c
)
target_include_directories(libretro_common PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common/include
)

@ -0,0 +1 @@
Subproject commit 7fc7feeddca391be65c94e6541381467684b814d

View file

@ -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
)
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()

View file

@ -38,6 +38,7 @@ add_library(audio_core STATIC
$<$<BOOL:${ENABLE_SDL2}>:sdl2_sink.cpp sdl2_sink.h>
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h>
$<$<BOOL:${ENABLE_LIBRETRO}>:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h>
$<$<BOOL:${ENABLE_OPENAL}>:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h>
)

View file

@ -41,7 +41,11 @@ void DspInterface::OutputFrame(StereoFrame16 frame) {
return;
}
if (sink->ImmediateSubmission()) {
sink->PushSamples(frame.data(), frame.size());
} else {
fifo.Push(frame.data(), frame.size());
}
auto video_dumper = system.GetVideoDumper();
if (video_dumper && video_dumper->IsDumping()) {
@ -54,7 +58,11 @@ void DspInterface::OutputSample(std::array<s16, 2> sample) {
return;
}
if (sink->ImmediateSubmission()) {
sink->PushSamples(&sample, 1);
} else {
fifo.Push(&sample, 1);
}
auto video_dumper = system.GetVideoDumper();
if (video_dumper && video_dumper->IsDumping()) {

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -15,6 +15,9 @@
#ifdef HAVE_OPENAL
#include "audio_core/openal_input.h"
#endif
#ifdef HAVE_LIBRETRO
#include "audio_core/libretro_input.h"
#endif
#include "common/logging/log.h"
#include "core/core.h"
@ -22,6 +25,18 @@ namespace AudioCore {
namespace {
// input_details is ordered in terms of desirability, with the best choice at the top.
constexpr std::array input_details = {
#ifdef HAVE_LIBRETRO
InputDetails{InputType::LibRetro, "Real Device (LibRetro)", true,
[](Core::System& system, std::string_view device_id) -> std::unique_ptr<Input> {
if (!system.HasMicPermission()) {
LOG_WARNING(Audio,
"Microphone permission denied, falling back to null input.");
return std::make_unique<NullInput>();
}
return std::make_unique<LibRetroInput>();
},
[] { return std::vector<std::string>{"LibRetro Microphone"}; }},
#endif
#ifdef HAVE_CUBEB
InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true,
[](Core::System& system, std::string_view device_id) -> std::unique_ptr<Input> {

View file

@ -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 {

View file

@ -0,0 +1,327 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <atomic>
#include <cstring>
#include <mutex>
#include <optional>
#include <vector>
#include "audio_core/libretro_input.h"
#include "citra_libretro/environment.h"
#include "common/logging/log.h"
#include "common/ring_buffer.h"
#include "libretro.h"
namespace AudioCore {
namespace {
// Global instance pointer for access from retro_run
LibRetroInput* g_libretro_input = nullptr;
} // namespace
struct LibRetroInput::Impl {
std::optional<retro_microphone_interface> mic_interface;
retro_microphone_t* mic_handle = nullptr;
bool is_sampling = false;
u8 sample_size_in_bytes = 2;
int warmup_frames = 0;
// The rate at which the frontend actually provides samples (may differ from
// what the 3DS mic service requested). We open the mic at this rate to avoid
// RetroArch's internal resampler path, which has a convergence bug when
// downsampling (ratio < 1). We resample ourselves in Read() instead.
u32 native_sample_rate = 0;
// Ring buffer for thread-safe sample storage
// Capacity: 4096 samples should be plenty for buffering between frames
// The 3DS mic service reads 16 samples at a time at ~32728 Hz
Common::RingBuffer<s16, 4096> sample_buffer;
// Temporary buffer for reading from frontend
std::vector<s16> read_buffer;
Impl() {
// Try to get the microphone interface from the frontend
retro_microphone_interface interface{};
interface.interface_version = RETRO_MICROPHONE_INTERFACE_VERSION;
if (LibRetro::GetMicrophoneInterface(&interface)) {
if (interface.interface_version == RETRO_MICROPHONE_INTERFACE_VERSION) {
mic_interface = interface;
LOG_INFO(Audio, "LibRetro microphone interface available (version {})",
interface.interface_version);
} else {
LOG_WARNING(Audio,
"LibRetro microphone interface version mismatch: expected {}, got {}",
RETRO_MICROPHONE_INTERFACE_VERSION, interface.interface_version);
}
} else {
LOG_WARNING(Audio, "LibRetro microphone interface not available");
}
// Keep this small enough that RetroArch's microphone_driver_read can
// fill its outgoing FIFO in a single flush iteration. The CoreAudio
// driver's internal FIFO is ~480 samples (10ms at 48kHz). If we
// request more than that, the blocking while-loop in
// microphone_driver_read must wait for the next hardware callback,
// and on ARM64 without memory barriers in the FIFO, it may never
// see the new data. 128 samples is conservative enough to succeed
// in one pass.
read_buffer.resize(128);
}
~Impl() {
CloseMicrophone();
}
bool EnsureMicrophoneOpen() {
if (mic_handle) {
return true;
}
if (!mic_interface) {
return false;
}
// Always open at 48000 Hz regardless of what the game requests.
// RetroArch's microphone_driver_read has a resampler whose while-loop
// deadlocks when the ratio is < 1 (core rate < device rate). The
// libretro get_params API only returns the effective (requested) rate,
// not the device's native rate, so we can't detect the mismatch.
// Opening at 48000 Hz (the most common hardware rate) keeps the
// frontend's internal resampling ratio at or near 1.0, avoiding the
// bug. We resample to the game's requested rate ourselves in Read().
static constexpr u32 kMicOpenRate = 48000;
native_sample_rate = kMicOpenRate;
retro_microphone_params_t params{};
params.rate = kMicOpenRate;
mic_handle = mic_interface->open_mic(&params);
if (!mic_handle) {
LOG_ERROR(Audio, "Failed to open LibRetro microphone");
return false;
}
// The frontend may start recording immediately in open_mic (e.g.
// CoreAudio calls AudioOutputUnitStart). Pause it right away so the
// mic is available but idle until StartSampling enables it.
mic_interface->set_mic_state(mic_handle, false);
LOG_INFO(Audio, "LibRetro microphone opened at {} Hz (idle)", native_sample_rate);
return true;
}
void CloseMicrophone() {
if (mic_interface && mic_handle) {
mic_interface->close_mic(mic_handle);
mic_handle = nullptr;
}
}
bool SetMicrophoneActive(bool active) {
if (!mic_interface || !mic_handle) {
return false;
}
return mic_interface->set_mic_state(mic_handle, active);
}
bool IsMicrophoneActive() const {
if (!mic_interface || !mic_handle) {
return false;
}
return mic_interface->get_mic_state(mic_handle);
}
};
LibRetroInput::LibRetroInput() : impl(std::make_unique<Impl>()) {
g_libretro_input = this;
}
LibRetroInput::~LibRetroInput() {
StopSampling();
if (g_libretro_input == this) {
g_libretro_input = nullptr;
}
}
void LibRetroInput::StartSampling(const InputParameters& params) {
if (IsSampling()) {
return;
}
// LibRetro only provides signed 16-bit PCM samples
// We'll convert to the requested format in Read()
if (params.sign == Signedness::Unsigned) {
LOG_DEBUG(Audio, "Application requested unsigned PCM format; will convert from signed.");
}
parameters = params;
impl->sample_size_in_bytes = params.sample_size / 8;
if (!impl->EnsureMicrophoneOpen()) {
LOG_WARNING(Audio, "Cannot start sampling: microphone not available");
return;
}
// Enable the microphone (transitions from idle to recording)
if (!impl->SetMicrophoneActive(true)) {
LOG_ERROR(Audio, "Failed to activate microphone");
return;
}
impl->is_sampling = true;
// Give the audio hardware a few frames to start delivering data before
// we attempt a (blocking) read_mic call. Without this, the very first
// read can hang because the CoreAudio callback hasn't fired yet.
impl->warmup_frames = 10;
LOG_INFO(Audio, "LibRetro microphone sampling started at {} Hz, {} bit", params.sample_rate,
params.sample_size);
}
void LibRetroInput::StopSampling() {
if (!impl->is_sampling) {
return;
}
impl->SetMicrophoneActive(false);
impl->is_sampling = false;
LOG_INFO(Audio, "LibRetro microphone sampling stopped (mic remains idle)");
}
bool LibRetroInput::IsSampling() {
return impl->is_sampling;
}
void LibRetroInput::AdjustSampleRate(u32 sample_rate) {
if (!IsSampling()) {
return;
}
// Restart with new sample rate
auto new_parameters = parameters;
new_parameters.sample_rate = sample_rate;
StopSampling();
StartSampling(new_parameters);
}
void LibRetroInput::PollMicrophone() {
// This is called from the main thread (retro_run)
// Read samples from the frontend and push to the ring buffer
if (!impl->is_sampling || !impl->mic_interface || !impl->mic_handle) {
return;
}
// Wait for the audio hardware to start delivering data before making
// any blocking read_mic calls.
if (impl->warmup_frames > 0) {
impl->warmup_frames--;
return;
}
// Issue a memory fence before reading. RetroArch's CoreAudio mic driver
// fills its FIFO from a callback thread without memory barriers. On ARM64
// (weak memory model), the main thread may not see the callback's writes
// without an explicit barrier.
std::atomic_thread_fence(std::memory_order_acquire);
int samples_read = impl->mic_interface->read_mic(impl->mic_handle, impl->read_buffer.data(),
static_cast<size_t>(impl->read_buffer.size()));
if (samples_read > 0) {
impl->sample_buffer.Push(
std::span<const s16>(impl->read_buffer.data(), static_cast<size_t>(samples_read)));
}
}
Samples LibRetroInput::Read() {
// This is called from the CoreTiming scheduler thread
// Pop samples from the ring buffer (thread-safe)
if (!impl->is_sampling) {
return {};
}
// Pop available samples from the buffer (at native device rate)
std::vector<s16> raw_samples = impl->sample_buffer.Pop();
if (raw_samples.empty()) {
return {};
}
// Resample from native device rate to the rate the 3DS mic service expects
if (impl->native_sample_rate != 0 && impl->native_sample_rate != parameters.sample_rate) {
double ratio = static_cast<double>(parameters.sample_rate) / impl->native_sample_rate;
auto output_count = static_cast<std::size_t>(raw_samples.size() * ratio);
if (output_count == 0) {
return {};
}
std::vector<s16> resampled(output_count);
for (std::size_t i = 0; i < output_count; i++) {
double src_pos = i / ratio;
auto idx = static_cast<std::size_t>(src_pos);
double frac = src_pos - idx;
if (idx + 1 < raw_samples.size()) {
resampled[i] =
static_cast<s16>(raw_samples[idx] * (1.0 - frac) + raw_samples[idx + 1] * frac);
} else {
resampled[i] = raw_samples[std::min(idx, raw_samples.size() - 1)];
}
}
raw_samples = std::move(resampled);
}
// Convert sample format if needed
constexpr auto convert_s16_to_u16 = [](s16 sample) -> u16 {
return static_cast<u16>(sample) ^ 0x8000;
};
constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 {
return static_cast<s8>(sample >> 8);
};
constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 {
return static_cast<u8>((static_cast<u16>(sample) ^ 0x8000) >> 8);
};
Samples output;
output.reserve(raw_samples.size() * impl->sample_size_in_bytes);
if (impl->sample_size_in_bytes == 1) {
// 8-bit output
if (parameters.sign == Signedness::Unsigned) {
for (s16 sample : raw_samples) {
output.push_back(convert_s16_to_u8(sample));
}
} else {
for (s16 sample : raw_samples) {
output.push_back(static_cast<u8>(convert_s16_to_s8(sample)));
}
}
} else {
// 16-bit output
if (parameters.sign == Signedness::Unsigned) {
for (s16 sample : raw_samples) {
u16 converted = convert_s16_to_u16(sample);
output.push_back(static_cast<u8>(converted & 0xFF));
output.push_back(static_cast<u8>(converted >> 8));
}
} else {
// Signed 16-bit - just copy the raw bytes
const u8* data = reinterpret_cast<const u8*>(raw_samples.data());
output.insert(output.end(), data, data + raw_samples.size() * 2);
}
}
return output;
}
LibRetroInput* GetLibRetroInput() {
return g_libretro_input;
}
} // namespace AudioCore

View file

@ -0,0 +1,36 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include "audio_core/input.h"
namespace AudioCore {
class LibRetroInput final : public Input {
public:
LibRetroInput();
~LibRetroInput() override;
void StartSampling(const InputParameters& params) override;
void StopSampling() override;
bool IsSampling() override;
void AdjustSampleRate(u32 sample_rate) override;
Samples Read() override;
/// Called from main thread (retro_run) to read samples from the frontend
/// and store them in the thread-safe buffer for Read() to consume.
void PollMicrophone();
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
/// Returns the global LibRetroInput instance, or nullptr if not initialized.
/// This is used by citra_libretro.cpp to poll the microphone from the main thread.
LibRetroInput* GetLibRetroInput();
} // namespace AudioCore

View file

@ -0,0 +1,27 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "audio_core/libretro_sink.h"
#include "citra_libretro/environment.h"
namespace AudioCore {
LibRetroSink::LibRetroSink(std::string) {}
LibRetroSink::~LibRetroSink() = default;
unsigned int LibRetroSink::GetNativeSampleRate() const {
return native_sample_rate;
}
void LibRetroSink::PushSamples(const void* data, std::size_t num_samples) {
// libretro calls stereo pairs "frames", Azahar calls them "samples"
LibRetro::SubmitAudio(static_cast<const s16*>(data), num_samples);
}
std::vector<std::string> ListLibretroSinkDevices() {
return std::vector<std::string>{"LibRetro"};
}
} // namespace AudioCore

View file

@ -0,0 +1,33 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include "audio_core/sink.h"
namespace AudioCore {
class LibRetroSink final : public Sink {
public:
explicit LibRetroSink(std::string target_device_name);
~LibRetroSink() override;
unsigned int GetNativeSampleRate() const override;
// Not used for immediate submission sinks
void SetCallback(std::function<void(s16*, std::size_t)> cb) override {};
bool ImmediateSubmission() override {
return true;
}
void PushSamples(const void* data, std::size_t num_samples) override;
};
std::vector<std::string> ListLibretroSinkDevices();
} // namespace AudioCore

View file

@ -5,7 +5,7 @@
#pragma once
#include <functional>
#include "common/common_types.h"
#include "audio_types.h"
namespace AudioCore {
@ -30,6 +30,23 @@ public:
* @param sample_count Number of samples.
*/
virtual void SetCallback(std::function<void(s16*, std::size_t)> cb) = 0;
/**
* Override and set this to true if the sink wants audio data submitted
* immediately rather than requesting audio on demand
* @return true if audio data should be pushed to the sink
*/
virtual bool ImmediateSubmission() {
return false;
}
/**
* Push audio samples directly to the sink, bypassing the FIFO.
* Only called when ImmediateSubmission() returns true.
* @param data Pointer to stereo PCM16 samples (each sample is L+R pair)
* @param num_samples Number of stereo samples
*/
virtual void PushSamples(const void* data, std::size_t num_samples) {}
};
} // namespace AudioCore

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -11,6 +11,9 @@
#ifdef HAVE_SDL2
#include "audio_core/sdl2_sink.h"
#endif
#ifdef HAVE_LIBRETRO
#include "audio_core/libretro_sink.h"
#endif
#ifdef HAVE_CUBEB
#include "audio_core/cubeb_sink.h"
#endif
@ -23,6 +26,13 @@ namespace AudioCore {
namespace {
// sink_details is ordered in terms of desirability, with the best choice at the top.
constexpr std::array sink_details = {
#ifdef HAVE_LIBRETRO
SinkDetails{SinkType::LibRetro, "libretro",
[](std::string_view device_id) -> std::unique_ptr<Sink> {
return std::make_unique<LibRetroSink>(std::string(device_id));
},
&ListLibretroSinkDevices},
#endif
#ifdef HAVE_CUBEB
SinkDetails{SinkType::Cubeb, "Cubeb",
[](std::string_view device_id) -> std::unique_ptr<Sink> {

View file

@ -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 {

View file

@ -0,0 +1,97 @@
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$<CONFIG>)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules)
# Object library for libretro code (can be linked into both shared lib and tests)
add_library(azahar_libretro_common OBJECT
emu_window/libretro_window.cpp
emu_window/libretro_window.h
input/input_factory.cpp
input/input_factory.h
input/mouse_tracker.cpp
input/mouse_tracker.h
$<$<BOOL:${ENABLE_VULKAN}>: libretro_vk.cpp libretro_vk.h>
environment.cpp
environment.h
core_settings.cpp
core_settings.h)
target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO)
target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map)
if(ENABLE_OPENGL)
target_link_libraries(azahar_libretro_common PRIVATE glad)
endif()
if(ENABLE_VULKAN)
target_link_libraries(azahar_libretro_common PRIVATE sirit vulkan-headers vma)
endif()
add_library(azahar_libretro SHARED
citra_libretro.cpp
citra_libretro.h
$<TARGET_OBJECTS:azahar_libretro_common>)
create_target_directory_groups(azahar_libretro)
target_link_libraries(citra_common PRIVATE libretro)
target_link_libraries(citra_core PRIVATE libretro)
target_link_libraries(video_core PRIVATE libretro)
target_link_libraries(audio_core PRIVATE libretro)
target_link_libraries(input_common PRIVATE libretro)
target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO)
target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO)
target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO)
target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO)
target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO)
target_link_libraries(azahar_libretro PRIVATE citra_common citra_core)
target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map)
if(ENABLE_VULKAN)
target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma)
endif()
if(ENABLE_OPENGL)
target_link_libraries(azahar_libretro PRIVATE glad)
endif()
target_link_libraries(azahar_libretro PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
if(DEFINED LIBRETRO_STATIC)
target_link_libraries(azahar_libretro PRIVATE -static-libstdc++)
endif()
set_target_properties(azahar_libretro PROPERTIES PREFIX "")
target_compile_definitions(azahar_libretro PRIVATE HAVE_LIBRETRO)
if(ANDROID)
target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO_VFS)
target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO_VFS)
target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO_VFS)
target_compile_definitions(azahar_libretro_common PRIVATE USING_GLES HAVE_LIBRETRO_VFS)
target_compile_definitions(azahar_libretro PRIVATE USING_GLES HAVE_LIBRETRO_VFS)
target_link_libraries(citra_common PRIVATE libretro_common)
target_link_libraries(citra_core PRIVATE libretro_common)
target_link_libraries(video_core PRIVATE libretro_common)
target_link_libraries(azahar_libretro_common PRIVATE libretro_common)
target_link_libraries(azahar_libretro PRIVATE libretro_common)
# Link Android log library for __android_log_print
target_link_libraries(azahar_libretro PRIVATE log)
set_target_properties(azahar_libretro PROPERTIES SUFFIX "_android.so")
endif()
if(MINGW)
target_link_libraries(azahar_libretro PRIVATE crypt32)
endif()
if(IOS)
target_compile_definitions(azahar_libretro_common PRIVATE IOS)
target_compile_definitions(azahar_libretro PRIVATE IOS)
target_link_libraries(azahar_libretro PRIVATE "-framework CoreFoundation" "-framework Foundation")
endif()
if (SSE42_COMPILE_OPTION)
target_compile_definitions(azahar_libretro PRIVATE CITRA_HAS_SSE42)
endif()
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR
CMAKE_SYSTEM_NAME STREQUAL "iOS" OR
CMAKE_SYSTEM_NAME STREQUAL "tvOS")
target_link_libraries(azahar_libretro PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/libretro.osx.def")
else()
target_link_libraries(azahar_libretro PRIVATE "-Wl,-Bsymbolic")
endif()

View file

@ -0,0 +1,717 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <list>
#include <numeric>
#include <vector>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef ENABLE_OPENGL
#include "glad/glad.h"
#include "video_core/renderer_opengl/gl_vars.h"
#endif
#include "libretro.h"
#include "audio_core/libretro_input.h"
#include "audio_core/libretro_sink.h"
#include "video_core/gpu.h"
#ifdef ENABLE_OPENGL
#include "video_core/renderer_opengl/renderer_opengl.h"
#endif
#ifdef ENABLE_VULKAN
#include "citra_libretro/libretro_vk.h"
#endif
#include "video_core/renderer_software/renderer_software.h"
#include "video_core/video_core.h"
#include "citra_libretro/citra_libretro.h"
#include "citra_libretro/core_settings.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/input_factory.h"
#include "common/arch.h"
#if CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h"
#endif
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/frontend/applets/default_applets.h"
#include "core/frontend/image_interface.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/memory.h"
#include "core/hle/kernel/process.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#ifdef HAVE_LIBRETRO_VFS
#include <streams/file_stream_transforms.h>
#endif
class CitraLibRetro {
public:
CitraLibRetro() : log_filter(Common::Log::Level::Debug) {}
Common::Log::Filter log_filter;
std::unique_ptr<EmuWindow_LibRetro> emu_window;
bool game_loaded = false;
struct retro_hw_render_callback hw_render{};
};
CitraLibRetro* emu_instance;
void retro_init() {
emu_instance = new CitraLibRetro();
Common::Log::LibRetroStart(LibRetro::GetLoggingBackend());
Common::Log::SetGlobalFilter(emu_instance->log_filter);
LOG_DEBUG(Frontend, "Initializing core...");
// Set up LLE cores
for (const auto& service_module : Service::service_module_map) {
Settings::values.lle_modules.emplace(service_module.name, false);
}
// Setup default, stub handlers for HLE applets
Frontend::RegisterDefaultApplets(Core::System::GetInstance());
// Register generic image interface
Core::System::GetInstance().RegisterImageInterface(
std::make_shared<Frontend::ImageInterface>());
LibRetro::Input::Init();
}
void retro_deinit() {
LOG_DEBUG(Frontend, "Shutting down core...");
if (Core::System::GetInstance().IsPoweredOn()) {
Core::System::GetInstance().Shutdown();
}
LibRetro::Input::Shutdown();
delete emu_instance;
Common::Log::Stop();
}
unsigned retro_api_version() {
return RETRO_API_VERSION;
}
void LibRetro::OnConfigureEnvironment() {
#ifdef HAVE_LIBRETRO_VFS
struct retro_vfs_interface_info vfs_iface_info{1, nullptr};
LibRetro::SetVFSCallback(&vfs_iface_info);
#endif
LibRetro::RegisterCoreOptions();
static const struct retro_controller_description controllers[] = {
{"Nintendo 3DS", RETRO_DEVICE_JOYPAD},
};
static const struct retro_controller_info ports[] = {
{controllers, 1},
{nullptr, 0},
};
LibRetro::SetControllerInfo(ports);
}
uintptr_t LibRetro::GetFramebuffer() {
return emu_instance->hw_render.get_current_framebuffer();
}
/**
* Updates Citra's settings with Libretro's.
*/
static void UpdateSettings() {
LibRetro::ParseCoreOptions();
struct retro_input_descriptor desc[] = {
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2, "ZL"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2, "ZR"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3, "Home/Swap screens"},
{0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3, "Touch Screen Touch"},
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X,
"Circle Pad X"},
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y,
"Circle Pad Y"},
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X,
"C-Stick / Pointer X"},
{0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y,
"C-Stick / Pointer Y"},
{0, 0},
};
LibRetro::SetInputDescriptors(desc);
Settings::values.current_input_profile.touch_device = "engine:emu_window";
// Hardcode buttons to bind to libretro - it is entirely redundant to have
// two methods of rebinding controls.
// Citra: A = RETRO_DEVICE_ID_JOYPAD_A (8)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::A] =
"button:8,joystick:0,engine:libretro";
// Citra: B = RETRO_DEVICE_ID_JOYPAD_B (0)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::B] =
"button:0,joystick:0,engine:libretro";
// Citra: X = RETRO_DEVICE_ID_JOYPAD_X (9)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::X] =
"button:9,joystick:0,engine:libretro";
// Citra: Y = RETRO_DEVICE_ID_JOYPAD_Y (1)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Y] =
"button:1,joystick:0,engine:libretro";
// Citra: UP = RETRO_DEVICE_ID_JOYPAD_UP (4)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Up] =
"button:4,joystick:0,engine:libretro";
// Citra: DOWN = RETRO_DEVICE_ID_JOYPAD_DOWN (5)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Down] =
"button:5,joystick:0,engine:libretro";
// Citra: LEFT = RETRO_DEVICE_ID_JOYPAD_LEFT (6)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Left] =
"button:6,joystick:0,engine:libretro";
// Citra: RIGHT = RETRO_DEVICE_ID_JOYPAD_RIGHT (7)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Right] =
"button:7,joystick:0,engine:libretro";
// Citra: L = RETRO_DEVICE_ID_JOYPAD_L (10)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::L] =
"button:10,joystick:0,engine:libretro";
// Citra: R = RETRO_DEVICE_ID_JOYPAD_R (11)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::R] =
"button:11,joystick:0,engine:libretro";
// Citra: START = RETRO_DEVICE_ID_JOYPAD_START (3)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Start] =
"button:3,joystick:0,engine:libretro";
// Citra: SELECT = RETRO_DEVICE_ID_JOYPAD_SELECT (2)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Select] =
"button:2,joystick:0,engine:libretro";
// Citra: ZL = RETRO_DEVICE_ID_JOYPAD_L2 (12)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZL] =
"button:12,joystick:0,engine:libretro";
// Citra: ZR = RETRO_DEVICE_ID_JOYPAD_R2 (13)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZR] =
"button:13,joystick:0,engine:libretro";
// Citra: HOME = RETRO_DEVICE_ID_JOYPAD_L3 (as per above bindings) (14)
Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Home] =
"button:14,joystick:0,engine:libretro";
// Circle Pad
Settings::values.current_input_profile.analogs[0] = "axis:0,joystick:0,engine:libretro";
// C-Stick
if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) {
Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro";
} else {
Settings::values.current_input_profile.analogs[1] = "";
}
if (!emu_instance->emu_window) {
emu_instance->emu_window = std::make_unique<EmuWindow_LibRetro>();
}
// Update the framebuffer sizing.
emu_instance->emu_window->UpdateLayout();
Core::System::GetInstance().ApplySettings();
}
/**
* libretro callback; Called every game tick.
*/
void retro_run() {
// Check to see if we actually have any config updates to process.
if (LibRetro::HasUpdatedConfig()) {
LibRetro::ParseCoreOptions();
Core::System::GetInstance().ApplySettings();
emu_instance->emu_window->UpdateLayout();
}
// Poll microphone input from the frontend and buffer it for the emulator
// This must be done from the main thread as LibRetro's mic interface is not thread-safe
if (auto* mic_input = AudioCore::GetLibRetroInput()) {
mic_input->PollMicrophone();
}
// Check if the screen swap button is pressed
static bool screen_swap_btn_state = false;
static bool screen_swap_toggled = false;
bool screen_swap_btn =
!!LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3);
if (screen_swap_btn != screen_swap_btn_state) {
if (LibRetro::settings.toggle_swap_screen) {
if (!screen_swap_btn_state)
screen_swap_toggled = !screen_swap_toggled;
if (screen_swap_toggled)
Settings::values.swap_screen =
LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom";
else
Settings::values.swap_screen =
LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom";
} else {
if (screen_swap_btn)
Settings::values.swap_screen =
LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom";
else
Settings::values.swap_screen =
LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom";
}
Core::System::GetInstance().ApplySettings();
// Update the framebuffer sizing.
emu_instance->emu_window->UpdateLayout();
screen_swap_btn_state = screen_swap_btn;
}
#ifdef ENABLE_OPENGL
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
// We can't assume that the frontend has been nice and preserved all OpenGL settings. Reset.
auto last_state = OpenGL::OpenGLState::GetCurState();
ResetGLState();
last_state.Apply();
}
#endif
while (!emu_instance->emu_window->HasSubmittedFrame()) {
auto result = Core::System::GetInstance().RunLoop();
if (result != Core::System::ResultStatus::Success) {
std::string errorContent = Core::System::GetInstance().GetStatusDetails();
std::string msg;
switch (result) {
case Core::System::ResultStatus::ErrorSystemFiles:
msg = "Azahar was unable to locate a 3DS system archive: " + errorContent;
break;
default:
msg = "Fatal Error encountered (" + std::to_string(static_cast<int>(result)) +
"): " + errorContent;
break;
}
LibRetro::DisplayMessage(msg.c_str());
}
}
}
static void setup_memory_maps() {
auto process = Core::System::GetInstance().Kernel().GetCurrentProcess();
if (!process)
return;
std::vector<retro_memory_descriptor> descs;
for (const auto& [addr, vma] : process->vm_manager.vma_map) {
if (vma.type != Kernel::VMAType::BackingMemory)
continue;
if (vma.size == 0 || !vma.backing_memory)
continue;
// Only expose the well-known user-accessible memory regions
uint64_t flags = 0;
if (vma.base >= Memory::HEAP_VADDR && vma.base < Memory::HEAP_VADDR_END) {
flags = RETRO_MEMDESC_SYSTEM_RAM;
} else if (vma.base >= Memory::LINEAR_HEAP_VADDR &&
vma.base < Memory::LINEAR_HEAP_VADDR_END) {
flags = RETRO_MEMDESC_SYSTEM_RAM;
} else if (vma.base >= Memory::NEW_LINEAR_HEAP_VADDR &&
vma.base < Memory::NEW_LINEAR_HEAP_VADDR_END) {
flags = RETRO_MEMDESC_SYSTEM_RAM;
} else if (vma.base >= Memory::VRAM_VADDR && vma.base < Memory::VRAM_VADDR_END) {
flags = RETRO_MEMDESC_VIDEO_RAM;
} else {
continue;
}
retro_memory_descriptor desc = {};
desc.flags = flags;
desc.ptr = const_cast<u8*>(vma.backing_memory.GetPtr());
desc.start = vma.base;
desc.len = vma.size;
// select=0 requires power-of-2 len AND start aligned to len.
// When that doesn't hold, compute a select mask instead.
bool need_select = (vma.size & (vma.size - 1)) != 0;
if (!need_select && (vma.base & (vma.size - 1)) != 0)
need_select = true;
if (need_select) {
uint64_t np2 = 1;
while (np2 < vma.size)
np2 <<= 1;
if (vma.base & (np2 - 1)) {
LOG_WARNING(Frontend, "VMA at 0x{:08X} size 0x{:X} not aligned, skipping", vma.base,
vma.size);
continue;
}
desc.select = ~(np2 - 1);
}
descs.push_back(desc);
}
if (!descs.empty()) {
retro_memory_map map = {descs.data(), static_cast<unsigned>(descs.size())};
LibRetro::SetMemoryMaps(&map);
}
}
static bool do_load_game() {
const Core::System::ResultStatus load_result{
Core::System::GetInstance().Load(*emu_instance->emu_window, LibRetro::settings.file_path)};
switch (load_result) {
case Core::System::ResultStatus::Success:
break; // Expected case
case Core::System::ResultStatus::ErrorGetLoader:
LibRetro::DisplayMessage("Failed to obtain loader for specified ROM!");
return false;
case Core::System::ResultStatus::ErrorLoader:
LibRetro::DisplayMessage("Failed to load ROM!");
return false;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted:
LibRetro::DisplayMessage("The game that you are trying to load must be decrypted before "
"being used with Azahar.");
return false;
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
LibRetro::DisplayMessage("Error while loading ROM: The ROM format is not supported.");
return false;
case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
LibRetro::DisplayMessage(
"Error loading the specified application as it is GBA Virtual Console");
return false;
case Core::System::ResultStatus::ErrorNotInitialized:
LibRetro::DisplayMessage("CPUCore not initialized");
return false;
case Core::System::ResultStatus::ErrorSystemMode:
LibRetro::DisplayMessage("Failed to determine system mode!");
return false;
default:
LibRetro::DisplayMessage(
("Unknown error: " + std::to_string(static_cast<int>(load_result))).c_str());
return false;
}
u64 program_id{};
Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
Core::System::GetInstance().GPU().ApplyPerProgramSettings(program_id);
if (Settings::values.use_disk_shader_cache) {
Core::System::GetInstance().GPU().Renderer().Rasterizer()->LoadDefaultDiskResources(
false, nullptr);
}
setup_memory_maps();
return true;
}
#ifdef ENABLE_OPENGL
static void* load_opengl_func(const char* name) {
return (void*)emu_instance->hw_render.get_proc_address(name);
}
#endif
static void context_reset() {
LOG_DEBUG(Frontend, "context_reset");
switch (Settings::values.graphics_api.GetValue()) {
#ifdef ENABLE_OPENGL
case Settings::GraphicsAPI::OpenGL:
#if defined(USING_GLES)
Settings::values.use_gles = true;
// Set the global GLES flag immediately to ensure any shader compilation
// that happens before the Driver is created uses the correct version
OpenGL::GLES = true;
#else
Settings::values.use_gles = false;
OpenGL::GLES = false;
#endif
// Check to see if the frontend provides us with OpenGL symbols
if (emu_instance->hw_render.get_proc_address != nullptr) {
bool loaded = Settings::values.use_gles
? gladLoadGLES2Loader((GLADloadproc)load_opengl_func)
: gladLoadGLLoader((GLADloadproc)load_opengl_func);
if (!loaded) {
LOG_CRITICAL(Frontend, "Glad failed to load (frontend-provided symbols)!");
return;
}
} else {
// Else, try to load them on our own
if (!gladLoadGL()) {
LOG_CRITICAL(Frontend, "Glad failed to load (internal symbols)!");
return;
}
}
break;
#endif
#ifdef ENABLE_VULKAN
case Settings::GraphicsAPI::Vulkan:
LibRetro::VulkanResetContext();
break;
#endif
default:
// software renderer never gets here
break;
}
emu_instance->emu_window->CreateContext();
if (!emu_instance->game_loaded) {
emu_instance->game_loaded = do_load_game();
} else {
// Game is already loaded, just recreate the renderer for the new GL context
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
Core::System::GetInstance().GPU().RecreateRenderer(*emu_instance->emu_window, nullptr);
}
}
}
static void context_destroy() {
LOG_DEBUG(Frontend, "context_destroy");
if (emu_instance->game_loaded &&
Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) {
// Release the renderer's OpenGL resources
Core::System::GetInstance().GPU().ReleaseRenderer();
}
emu_instance->emu_window->DestroyContext();
}
void retro_reset() {
LOG_DEBUG(Frontend, "retro_reset");
Core::System::GetInstance().Shutdown();
emu_instance->game_loaded = do_load_game();
}
/**
* libretro callback; Called when a game is to be loaded.
*/
bool retro_load_game(const struct retro_game_info* info) {
LOG_INFO(Frontend, "Starting Azahar RetroArch game...");
#if CITRA_ARCH(x86_64) && CITRA_HAS_SSE42
if (!Common::GetCPUCaps().sse4_2) {
LOG_CRITICAL(Frontend, "This CPU does not support SSE4.2, which is required by this build");
LibRetro::DisplayMessage(
"This CPU does not support SSE4.2, which is required by this build");
return false;
}
#endif
UpdateSettings();
// If using HW rendering, don't actually load the game here. azahar wants
// the graphics context ready and available before calling System::Load.
LibRetro::settings.file_path = info->path;
if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) {
LibRetro::DisplayMessage("XRGB8888 is not supported.");
return false;
}
emu_instance->emu_window->UpdateLayout();
switch (Settings::values.graphics_api.GetValue()) {
case Settings::GraphicsAPI::OpenGL:
#ifdef ENABLE_OPENGL
LOG_INFO(Frontend, "Using OpenGL hw renderer");
LibRetro::SetHWSharedContext();
#if defined(USING_GLES)
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGLES3;
emu_instance->hw_render.version_major = 3;
emu_instance->hw_render.version_minor = 2;
#else
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGL_CORE;
emu_instance->hw_render.version_major = 4;
emu_instance->hw_render.version_minor = 3;
#endif
emu_instance->hw_render.context_reset = context_reset;
emu_instance->hw_render.context_destroy = context_destroy;
emu_instance->hw_render.cache_context = false;
emu_instance->hw_render.bottom_left_origin = true;
if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) {
LibRetro::DisplayMessage("Failed to set HW renderer");
return false;
}
#endif
break;
case Settings::GraphicsAPI::Vulkan:
#ifdef ENABLE_VULKAN
LOG_INFO(Frontend, "Using Vulkan hw renderer");
emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_VULKAN;
emu_instance->hw_render.version_major = VK_MAKE_VERSION(1, 1, 0);
emu_instance->hw_render.version_minor = 0;
emu_instance->hw_render.context_reset = context_reset;
emu_instance->hw_render.context_destroy = context_destroy;
emu_instance->hw_render.cache_context = true;
if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) {
LibRetro::DisplayMessage("Failed to set HW renderer");
return false;
}
// Set up Vulkan context negotiation interface
static const struct retro_hw_render_context_negotiation_interface_vulkan vk_negotiation = {
RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN,
RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION,
LibRetro::GetVulkanApplicationInfo,
LibRetro::CreateVulkanDevice,
nullptr, // destroy_device - not needed (frontend owns the device)
};
LibRetro::SetHWRenderContextNegotiationInterface((void**)&vk_negotiation);
#endif
break;
case Settings::GraphicsAPI::Software:
emu_instance->game_loaded = do_load_game();
if (!emu_instance->game_loaded)
return false;
break;
}
uint64_t quirks =
RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE | RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE;
LibRetro::SetSerializationQuirks(quirks);
return true;
}
void retro_unload_game() {
LOG_DEBUG(Frontend, "Unloading game...");
Core::System::GetInstance().Shutdown();
}
unsigned retro_get_region() {
return RETRO_REGION_NTSC;
}
bool retro_load_game_special(unsigned game_type, const struct retro_game_info* info,
size_t num_info) {
return retro_load_game(info);
}
/// Drain any pending async kernel operations by running the emulation loop.
///
/// Savestates are unsafe to create while RunAsync operations (file I/O, network, etc.)
/// are in flight. The Qt frontend handles this by deferring serialization inside
/// System::RunLoop(): it sets a request flag via SendSignal(Signal::Save), and RunLoop
/// only performs the save when !kernel->AreAsyncOperationsPending() (see core.cpp).
///
/// The Qt frontend needs that indirection because its UI and emulation run on separate
/// threads. In libretro, the frontend calls API entry points (retro_run, retro_serialize,
/// etc.) sequentially, so we can call RunLoop() directly from here to drain pending ops,
/// then call SaveStateBuffer()/LoadStateBuffer() ourselves.
///
/// Note: RunLoop() can itself start new async operations (CPU executes HLE service calls),
/// so the pending count may not decrease monotonically. In practice games reach quiescent
/// points between frames; the 5-second timeout (matching RunLoop's existing handler)
/// covers the pathological case.
static bool DrainAsyncOperations(Core::System& system) {
if (!system.KernelRunning() || !system.Kernel().AreAsyncOperationsPending()) {
return true;
}
emu_instance->emu_window->suppressPresentation = true;
auto start = std::chrono::steady_clock::now();
while (system.Kernel().AreAsyncOperationsPending()) {
if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
LOG_ERROR(Frontend, "Timed out waiting for async operations to complete");
emu_instance->emu_window->suppressPresentation = false;
return false;
}
auto result = system.RunLoop();
if (result != Core::System::ResultStatus::Success) {
emu_instance->emu_window->suppressPresentation = false;
return false;
}
}
emu_instance->emu_window->suppressPresentation = false;
return true;
}
std::optional<std::vector<u8>> savestate = {};
size_t retro_serialize_size() {
auto& system = Core::System::GetInstance();
if (!system.IsPoweredOn())
return 0;
if (!DrainAsyncOperations(system)) {
savestate.reset();
return 0;
}
try {
savestate = system.SaveStateBuffer();
return savestate->size();
} catch (const std::exception& e) {
LOG_ERROR(Frontend, "Error saving state: {}", e.what());
savestate.reset();
return 0;
}
}
bool retro_serialize(void* data, size_t size) {
if (!savestate.has_value())
return false;
if (size < savestate->size())
return false;
memcpy(data, savestate->data(), savestate->size());
savestate.reset();
return true;
}
bool retro_unserialize(const void* data, size_t size) {
auto& system = Core::System::GetInstance();
if (!system.IsPoweredOn())
return false;
if (!DrainAsyncOperations(system)) {
return false;
}
std::vector<u8> buffer(static_cast<const u8*>(data), static_cast<const u8*>(data) + size);
try {
return system.LoadStateBuffer(std::move(buffer));
} catch (const std::exception& e) {
LOG_ERROR(Frontend, "Error loading state: {}", e.what());
return false;
}
}
void* retro_get_memory_data(unsigned id) {
// Memory is exposed via RETRO_ENVIRONMENT_SET_MEMORY_MAPS instead,
// using virtual addresses for stable cheat/achievement support.
return NULL;
}
size_t retro_get_memory_size(unsigned id) {
return 0;
}
void retro_cheat_reset() {}
void retro_cheat_set(unsigned index, bool enabled, const char* code) {}

View file

@ -0,0 +1,10 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "core/core.h"
#include "emu_window/libretro_window.h"
namespace LibRetro {} // namespace LibRetro

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include "core/hle/service/cfg/cfg.h"
namespace LibRetro {
enum CStickFunction { Both, CStick, Touchscreen };
struct CoreSettings {
std::string file_path;
float deadzone = 1.f;
LibRetro::CStickFunction analog_function;
bool mouse_touchscreen;
Service::CFG::SystemLanguage language_value;
bool touch_touchscreen;
bool render_touchscreen;
bool toggle_swap_screen;
bool motion_enabled;
float motion_sensitivity;
} extern settings;
void RegisterCoreOptions(void);
void ParseCoreOptions(void);
} // namespace LibRetro

View file

@ -0,0 +1,342 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#ifdef ENABLE_OPENGL
#include <glad/glad.h>
#endif
#include <libretro.h>
#include "audio_core/audio_types.h"
#include "citra_libretro/citra_libretro.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/input_factory.h"
#include "common/settings.h"
#include "core/3ds.h"
#ifdef ENABLE_OPENGL
#include "video_core/renderer_opengl/gl_state.h"
#endif
#include "video_core/gpu.h"
#include "video_core/renderer_software/renderer_software.h"
#ifdef ENABLE_OPENGL
/// LibRetro expects a "default" GL state.
void ResetGLState() {
// Reset internal state.
OpenGL::OpenGLState state{};
state.Apply();
// Clean up global state.
if (!Settings::values.use_gles) {
glLogicOp(GL_COPY);
}
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDisable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFF);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
glBlendColor(0, 0, 0, 0);
glDisable(GL_COLOR_LOGIC_OP);
glDisable(GL_DITHER);
glDisable(GL_CULL_FACE);
glCullFace(GL_BACK);
glActiveTexture(GL_TEXTURE0);
}
#endif
EmuWindow_LibRetro::EmuWindow_LibRetro() {
strict_context_required = true;
window_info.type = Frontend::WindowSystemType::LibRetro;
}
EmuWindow_LibRetro::~EmuWindow_LibRetro() {}
void EmuWindow_LibRetro::SwapBuffers() {
if (suppressPresentation)
return;
submittedFrame = true;
switch (Settings::values.graphics_api.GetValue()) {
case Settings::GraphicsAPI::OpenGL: {
#ifdef ENABLE_OPENGL
auto current_state = OpenGL::OpenGLState::GetCurState();
ResetGLState();
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height);
}
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
current_state.Apply();
#endif
break;
}
case Settings::GraphicsAPI::Vulkan: {
#ifdef ENABLE_VULKAN
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height);
}
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
#endif
break;
}
case Settings::GraphicsAPI::Software: {
retro_framebuffer fb;
u8* data;
size_t pitch;
bool did_malloc = false;
if (LibRetro::GetSoftwareFramebuffer(&fb, width, height)) {
data = static_cast<u8*>(fb.data);
pitch = fb.pitch;
} else {
pitch = static_cast<size_t>(width) * 4;
data = static_cast<u8*>(calloc(1, pitch * height));
did_malloc = true;
}
std::memset(data, 0, pitch * height);
auto& system = Core::System::GetInstance();
const auto& renderer = static_cast<SwRenderer::RendererSoftware&>(system.GPU().Renderer());
const auto& layout = GetFramebufferLayout();
// Blit a single screen from ScreenInfo (column-major RGBA) to the
// output buffer (row-major XRGB8888), rotating and scaling as needed.
// The 3DS framebuffer is portrait-oriented; ScreenInfo stores pixels
// column-major so the transpose gives us the landscape orientation:
// display (dx, dy) -> ScreenInfo (x=dy, y=dx)
auto blit_screen = [&](VideoCore::ScreenId screen_id, const Common::Rectangle<u32>& rect) {
const auto& info = renderer.Screen(screen_id);
if (info.pixels.empty())
return;
const u32 rect_w = rect.GetWidth();
const u32 rect_h = rect.GetHeight();
if (rect_w == 0 || rect_h == 0)
return;
// Landscape display dimensions (transposed from portrait storage)
const u32 native_w = info.height;
const u32 native_h = info.width;
for (u32 oy = 0; oy < rect_h; oy++) {
for (u32 ox = 0; ox < rect_w; ox++) {
const u32 dx = ox * native_w / rect_w;
const u32 dy = oy * native_h / rect_h;
const u32 src_off = (dy * info.height + dx) * 4;
if (src_off + 3 >= info.pixels.size())
continue;
const u8* src = info.pixels.data() + src_off;
const size_t dst_off = static_cast<size_t>(rect.top + oy) * pitch +
static_cast<size_t>(rect.left + ox) * 4;
// RGBA -> XRGB8888 (little-endian: B, G, R, 0)
data[dst_off + 0] = src[2];
data[dst_off + 1] = src[1];
data[dst_off + 2] = src[0];
data[dst_off + 3] = 0;
}
}
};
if (layout.top_screen_enabled) {
blit_screen(VideoCore::ScreenId::TopLeft, layout.top_screen);
}
if (layout.bottom_screen_enabled) {
blit_screen(VideoCore::ScreenId::Bottom, layout.bottom_screen);
}
// Software cursor rendering with framebuffer access
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height, data);
}
LibRetro::UploadVideoFrame(data, static_cast<unsigned>(width),
static_cast<unsigned>(height), pitch);
if (did_malloc)
free(data);
break;
}
}
}
void EmuWindow_LibRetro::SetupFramebuffer() {
if (Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::OpenGL)
return;
#ifdef ENABLE_OPENGL
// TODO: Expose interface in renderer_opengl to configure this in it's internal state
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(LibRetro::GetFramebuffer()));
// glClear can be a slow path - skip clearing if we don't need to.
if (doCleanFrame) {
glClear(GL_COLOR_BUFFER_BIT);
doCleanFrame = false;
}
#endif
}
void EmuWindow_LibRetro::PollEvents() {
// The software renderer doesn't call render_window.SwapBuffers() — standalone
// frontends (Qt/SDL) use separate presentation threads that pull from screen_infos
// instead. In libretro there's no such thread, so we present here: PollEvents is
// called from EndFrame() during each VBlank, right after PrepareRenderTarget has
// filled the screen pixel buffers.
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::Software) {
SwapBuffers();
}
LibRetro::PollInput();
// TODO: Poll for right click for motion emu
if (enableEmulatedPointer && tracker) {
tracker->Update(width, height, GetFramebufferLayout());
if (tracker->IsPressed()) {
auto mousePos = tracker->GetPressedPosition();
if (hasTouched) {
TouchMoved(mousePos.first, mousePos.second);
} else {
TouchPressed(mousePos.first, mousePos.second);
hasTouched = true;
}
} else if (hasTouched) {
hasTouched = false;
TouchReleased();
}
}
}
void EmuWindow_LibRetro::MakeCurrent() {
// They don't get any say in the matter - GL context is always current!
}
void EmuWindow_LibRetro::DoneCurrent() {
// They don't get any say in the matter - GL context is always current!
}
void EmuWindow_LibRetro::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> _minimal_size) {}
LayoutGeometry ComputeLayoutGeometry() {
unsigned baseX;
unsigned baseY;
bool emulated_pointer = true;
float scaling = Settings::values.resolution_factor.GetValue();
bool swapped = Settings::values.swap_screen.GetValue();
switch (Settings::values.layout_option.GetValue()) {
case Settings::LayoutOption::SingleScreen:
if (swapped) { // Bottom screen visible
baseX = Core::kScreenBottomWidth;
baseY = Core::kScreenBottomHeight;
} else { // Top screen visible
baseX = Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight;
emulated_pointer = false;
}
baseX *= scaling;
baseY *= scaling;
break;
case Settings::LayoutOption::LargeScreen:
if (swapped) { // Bottom screen biggest
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth / 4;
baseY = Core::kScreenBottomHeight;
} else { // Top screen biggest
baseX = Core::kScreenTopWidth + Core::kScreenBottomWidth / 4;
baseY = Core::kScreenTopHeight;
}
if (scaling < 4) {
// Unfortunately, to get this aspect ratio correct (and have non-blurry 1x scaling),
// we have to have a pretty large buffer for the minimum ratio.
baseX *= 4;
baseY *= 4;
} else {
baseX *= scaling;
baseY *= scaling;
}
break;
case Settings::LayoutOption::SideScreen:
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight;
baseX *= scaling;
baseY *= scaling;
break;
case Settings::LayoutOption::Default:
default:
baseX = Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight + Core::kScreenBottomHeight;
baseX *= scaling;
baseY *= scaling;
break;
}
return {baseX, baseY, emulated_pointer};
}
void EmuWindow_LibRetro::UpdateLayout() {
auto geom = ComputeLayoutGeometry();
unsigned baseX = geom.width;
unsigned baseY = geom.height;
enableEmulatedPointer = geom.emulated_pointer;
// Update Libretro with our status
struct retro_system_av_info info{};
info.timing.fps = 60.0;
info.timing.sample_rate = AudioCore::native_sample_rate;
info.geometry.aspect_ratio = (float)baseX / (float)baseY;
info.geometry.base_width = baseX;
info.geometry.base_height = baseY;
info.geometry.max_width = baseX;
info.geometry.max_height = baseY;
if (!LibRetro::SetGeometry(&info)) {
LOG_CRITICAL(Frontend, "Failed to update 3DS layout in frontend!");
}
width = baseX;
height = baseY;
UpdateCurrentFramebufferLayout(baseX, baseY);
doCleanFrame = true;
}
bool EmuWindow_LibRetro::NeedsClearing() const {
// We manage this ourselves.
return false;
}
bool EmuWindow_LibRetro::HasSubmittedFrame() {
bool state = submittedFrame;
submittedFrame = false;
return state;
}
void EmuWindow_LibRetro::CreateContext() {
tracker = std::make_unique<LibRetro::Input::MouseTracker>();
doCleanFrame = true;
}
void EmuWindow_LibRetro::DestroyContext() {
tracker = nullptr;
}

View file

@ -0,0 +1,79 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <utility>
#include "citra_libretro/input/mouse_tracker.h"
#include "core/frontend/emu_window.h"
struct LayoutGeometry {
unsigned width;
unsigned height;
bool emulated_pointer;
};
/// Compute framebuffer dimensions from current layout/scaling/swap settings.
LayoutGeometry ComputeLayoutGeometry();
void ResetGLState();
class EmuWindow_LibRetro : public Frontend::EmuWindow {
public:
EmuWindow_LibRetro();
~EmuWindow_LibRetro();
/// Swap buffers to display the next frame
void SwapBuffers() override;
/// Polls window events
void PollEvents() override;
/// Makes the graphics context current for the caller thread
void MakeCurrent() override;
/// Releases the GL context from the caller thread
void DoneCurrent() override;
void SetupFramebuffer() override;
/// Prepares the window for rendering
void UpdateLayout();
/// States whether a frame has been submitted. Resets after call.
bool HasSubmittedFrame();
/// Flags that the framebuffer should be cleared.
bool NeedsClearing() const override;
/// Creates state for a currently running OpenGL context.
void CreateContext();
/// Destroys a currently running OpenGL context.
void DestroyContext();
/// When true, SwapBuffers() is suppressed (used during savestate drain loops)
bool suppressPresentation = false;
private:
/// Called when a configuration change affects the minimal size of the window
void OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal_size) override;
int width;
int height;
bool submittedFrame = false;
// Hack to ensure stuff runs on the main thread
bool doCleanFrame = false;
// For tracking LibRetro state
bool hasTouched = false;
// For tracking mouse cursor
std::unique_ptr<LibRetro::Input::MouseTracker> tracker = nullptr;
bool enableEmulatedPointer = false;
};

View file

@ -0,0 +1,281 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cstring>
#include "audio_core/audio_types.h"
#include "audio_core/libretro_sink.h"
#include "common/scm_rev.h"
#include "core/3ds.h"
#include "emu_window/libretro_window.h"
#include "environment.h"
#ifdef HAVE_LIBRETRO_VFS
#include "streams/file_stream.h"
#endif
using namespace LibRetro;
namespace LibRetro {
namespace {
static retro_video_refresh_t video_cb;
static retro_audio_sample_batch_t audio_batch_cb;
static retro_environment_t environ_cb;
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;
} // namespace
bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height) {
fb->data = nullptr;
fb->width = width;
fb->height = height;
fb->pitch = 0;
fb->format = RETRO_PIXEL_FORMAT_XRGB8888;
fb->access_flags = RETRO_MEMORY_ACCESS_WRITE;
fb->memory_flags = 0;
return environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER, fb);
}
void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch) {
return video_cb(data, width, height, pitch);
}
bool SetHWSharedContext() {
return environ_cb(RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT, NULL);
}
void PollInput() {
return input_poll_cb();
}
bool GetSensorInterface(struct retro_sensor_interface* sensor_interface) {
return environ_cb(RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE, sensor_interface);
}
bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) {
return environ_cb(RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE, mic_interface);
}
Settings::GraphicsAPI GetPreferredRenderer() {
// try and maintain the current driver
retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL;
environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type);
switch (context_type) {
#ifdef ENABLE_OPENGL
case RETRO_HW_CONTEXT_OPENGL:
case RETRO_HW_CONTEXT_OPENGL_CORE:
case RETRO_HW_CONTEXT_OPENGLES2:
case RETRO_HW_CONTEXT_OPENGLES3:
case RETRO_HW_CONTEXT_OPENGLES_VERSION:
return Settings::GraphicsAPI::OpenGL;
#endif
#ifdef ENABLE_VULKAN
case RETRO_HW_CONTEXT_VULKAN:
return Settings::GraphicsAPI::Vulkan;
#endif
default:
break;
}
// we can't maintain the current driver, need to switch
#if defined(ENABLE_VULKAN)
return Settings::GraphicsAPI::Vulkan;
#elif defined(ENABLE_OPENGL)
return Settings::GraphicsAPI::OpenGL;
#else
return Settings::GraphicsAPI::Software;
#endif
}
bool SetVariables(const retro_variable vars[]) {
return environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)vars);
}
bool SetCoreOptionsV2(const retro_core_options_v2* options) {
return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, (void*)options);
}
bool SetCoreOptionsV1(const retro_core_option_definition* options) {
return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, (void*)options);
}
bool GetCoreOptionsVersion(unsigned* version) {
return environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, version);
}
bool SetMemoryMaps(const retro_memory_map* map) {
return environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, (void*)map);
}
bool SetControllerInfo(const retro_controller_info info[]) {
return environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)info);
}
bool SetPixelFormat(const retro_pixel_format fmt) {
return environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, (void*)&fmt);
}
bool SetHWRenderer(retro_hw_render_callback* cb) {
return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, cb);
}
bool GetHWRenderInterface(void** interface) {
return environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, interface) && !!*interface;
}
bool SetHWRenderContextNegotiationInterface(void** interface) {
return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, interface) &&
!!*interface;
}
bool SetAudioCallback(retro_audio_callback* cb) {
return environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, cb);
}
bool SetFrameTimeCallback(retro_frame_time_callback* cb) {
return environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, cb);
}
bool SetGeometry(retro_system_av_info* cb) {
return environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, cb);
}
bool SetInputDescriptors(const retro_input_descriptor desc[]) {
return environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, (void*)desc);
}
bool HasUpdatedConfig() {
bool updated = false;
return environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated;
}
bool Shutdown() {
return environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, NULL);
}
/// Displays the specified message to the screen.
bool DisplayMessage(const char* sg) {
retro_message msg;
msg.msg = sg;
msg.frames = 60 * 10;
return environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
}
bool SetSerializationQuirks(uint64_t quirks) {
return environ_cb(RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS, &quirks);
}
std::string FetchVariable(std::string key, std::string def) {
struct retro_variable var = {nullptr};
var.key = key.c_str();
if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) {
// Fetching variable failed.
LOG_ERROR(Frontend, "Fetching variable {} failed.", key);
return def;
}
return std::string(var.value);
}
std::string GetSaveDir() {
char* var = nullptr;
if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &var) || var == nullptr) {
// Fetching variable failed.
LOG_ERROR(Frontend, "No save directory provided by LibRetro.");
return std::string();
}
return std::string(var);
}
std::string GetSystemDir() {
char* var = nullptr;
if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &var) || var == nullptr) {
// Fetching variable failed.
LOG_ERROR(Frontend, "No system directory provided by LibRetro.");
return std::string();
}
return std::string(var);
}
retro_log_printf_t GetLoggingBackend() {
retro_log_callback callback{};
if (!environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &callback)) {
return nullptr;
}
return callback.log;
}
int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id) {
return input_state_cb(port, device, index, id);
}
#ifdef HAVE_LIBRETRO_VFS
void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info) {
if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, vfs_iface_info))
filestream_vfs_init(vfs_iface_info);
}
#endif
#ifdef IOS
bool CanUseJIT() {
bool can_jit = false;
return environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && can_jit;
}
#endif
}; // namespace LibRetro
void retro_get_system_info(struct retro_system_info* info) {
memset(info, 0, sizeof(*info));
info->library_name = "Azahar";
info->library_version = Common::g_build_fullname;
info->need_fullpath = true;
info->valid_extensions = "3ds|3dsx|z3dsx|elf|axf|cci|zcci|cxi|zcxi|app";
}
void LibRetro::SubmitAudio(const int16_t* data, size_t frames) {
audio_batch_cb(data, frames);
}
void retro_set_audio_sample(retro_audio_sample_t cb) {
// We don't need single audio sample callbacks.
}
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) {
LibRetro::audio_batch_cb = cb;
}
void retro_set_input_poll(retro_input_poll_t cb) {
LibRetro::input_poll_cb = cb;
}
void retro_set_video_refresh(retro_video_refresh_t cb) {
LibRetro::video_cb = cb;
}
void retro_set_environment(retro_environment_t cb) {
LibRetro::environ_cb = cb;
LibRetro::OnConfigureEnvironment();
}
void retro_set_controller_port_device(unsigned port, unsigned device) {}
void retro_set_input_state(retro_input_state_t cb) {
input_state_cb = cb;
}
void retro_get_system_av_info(struct retro_system_av_info* info) {
info->timing.fps = 60.0;
info->timing.sample_rate = AudioCore::native_sample_rate;
// Compute geometry from current settings so the frontend allocates the
// correct framebuffer on first use.
auto geom = ComputeLayoutGeometry();
info->geometry.base_width = geom.width;
info->geometry.base_height = geom.height;
// Max must cover the largest possible layout (SideScreen at 10x = 7200).
info->geometry.max_width = (Core::kScreenBottomWidth + Core::kScreenTopWidth) * 10;
info->geometry.max_height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * 10;
info->geometry.aspect_ratio = (float)geom.width / (float)geom.height;
}

View file

@ -0,0 +1,129 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <cstdint>
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
#include "libretro.h"
namespace LibRetro {
/// May fetch a framebuffer that can be rendered into for software rendering
/// @see RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER
/// @see retro_framebuffer
/// @see retro_video_refresh_t
bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height);
/// Calls back to LibRetro to upload a particular video frame.
/// @see retro_video_refresh_t
void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch);
/// Calls back to LibRetro to poll input.
/// @see retro_input_poll_t
void PollInput();
/// Gets the sensor interface for motion input
bool GetSensorInterface(struct retro_sensor_interface* sensor_interface);
/// Gets the microphone interface for audio input
bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface);
/// Sets the environmental variables used for settings.
bool SetVariables(const retro_variable vars[]);
/// Sets the core options using the v2 interface with categories.
bool SetCoreOptionsV2(const retro_core_options_v2* options);
/// Sets the core options using the v1 interface.
bool SetCoreOptionsV1(const retro_core_option_definition* options);
/// Gets the core options version supported by the frontend.
bool GetCoreOptionsVersion(unsigned* version);
bool SetHWSharedContext(void);
/// Returns the LibRetro save directory, or a empty string if one doesn't exist.
std::string GetSaveDir();
/// Returns the LibRetro system directory, or a empty string if one doesn't exist.
std::string GetSystemDir();
/// Fetches a variable by key name.
std::string FetchVariable(std::string key, std::string def);
/// Returns a logging backend, or null if the frontend refuses to provide one.
retro_log_printf_t GetLoggingBackend();
/// Returns graphics api based on global frontend setting
Settings::GraphicsAPI GetPreferredRenderer();
/// Displays information about the kinds of controllers that this Citra recreates.
bool SetControllerInfo(const retro_controller_info info[]);
/// Sets the memory maps for the core.
bool SetMemoryMaps(const retro_memory_map* map);
/// Sets the framebuffer pixel format.
bool SetPixelFormat(const retro_pixel_format fmt);
/// Sets the H/W rendering context.
bool SetHWRenderer(retro_hw_render_callback* cb);
/// Gets the H/W rendering interface.
bool GetHWRenderInterface(void** interface);
/// Sets the H/W rendering context negotiation interface.
bool SetHWRenderContextNegotiationInterface(void** interface);
/// Sets the async audio callback.
bool SetAudioCallback(retro_audio_callback* cb);
/// Sets the frame time callback.
bool SetFrameTimeCallback(retro_frame_time_callback* cb);
/// Set the size of the new screen buffer.
bool SetGeometry(retro_system_av_info* cb);
/// Tells LibRetro what input buttons are labelled on the 3DS.
bool SetInputDescriptors(const retro_input_descriptor desc[]);
/// Returns the current status of a input.
int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id);
/// Called when the emulator environment is ready to be configured.
void OnConfigureEnvironment();
/// Submits audio frames to LibRetro.
/// @see retro_audio_sample_batch_t
void SubmitAudio(const int16_t* data, size_t frames);
/// Checks to see if the frontend configuration has been updated.
bool HasUpdatedConfig();
/// Returns the current framebuffer.
uintptr_t GetFramebuffer();
/// Tells the frontend that we are done.
bool Shutdown();
/// Displays the specified message to the screen.
bool DisplayMessage(const char* sg);
/// Sets serialization quirks for the core.
bool SetSerializationQuirks(uint64_t quirks);
#ifdef HAVE_LIBRETRO_VFS
void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info);
#endif
#ifdef IOS
bool CanUseJIT();
#endif
} // namespace LibRetro

View file

@ -0,0 +1,216 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cmath>
#include <memory>
#include <unordered_map>
#include <libretro.h>
#include "common/math_util.h"
#include "common/vector_math.h"
#include "core/frontend/input.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/input_factory.h"
namespace LibRetro {
namespace Input {
class LibRetroButtonFactory;
class LibRetroAxisFactory;
class LibRetroMotionFactory;
class LibRetroButton final : public ::Input::ButtonDevice {
public:
explicit LibRetroButton(int joystick_, int button_) : joystick(joystick_), button(button_) {}
bool GetStatus() const override {
return CheckInput((unsigned int)joystick, RETRO_DEVICE_JOYPAD, 0, (unsigned int)button) > 0;
}
private:
int joystick;
int button;
};
/// A button device factory that creates button devices from LibRetro joystick
class LibRetroButtonFactory final : public ::Input::Factory<::Input::ButtonDevice> {
public:
/**
* Creates a button device from a joystick button
* @param params contains parameters for creating the device:
* - "joystick": the index of the joystick to bind
* - "button": the index of the button to bind
*/
std::unique_ptr<::Input::ButtonDevice> Create(const Common::ParamPackage& params) override {
const int joystick_index = params.Get("joystick", 0);
const int button = params.Get("button", 0);
return std::make_unique<LibRetroButton>(joystick_index, button);
}
};
/// A axis device factory that creates axis devices from LibRetro joystick
class LibRetroAxis final : public ::Input::AnalogDevice {
public:
explicit LibRetroAxis(int joystick_, int button_) : joystick(joystick_), button(button_) {}
std::tuple<float, float> GetStatus() const override {
auto axis_x =
(float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 0);
auto axis_y =
(float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 1);
return std::make_tuple(axis_x / INT16_MAX, -axis_y / INT16_MAX);
}
private:
int joystick;
int button;
};
/// A axis device factory that creates axis devices from SDL joystick
class LibRetroAxisFactory final : public ::Input::Factory<::Input::AnalogDevice> {
public:
/**
* Creates a button device from a joystick button
* @param params contains parameters for creating the device:
* - "joystick": the index of the joystick to bind
* - "button"(optional): the index of the button to bind
* - "hat"(optional): the index of the hat to bind as direction buttons
* - "axis"(optional): the index of the axis to bind
* - "direction"(only used for hat): the direction name of the hat to bind. Can be "up",
* "down", "left" or "right"
* - "threshould"(only used for axis): a float value in (-1.0, 1.0) which the button is
* triggered if the axis value crosses
* - "direction"(only used for axis): "+" means the button is triggered when the axis value
* is greater than the threshold; "-" means the button is triggered when the axis value
* is smaller than the threshold
*/
std::unique_ptr<::Input::AnalogDevice> Create(const Common::ParamPackage& params) override {
const int joystick_index = params.Get("joystick", 0);
const int button = params.Get("axis", 0);
return std::make_unique<LibRetroAxis>(joystick_index, button);
}
};
/// Static sensor interface callbacks for LibRetro motion input
static retro_sensor_get_input_t sensor_get_input_callback = nullptr;
static retro_set_sensor_state_t sensor_set_state_callback = nullptr;
static bool gyro_enabled = false;
static bool accel_enabled = false;
/// LibRetro motion device that implements 3DS gyroscope and accelerometer input
class LibRetroMotion final : public ::Input::MotionDevice {
public:
explicit LibRetroMotion(int port_, float sensitivity_)
: port(port_), sensitivity(sensitivity_) {
InitSensors();
}
std::tuple<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const override {
Common::Vec3<float> accel = {0.0f, 0.0f, -1.0f}; // Default gravity pointing down
Common::Vec3<float> gyro = {0.0f, 0.0f, 0.0f}; // Default no rotation
if (sensor_get_input_callback) {
if (accel_enabled) {
// Get accelerometer data (in g units)
// LibRetro coordinate system matches 3DS: X=LEFT, Y=OUT, Z=UP
accel.x =
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_X) * sensitivity;
accel.y =
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Y) * sensitivity;
accel.z =
sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Z) * sensitivity;
}
if (gyro_enabled) {
// Get gyroscope data (convert to degrees/sec)
// LibRetro gives radians/sec, 3DS expects degrees/sec
constexpr float RAD_TO_DEG = 180.0f / 3.14159265f;
gyro.x = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_X) * RAD_TO_DEG *
sensitivity;
gyro.y = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Y) * RAD_TO_DEG *
sensitivity;
gyro.z = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Z) * RAD_TO_DEG *
sensitivity;
}
}
return std::make_tuple(accel, gyro);
}
private:
int port;
float sensitivity;
void InitSensors() const {
// Initialize sensors if not already done
if (!sensor_get_input_callback || !sensor_set_state_callback) {
struct retro_sensor_interface sensor_interface;
if (LibRetro::GetSensorInterface(&sensor_interface)) {
sensor_get_input_callback = sensor_interface.get_sensor_input;
sensor_set_state_callback = sensor_interface.set_sensor_state;
}
}
// Enable sensors at 60Hz rate (matching 3DS update frequency)
const unsigned int event_rate = 60;
if (sensor_set_state_callback) {
if (!accel_enabled &&
sensor_set_state_callback(port, RETRO_SENSOR_ACCELEROMETER_ENABLE, event_rate)) {
accel_enabled = true;
}
if (!gyro_enabled &&
sensor_set_state_callback(port, RETRO_SENSOR_GYROSCOPE_ENABLE, event_rate)) {
gyro_enabled = true;
}
}
}
};
/// Motion device factory that creates motion devices from LibRetro sensor interface
class LibRetroMotionFactory final : public ::Input::Factory<::Input::MotionDevice> {
public:
/**
* Creates a motion device from LibRetro sensor interface
* @param params contains parameters for creating the device:
* - "port": the controller port to read motion from (default 0)
* - "sensitivity": motion sensitivity multiplier (default 1.0)
*/
std::unique_ptr<::Input::MotionDevice> Create(const Common::ParamPackage& params) override {
const int port = params.Get("port", 0);
const float sensitivity = params.Get("sensitivity", 1.0f);
return std::make_unique<LibRetroMotion>(port, sensitivity);
}
};
void Init() {
using namespace ::Input;
RegisterFactory<ButtonDevice>("libretro", std::make_shared<LibRetroButtonFactory>());
RegisterFactory<AnalogDevice>("libretro", std::make_shared<LibRetroAxisFactory>());
RegisterFactory<MotionDevice>("libretro", std::make_shared<LibRetroMotionFactory>());
}
void Shutdown() {
using namespace ::Input;
UnregisterFactory<ButtonDevice>("libretro");
UnregisterFactory<AnalogDevice>("libretro");
UnregisterFactory<MotionDevice>("libretro");
// Disable sensors on shutdown
if (sensor_set_state_callback) {
sensor_set_state_callback(0, RETRO_SENSOR_ACCELEROMETER_DISABLE, 60);
sensor_set_state_callback(0, RETRO_SENSOR_GYROSCOPE_DISABLE, 60);
sensor_get_input_callback = nullptr;
sensor_set_state_callback = nullptr;
accel_enabled = false;
gyro_enabled = false;
}
}
} // namespace Input
} // namespace LibRetro

View file

@ -0,0 +1,20 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "core/frontend/input.h"
namespace LibRetro {
namespace Input {
/// Initializes and registers LibRetro device factories
void Init();
/// Unresisters LibRetro device factories and shut them down.
void Shutdown();
} // namespace Input
} // namespace LibRetro

View file

@ -0,0 +1,440 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cmath>
#include <memory>
#include "citra_libretro/core_settings.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/mouse_tracker.h"
#include "common/settings.h"
#include "core/frontend/framebuffer_layout.h"
#ifdef ENABLE_OPENGL
#include <glad/glad.h>
#include "video_core/shader/generator/glsl_shader_gen.h"
#endif
#ifdef ENABLE_VULKAN
#include "core/core.h"
#include "video_core/gpu.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#endif
namespace LibRetro {
namespace Input {
/// Shared cursor coordinate calculation
struct CursorCoordinates {
float centerX, centerY;
float renderWidth, renderHeight;
float boundingLeft, boundingTop, boundingRight, boundingBottom;
float verticalLeft, verticalRight, verticalTop, verticalBottom;
float horizontalLeft, horizontalRight, horizontalTop, horizontalBottom;
CursorCoordinates(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
float renderRatio, const Layout::FramebufferLayout& layout) {
// Convert to normalized device coordinates
centerX = (projectedX / bufferWidth) * 2 - 1;
centerY = (projectedY / bufferHeight) * 2 - 1;
renderWidth = renderRatio / bufferWidth;
renderHeight = renderRatio / bufferHeight;
boundingLeft = (layout.bottom_screen.left / (float)bufferWidth) * 2 - 1;
boundingTop = (layout.bottom_screen.top / (float)bufferHeight) * 2 - 1;
boundingRight = (layout.bottom_screen.right / (float)bufferWidth) * 2 - 1;
boundingBottom = (layout.bottom_screen.bottom / (float)bufferHeight) * 2 - 1;
// Calculate cursor dimensions
verticalLeft = std::fmax(centerX - renderWidth / 5, boundingLeft);
verticalRight = std::fmin(centerX + renderWidth / 5, boundingRight);
verticalTop = -std::fmax(centerY - renderHeight, boundingTop);
verticalBottom = -std::fmin(centerY + renderHeight, boundingBottom);
horizontalLeft = std::fmax(centerX - renderWidth, boundingLeft);
horizontalRight = std::fmin(centerX + renderWidth, boundingRight);
horizontalTop = -std::fmax(centerY - renderHeight / 5, boundingTop);
horizontalBottom = -std::fmin(centerY + renderHeight / 5, boundingBottom);
}
};
/// Helper function to check if coordinates are within the touchscreen area
/// (uses the same logic as EmuWindow::IsWithinTouchscreen)
static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x,
unsigned framebuffer_y) {
// Note: LibRetro doesn't support SeparateWindows, so we can skip that check
Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue();
if (render_3d_mode == Settings::StereoRenderOption::SideBySide ||
render_3d_mode == Settings::StereoRenderOption::SideBySideFull) {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
((framebuffer_x >= layout.bottom_screen.left / 2 &&
framebuffer_x < layout.bottom_screen.right / 2) ||
(framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) &&
framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2))));
} else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
((framebuffer_x >= layout.bottom_screen.left &&
framebuffer_x < layout.bottom_screen.right) ||
(framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) &&
framebuffer_x < layout.cardboard.bottom_screen_right_eye +
layout.bottom_screen.GetWidth() + (layout.width / 2))));
} else {
return (framebuffer_y >= layout.bottom_screen.top &&
framebuffer_y < layout.bottom_screen.bottom &&
framebuffer_x >= layout.bottom_screen.left &&
framebuffer_x < layout.bottom_screen.right);
}
}
MouseTracker::MouseTracker() {
// Create renderer-specific cursor renderer based on current graphics API
cursor_renderer = nullptr;
switch (Settings::values.graphics_api.GetValue()) {
case Settings::GraphicsAPI::OpenGL:
#ifdef ENABLE_OPENGL
cursor_renderer = std::make_unique<OpenGLCursorRenderer>();
#endif
break;
case Settings::GraphicsAPI::Vulkan:
#ifdef ENABLE_VULKAN
cursor_renderer = std::make_unique<VulkanCursorRenderer>();
#endif
break;
case Settings::GraphicsAPI::Software:
cursor_renderer = std::make_unique<SoftwareCursorRenderer>();
break;
}
}
MouseTracker::~MouseTracker() = default;
void MouseTracker::OnMouseMove(int deltaX, int deltaY) {
x += deltaX;
y += deltaY;
}
void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) {
x = std::clamp(x, minX, maxX);
y = std::clamp(y, minY, maxY);
}
void MouseTracker::Update(int bufferWidth, int bufferHeight,
const Layout::FramebufferLayout& layout) {
bool state = false;
if (LibRetro::settings.mouse_touchscreen) {
// Check mouse input
state |= LibRetro::CheckInput(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT);
// Read in and convert pointer values to absolute values on the canvas
auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X);
auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y);
auto newX = static_cast<int>((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth);
auto newY = static_cast<int>((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight);
// Use mouse pointer movement
if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) {
lastMouseX = newX;
lastMouseY = newY;
// Use layout system to validate and map coordinates
if (IsWithinTouchscreen(layout, newX, newY)) {
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
static_cast<int>(layout.bottom_screen.right)) -
layout.bottom_screen.left;
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
static_cast<int>(layout.bottom_screen.bottom)) -
layout.bottom_screen.top;
}
}
}
if (LibRetro::settings.touch_touchscreen) {
// Check touchscreen input
state |= LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED);
// Read in and convert pointer values to absolute values on the canvas
auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X);
auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y);
auto newX = static_cast<int>((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth);
auto newY = static_cast<int>((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight);
// Use mouse pointer movement
if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) {
lastMouseX = newX;
lastMouseY = newY;
// Use layout system to validate and map coordinates
if (IsWithinTouchscreen(layout, newX, newY)) {
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
static_cast<int>(layout.bottom_screen.right)) -
layout.bottom_screen.left;
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
static_cast<int>(layout.bottom_screen.bottom)) -
layout.bottom_screen.top;
}
}
}
if (LibRetro::settings.analog_function != LibRetro::CStickFunction::CStick) {
// Check right analog input
state |= LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3);
// TODO: Provide config option for ratios here
auto widthSpeed = (layout.bottom_screen.GetWidth() / 20.0);
auto heightSpeed = (layout.bottom_screen.GetHeight() / 20.0);
// Use controller movement
float controllerX =
((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT,
RETRO_DEVICE_ID_ANALOG_X) /
INT16_MAX);
float controllerY =
((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT,
RETRO_DEVICE_ID_ANALOG_Y) /
INT16_MAX);
// Deadzone the controller inputs
float smoothedX = std::abs(controllerX);
float smoothedY = std::abs(controllerY);
if (smoothedX < LibRetro::settings.deadzone) {
controllerX = 0;
}
if (smoothedY < LibRetro::settings.deadzone) {
controllerY = 0;
}
OnMouseMove(static_cast<int>(controllerX * widthSpeed),
static_cast<int>(controllerY * heightSpeed));
}
Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight());
// Make the coordinates 0 -> 1
projectedX = (float)x / layout.bottom_screen.GetWidth();
projectedY = (float)y / layout.bottom_screen.GetHeight();
// Ensure that the projected position doesn't overlap outside the bottom screen framebuffer.
// TODO: Provide config option
renderRatio = (float)layout.bottom_screen.GetHeight() / 30;
// Map the mouse coord to the bottom screen's position
projectedX = layout.bottom_screen.left + projectedX * layout.bottom_screen.GetWidth();
projectedY = layout.bottom_screen.top + projectedY * layout.bottom_screen.GetHeight();
isPressed = state;
this->framebuffer_layout = layout;
}
void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) {
if (!LibRetro::settings.render_touchscreen) {
return;
}
// Delegate to renderer-specific implementation
if (cursor_renderer) {
cursor_renderer->Render(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
framebuffer_layout, framebuffer_data);
}
}
#ifdef ENABLE_OPENGL
// OpenGL-specific cursor renderer implementation
OpenGLCursorRenderer::OpenGLCursorRenderer() {
// Could potentially also use Citra's built-in shaders, if they can be
// wrangled to cooperate.
std::string vertex;
if (Settings::values.use_gles) {
vertex += fragment_shader_precision_OES;
}
vertex += R"(
in vec2 position;
void main()
{
gl_Position = vec4(position, 0.0, 1.0);
}
)";
std::string fragment;
if (Settings::values.use_gles) {
fragment += fragment_shader_precision_OES;
}
fragment += R"(
out vec4 color;
void main()
{
color = vec4(1.0, 1.0, 1.0, 1.0);
}
)";
vao.Create();
vbo.Create();
glBindVertexArray(vao.handle);
glBindBuffer(GL_ARRAY_BUFFER, vbo.handle);
shader.Create(vertex.c_str(), fragment.c_str());
auto positionVariable = (GLuint)glGetAttribLocation(shader.handle, "position");
glEnableVertexAttribArray(positionVariable);
glVertexAttribPointer(positionVariable, 2, GL_FLOAT, GL_FALSE, 0, 0);
}
OpenGLCursorRenderer::~OpenGLCursorRenderer() {
shader.Release();
vao.Release();
vbo.Release();
}
void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
float projectedY, float renderRatio,
const Layout::FramebufferLayout& layout, void* framebuffer_data) {
// Use shared coordinate calculation
CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
layout);
glUseProgram(shader.handle);
glBindVertexArray(vao.handle);
// clang-format off
GLfloat cursor[] = {
// | in the cursor
coords.verticalLeft, coords.verticalTop,
coords.verticalRight, coords.verticalTop,
coords.verticalRight, coords.verticalBottom,
coords.verticalLeft, coords.verticalTop,
coords.verticalRight, coords.verticalBottom,
coords.verticalLeft, coords.verticalBottom,
// - in the cursor
coords.horizontalLeft, coords.horizontalTop,
coords.horizontalRight, coords.horizontalTop,
coords.horizontalRight, coords.horizontalBottom,
coords.horizontalLeft, coords.horizontalTop,
coords.horizontalRight, coords.horizontalBottom,
coords.horizontalLeft, coords.horizontalBottom
};
// clang-format on
glEnable(GL_BLEND);
glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR);
glBindBuffer(GL_ARRAY_BUFFER, vbo.handle);
glBufferData(GL_ARRAY_BUFFER, sizeof(cursor), cursor, GL_STATIC_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 12);
glBindVertexArray(0);
glUseProgram(0);
glDisable(GL_BLEND);
}
#endif
#ifdef ENABLE_VULKAN
// Vulkan-specific cursor renderer implementation
VulkanCursorRenderer::VulkanCursorRenderer() {
// Vulkan cursor rendering will be integrated into the main rendering pipeline
}
VulkanCursorRenderer::~VulkanCursorRenderer() = default;
void VulkanCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
float projectedY, float renderRatio,
const Layout::FramebufferLayout& layout, void* framebuffer_data) {
// Use shared coordinate calculation
CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
layout);
// TODO: Implement actual Vulkan cursor drawing using the renderer's command buffer
// This would involve:
// 1. Creating a simple vertex buffer with cursor geometry using coords
// 2. Using a basic shader pipeline
// 3. Recording draw commands into the current command buffer
// 4. Using blend mode similar to OpenGL (ONE_MINUS_DST_COLOR, ONE_MINUS_SRC_COLOR)
// For now, this is a placeholder - the cursor won't be visible in Vulkan mode
// but the touchscreen input will still work
}
#endif
// Software-specific cursor renderer implementation
SoftwareCursorRenderer::SoftwareCursorRenderer() {
// Software renderer initialization
}
SoftwareCursorRenderer::~SoftwareCursorRenderer() = default;
void SoftwareCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
float projectedY, float renderRatio,
const Layout::FramebufferLayout& layout,
void* framebuffer_data) {
if (!framebuffer_data) {
return; // No framebuffer data available
}
// Convert coordinates to screen space
int centerX = static_cast<int>(projectedX);
int centerY = static_cast<int>(projectedY);
int radius = static_cast<int>(renderRatio);
// Calculate cursor dimensions within bounds
int verticalLeft = std::max(centerX - radius / 5, static_cast<int>(layout.bottom_screen.left));
int verticalRight =
std::min(centerX + radius / 5, static_cast<int>(layout.bottom_screen.right));
int verticalTop = std::max(centerY - radius, static_cast<int>(layout.bottom_screen.top));
int verticalBottom = std::min(centerY + radius, static_cast<int>(layout.bottom_screen.bottom));
int horizontalLeft = std::max(centerX - radius, static_cast<int>(layout.bottom_screen.left));
int horizontalRight = std::min(centerX + radius, static_cast<int>(layout.bottom_screen.right));
int horizontalTop = std::max(centerY - radius / 5, static_cast<int>(layout.bottom_screen.top));
int horizontalBottom =
std::min(centerY + radius / 5, static_cast<int>(layout.bottom_screen.bottom));
// Draw cursor directly to framebuffer (assuming RGBA8888 format)
uint32_t* pixels = static_cast<uint32_t*>(framebuffer_data);
const uint32_t cursorColor = 0xFFFFFFFF; // White cursor
// Draw vertical line of cursor
for (int y = verticalTop; y < verticalBottom; ++y) {
for (int x = verticalLeft; x < verticalRight; ++x) {
if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) {
int pixelIndex = y * bufferWidth + x;
// XOR blend for visibility on any background
pixels[pixelIndex] ^= cursorColor;
}
}
}
// Draw horizontal line of cursor
for (int y = horizontalTop; y < horizontalBottom; ++y) {
for (int x = horizontalLeft; x < horizontalRight; ++x) {
if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) {
int pixelIndex = y * bufferWidth + x;
// XOR blend for visibility on any background
pixels[pixelIndex] ^= cursorColor;
}
}
}
}
} // namespace Input
} // namespace LibRetro

View file

@ -0,0 +1,111 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include "common/math_util.h"
#include "core/frontend/framebuffer_layout.h"
#ifdef ENABLE_OPENGL
#include "video_core/renderer_opengl/gl_resource_manager.h"
#endif
namespace LibRetro {
namespace Input {
class CursorRenderer {
public:
virtual ~CursorRenderer() = default;
virtual void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
float renderRatio, const Layout::FramebufferLayout& layout,
void* framebuffer_data = nullptr) = 0;
};
/// The mouse tracker provides a mechanism to handle relative mouse/joypad input
/// for a touch-screen device.
class MouseTracker {
public:
MouseTracker();
~MouseTracker();
/// Called whenever a mouse moves.
void OnMouseMove(int xDelta, int yDelta);
/// Restricts the mouse cursor to a specified rectangle.
void Restrict(int minX, int minY, int maxX, int maxY);
/// Updates the tracker.
void Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout);
/// Renders the cursor to the screen (delegates to renderer-specific implementation).
void Render(int bufferWidth, int bufferHeight, void* framebuffer_data = nullptr);
/// If the touchscreen is being pressed.
bool IsPressed() {
return isPressed;
}
/// Get the pressed position, relative to the framebuffer.
std::pair<unsigned, unsigned> GetPressedPosition() {
return {static_cast<const unsigned int&>(projectedX),
static_cast<const unsigned int&>(projectedY)};
}
private:
int x;
int y;
float lastMouseX;
float lastMouseY;
float projectedX;
float projectedY;
float renderRatio;
bool isPressed;
Layout::FramebufferLayout framebuffer_layout;
std::unique_ptr<CursorRenderer> cursor_renderer;
};
#ifdef ENABLE_OPENGL
class OpenGLCursorRenderer : public CursorRenderer {
public:
OpenGLCursorRenderer();
~OpenGLCursorRenderer();
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
float renderRatio, const Layout::FramebufferLayout& layout,
void* framebuffer_data = nullptr) override;
private:
OpenGL::OGLProgram shader;
OpenGL::OGLVertexArray vao;
OpenGL::OGLBuffer vbo;
};
#endif
#ifdef ENABLE_VULKAN
class VulkanCursorRenderer : public CursorRenderer {
public:
VulkanCursorRenderer();
~VulkanCursorRenderer();
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
float renderRatio, const Layout::FramebufferLayout& layout,
void* framebuffer_data = nullptr) override;
};
#endif
class SoftwareCursorRenderer : public CursorRenderer {
public:
SoftwareCursorRenderer();
~SoftwareCursorRenderer();
void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY,
float renderRatio, const Layout::FramebufferLayout& layout,
void* framebuffer_data = nullptr) override;
};
} // namespace Input
} // namespace LibRetro

View file

@ -0,0 +1,27 @@
#LIBRARY "libretro"
#EXPORTS
_retro_set_environment
_retro_set_video_refresh
_retro_set_audio_sample
_retro_set_audio_sample_batch
_retro_set_input_poll
_retro_set_input_state
_retro_init
_retro_deinit
_retro_api_version
_retro_get_system_info
_retro_get_system_av_info
_retro_set_controller_port_device
_retro_reset
_retro_run
_retro_serialize_size
_retro_serialize
_retro_unserialize
_retro_cheat_reset
_retro_cheat_set
_retro_load_game
_retro_load_game_special
_retro_unload_game
_retro_get_region
_retro_get_memory_data
_retro_get_memory_size

View file

@ -0,0 +1,860 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <memory>
#include <stdexcept>
#include <vector>
#include <boost/container/static_vector.hpp>
#include <fmt/format.h>
#include "citra_libretro/environment.h"
#include "citra_libretro/libretro_vk.h"
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/frontend/emu_window.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include <vk_mem_alloc.h>
static const struct retro_hw_render_interface_vulkan* vulkan_intf;
namespace LibRetro {
const VkApplicationInfo* GetVulkanApplicationInfo() {
static VkApplicationInfo app_info{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app_info.pApplicationName = "Azahar";
app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
app_info.pEngineName = "Azahar";
app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0);
// Request Vulkan 1.1 for better compatibility (especially on Android)
// Extensions can be used for features beyond 1.1
app_info.apiVersion = VK_API_VERSION_1_1;
return &app_info;
}
void AddExtensionIfAvailable(std::vector<const char*>& enabled_exts,
const std::vector<VkExtensionProperties>& available_exts,
const char* ext_name) {
// Check if already in the list
for (const char* ext : enabled_exts) {
if (ext && !strcmp(ext, ext_name)) {
return; // Already enabled
}
}
// Check if available
for (const auto& ext : available_exts) {
if (!strcmp(ext.extensionName, ext_name)) {
enabled_exts.push_back(ext_name);
LOG_INFO(Render_Vulkan, "Enabling Vulkan extension: {}", ext_name);
return;
}
}
LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name);
}
bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance,
VkPhysicalDevice gpu, VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char** required_device_extensions,
unsigned num_required_device_extensions,
const char** required_device_layers, unsigned num_required_device_layers,
const VkPhysicalDeviceFeatures* required_features) {
LOG_INFO(Render_Vulkan, "CreateDevice callback invoked - negotiating Vulkan device creation");
// Get available extensions for this physical device
uint32_t ext_count = 0;
PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties =
(PFN_vkEnumerateDeviceExtensionProperties)get_instance_proc_addr(
instance, "vkEnumerateDeviceExtensionProperties");
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, nullptr);
std::vector<VkExtensionProperties> available_exts(ext_count);
if (ext_count > 0) {
vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data());
}
// Start with frontend's required extensions
std::vector<const char*> enabled_exts;
enabled_exts.reserve(num_required_device_extensions + 10);
for (unsigned i = 0; i < num_required_device_extensions; i++) {
if (required_device_extensions[i]) {
enabled_exts.push_back(required_device_extensions[i]);
}
}
// Add extensions we want (if available)
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_SWAPCHAIN_EXTENSION_NAME);
AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
AddExtensionIfAvailable(enabled_exts, available_exts,
VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
AddExtensionIfAvailable(enabled_exts, available_exts,
VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
AddExtensionIfAvailable(enabled_exts, available_exts, VK_EXT_TOOLING_INFO_EXTENSION_NAME);
// These are beneficial but blacklisted on some platforms due to driver bugs
// For now, let the Instance class handle these decisions
// AddExtensionIfAvailable(enabled_exts, available_exts,
// VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME);
// AddExtensionIfAvailable(enabled_exts, available_exts,
// VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME);
// Merge frontend's required features with our baseline
VkPhysicalDeviceFeatures merged_features{};
if (required_features) {
// Copy all frontend requirements
for (unsigned i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) {
if (reinterpret_cast<const VkBool32*>(required_features)[i]) {
reinterpret_cast<VkBool32*>(&merged_features)[i] = VK_TRUE;
}
}
}
// Request features we need (these will be OR'd with frontend requirements)
// The Instance class will validate these against actual device capabilities
merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects
merged_features.logicOp = VK_TRUE; // Used for blending modes
merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering
// Find queue family with graphics support
PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties =
(PFN_vkGetPhysicalDeviceQueueFamilyProperties)get_instance_proc_addr(
instance, "vkGetPhysicalDeviceQueueFamilyProperties");
uint32_t queue_family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, nullptr);
std::vector<VkQueueFamilyProperties> queue_families(queue_family_count);
vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, queue_families.data());
uint32_t graphics_queue_family = VK_QUEUE_FAMILY_IGNORED;
for (uint32_t i = 0; i < queue_family_count; i++) {
if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
graphics_queue_family = i;
break;
}
}
if (graphics_queue_family == VK_QUEUE_FAMILY_IGNORED) {
LOG_CRITICAL(Render_Vulkan, "No graphics queue family found!");
return false;
}
// Create device
const float queue_priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queue_info.queueFamilyIndex = graphics_queue_family;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = &queue_priority;
VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_info.queueCreateInfoCount = 1;
device_info.pQueueCreateInfos = &queue_info;
device_info.enabledExtensionCount = static_cast<uint32_t>(enabled_exts.size());
device_info.ppEnabledExtensionNames = enabled_exts.data();
device_info.enabledLayerCount = num_required_device_layers;
device_info.ppEnabledLayerNames = required_device_layers;
device_info.pEnabledFeatures = &merged_features;
PFN_vkCreateDevice vkCreateDevice =
(PFN_vkCreateDevice)get_instance_proc_addr(instance, "vkCreateDevice");
VkDevice device = VK_NULL_HANDLE;
VkResult result = vkCreateDevice(gpu, &device_info, nullptr, &device);
if (result != VK_SUCCESS) {
LOG_CRITICAL(Render_Vulkan, "vkCreateDevice failed: {}", static_cast<int>(result));
return false;
}
// Get the queue
PFN_vkGetDeviceQueue vkGetDeviceQueue =
(PFN_vkGetDeviceQueue)get_instance_proc_addr(instance, "vkGetDeviceQueue");
VkQueue queue = VK_NULL_HANDLE;
vkGetDeviceQueue(device, graphics_queue_family, 0, &queue);
// Fill in the context for the frontend
context->gpu = gpu;
context->device = device;
context->queue = queue;
context->queue_family_index = graphics_queue_family;
context->presentation_queue = queue; // Same queue for LibRetro
context->presentation_queue_family_index = graphics_queue_family;
LOG_INFO(Render_Vulkan,
"Vulkan device created successfully via negotiation interface (GPU: {}, Queue "
"Family: {})",
static_cast<void*>(gpu), graphics_queue_family);
return true;
}
void VulkanResetContext() {
LibRetro::GetHWRenderInterface((void**)&vulkan_intf);
// Initialize dispatcher with LibRetro's function pointers
VULKAN_HPP_DEFAULT_DISPATCHER.init(vulkan_intf->get_instance_proc_addr);
vk::Instance vk_instance{vulkan_intf->instance};
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance);
}
} // namespace LibRetro
namespace Vulkan {
std::shared_ptr<Common::DynamicLibrary> OpenLibrary(
[[maybe_unused]] Frontend::GraphicsContext* context) {
// the frontend takes care of this, we'll get the instance later
return std::make_shared<Common::DynamicLibrary>();
}
vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) {
// LibRetro cores don't use surfaces - we render to our own output texture
// This function should not be called in LibRetro mode
LOG_WARNING(Render_Vulkan, "CreateSurface called in LibRetro mode - this should not happen");
return VK_NULL_HANDLE;
}
vk::UniqueInstance CreateInstance([[maybe_unused]] const Common::DynamicLibrary& library,
[[maybe_unused]] Frontend::WindowSystemType window_type,
[[maybe_unused]] bool enable_validation,
[[maybe_unused]] bool dump_command_buffers) {
// LibRetro cores don't create instances - frontend handles this
LOG_WARNING(Render_Vulkan, "CreateInstance called in LibRetro mode - this should not happen");
return vk::UniqueInstance{};
}
DebugCallback CreateDebugCallback(vk::Instance instance, bool& debug_utils_supported) {
// LibRetro handles debugging, return empty callback
debug_utils_supported = false;
return {};
}
LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window,
[[maybe_unused]] u32 physical_device_index)
: Instance(Instance::NoInit{}) {
// Ensure LibRetro interface is available
if (!vulkan_intf) {
LOG_CRITICAL(Render_Vulkan, "LibRetro Vulkan interface not initialized!");
throw std::runtime_error("LibRetro Vulkan interface not available");
}
// Initialize basic Vulkan objects from LibRetro
physical_device = vulkan_intf->gpu;
if (!physical_device) {
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid physical device!");
throw std::runtime_error("Invalid physical device from LibRetro");
}
// Get device properties and features
properties = physical_device.getProperties();
const std::vector extensions = physical_device.enumerateDeviceExtensionProperties();
available_extensions.reserve(extensions.size());
for (const auto& extension : extensions) {
available_extensions.emplace_back(extension.extensionName.data());
}
// Get queues from LibRetro
graphics_queue = vulkan_intf->queue;
queue_family_index = vulkan_intf->queue_index;
present_queue = graphics_queue; // Same queue for LibRetro
if (!graphics_queue) {
LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid graphics queue!");
throw std::runtime_error("Invalid graphics queue from LibRetro");
}
// Initialize Vulkan HPP dispatcher with LibRetro's device
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device});
// Now run device capability detection with dispatcher initialized
CreateDevice();
// LibRetro-specific: Validate function pointers are actually available
// LibRetro's device may not have loaded all extension functions even if extensions are
// available
if (extended_dynamic_state) {
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetCullModeEXT ||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthTestEnableEXT ||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthWriteEnableEXT ||
!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetFrontFaceEXT) {
LOG_WARNING(Render_Vulkan, "Extended dynamic state function pointers not available in "
"LibRetro context, disabling");
extended_dynamic_state = false;
}
}
if (timeline_semaphores) {
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkGetSemaphoreCounterValueKHR) {
LOG_WARNING(Render_Vulkan, "Timeline semaphore function pointers not available in "
"LibRetro context, disabling");
timeline_semaphores = false;
}
}
// Initialize subsystems
CreateAllocator();
CreateFormatTable();
CollectToolingInfo();
CreateCustomFormatTable();
CreateAttribTable();
LOG_INFO(Render_Vulkan, "LibRetro Vulkan Instance initialized successfully");
LOG_INFO(Render_Vulkan, "Device: {} ({})", properties.deviceName.data(), GetVendorName());
LOG_INFO(Render_Vulkan, "Driver: {}", GetDriverVersionName());
}
vk::Instance LibRetroVKInstance::GetInstance() const {
return vk::Instance{vulkan_intf->instance};
}
vk::Device LibRetroVKInstance::GetDevice() const {
return vk::Device{vulkan_intf->device};
}
// ============================================================================
// PresentWindow Implementation (LibRetro version)
// ============================================================================
PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_,
Scheduler& scheduler_, [[maybe_unused]] bool low_refresh_rate)
: emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_},
graphics_queue{instance.GetGraphicsQueue()} {
const vk::Device device = instance.GetDevice();
LOG_INFO(Render_Vulkan, "Initializing LibRetro PresentWindow");
// Create command pool for frame operations
const vk::CommandPoolCreateInfo pool_info = {
.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer |
vk::CommandPoolCreateFlagBits::eTransient,
.queueFamilyIndex = instance.GetGraphicsQueueFamilyIndex(),
};
command_pool = device.createCommandPool(pool_info);
// Create render pass for LibRetro output
present_renderpass = CreateRenderpass();
// Start with initial dimensions from layout
const auto& layout = emu_window.GetFramebufferLayout();
CreateOutputTexture(layout.width, layout.height);
CreateFrameResources();
LOG_INFO(Render_Vulkan, "LibRetro PresentWindow initialized with {}x{}", layout.width,
layout.height);
}
PresentWindow::~PresentWindow() {
const vk::Device device = instance.GetDevice();
LOG_DEBUG(Render_Vulkan, "Destroying LibRetro PresentWindow");
// Wait for any pending operations
WaitPresent();
device.waitIdle();
// Destroy frame resources
DestroyFrameResources();
// Destroy output texture
DestroyOutputTexture();
// Destroy Vulkan objects
if (command_pool) {
device.destroyCommandPool(command_pool);
}
if (present_renderpass) {
device.destroyRenderPass(present_renderpass);
}
}
void PresentWindow::CreateOutputTexture(u32 width, u32 height) {
if (width == 0 || height == 0) {
LOG_ERROR(Render_Vulkan, "Invalid output texture dimensions: {}x{}", width, height);
return;
}
// Destroy existing texture if dimensions changed
if (output_image && (output_width != width || output_height != height)) {
DestroyOutputTexture();
}
// Skip if already created with correct dimensions
if (output_image && output_width == width && output_height == height) {
return;
}
const vk::Device device = instance.GetDevice();
output_width = width;
output_height = height;
// Create output image with LibRetro requirements
const vk::ImageCreateInfo image_info = {
.imageType = vk::ImageType::e2D,
.format = output_format,
.extent = {width, height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage = vk::ImageUsageFlagBits::eColorAttachment | // For rendering
vk::ImageUsageFlagBits::eTransferSrc | // Required by LibRetro
vk::ImageUsageFlagBits::eSampled | // Required by LibRetro
vk::ImageUsageFlagBits::eTransferDst, // For clearing
.sharingMode = vk::SharingMode::eExclusive,
.initialLayout = vk::ImageLayout::eUndefined,
};
// Create image with VMA - using budget-aware allocation like standalone version
VmaAllocationCreateInfo alloc_info = {};
alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
alloc_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
VkImage vk_image;
const VkResult result = vmaCreateImage(instance.GetAllocator(),
reinterpret_cast<const VkImageCreateInfo*>(&image_info),
&alloc_info, &vk_image, &output_allocation, nullptr);
if (result != VK_SUCCESS) {
LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast<int>(result));
throw std::runtime_error("Failed to create LibRetro output texture");
}
output_image = vk::Image{vk_image};
// Create image view
output_view_create_info = {
.image = output_image,
.viewType = vk::ImageViewType::e2D,
.format = output_format,
.components =
{
.r = vk::ComponentSwizzle::eIdentity,
.g = vk::ComponentSwizzle::eIdentity,
.b = vk::ComponentSwizzle::eIdentity,
.a = vk::ComponentSwizzle::eIdentity,
},
.subresourceRange =
{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
output_image_view = device.createImageView(output_view_create_info);
LOG_DEBUG(Render_Vulkan, "Created LibRetro output texture: {}x{}", width, height);
}
void PresentWindow::DestroyOutputTexture() {
if (!output_image) {
return;
}
const vk::Device device = instance.GetDevice();
if (output_image_view) {
device.destroyImageView(output_image_view);
output_image_view = nullptr;
}
if (output_allocation) {
vmaDestroyImage(instance.GetAllocator(), static_cast<VkImage>(output_image),
output_allocation);
output_allocation = {};
}
output_image = nullptr;
output_width = 0;
output_height = 0;
}
vk::RenderPass PresentWindow::CreateRenderpass() {
const vk::AttachmentDescription color_attachment = {
.format = output_format,
.samples = vk::SampleCountFlagBits::e1,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.stencilLoadOp = vk::AttachmentLoadOp::eDontCare,
.stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
.initialLayout = vk::ImageLayout::eUndefined,
.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal, // Ready for LibRetro
};
const vk::AttachmentReference color_ref = {
.attachment = 0,
.layout = vk::ImageLayout::eColorAttachmentOptimal,
};
const vk::SubpassDescription subpass = {
.pipelineBindPoint = vk::PipelineBindPoint::eGraphics,
.colorAttachmentCount = 1,
.pColorAttachments = &color_ref,
};
const vk::SubpassDependency dependency = {
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0,
.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput,
.srcAccessMask = {},
.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
};
const vk::RenderPassCreateInfo renderpass_info = {
.attachmentCount = 1,
.pAttachments = &color_attachment,
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency,
};
return instance.GetDevice().createRenderPass(renderpass_info);
}
void PresentWindow::CreateFrameResources() {
const vk::Device device = instance.GetDevice();
const u32 frame_count = 2; // Double buffering for LibRetro
// Destroy existing frames
DestroyFrameResources();
// Create frame pool
frame_pool.resize(frame_count);
// Allocate command buffers
const vk::CommandBufferAllocateInfo alloc_info = {
.commandPool = command_pool,
.level = vk::CommandBufferLevel::ePrimary,
.commandBufferCount = frame_count,
};
const std::vector command_buffers = device.allocateCommandBuffers(alloc_info);
// Initialize frames
for (u32 i = 0; i < frame_count; i++) {
Frame& frame = frame_pool[i];
frame.width = output_width;
frame.height = output_height;
frame.image = output_image; // All frames use the same output texture
frame.image_view = output_image_view;
frame.allocation = {}; // VMA allocation handled separately
frame.cmdbuf = command_buffers[i];
frame.render_ready = device.createSemaphore({});
frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled});
// Create framebuffer for this frame
const vk::FramebufferCreateInfo fb_info = {
.renderPass = present_renderpass,
.attachmentCount = 1,
.pAttachments = &output_image_view,
.width = output_width,
.height = output_height,
.layers = 1,
};
frame.framebuffer = device.createFramebuffer(fb_info);
}
LOG_DEBUG(Render_Vulkan, "Created {} frame resources for LibRetro", frame_count);
}
void PresentWindow::DestroyFrameResources() {
if (frame_pool.empty()) {
return;
}
const vk::Device device = instance.GetDevice();
for (auto& frame : frame_pool) {
if (frame.framebuffer) {
device.destroyFramebuffer(frame.framebuffer);
}
if (frame.render_ready) {
device.destroySemaphore(frame.render_ready);
}
if (frame.present_done) {
device.destroyFence(frame.present_done);
}
}
frame_pool.clear();
current_frame_index = 0;
}
Frame* PresentWindow::GetRenderFrame() {
if (frame_pool.empty()) {
LOG_ERROR(Render_Vulkan, "No frames available in LibRetro PresentWindow");
return nullptr;
}
// RetroArch may not call context_reset during fullscreen toggle, leaving us
// with a stale interface pointer that can crash
const struct retro_hw_render_interface_vulkan* current_intf = nullptr;
if (!LibRetro::GetHWRenderInterface((void**)&current_intf) || !current_intf) {
LOG_ERROR(Render_Vulkan, "Failed to get current Vulkan interface");
return &frame_pool[current_frame_index];
}
// Update global interface if it changed
if (current_intf != vulkan_intf) {
LOG_INFO(Render_Vulkan, "Vulkan interface changed during runtime from {} to {}",
static_cast<const void*>(vulkan_intf), static_cast<const void*>(current_intf));
vulkan_intf = current_intf;
}
// LibRetro synchronization: Use LibRetro's wait mechanism instead of fences
if (vulkan_intf && vulkan_intf->wait_sync_index && vulkan_intf->handle) {
vulkan_intf->wait_sync_index(vulkan_intf->handle);
}
// Use LibRetro's sync index for frame selection if available
u32 frame_index = current_frame_index;
if (vulkan_intf && vulkan_intf->get_sync_index && vulkan_intf->handle) {
LOG_TRACE(Render_Vulkan, "Calling get_sync_index with handle: {}",
static_cast<void*>(vulkan_intf->handle));
const u32 sync_index = vulkan_intf->get_sync_index(vulkan_intf->handle);
frame_index = sync_index % frame_pool.size();
LOG_TRACE(Render_Vulkan, "LibRetro sync index: {}, using frame: {}", sync_index,
frame_index);
}
return &frame_pool[frame_index];
}
void PresentWindow::RecreateFrame(Frame* frame, u32 width, u32 height) {
if (!frame) {
LOG_ERROR(Render_Vulkan, "Invalid frame for recreation");
return;
}
if (frame->width == width && frame->height == height) {
return; // No change needed
}
LOG_DEBUG(Render_Vulkan, "Recreating LibRetro frame: {}x{} -> {}x{}", frame->width,
frame->height, width, height);
// Wait for frame to be idle
const vk::Device device = instance.GetDevice();
[[maybe_unused]] const vk::Result wait_result =
device.waitForFences(frame->present_done, VK_TRUE, UINT64_MAX);
// Recreate output texture with new dimensions
CreateOutputTexture(width, height);
// Recreate frame resources
CreateFrameResources();
LOG_INFO(Render_Vulkan, "LibRetro frame recreated for {}x{}", width, height);
}
void PresentWindow::Present(Frame* frame) {
if (!frame) {
LOG_ERROR(Render_Vulkan, "Cannot present null frame");
return;
}
if (!vulkan_intf) {
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for presentation");
return;
}
// CRITICAL: Use persistent struct to avoid stack lifetime issues!
// RetroArch may cache this pointer for frame duping during pause
persistent_libretro_image.image_view = static_cast<VkImageView>(frame->image_view);
persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
persistent_libretro_image.create_info =
static_cast<VkImageViewCreateInfo>(output_view_create_info);
vulkan_intf->set_image(vulkan_intf->handle, &persistent_libretro_image, 0, nullptr,
instance.GetGraphicsQueueFamilyIndex());
// Call EmuWindow SwapBuffers to trigger LibRetro video frame submission
emu_window.SwapBuffers();
// LibRetro manages frame indices via sync_index, so we don't manually increment
// current_frame_index = (current_frame_index + 1) % frame_pool.size();
LOG_TRACE(Render_Vulkan, "Frame presented to LibRetro: {}x{}", frame->width, frame->height);
}
void PresentWindow::WaitPresent() {
if (frame_pool.empty()) {
return;
}
const vk::Device device = instance.GetDevice();
// Wait for all frames to complete
std::vector<vk::Fence> fences;
fences.reserve(frame_pool.size());
for (const auto& frame : frame_pool) {
fences.push_back(frame.present_done);
}
if (!fences.empty()) {
[[maybe_unused]] const vk::Result wait_result =
device.waitForFences(fences, VK_TRUE, UINT64_MAX);
}
}
void PresentWindow::NotifySurfaceChanged() {
// LibRetro doesn't use surfaces, so this is a no-op
LOG_DEBUG(Render_Vulkan, "Surface change notification ignored in LibRetro mode");
}
// ============================================================================
// MasterSemaphoreLibRetro Implementation
// ============================================================================
constexpr u64 FENCE_RESERVE = 8;
MasterSemaphoreLibRetro::MasterSemaphoreLibRetro(const Instance& instance_) : instance{instance_} {
const vk::Device device{instance.GetDevice()};
// Pre-allocate fence pool
for (u64 i = 0; i < FENCE_RESERVE; i++) {
free_queue.push_back(device.createFence({}));
}
// Start background wait thread
wait_thread = std::jthread([this](std::stop_token token) { WaitThread(token); });
}
MasterSemaphoreLibRetro::~MasterSemaphoreLibRetro() {
// wait_thread will be automatically stopped by jthread destructor
// Clean up remaining fences
const vk::Device device{instance.GetDevice()};
for (const auto& fence : free_queue) {
device.destroyFence(fence);
}
}
void MasterSemaphoreLibRetro::Refresh() {}
void MasterSemaphoreLibRetro::Wait(u64 tick) {
std::unique_lock lock{free_mutex};
free_cv.wait(lock, [this, tick] { return gpu_tick.load(std::memory_order_relaxed) >= tick; });
}
void MasterSemaphoreLibRetro::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait,
vk::Semaphore signal, u64 signal_value) {
if (!vulkan_intf) {
LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for command submission");
return;
}
cmdbuf.end();
// Get a fence from the pool
const vk::Fence fence = GetFreeFence();
// Strip semaphores - RetroArch handles frame sync, we track resources internally
const vk::SubmitInfo submit_info = {
.waitSemaphoreCount = 0,
.pWaitSemaphores = nullptr,
.pWaitDstStageMask = nullptr,
.commandBufferCount = 1u,
.pCommandBuffers = &cmdbuf,
.signalSemaphoreCount = 0,
.pSignalSemaphores = nullptr,
};
// Use LibRetro's queue coordination
if (vulkan_intf->lock_queue) {
vulkan_intf->lock_queue(vulkan_intf->handle);
}
try {
// Submit with fence for internal resource tracking
vk::Queue queue{vulkan_intf->queue};
queue.submit(submit_info, fence);
if (vulkan_intf->unlock_queue) {
vulkan_intf->unlock_queue(vulkan_intf->handle);
}
} catch (vk::DeviceLostError& err) {
if (vulkan_intf->unlock_queue) {
vulkan_intf->unlock_queue(vulkan_intf->handle);
}
UNREACHABLE_MSG("Device lost during submit: {}", err.what());
} catch (...) {
if (vulkan_intf->unlock_queue) {
vulkan_intf->unlock_queue(vulkan_intf->handle);
}
throw;
}
// Enqueue fence for wait thread to process
{
std::scoped_lock lock{wait_mutex};
wait_queue.emplace(fence, signal_value);
wait_cv.notify_one();
}
}
void MasterSemaphoreLibRetro::WaitThread(std::stop_token token) {
const vk::Device device{instance.GetDevice()};
while (!token.stop_requested()) {
vk::Fence fence;
u64 signal_value;
// Wait for work
{
std::unique_lock lock{wait_mutex};
Common::CondvarWait(wait_cv, lock, token, [this] { return !wait_queue.empty(); });
if (token.stop_requested()) {
return;
}
std::tie(fence, signal_value) = wait_queue.front();
wait_queue.pop();
}
// Wait for fence (blocks only this background thread)
const vk::Result result = device.waitForFences(fence, true, UINT64_MAX);
if (result != vk::Result::eSuccess) {
LOG_ERROR(Render_Vulkan, "Fence wait failed: {}", vk::to_string(result));
}
// Reset fence and return to pool
device.resetFences(fence);
// Update GPU tick - signals main thread's Wait()
gpu_tick.store(signal_value, std::memory_order_release);
// Return fence to pool
{
std::scoped_lock lock{free_mutex};
free_queue.push_back(fence);
free_cv.notify_all();
}
}
}
vk::Fence MasterSemaphoreLibRetro::GetFreeFence() {
std::scoped_lock lock{free_mutex};
if (free_queue.empty()) {
// Pool exhausted - create new fence
return instance.GetDevice().createFence({});
}
const vk::Fence fence = free_queue.front();
free_queue.pop_front();
return fence;
}
// Factory function for scheduler to create LibRetro MasterSemaphore
std::unique_ptr<MasterSemaphore> CreateLibRetroMasterSemaphore(const Instance& instance) {
return std::make_unique<MasterSemaphoreLibRetro>(instance);
}
} // namespace Vulkan

View file

@ -0,0 +1,175 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <variant>
#include <fmt/format.h>
#include "common/common_types.h"
#include "common/dynamic_library/dynamic_library.h"
#include "video_core/renderer_vulkan/vk_common.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
#include "video_core/renderer_vulkan/vk_platform.h"
#include "libretro_vulkan.h"
VK_DEFINE_HANDLE(VmaAllocation)
namespace LibRetro {
extern void VulkanResetContext();
/// Returns VkApplicationInfo for negotiation interface
const VkApplicationInfo* GetVulkanApplicationInfo();
/// CreateDevice callback for negotiation interface
bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance,
VkPhysicalDevice gpu, VkSurfaceKHR surface,
PFN_vkGetInstanceProcAddr get_instance_proc_addr,
const char** required_device_extensions,
unsigned num_required_device_extensions,
const char** required_device_layers, unsigned num_required_device_layers,
const VkPhysicalDeviceFeatures* required_features);
} // namespace LibRetro
namespace Vulkan {
class LibRetroVKInstance : public Instance {
public:
explicit LibRetroVKInstance(Frontend::EmuWindow& window, u32 physical_device_index);
/// Returns the Vulkan instance
vk::Instance GetInstance() const override;
/// Returns the Vulkan device
vk::Device GetDevice() const override;
};
class Scheduler;
class RenderManager;
class MasterSemaphore;
/// LibRetro-specific MasterSemaphore implementation
class MasterSemaphoreLibRetro : public MasterSemaphore {
using Waitable = std::pair<vk::Fence, u64>;
public:
explicit MasterSemaphoreLibRetro(const Instance& instance);
~MasterSemaphoreLibRetro() override;
void Refresh() override;
void Wait(u64 tick) override;
void SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, vk::Semaphore signal,
u64 signal_value) override;
private:
void WaitThread(std::stop_token token);
vk::Fence GetFreeFence();
const Instance& instance;
std::deque<vk::Fence> free_queue;
std::queue<Waitable> wait_queue;
std::mutex free_mutex;
std::mutex wait_mutex;
std::condition_variable free_cv;
std::condition_variable_any wait_cv;
std::jthread wait_thread;
};
/// Factory function for scheduler to create LibRetro MasterSemaphore
std::unique_ptr<MasterSemaphore> CreateLibRetroMasterSemaphore(const Instance& instance);
struct Frame {
u32 width;
u32 height;
VmaAllocation allocation;
vk::Framebuffer framebuffer;
vk::Image image;
vk::ImageView image_view;
vk::Semaphore render_ready;
vk::Fence present_done;
vk::CommandBuffer cmdbuf;
};
/// LibRetro-specific PresentWindow implementation (same interface as desktop version)
class PresentWindow final {
public:
explicit PresentWindow(Frontend::EmuWindow& emu_window, const Instance& instance,
Scheduler& scheduler, bool low_refresh_rate);
~PresentWindow();
/// Waits for all queued frames to finish presenting.
void WaitPresent();
/// Returns the last used render frame.
Frame* GetRenderFrame();
/// Recreates the render frame to match provided parameters.
void RecreateFrame(Frame* frame, u32 width, u32 height);
/// Queues the provided frame for presentation.
void Present(Frame* frame);
/// This is called to notify the rendering backend of a surface change
void NotifySurfaceChanged();
[[nodiscard]] vk::RenderPass Renderpass() const noexcept {
return present_renderpass;
}
u32 ImageCount() const noexcept {
return static_cast<u32>(frame_pool.size());
}
private:
/// Creates the render pass for LibRetro output
vk::RenderPass CreateRenderpass();
/// Creates output texture for LibRetro submission
void CreateOutputTexture(u32 width, u32 height);
/// Destroys current output texture
void DestroyOutputTexture();
/// Creates frame resources
void CreateFrameResources();
/// Destroys frame resources
void DestroyFrameResources();
private:
Frontend::EmuWindow& emu_window;
const Instance& instance;
[[maybe_unused]] Scheduler& scheduler;
// LibRetro output texture (replaces swapchain)
vk::Image output_image{};
vk::ImageView output_image_view{};
VmaAllocation output_allocation{};
vk::Format output_format{vk::Format::eR8G8B8A8Unorm};
vk::ImageViewCreateInfo output_view_create_info{};
// Frame management
vk::RenderPass present_renderpass{};
vk::CommandPool command_pool{};
std::vector<Frame> frame_pool;
u32 current_frame_index{0};
// Current output dimensions
u32 output_width{0};
u32 output_height{0};
// Vulkan objects
vk::Queue graphics_queue{};
// Persistent LibRetro image descriptor, must persist across frames for RetroArch frame duping
// during pause
retro_vulkan_image persistent_libretro_image{};
};
} // namespace Vulkan

View file

@ -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

View file

@ -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);

View file

@ -43,6 +43,12 @@
#define fseeko _fseeki64
#define ftello _ftelli64
#define fileno _fileno
typedef struct _stat64 file_stat_t;
#define fstat _fstat64
#elif defined(HAVE_LIBRETRO)
typedef struct _stat64 file_stat_t;
#else
typedef struct stat file_stat_t;
#endif
#else
@ -56,6 +62,7 @@
#include <dirent.h>
#include <pwd.h>
#include <unistd.h>
typedef struct stat file_stat_t;
#endif
#if defined(__APPLE__)
@ -75,7 +82,7 @@
#endif
#ifdef ANDROID
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
#include "common/android_storage.h"
#include "common/string_util.h"
#endif
@ -87,6 +94,36 @@
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#endif
#ifdef HAVE_LIBRETRO_VFS
#define SKIP_STDIO_REDEFINES
#include <streams/file_stream.h>
#include <streams/file_stream_transforms.h>
#define FILE RFILE
#define FTELL rftell
#define FOPEN rfopen
#define FCLOSE rfclose
#define FSEEK rfseek
#define FREAD rfread
#define FWRITE rfwrite
#define FEOF rfeof
#define FERROR rferror
#define FFLUSH rfflush
#else
#define FTELL ftello
#define FOPEN fopen
#define FCLOSE std::fclose
#define FSEEK fseeko
#define FREAD std::fread
#define FWRITE std::fwrite
#define FEOF feof
#define FERROR ferror
#define FFLUSH std::fflush
#endif
// This namespace has various generic functions related to files and paths.
// The code still needs a ton of cleanup.
// REMEMBER: strdup considered harmful!
@ -119,7 +156,7 @@ bool Exists(const std::string& filename) {
copy += DIR_SEP_CHR;
int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
int result = AndroidStorage::FileExists(filename) ? 0 : -1;
#else
struct stat file_info;
@ -130,7 +167,7 @@ bool Exists(const std::string& filename) {
}
bool IsDirectory(const std::string& filename) {
#ifdef ANDROID
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
return AndroidStorage::IsDirectory(filename);
#endif
@ -178,7 +215,7 @@ bool Delete(const std::string& filename) {
LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg());
return false;
}
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
if (!AndroidStorage::DeleteDocument(filename)) {
LOG_ERROR(Common_Filesystem, "unlink failed on {}", filename);
return false;
@ -205,7 +242,7 @@ bool CreateDir(const std::string& path) {
}
LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error);
return false;
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
std::string directory = path;
std::string filename = path;
if (Common::EndsWith(path, "/")) {
@ -292,7 +329,7 @@ bool DeleteDir(const std::string& filename) {
#ifdef _WIN32
if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str()))
return true;
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
if (AndroidStorage::DeleteDocument(filename))
return true;
#else
@ -310,7 +347,7 @@ bool Rename(const std::string& srcFullPath, const std::string& destFullPath) {
if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(),
Common::UTF8ToUTF16W(destFullPath).c_str()) == 0)
return true;
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
// srcFullPath and destFullPath are relative to the user directory
if (AndroidStorage::GetBuildFlavor() == AndroidStorage::AndroidBuildFlavors::GOOGLEPLAY) {
if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath))
@ -343,36 +380,36 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) {
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
GetLastErrorMsg());
return false;
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
return AndroidStorage::CopyFile(srcFilename, std::string(GetParentPath(destFilename)),
std::string(GetFilename(destFilename)));
#else
// Open input file
FILE* input = fopen(srcFilename.c_str(), "rb");
FILE* input = FOPEN(srcFilename.c_str(), "rb");
if (!input) {
LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename,
destFilename, GetLastErrorMsg());
return false;
}
SCOPE_EXIT({ fclose(input); });
SCOPE_EXIT({ FCLOSE(input); });
// open output file
FILE* output = fopen(destFilename.c_str(), "wb");
FILE* output = FOPEN(destFilename.c_str(), "wb");
if (!output) {
LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename,
destFilename, GetLastErrorMsg());
return false;
}
SCOPE_EXIT({ fclose(output); });
SCOPE_EXIT({ FCLOSE(output); });
// copy loop
std::array<char, 1024> buffer;
while (!feof(input)) {
while (!FEOF(input)) {
// read input
std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input);
std::size_t rnum = FREAD(buffer.data(), sizeof(char), buffer.size(), input);
if (rnum != buffer.size()) {
if (ferror(input) != 0) {
if (FERROR(input) != 0) {
LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}",
srcFilename, destFilename, GetLastErrorMsg());
return false;
@ -380,7 +417,7 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) {
}
// write output
std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output);
std::size_t wnum = FWRITE(buffer.data(), sizeof(char), rnum, output);
if (wnum != rnum) {
LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename,
destFilename, GetLastErrorMsg());
@ -408,7 +445,7 @@ u64 GetSize(const std::string& filename) {
#ifdef _WIN32
struct _stat64 buf;
if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0)
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
u64 result = AndroidStorage::GetSize(filename);
LOG_TRACE(Common_Filesystem, "{}: {}", filename, result);
return result;
@ -425,7 +462,7 @@ u64 GetSize(const std::string& filename) {
}
u64 GetSize(const int fd) {
struct stat buf;
file_stat_t buf;
if (fstat(fd, &buf) != 0) {
LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg());
return 0;
@ -435,13 +472,13 @@ u64 GetSize(const int fd) {
u64 GetSize(FILE* f) {
// can't use off_t here because it can be 32-bit
u64 pos = ftello(f);
if (fseeko(f, 0, SEEK_END) != 0) {
u64 pos = FTELL(f);
if (FSEEK(f, 0, SEEK_END) != 0) {
LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
return 0;
}
u64 size = ftello(f);
if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) {
u64 size = FTELL(f);
if ((size != pos) && (FSEEK(f, pos, SEEK_SET) != 0)) {
LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
return 0;
}
@ -481,7 +518,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
// windows loop
do {
const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName));
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
// android loop
auto result = AndroidStorage::GetFilesName(directory);
for (auto virtual_name : result) {
@ -508,7 +545,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
#ifdef _WIN32
} while (FindNextFileW(handle_find, &ffd) != 0);
FindClose(handle_find);
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
}
#else
}
@ -605,7 +642,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path,
if (!FileUtil::Exists(dest_path))
FileUtil::CreateFullPath(dest_path);
#ifdef ANDROID
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
auto result = AndroidStorage::GetFilesName(source_path);
for (auto virtualName : result) {
#else
@ -635,7 +672,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path,
FileUtil::Copy(source, dest);
}
#ifndef ANDROID
#if !(defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS))
closedir(dirp);
#endif // ANDROID
#endif // _WIN32
@ -811,7 +848,7 @@ void SetUserPath(const std::string& path) {
g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
user_path = "/";
g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
@ -1070,7 +1107,7 @@ std::string_view RemoveTrailingSlash(std::string_view path) {
std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
std::string path(path_);
#ifdef ANDROID
#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
return std::string(RemoveTrailingSlash(path));
#endif
char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
@ -1137,7 +1174,7 @@ bool IOFile::Open() {
Common::UTF8ToUTF16W(openmode).c_str(), flags);
m_good = m_file != nullptr;
#elif ANDROID
#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)
// Check whether filepath is startsWith content
AndroidStorage::AndroidOpenMode android_open_mode = AndroidStorage::ParseOpenmode(openmode);
if (android_open_mode == AndroidStorage::AndroidOpenMode::WRITE ||
@ -1168,7 +1205,7 @@ bool IOFile::Open() {
m_good = m_file != nullptr;
#else
m_file = std::fopen(filename.c_str(), openmode.c_str());
m_file = FOPEN(filename.c_str(), openmode.c_str());
m_good = m_file != nullptr;
#endif
@ -1176,7 +1213,7 @@ bool IOFile::Open() {
}
bool IOFile::Close() {
if (!IsOpen() || 0 != std::fclose(m_file))
if (!IsOpen() || 0 != FCLOSE(m_file))
m_good = false;
m_file = nullptr;
@ -1191,7 +1228,7 @@ u64 IOFile::GetSize() const {
}
bool IOFile::SeekImpl(s64 off, int origin) {
if (!IsOpen() || 0 != fseeko(m_file, off, origin))
if (!IsOpen() || 0 != FSEEK(m_file, off, origin))
m_good = false;
return m_good;
@ -1199,13 +1236,13 @@ bool IOFile::SeekImpl(s64 off, int origin) {
u64 IOFile::TellImpl() const {
if (IsOpen())
return ftello(m_file);
return FTELL(m_file);
return std::numeric_limits<u64>::max();
}
bool IOFile::Flush() {
if (!IsOpen() || 0 != std::fflush(m_file))
if (!IsOpen() || 0 != FFLUSH(m_file))
m_good = false;
return m_good;
@ -1223,7 +1260,7 @@ std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_si
DEBUG_ASSERT(data != nullptr);
return std::fread(data, data_size, length, m_file);
return FREAD(data, data_size, length, m_file);
}
#ifdef _WIN32
@ -1266,7 +1303,16 @@ std::size_t IOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t o
DEBUG_ASSERT(data != nullptr);
#ifdef HAVE_LIBRETRO_VFS
std::scoped_lock lock(m_file_pos_mutex);
int64_t pos = filestream_tell(m_file);
FSEEK(m_file, offset, RETRO_VFS_SEEK_POSITION_START);
int64_t rv = FREAD(data, 1, byte_count, m_file);
FSEEK(m_file, pos, RETRO_VFS_SEEK_POSITION_START);
return rv;
#else
return pread(fileno(m_file), data, byte_count, offset);
#endif
}
std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) {
@ -1281,12 +1327,18 @@ std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t
DEBUG_ASSERT(data != nullptr);
#if defined(HAVE_LIBRETRO_VFS)
return rfwrite(data, data_size, length, m_file) / data_size;
#else
return std::fwrite(data, data_size, length, m_file);
#endif
}
bool IOFile::Resize(u64 size) {
if (!IsOpen() || 0 !=
#ifdef _WIN32
#if defined(HAVE_LIBRETRO_VFS)
filestream_truncate(m_file, size)
#elif defined(_WIN32)
// ector: _chsize sucks, not 64-bit safe
// F|RES: changed to _chsize_s. i think it is 64-bit safe
_chsize_s(_fileno(m_file), size)

View file

@ -15,6 +15,7 @@
#include <ios>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
@ -32,6 +33,14 @@
#include "common/string_util.h"
#endif
#ifdef HAVE_LIBRETRO_VFS
#define SKIP_STDIO_REDEFINES
#include <streams/file_stream_transforms.h>
#define CORE_FILE RFILE
#else
#define CORE_FILE std::FILE
#endif
namespace FileUtil {
// User paths for GetUserPath
@ -120,7 +129,7 @@ private:
[[nodiscard]] u64 GetSize(int fd);
// Overloaded GetSize, accepts FILE*
[[nodiscard]] u64 GetSize(FILE* f);
[[nodiscard]] u64 GetSize(CORE_FILE* f);
// Returns true if successful, or path already exists.
bool CreateDir(const std::string& filename);
@ -423,7 +432,11 @@ public:
return m_good;
}
[[nodiscard]] virtual int GetFd() const {
#ifdef ANDROID
#ifdef HAVE_LIBRETRO_VFS
if (m_file == nullptr)
return -1;
return fileno(filestream_get_vfs_handle(m_file)->fp);
#elif defined(ANDROID)
return m_fd;
#else
if (m_file == nullptr)
@ -448,7 +461,12 @@ public:
// clear error state
virtual void Clear() {
m_good = true;
#ifdef HAVE_LIBRETRO_VFS
filestream_rewind(m_file);
#else
std::clearerr(m_file);
#endif
}
virtual bool IsCrypto() {
@ -476,9 +494,16 @@ protected:
virtual u64 TellImpl() const;
private:
std::FILE* m_file = nullptr;
CORE_FILE* m_file = nullptr;
int m_fd = -1;
bool m_good = true;
#ifdef HAVE_LIBRETRO_VFS
// pread() doesn't touch the file position, so it's safe alongside
// concurrent fread/fwrite. Libretro VFS has no pread equivalent, so
// ReadAtImpl emulates it with seek+read+seek, which would corrupt the
// file position for concurrent Read/Write operations.
mutable std::mutex m_file_pos_mutex;
#endif
std::string filename;
std::string openmode;

View file

@ -55,6 +55,61 @@ public:
virtual void Close() = 0;
};
#ifdef HAVE_LIBRETRO
/**
* LibRetro backend
*/
class LibRetroBackend : public Backend {
public:
explicit LibRetroBackend() {}
explicit LibRetroBackend(retro_log_printf_t callback) : callback(callback) {}
~LibRetroBackend() override = default;
void Write(const Entry& entry) override {
if (callback == nullptr) {
return;
}
retro_log_level log_level;
switch (entry.log_level) {
case Common::Log::Level::Trace:
log_level = retro_log_level::RETRO_LOG_DEBUG;
break;
case Common::Log::Level::Debug:
log_level = retro_log_level::RETRO_LOG_DEBUG;
break;
case Common::Log::Level::Info:
log_level = retro_log_level::RETRO_LOG_INFO;
break;
case Common::Log::Level::Warning:
log_level = retro_log_level::RETRO_LOG_WARN;
break;
case Common::Log::Level::Error:
log_level = retro_log_level::RETRO_LOG_ERROR;
break;
case Common::Log::Level::Critical:
log_level = retro_log_level::RETRO_LOG_ERROR;
break;
default:
log_level = retro_log_level::RETRO_LOG_DUMMY;
}
auto str = FormatLogMessage(entry).append(1, '\n');
callback(log_level, str.c_str());
}
void Flush() override {}
void Close() override {}
void EnableForStacktrace() override {}
private:
retro_log_printf_t callback = nullptr;
};
#endif
/**
* Backend that writes to stderr and with color
*/
@ -218,7 +273,19 @@ public:
}
return *instance;
}
#ifdef HAVE_LIBRETRO
static void Initialize(retro_log_printf_t callback) {
if (instance) {
LOG_WARNING(Log, "Reinitializing logging backend");
return;
}
initialization_in_progress_suppress_logging = true;
Filter filter;
filter.ParseFilterString(Settings::values.log_filter.GetValue());
instance = std::unique_ptr<Impl, decltype(&Deleter)>(new Impl(callback, filter), Deleter);
initialization_in_progress_suppress_logging = false;
}
#endif
static void Initialize(std::string_view log_file) {
if (instance) {
LOG_WARNING(Log, "Reinitializing logging backend");
@ -310,6 +377,10 @@ public:
}
private:
#ifdef HAVE_LIBRETRO
Impl(retro_log_printf_t callback, const Filter& filter_)
: filter{filter_}, file_backend{""}, libretro_backend{callback} {}
#endif
Impl(const std::string& file_backend_filename, const Filter& filter_)
: filter{filter_}, file_backend{file_backend_filename} {
#ifdef CITRA_LINUX_GCC_BACKTRACE
@ -412,12 +483,16 @@ private:
}
void ForEachBackend(auto lambda) {
#ifdef HAVE_LIBRETRO
lambda(static_cast<Backend&>(libretro_backend));
#else
lambda(static_cast<Backend&>(debugger_backend));
lambda(static_cast<Backend&>(color_console_backend));
lambda(static_cast<Backend&>(file_backend));
#ifdef ANDROID
lambda(static_cast<Backend&>(lc_backend));
#endif
#endif // ANDROID
#endif // HAVE_LIBRETRO
}
static void Deleter(Impl* ptr) {
@ -464,6 +539,9 @@ private:
#ifdef ANDROID
LogcatBackend lc_backend{};
#endif
#ifdef HAVE_LIBRETRO
LibRetroBackend libretro_backend;
#endif
MPSCQueue<Entry> message_queue{};
std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()};
@ -478,6 +556,13 @@ private:
};
} // namespace
#ifdef HAVE_LIBRETRO
void LibRetroStart(retro_log_printf_t callback) {
Impl::Initialize(callback);
Impl::Start();
}
#endif
void Initialize(std::string_view log_file) {
Impl::Initialize(log_file.empty() ? LOG_FILE : log_file);
}

View file

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -6,6 +6,9 @@
#include <string_view>
#include "common/logging/filter.h"
#ifdef HAVE_LIBRETRO
#include "libretro.h"
#endif
namespace Common::Log {
@ -13,6 +16,9 @@ class Filter;
/// Initializes the logging system. This should be the first thing called in main.
void Initialize(std::string_view log_file = "");
#ifdef HAVE_LIBRETRO
void LibRetroStart(retro_log_printf_t callback);
#endif
void Start();

View file

@ -9,7 +9,6 @@
#include <algorithm>
#include <chrono>
#include <ctime>
#include <format>
#include <mutex>
#include <sstream>
#include <zstd.h>

View file

@ -362,6 +362,10 @@ public:
void LoadState(u32 slot);
std::vector<u8> SaveStateBuffer() const;
bool LoadStateBuffer(std::vector<u8> buffer);
/// Self delete ncch
bool SetSelfDelete(const std::string& file) {
if (m_filepath == file) {

View file

@ -27,6 +27,7 @@ enum class WindowSystemType : u8 {
MacOS,
X11,
Wayland,
LibRetro,
};
struct Frame;
@ -258,6 +259,16 @@ public:
return is_secondary;
}
/**
* Requests for a frontend to setup a framebuffer.
*/
virtual void SetupFramebuffer() {}
/// Flags that the framebuffer should be cleared.
virtual bool NeedsClearing() const {
return true;
}
Settings::StereoRenderOption get3DMode() const;
protected:

View file

@ -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();

View file

@ -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;
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");
}

View file

@ -2308,7 +2308,9 @@ std::optional<SOC_U::InterfaceInfo> SOC_U::GetDefaultInterfaceInfo() {
break;
}
}
#else
#elif !(defined(ANDROID) && defined(HAVE_LIBRETRO))
// Libretro Android builds target API 21, but getifaddrs() requires API 24+.
// Standalone Android (minSdk 29) and other platforms have getifaddrs().
struct ifaddrs* ifaddr;
struct ifaddrs* ifa;
if (getifaddrs(&ifaddr) == -1) {

View file

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -217,4 +217,77 @@ void System::LoadState(u32 slot) {
ia&* this;
}
std::vector<u8> System::SaveStateBuffer() const {
std::ostringstream sstream{std::ios_base::binary};
// Serialize
oarchive oa{sstream};
oa&* this;
const std::string& str{sstream.str()};
const auto data = std::span<const u8>{reinterpret_cast<const u8*>(str.data()), str.size()};
auto buffer = Common::Compression::CompressDataZSTDDefault(data);
CSTHeader header{};
header.filetype = header_magic_bytes;
header.program_id = title_id;
std::string rev_bytes;
CryptoPP::StringSource ss(Common::g_scm_rev, true,
new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes)));
std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision));
header.time = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
const std::string build_fullname = Common::g_build_fullname;
std::memset(header.build_name.data(), 0, sizeof(header.build_name));
std::memcpy(header.build_name.data(), build_fullname.c_str(),
std::min(build_fullname.length(), sizeof(header.build_name) - 1));
std::vector<u8> result((u8*)&header, (u8*)&header + sizeof(header));
std::copy(buffer.begin(), buffer.end(), std::back_inserter(result));
return result;
}
bool System::LoadStateBuffer(std::vector<u8> buffer) {
CSTHeader header;
if (buffer.size() < sizeof(header)) {
LOG_ERROR(Core, "Save state too small");
return false;
}
header = *((CSTHeader*)buffer.data());
if (header.filetype != header_magic_bytes) {
LOG_ERROR(Core, "Invalid save state");
return false;
}
if (header.program_id != title_id) {
LOG_ERROR(Core, "Save state isn't for the current game");
return false;
}
std::string revision = fmt::format("{:02x}", fmt::join(header.revision, ""));
if (revision != Common::g_scm_rev) {
LOG_ERROR(Core,
"Save state file created from a different revision (core: {}, savestate: {})",
Common::g_scm_rev, revision);
return false;
}
std::vector<u8> state(buffer.begin() + sizeof(CSTHeader), buffer.end());
auto decompressed = Common::Compression::DecompressDataZSTD(state);
std::istringstream sstream{
std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()},
std::ios_base::binary};
decompressed.clear();
// Deserialize
iarchive ia{sstream};
ia&* this;
return true;
}
} // namespace Core

View file

@ -26,6 +26,10 @@ create_target_directory_groups(tests)
target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core)
target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads)
if (ENABLE_LIBRETRO)
target_link_libraries(tests PRIVATE $<TARGET_OBJECTS:azahar_libretro_common>)
endif()
add_test(NAME tests COMMAND tests)
if (CITRA_USE_PRECOMPILED_HEADERS)

View file

@ -183,10 +183,10 @@ if (ENABLE_VULKAN)
renderer_vulkan/vk_instance.h
renderer_vulkan/vk_pipeline_cache.cpp
renderer_vulkan/vk_pipeline_cache.h
renderer_vulkan/vk_platform.cpp
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_platform.cpp>
renderer_vulkan/vk_platform.h
renderer_vulkan/vk_present_window.cpp
renderer_vulkan/vk_present_window.h
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_present_window.cpp>
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_present_window.h>
renderer_vulkan/vk_render_manager.cpp
renderer_vulkan/vk_render_manager.h
renderer_vulkan/vk_shader_disk_cache.cpp
@ -195,8 +195,8 @@ if (ENABLE_VULKAN)
renderer_vulkan/vk_shader_util.h
renderer_vulkan/vk_stream_buffer.cpp
renderer_vulkan/vk_stream_buffer.h
renderer_vulkan/vk_swapchain.cpp
renderer_vulkan/vk_swapchain.h
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_swapchain.cpp>
$<$<NOT:$<BOOL:${ENABLE_LIBRETRO}>>:renderer_vulkan/vk_swapchain.h>
renderer_vulkan/vk_texture_runtime.cpp
renderer_vulkan/vk_texture_runtime.h
shader/generator/spv_fs_shader_gen.cpp

View file

@ -9,6 +9,7 @@
#include "core/core_timing.h"
#include "core/hle/service/gsp/gsp_gpu.h"
#include "core/hle/service/plgldr/plgldr.h"
#include "core/loader/loader.h"
#include "video_core/debug_utils/debug_utils.h"
#include "video_core/gpu.h"
#include "video_core/gpu_debugger.h"
@ -421,6 +422,53 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) {
impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event);
}
void GPU::RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window) {
// Reset the renderer (this will destroy OpenGL resources)
impl->renderer.reset();
// Create a new renderer
impl->renderer =
VideoCore::CreateRenderer(emu_window, secondary_window, impl->pica, impl->system);
impl->rasterizer = impl->renderer->Rasterizer();
// Rebind the rasterizer to the PICA GPU
impl->pica.BindRasterizer(impl->rasterizer);
// Update the sw_blitter with the new rasterizer
impl->sw_blitter = std::make_unique<SwRenderer::SwBlitter>(impl->memory, impl->rasterizer);
// Re-apply per-game configuration and reload disk shader cache
u64 program_id{};
impl->system.GetAppLoader().ReadProgramId(program_id);
ApplyPerProgramSettings(program_id);
if (Settings::values.use_disk_shader_cache) {
impl->renderer->Rasterizer()->LoadDefaultDiskResources(false, nullptr);
}
// Mark ALL GPU registers as dirty so current state gets uploaded to new renderer
impl->pica.dirty_regs.SetAllDirty();
// Also mark shader setups as dirty so uniforms get re-uploaded and
// stale pointers to the old rasterizer's JIT cache are cleared.
impl->pica.vs_setup.uniforms_dirty = true;
impl->pica.vs_setup.cached_shader = nullptr;
impl->pica.gs_setup.uniforms_dirty = true;
impl->pica.gs_setup.cached_shader = nullptr;
// Mark all cached LUT/table state in pica as dirty
impl->pica.lighting.lut_dirty = impl->pica.lighting.LutAllDirty;
impl->pica.fog.lut_dirty = true;
impl->pica.proctex.table_dirty = impl->pica.proctex.TableAllDirty;
}
void GPU::ReleaseRenderer() {
// Just reset the renderer to release OpenGL resources
// Don't null out rasterizer pointer as it will become dangling
impl->renderer.reset();
impl->sw_blitter.reset();
LOG_INFO(HW_GPU, "Renderer released for context destroy");
}
template <class Archive>
void GPU::serialize(Archive& ar, const u32 file_version) {
ar & impl->pica;

View file

@ -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);

View file

@ -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());

View file

@ -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);
if (render_window.NeedsClearing()) {
glClear(GL_COLOR_BUFFER_BIT);
}
// Set projection matrix
std::array<GLfloat, 3 * 2> ortho_matrix =

View file

@ -22,7 +22,7 @@
#include <vk_mem_alloc.h>
#ifdef __APPLE__
#if defined(__APPLE__) && !defined(HAVE_LIBRETRO)
#include "common/apple_utils.h"
#endif
@ -60,11 +60,11 @@ constexpr static std::array<vk::DescriptorSetLayoutBinding, 1> PRESENT_BINDINGS
namespace {
static bool IsLowRefreshRate() {
#if (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO)
if (!Settings::values.use_display_refresh_rate_detection) {
LOG_INFO(Render_Vulkan, "Refresh rate detection is currently disabled via settings");
return false;
}
#if defined(__APPLE__) || defined(ENABLE_SDL2)
#ifdef __APPLE__
// Apple's low power mode sometimes limits applications to 30fps without changing the refresh
// rate, meaning the above code doesn't catch it.
@ -98,7 +98,7 @@ static bool IsLowRefreshRate() {
LOG_INFO(Render_Vulkan, "Refresh rate is above emulated 3DS screen: {}hz. Good.",
cur_refresh_rate);
}
#endif // defined(__APPLE__) || defined(ENABLE_SDL2)
#endif // (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO)
// We have no available method of checking refresh rate. Just assume that everything is fine :)
return false;

View file

@ -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;

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -464,7 +464,9 @@ bool Instance::CreateDevice() {
const bool has_custom_border_color =
add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, is_qualcomm,
"it is broken on most Qualcomm driver versions");
const bool has_index_type_uint8 = add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME);
const bool has_index_type_uint8 =
add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME, is_moltenvk,
"uint8 index conversion causes memory leaks in MoltenVK");
const bool has_fragment_shader_interlock =
add_extension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, is_nvidia,
"it is broken on Nvidia drivers");
@ -481,6 +483,9 @@ bool Instance::CreateDevice() {
return false;
}
#ifndef HAVE_LIBRETRO
// Find graphics queue family. LibRetro builds skip this since queue_family_index
// is already set by LibRetroVKInstance from the frontend-provided context.
bool graphics_queue_found = false;
for (std::size_t i = 0; i < family_properties.size(); i++) {
const u32 index = static_cast<u32>(i);
@ -494,6 +499,7 @@ bool Instance::CreateDevice() {
LOG_CRITICAL(Render_Vulkan, "Unable to find graphics and/or present queues.");
return false;
}
#endif
static constexpr std::array<f32, 1> queue_priorities = {1.0f};
@ -612,6 +618,10 @@ bool Instance::CreateDevice() {
#undef PROP_GET
#undef FEAT_SET
#ifdef HAVE_LIBRETRO
// LibRetro builds: device already created by frontend, just return after feature detection
return true;
#else
try {
device = physical_device.createDeviceUnique(device_chain.get());
} catch (vk::ExtensionNotPresentError& err) {
@ -626,6 +636,7 @@ bool Instance::CreateDevice() {
CreateAllocator();
return true;
#endif
}
void Instance::CreateAllocator() {
@ -636,9 +647,9 @@ void Instance::CreateAllocator() {
const VmaAllocatorCreateInfo allocator_info = {
.physicalDevice = physical_device,
.device = *device,
.device = GetDevice(),
.pVulkanFunctions = &functions,
.instance = *instance,
.instance = GetInstance(),
.vulkanApiVersion = TargetVulkanApiVersion,
};

View file

@ -38,9 +38,11 @@ struct FormatTraits {
class Instance {
public:
struct NoInit {};
explicit Instance(bool validation = false, bool dump_command_buffers = false);
explicit Instance(Frontend::EmuWindow& window, u32 physical_device_index);
~Instance();
explicit Instance(NoInit) {} // For LibRetro inheritance - does minimal setup
virtual ~Instance();
/// Returns the FormatTraits struct for the provided pixel format
const FormatTraits& GetTraits(VideoCore::PixelFormat pixel_format) const;
@ -58,7 +60,7 @@ public:
std::string GetDriverVersionName();
/// Returns the Vulkan instance
vk::Instance GetInstance() const {
virtual vk::Instance GetInstance() const {
return *instance;
}
@ -68,7 +70,7 @@ public:
}
/// Returns the Vulkan device
vk::Device GetDevice() const {
virtual vk::Device GetDevice() const {
return *device;
}
@ -254,6 +256,11 @@ public:
return features.shaderSampledImageArrayDynamicIndexing;
}
/// Returns true if layered rendering (array attachments) is supported
bool IsLayeredRenderingSupported() const {
return layered_rendering_supported;
}
/// Returns the minimum vertex stride alignment
u32 GetMinVertexStrideAlignment() const {
return min_vertex_stride_alignment;
@ -270,7 +277,7 @@ public:
driver_id == vk::DriverIdKHR::eQualcommProprietary;
}
private:
protected:
/// Returns the optimal supported usage for the requested format
[[nodiscard]] FormatTraits DetermineTraits(VideoCore::PixelFormat pixel_format,
vk::Format format);
@ -294,7 +301,7 @@ private:
// Collects logging gpu info
void CollectToolingInfo();
private:
protected:
std::shared_ptr<Common::DynamicLibrary> library;
vk::UniqueInstance instance;
vk::PhysicalDevice physical_device;
@ -328,6 +335,7 @@ 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{};

View file

@ -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!");

View file

@ -1,4 +1,4 @@
// Copyright 2019 yuzu Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -8,6 +8,9 @@
#include "common/thread.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#ifdef HAVE_LIBRETRO
#include "citra_libretro/libretro_vk.h"
#endif
MICROPROFILE_DEFINE(Vulkan_WaitForWorker, "Vulkan", "Wait for worker", MP_RGB(255, 192, 192));
MICROPROFILE_DEFINE(Vulkan_Submit, "Vulkan", "Submit Exectution", MP_RGB(255, 192, 255));
@ -17,11 +20,15 @@ namespace Vulkan {
namespace {
std::unique_ptr<MasterSemaphore> MakeMasterSemaphore(const Instance& instance) {
#ifdef HAVE_LIBRETRO
return CreateLibRetroMasterSemaphore(instance);
#else
if (instance.IsTimelineSemaphoreSupported()) {
return std::make_unique<MasterSemaphoreTimeline>(instance);
} else {
return std::make_unique<MasterSemaphoreFence>(instance);
}
#endif
}
} // Anonymous namespace

View file

@ -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,