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