libretro core: address review feedback

This commit is contained in:
Eric Warmenhoven 2026-02-04 23:37:31 -05:00
parent 6f5318369b
commit 2831ddf805
No known key found for this signature in database
33 changed files with 399 additions and 164 deletions

View File

@ -9,10 +9,11 @@ on:
workflow_dispatch:
env:
CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_SDL2=OFF -DENABLE_QT=OFF -DENABLE_TESTS=OFF -DENABLE_ROOM=OFF -DENABLE_WEB_SERVICE=OFF -DENABLE_SCRIPTING=OFF -DENABLE_CUBEB=OFF -DENABLE_OPENAL=OFF -DENABLE_LIBUSB=OFF -DCITRA_WARNINGS_AS_ERRORS=OFF
CORE_ARGS: -DENABLE_LIBRETRO=ON
jobs:
android:
if: github.event_name != 'pull_request'
runs-on: ubuntu-22.04
env:
OS: android
@ -68,6 +69,7 @@ jobs:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.so
windows:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
env:
OS: windows
@ -103,6 +105,7 @@ jobs:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dll
macos:
if: github.event_name != 'pull_request'
runs-on: macos-14
strategy:
matrix:
@ -150,6 +153,7 @@ jobs:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib
tvos:
if: github.event_name != 'pull_request'
runs-on: macos-14
env:
OS: tvos

View File

@ -3,7 +3,7 @@
JNI_PATH: .
CORENAME: azahar
API_LEVEL: 21
BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_SDL2=OFF -DENABLE_QT=OFF -DENABLE_TESTS=OFF -DENABLE_ROOM=OFF -DENABLE_WEB_SERVICE=OFF -DENABLE_SCRIPTING=OFF -DENABLE_CUBEB=OFF -DENABLE_OPENAL=OFF -DENABLE_LIBUSB=OFF -DCITRA_WARNINGS_AS_ERRORS=OFF
BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF
CORE_ARGS: ${BASE_CORE_ARGS}
EXTRA_PATH: bin/Release
@ -22,11 +22,11 @@ include:
- project: 'libretro-infrastructure/ci-templates'
file: '/linux-cmake.yml'
# MacOS 64-bit
# MacOS x86_64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-x86.yml'
# MacOS arm64
# MacOS ARM64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-arm64.yml'
@ -61,17 +61,9 @@ 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:
EXTRA_PATH: bin/Release
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja
before_script:
- export NUMPROC=$(($(nproc)/5))
- sudo apt-get update -qy
- sudo apt-get install -qy software-properties-common
- sudo add-apt-repository -y ppa:savoury1/build-tools
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test
- sudo apt-get update -qy
- sudo apt-get install -qy glslang-tools
# Linux 64-bit
libretro-build-linux-x64:
@ -82,7 +74,7 @@ libretro-build-linux-x64:
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DENABLE_OPT=OFF
# MacOS 64-bit
# MacOS x86_64
libretro-build-osx-x64:
tags:
- mac-apple-silicon
@ -93,7 +85,7 @@ libretro-build-osx-x64:
- .core-defs
- .libretro-osx-cmake-x86_64
# MacOS 64-bit
# MacOS ARM64
libretro-build-osx-arm64:
extends:
- .core-defs

2
.gitmodules vendored
View File

@ -104,5 +104,5 @@
path = externals/xxHash
url = https://github.com/Cyan4973/xxHash.git
[submodule "externals/libretro-common"]
path = externals/libretro-common
path = externals/libretro-common/libretro-common
url = https://github.com/libretro/libretro-common.git

View File

@ -1,10 +1,6 @@
# CMake >=3.12 required for 20 to be a valid value for CXX_STANDARD,
# and >=3.25 required to make LTO work on Android.
if(ANDROID)
cmake_minimum_required(VERSION 3.25)
else()
cmake_minimum_required(VERSION 3.23)
endif()
cmake_minimum_required(VERSION 3.25)
# Don't override the warning flags in MSVC:
cmake_policy(SET CMP0092 NEW)
@ -29,7 +25,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
enable_language(OBJC OBJCXX)
endif()
option(ENABLE_LIBRETRO "Enable the LibRetro frontend" OFF)
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.
@ -97,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_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)
@ -137,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)

View File

@ -295,21 +295,9 @@ endif()
# LibRetro
if (ENABLE_LIBRETRO)
add_library(libretro INTERFACE)
target_include_directories(libretro INTERFACE ./libretro-common/include)
target_include_directories(libretro INTERFACE ./libretro-common/libretro-common/include)
if (ANDROID)
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)
add_subdirectory(libretro-common EXCLUDE_FROM_ALL)
endif()
endif()

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
)

View File

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

View File

@ -2,41 +2,22 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <list>
#include <numeric>
#include <libretro.h>
#include "audio_core/libretro_sink.h"
#include "audio_types.h"
#include "common/settings.h"
namespace LibRetro {
static retro_audio_sample_batch_t audio_batch_cb;
}
#include "citra_libretro/environment.h"
namespace AudioCore {
struct LibRetroSink::Impl {
std::function<void(s16*, std::size_t)> cb;
};
LibRetroSink::LibRetroSink(std::string) {}
LibRetroSink::LibRetroSink(std::string target_device_name) : impl(std::make_unique<Impl>()) {}
LibRetroSink::~LibRetroSink() {}
LibRetroSink::~LibRetroSink() = default;
unsigned int LibRetroSink::GetNativeSampleRate() const {
return native_sample_rate; // We specify this.
return native_sample_rate;
}
void LibRetroSink::SetCallback(std::function<void(s16*, std::size_t)> cb) {
this->impl->cb = cb;
}
void LibRetroSink::OnAudioSubmission(std::size_t frames) {
std::vector<s16> buffer(frames * 2);
this->impl->cb(buffer.data(), buffer.size() / 2);
LibRetro::SubmitAudio(buffer.data(), buffer.size() / 2);
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() {
@ -44,11 +25,3 @@ std::vector<std::string> ListLibretroSinkDevices() {
}
} // namespace AudioCore
void LibRetro::SubmitAudio(const int16_t* data, size_t frames) {
LibRetro::audio_batch_cb(data, frames);
}
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) {
LibRetro::audio_batch_cb = cb;
}

View File

@ -5,15 +5,9 @@
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "audio_core/sink.h"
#include "libretro.h"
namespace LibRetro {
void SubmitAudio(const int16_t* data, size_t frames);
} // namespace LibRetro
namespace AudioCore {
@ -24,20 +18,14 @@ public:
unsigned int GetNativeSampleRate() const override;
void SetCallback(std::function<void(s16*, std::size_t)> cb) override;
// Not used for immediate submission sinks
void SetCallback(std::function<void(s16*, std::size_t)> cb) override {};
void OnAudioSubmission(std::size_t frames) override;
bool ImmediateSubmission() override { return true; }
struct Impl;
private:
std::unique_ptr<Impl> impl;
void PushSamples(const void* data, std::size_t num_samples) override;
};
void audio_callback();
void audio_set_state(bool new_state);
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 {
@ -31,8 +31,22 @@ public:
*/
virtual void SetCallback(std::function<void(s16*, std::size_t)> cb) = 0;
/// Optional callback to signify that a buffer has been written.
virtual void OnAudioSubmission(std::size_t frames) {}
/**
* 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,21 +1,34 @@
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$<CONFIG>)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules)
add_library(azahar_libretro SHARED
# 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
citra_libretro.cpp
citra_libretro.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)
@ -49,10 +62,12 @@ 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)
@ -64,9 +79,15 @@ if(MINGW)
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")

View File

@ -32,6 +32,10 @@
#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"
@ -39,7 +43,9 @@
#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"
@ -235,6 +241,8 @@ 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();
}
// Check if the screen swap button is pressed
@ -300,6 +308,68 @@ void retro_run() {
}
}
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)};
@ -331,7 +401,8 @@ static bool do_load_game() {
LibRetro::DisplayMessage("Failed to determine system mode!");
return false;
default:
LibRetro::DisplayMessage("Unknown error");
LibRetro::DisplayMessage(
("Unknown error: " + std::to_string(static_cast<int>(load_result))).c_str());
return false;
}
@ -344,6 +415,8 @@ static bool do_load_game() {
false, nullptr);
}
setup_memory_maps();
return true;
}
@ -405,10 +478,6 @@ static void context_reset() {
// 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);
if (Settings::values.use_disk_shader_cache) {
Core::System::GetInstance().GPU().Renderer().Rasterizer()->LoadDefaultDiskResources(
false, nullptr);
}
}
}
}
@ -426,10 +495,7 @@ static void context_destroy() {
void retro_reset() {
LOG_DEBUG(Frontend, "retro_reset");
Core::System::GetInstance().Shutdown();
if (Core::System::GetInstance().Load(*emu_instance->emu_window, LibRetro::settings.file_path) !=
Core::System::ResultStatus::Success) {
LOG_ERROR(Frontend, "Unable lo load on retro_reset");
}
emu_instance->game_loaded = do_load_game();
}
/**
@ -438,6 +504,14 @@ void retro_reset() {
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 build requires a CPU with SSE4.2 support.");
return false;
}
#endif
UpdateSettings();
// If using HW rendering, don't actually load the game here. azahar wants
@ -502,9 +576,15 @@ bool retro_load_game(const struct retro_game_info* info) {
break;
case Settings::GraphicsAPI::Software:
emu_instance->game_loaded = do_load_game();
return emu_instance->game_loaded;
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;
}
@ -522,14 +602,64 @@ bool retro_load_game_special(unsigned game_type, const struct retro_game_info* i
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 = Core::System::GetInstance().SaveStateBuffer();
return savestate.value().size();
savestate = system.SaveStateBuffer();
return savestate->size();
} catch (const std::exception& e) {
LOG_ERROR(Core, "Error saving savestate: {}", e.what());
LOG_ERROR(Frontend, "Error saving state: {}", e.what());
savestate.reset();
return 0;
}
@ -538,36 +668,38 @@ size_t retro_serialize_size() {
bool retro_serialize(void* data, size_t size) {
if (!savestate.has_value())
return false;
memcpy(data, (*savestate).data(), size);
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) {
try {
const std::vector<u8> buffer((const u8*)data, (const u8*)data + size);
auto& system = Core::System::GetInstance();
if (!system.IsPoweredOn())
return false;
return Core::System::GetInstance().LoadStateBuffer(buffer);
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(Core, "Error loading savestate: {}", e.what());
LOG_ERROR(Frontend, "Error loading state: {}", e.what());
return false;
}
}
void* retro_get_memory_data(unsigned id) {
if (id == RETRO_MEMORY_SYSTEM_RAM)
return Core::System::GetInstance().Memory().GetFCRAMPointer(
Core::System::GetInstance().Kernel().memory_regions[0]->base);
// 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) {
if (id == RETRO_MEMORY_SYSTEM_RAM)
return Core::System::GetInstance().Kernel().memory_regions[0]->size;
return 0;
}

View File

@ -994,7 +994,7 @@ static void ParseInputOptions(void) {
void ParseCoreOptions(void) {
// Override default values that aren't user-selectable and aren't correct for the core
Settings::values.enable_audio_stretching = false;
Settings::values.frame_limit = 10000;
Settings::values.frame_limit = 0;
#if defined(USING_GLES)
Settings::values.use_gles = true;
#else

View File

@ -13,7 +13,7 @@ enum CStickFunction { Both, CStick, Touchscreen };
struct CoreSettings {
::std::string file_path;
std::string file_path;
float deadzone = 1.f;

View File

@ -65,6 +65,8 @@ EmuWindow_LibRetro::EmuWindow_LibRetro() {
EmuWindow_LibRetro::~EmuWindow_LibRetro() {}
void EmuWindow_LibRetro::SwapBuffers() {
if (suppressPresentation)
return;
submittedFrame = true;
switch (Settings::values.graphics_api.GetValue()) {
@ -77,7 +79,6 @@ void EmuWindow_LibRetro::SwapBuffers() {
}
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
ResetGLState();
current_state.Apply();
#endif
break;

View File

@ -45,6 +45,9 @@ public:
/// 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;

View File

@ -20,7 +20,7 @@ namespace LibRetro {
namespace {
static retro_video_refresh_t video_cb;
// static retro_audio_sample_t audio_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;
@ -100,6 +100,10 @@ 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);
}
@ -148,13 +152,16 @@ bool Shutdown() {
/// Displays the specified message to the screen.
bool DisplayMessage(const char* sg) {
LOG_CRITICAL(Frontend, "{}", 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();
@ -217,15 +224,23 @@ bool CanUseJIT() {
void retro_get_system_info(struct retro_system_info* info) {
memset(info, 0, sizeof(*info));
info->library_name = "Azahar";
info->library_version = Common::g_scm_desc;
info->library_version = Common::g_build_fullname;
info->need_fullpath = true;
info->valid_extensions = "3ds|3dsx|cia|elf";
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;
}

View File

@ -5,7 +5,6 @@
#pragma once
#include <cstdint>
#include "citra_libretro.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
@ -64,6 +63,9 @@ 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);
@ -110,6 +112,9 @@ 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

View File

@ -2,6 +2,7 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cmath>
#include <memory>
@ -121,8 +122,8 @@ void MouseTracker::OnMouseMove(int deltaX, int deltaY) {
}
void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) {
x = std::min(std::max(minX, x), maxX);
y = std::min(std::max(minY, y), maxY);
x = std::clamp(x, minX, maxX);
y = std::clamp(y, minY, maxY);
}
void MouseTracker::Update(int bufferWidth, int bufferHeight,
@ -146,11 +147,11 @@ void MouseTracker::Update(int bufferWidth, int bufferHeight,
// Use layout system to validate and map coordinates
if (IsWithinTouchscreen(layout, newX, newY)) {
x = std::max(static_cast<int>(layout.bottom_screen.left),
std::min(newX, static_cast<int>(layout.bottom_screen.right))) -
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
static_cast<int>(layout.bottom_screen.right)) -
layout.bottom_screen.left;
y = std::max(static_cast<int>(layout.bottom_screen.top),
std::min(newY, static_cast<int>(layout.bottom_screen.bottom))) -
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
static_cast<int>(layout.bottom_screen.bottom)) -
layout.bottom_screen.top;
}
}
@ -173,11 +174,11 @@ void MouseTracker::Update(int bufferWidth, int bufferHeight,
// Use layout system to validate and map coordinates
if (IsWithinTouchscreen(layout, newX, newY)) {
x = std::max(static_cast<int>(layout.bottom_screen.left),
std::min(newX, static_cast<int>(layout.bottom_screen.right))) -
x = std::clamp(newX, static_cast<int>(layout.bottom_screen.left),
static_cast<int>(layout.bottom_screen.right)) -
layout.bottom_screen.left;
y = std::max(static_cast<int>(layout.bottom_screen.top),
std::min(newY, static_cast<int>(layout.bottom_screen.bottom))) -
y = std::clamp(newY, static_cast<int>(layout.bottom_screen.top),
static_cast<int>(layout.bottom_screen.bottom)) -
layout.bottom_screen.top;
}
}

View File

@ -274,7 +274,7 @@ LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window,
VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device});
// Now run device capability detection with dispatcher initialized
CreateDevice(true);
CreateDevice();
// LibRetro-specific: Validate function pointers are actually available
// LibRetro's device may not have loaded all extension functions even if extensions are

View File

@ -2,6 +2,10 @@
// 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.
#include <cstddef>
#ifdef _WIN32
#include <windows.h>

View File

@ -491,8 +491,8 @@ private:
lambda(static_cast<Backend&>(file_backend));
#ifdef ANDROID
lambda(static_cast<Backend&>(lc_backend));
#endif
#endif
#endif // ANDROID
#endif // HAVE_LIBRETRO
}
static void Deleter(Impl* ptr) {

View File

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

View File

@ -2308,7 +2308,9 @@ std::optional<SOC_U::InterfaceInfo> SOC_U::GetDefaultInterfaceInfo() {
break;
}
}
#elif !defined(HAVE_LIBRETRO)
#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

@ -217,7 +217,6 @@ void System::LoadState(u32 slot) {
ia&* this;
}
#ifdef HAVE_LIBRETRO
std::vector<u8> System::SaveStateBuffer() const {
std::ostringstream sstream{std::ios_base::binary};
// Serialize
@ -290,6 +289,5 @@ bool System::LoadStateBuffer(std::vector<u8> buffer) {
return true;
}
#endif
} // 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

@ -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"
@ -436,10 +437,25 @@ void GPU::RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow*
// Update the sw_blitter with the new rasterizer
impl->sw_blitter = std::make_unique<SwRenderer::SwBlitter>(impl->memory, impl->rasterizer);
// Mark ALL GPU registers as dirty so current state gets uploaded to new renderer
impl->pica.dirty_regs.qwords.fill(~0ULL);
// 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);
}
// Also mark all cached state in pica as dirty
// 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;

View File

@ -458,6 +458,8 @@ FileUtil::IOFile ShaderDiskCache::AppendTransferableFile() {
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);
}
@ -486,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);

View File

@ -159,7 +159,7 @@ Instance::Instance(Frontend::EmuWindow& window, u32 physical_device_index)
VK_VERSION_MAJOR(properties.apiVersion), VK_VERSION_MINOR(properties.apiVersion)));
}
CreateDevice(false);
CreateDevice();
CreateFormatTable();
CollectToolingInfo();
CreateCustomFormatTable();
@ -394,7 +394,7 @@ void Instance::CreateAttribTable() {
}
}
bool Instance::CreateDevice(bool libretro) {
bool Instance::CreateDevice() {
const vk::StructureChain feature_chain = physical_device.getFeatures2<
vk::PhysicalDeviceFeatures2, vk::PhysicalDevicePortabilitySubsetFeaturesKHR,
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT,
@ -483,8 +483,11 @@ bool Instance::CreateDevice(bool libretro) {
return false;
}
bool graphics_queue_found = libretro;
for (std::size_t i = 0; !libretro && i < family_properties.size(); i++) {
#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);
if (family_properties[i].queueFlags & vk::QueueFlagBits::eGraphics) {
queue_family_index = index;
@ -496,6 +499,7 @@ bool Instance::CreateDevice(bool libretro) {
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};
@ -614,10 +618,10 @@ bool Instance::CreateDevice(bool libretro) {
#undef PROP_GET
#undef FEAT_SET
if (libretro) {
return true;
}
#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) {
@ -632,6 +636,7 @@ bool Instance::CreateDevice(bool libretro) {
CreateAllocator();
return true;
#endif
}
void Instance::CreateAllocator() {

View File

@ -287,7 +287,7 @@ protected:
void CreateAttribTable();
/// Creates the logical device opportunistically enabling extensions
bool CreateDevice(bool libretro);
bool CreateDevice();
/// Creates the VMA allocator handle
void CreateAllocator();

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,6 +163,8 @@ void DescriptorHeap::Allocate(std::size_t begin, std::size_t end) {
if (result == vk::Result::eSuccess) {
break;
}
// 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++;

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.