// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include "common/detached_tasks.h" #include "common/fs/path_util.h" #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/microprofile.h" #include "common/scm_rev.h" #include "common/scope_exit.h" #include "common/settings.h" #include "core/core.h" #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/vfs_real.h" #include "core/hid/hid_core.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" #include "core/perf_stats.h" #include "jni/config.h" #include "jni/emu_window/emu_window.h" #include "jni/id_cache.h" #include "video_core/rasterizer_interface.h" namespace { class EmulationSession final { public: EmulationSession() { m_system.Initialize(); m_vfs = std::make_shared(); } ~EmulationSession() = default; static EmulationSession& GetInstance() { return s_instance; } const Core::System& System() const { return m_system; } Core::System& System() { return m_system; } const EmuWindow_Android& Window() const { return *m_window; } EmuWindow_Android& Window() { return *m_window; } ANativeWindow* NativeWindow() const { return m_native_window; } void SetNativeWindow(ANativeWindow* m_native_window_) { m_native_window = m_native_window_; } bool IsRunning() const { std::scoped_lock lock(m_mutex); return m_is_running; } const Core::PerfStatsResults& PerfStats() const { std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); return m_perf_stats; } void SurfaceChanged() { if (!IsRunning()) { return; } m_window->OnSurfaceChanged(m_native_window); } Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { std::scoped_lock lock(m_mutex); // Loads the configuration. Config{}; // Create the render window. m_window = std::make_unique(&m_input_subsystem, m_native_window); // Initialize system. m_system.SetShuttingDown(false); m_system.Initialize(); m_system.ApplySettings(); m_system.HIDCore().ReloadInputDevices(); m_system.SetContentProvider(std::make_unique()); m_system.SetFilesystem(std::make_shared()); m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem()); // Load the ROM. m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath); if (m_load_result != Core::SystemResultStatus::Success) { return m_load_result; } // Complete initialization. m_system.GPU().Start(); m_system.GetCpuManager().OnGpuReady(); m_system.RegisterExitCallback([&] { HaltEmulation(); }); return Core::SystemResultStatus::Success; } void ShutdownEmulation() { std::scoped_lock lock(m_mutex); m_is_running = false; // Unload user input. m_system.HIDCore().UnloadInputDevices(); // Shutdown the main emulated process if (m_load_result == Core::SystemResultStatus::Success) { m_system.DetachDebugger(); m_system.ShutdownMainProcess(); m_detached_tasks.WaitForAllTasks(); m_load_result = Core::SystemResultStatus::ErrorNotInitialized; } // Tear down the render window. m_window.reset(); } void PauseEmulation() { std::scoped_lock lock(m_mutex); m_system.Pause(); } void UnPauseEmulation() { std::scoped_lock lock(m_mutex); m_system.Run(); } void HaltEmulation() { std::scoped_lock lock(m_mutex); m_is_running = false; m_cv.notify_one(); } void RunEmulation() { { std::scoped_lock lock(m_mutex); m_is_running = true; } void(m_system.Run()); if (m_system.DebuggerEnabled()) { m_system.InitializeDebugger(); } while (true) { { std::unique_lock lock(m_mutex); if (m_cv.wait_for(lock, std::chrono::milliseconds(100), [&]() { return !m_is_running; })) { // Emulation halted. break; } } { // Refresh performance stats. std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); m_perf_stats = m_system.GetAndResetPerfStats(); } } } std::string GetRomTitle(const std::string& path) { return GetRomMetadata(path).title; } std::vector GetRomIcon(const std::string& path) { return GetRomMetadata(path).icon; } void ResetRomMetadata() { m_rom_metadata_cache.clear(); } private: struct RomMetadata { std::string title; std::vector icon; }; RomMetadata GetRomMetadata(const std::string& path) { if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { return search->second; } return CacheRomMetadata(path); } RomMetadata CacheRomMetadata(const std::string& path) { const auto file = Core::GetGameFileFromPath(m_vfs, path); const auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0); RomMetadata entry; loader->ReadTitle(entry.title); loader->ReadIcon(entry.icon); m_rom_metadata_cache[path] = entry; return entry; } private: static EmulationSession s_instance; // Frontend management std::unordered_map m_rom_metadata_cache; // Window management std::unique_ptr m_window; ANativeWindow* m_native_window{}; // Core emulation Core::System m_system; InputCommon::InputSubsystem m_input_subsystem; Common::DetachedTasks m_detached_tasks; Core::PerfStatsResults m_perf_stats{}; std::shared_ptr m_vfs; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; bool m_is_running{}; // Synchronization std::condition_variable_any m_cv; mutable std::mutex m_perf_stats_mutex; mutable std::mutex m_mutex; }; /*static*/ EmulationSession EmulationSession::s_instance; std::string UTF16ToUTF8(std::u16string_view input) { std::wstring_convert, char16_t> convert; return convert.to_bytes(input.data(), input.data() + input.size()); } std::string GetJString(JNIEnv* env, jstring jstr) { if (!jstr) { return {}; } const jchar* jchars = env->GetStringChars(jstr, nullptr); const jsize length = env->GetStringLength(jstr); const std::u16string_view string_view(reinterpret_cast(jchars), length); const std::string converted_string = UTF16ToUTF8(string_view); env->ReleaseStringChars(jstr, jchars); return converted_string; } } // Anonymous namespace static Core::SystemResultStatus RunEmulation(const std::string& filepath) { Common::Log::Initialize(); Common::Log::SetColorConsoleBackendEnabled(true); Common::Log::Start(); MicroProfileOnThreadCreate("EmuThread"); SCOPE_EXIT({ MicroProfileShutdown(); }); LOG_INFO(Frontend, "starting"); if (filepath.empty()) { LOG_CRITICAL(Frontend, "failed to load: filepath empty!"); return Core::SystemResultStatus::ErrorLoader; } SCOPE_EXIT({ EmulationSession::GetInstance().ShutdownEmulation(); }); const auto result = EmulationSession::GetInstance().InitializeEmulation(filepath); if (result != Core::SystemResultStatus::Success) { return result; } EmulationSession::GetInstance().RunEmulation(); return Core::SystemResultStatus::Success; } extern "C" { void Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject surf) { EmulationSession::GetInstance().SetNativeWindow(ANativeWindow_fromSurface(env, surf)); EmulationSession::GetInstance().SurfaceChanged(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, [[maybe_unused]] jclass clazz) { ANativeWindow_release(EmulationSession::GetInstance().NativeWindow()); EmulationSession::GetInstance().SetNativeWindow(nullptr); EmulationSession::GetInstance().SurfaceChanged(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_DoFrame(JNIEnv* env, [[maybe_unused]] jclass clazz) {} void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env, [[maybe_unused]] jclass clazz, jint layout_option, jint rotation) {} void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_directory) { Common::FS::SetAppDirectory(GetJString(env, j_directory)); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env, [[maybe_unused]] jclass clazz) { Core::Crypto::KeyManager::Instance().ReloadKeys(); return static_cast(Core::Crypto::KeyManager::Instance().AreKeysLoaded()); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { EmulationSession::GetInstance().UnPauseEmulation(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { EmulationSession::GetInstance().PauseEmulation(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { EmulationSession::GetInstance().HaltEmulation(); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_ResetRomMetadata([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { EmulationSession::GetInstance().ResetRomMetadata(); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return static_cast(EmulationSession::GetInstance().IsRunning()); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jint j_device, jint j_button, jint action) { if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device,j_button, action != 0); } return static_cast(true); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device, jint stick_id, jfloat x, jfloat y) { if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device,stick_id, x, y); } return static_cast(true); } jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint j_device,jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y, jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z){ if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnGamepadMotionEvent(j_device,delta_timestamp, gyro_x, gyro_y,gyro_z,accel_x,accel_y,accel_z); } return static_cast(true); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint id, jfloat x, jfloat y) { if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y); } } void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint id, jfloat x, jfloat y) { if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y); } } void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint id) { if (EmulationSession::GetInstance().IsRunning()) { EmulationSession::GetInstance().Window().OnTouchReleased(id); } } jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { auto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename)); jbyteArray icon = env->NewByteArray(static_cast(icon_data.size())); env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon), reinterpret_cast(icon_data.data())); return icon; } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { auto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename)); return env->NewStringUTF(title.c_str()); } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_filename) { return j_filename; } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_filename) { return j_filename; } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { return env->NewStringUTF(""); } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_filename) { return env->NewStringUTF(""); } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return {}; } void Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile [[maybe_unused]] (JNIEnv* env, [[maybe_unused]] jclass clazz) { Config{}; } jint Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { return {}; } void Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, [[maybe_unused]] jstring j_file, [[maybe_unused]] jstring j_savestate, [[maybe_unused]] jboolean j_delete_savestate) {} void Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { Config{}; } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id, jstring j_section, jstring j_key) { std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); std::string_view section = env->GetStringUTFChars(j_section, 0); std::string_view key = env->GetStringUTFChars(j_key, 0); env->ReleaseStringUTFChars(j_game_id, game_id.data()); env->ReleaseStringUTFChars(j_section, section.data()); env->ReleaseStringUTFChars(j_key, key.data()); return env->NewStringUTF(""); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id, jstring j_section, jstring j_key, jstring j_value) { std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); std::string_view section = env->GetStringUTFChars(j_section, 0); std::string_view key = env->GetStringUTFChars(j_key, 0); std::string_view value = env->GetStringUTFChars(j_value, 0); env->ReleaseStringUTFChars(j_game_id, game_id.data()); env->ReleaseStringUTFChars(j_section, section.data()); env->ReleaseStringUTFChars(j_key, key.data()); env->ReleaseStringUTFChars(j_value, value.data()); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_game_id) { std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); env->ReleaseStringUTFChars(j_game_id, game_id.data()); } jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { jdoubleArray j_stats = env->NewDoubleArray(4); if (EmulationSession::GetInstance().IsRunning()) { const auto results = EmulationSession::GetInstance().PerfStats(); // Converting the structure into an array makes it easier to pass it to the frontend double stats[4] = {results.system_fps, results.average_game_fps, results.frametime, results.emulation_speed}; env->SetDoubleArrayRegion(j_stats, 0, 4, stats); } return j_stats; } void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory( [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) {} void Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) { const std::string path = GetJString(env, j_path); const Core::SystemResultStatus result{RunEmulation(path)}; if (result != Core::SystemResultStatus::Success) { env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetExitEmulationActivity(), static_cast(result)); } } void Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) { LOG_INFO(Frontend, "yuzu Version: {}-{}", Common::g_scm_branch, Common::g_scm_desc); LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); } } // extern "C"