diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 40dbc94c9..728dd1f41 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -125,6 +125,10 @@ object NativeLibrary { external fun surfaceDestroyed() external fun doFrame() + //Second window + external fun secondarySurfaceChanged(secondary_surface: Surface) + external fun secondarySurfaceDestroyed() + external fun disableSecondaryScreen() /** * Unpauses emulation from a paused state. */ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 8d9bf24e3..789ed2acb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -4,14 +4,18 @@ package org.citra.citra_emu.activities +import SecondScreenPresentation import android.Manifest.permission import android.annotation.SuppressLint -import android.app.Activity +import android.app.Presentation +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager +import android.hardware.display.DisplayManager import android.net.Uri import android.os.Bundle +import android.view.Display import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent @@ -32,6 +36,7 @@ import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult import org.citra.citra_emu.contracts.OpenFileResultContract import org.citra.citra_emu.databinding.ActivityEmulationBinding import org.citra.citra_emu.display.ScreenAdjustmentUtil +import org.citra.citra_emu.display.SecondaryScreenLayout import org.citra.citra_emu.features.hotkeys.HotkeyUtility import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting @@ -56,6 +61,34 @@ class EmulationActivity : AppCompatActivity() { private lateinit var binding: ActivityEmulationBinding private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var hotkeyUtility: HotkeyUtility + private var secondScreenPresentation: Presentation? = null + private lateinit var displayManager: DisplayManager + + private fun updatePresentation() { + displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val display = getCustomerDisplay(); + if (secondScreenPresentation != null && (IntSetting.SECONDARY_SCREEN_LAYOUT.int == SecondaryScreenLayout.NONE.int || display == null)) { + releasePresentation(); + } + if (secondScreenPresentation == null || secondScreenPresentation?.display != display) { + secondScreenPresentation?.dismiss() + if (display != null && IntSetting.SECONDARY_SCREEN_LAYOUT.int != SecondaryScreenLayout.NONE.int) { + secondScreenPresentation = SecondScreenPresentation(this, display) + secondScreenPresentation?.show(); + } + } + } + private fun releasePresentation() { + if (secondScreenPresentation != null) { + NativeLibrary.disableSecondaryScreen() + secondScreenPresentation?.dismiss(); + secondScreenPresentation = null; + } + } + + private fun getCustomerDisplay(): Display? { + return displayManager?.displays?.firstOrNull { it.isValid && it.displayId != Display.DEFAULT_DISPLAY } + } private val emulationFragment: EmulationFragment get() { @@ -68,10 +101,9 @@ class EmulationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ThemeUtil.setTheme(this) - settingsViewModel.settings.loadSettings() - super.onCreate(savedInstanceState) + updatePresentation(); binding = ActivityEmulationBinding.inflate(layoutInflater) screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings) @@ -117,6 +149,11 @@ class EmulationActivity : AppCompatActivity() { applyOrientationSettings() // Check for orientation settings changes on runtime } + override fun onStop() { + releasePresentation() + super.onStop() + } + override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) enableFullscreenImmersive() @@ -124,6 +161,7 @@ class EmulationActivity : AppCompatActivity() { public override fun onRestart() { super.onRestart() + updatePresentation() NativeLibrary.reloadCameraDevices() } @@ -141,6 +179,7 @@ class EmulationActivity : AppCompatActivity() { EmulationLifecycleUtil.clear() isEmulationRunning = false instance = null + releasePresentation() super.onDestroy() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt index 2ec4716b7..17ea3c0be 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt @@ -48,4 +48,18 @@ enum class PortraitScreenLayout(val int: Int) { return entries.firstOrNull { it.int == int } ?: TOP_FULL_WIDTH } } +} + +enum class SecondaryScreenLayout(val int: Int) { + // These must match what is defined in src/common/settings.h + NONE(0), + TOP_SCREEN(1), + BOTTOM_SCREEN(2), + SIDE_BY_SIDE(3); + + companion object { + fun from(int: Int): SecondaryScreenLayout { + return entries.firstOrNull { it.int == int } ?: NONE + } + } } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondScreenPresentation.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondScreenPresentation.kt new file mode 100644 index 000000000..ba353787f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondScreenPresentation.kt @@ -0,0 +1,48 @@ +import android.app.Presentation +import android.content.Context +import android.os.Bundle +import android.view.Display +import android.view.SurfaceHolder +import android.view.SurfaceView +import org.citra.citra_emu.NativeLibrary + + + +class SecondScreenPresentation( + context: Context, + display: Display, +) : Presentation(context, display) { + private lateinit var surfaceView: SurfaceView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Initialize SurfaceView + surfaceView = SurfaceView(context) + surfaceView.holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + + } + + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + NativeLibrary.secondarySurfaceChanged(holder.surface) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + NativeLibrary.secondarySurfaceDestroyed() + } + }) + + setContentView(surfaceView) // Set SurfaceView as content + } + + // Publicly accessible method to get the SurfaceHolder + fun getSurfaceHolder(): SurfaceHolder { + return surfaceView.holder + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index 0f1ca8d43..22d6eb934 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -34,6 +34,7 @@ enum class IntSetting( LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640), LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480), PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0), + SECONDARY_SCREEN_LAYOUT("secondary_screen_layout",Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 31401567f..809ee5692 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -962,6 +962,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue ) ) + add( + SingleChoiceSetting( + IntSetting.SECONDARY_SCREEN_LAYOUT, + R.string.emulation_switch_secondary_layout, + 0, + R.array.secondaryLayouts, + R.array.secondaryLayoutValues, + IntSetting.SECONDARY_SCREEN_LAYOUT.key, + IntSetting.SECONDARY_SCREEN_LAYOUT.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.SMALL_SCREEN_POSITION, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index dec3e4e0a..c0d4907ce 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -272,6 +272,9 @@ object SettingsFile { val settings = section.settings val keySet: Set = settings.keys for (key in keySet) { + if (key.equals("secondary_screen_layout")) { + println("hi"); + } val setting = settings[key] parser.put(header, setting!!.key, setting.valueAsString) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index e91876584..f5e52b265 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -5,9 +5,12 @@ package org.citra.citra_emu.fragments import android.annotation.SuppressLint +import android.app.Presentation import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences +import android.hardware.display.DisplayManager +import android.media.MediaRouter import android.net.Uri import android.os.Bundle import android.os.Handler @@ -16,6 +19,7 @@ import android.os.SystemClock import android.text.Editable import android.text.TextWatcher import android.view.Choreographer +import android.view.Display import android.view.LayoutInflater import android.view.MotionEvent import android.view.Surface @@ -26,6 +30,7 @@ import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.Insets import androidx.core.view.ViewCompat @@ -79,6 +84,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private lateinit var emulationState: EmulationState private var perfStatsUpdater: Runnable? = null + private lateinit var emulationActivity: EmulationActivity private var _binding: FragmentEmulationBinding? = null @@ -146,6 +152,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram retainInstance = true emulationState = EmulationState(game.path) emulationActivity = requireActivity() as EmulationActivity + screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings) EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() }) EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() }) @@ -1214,6 +1221,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private class EmulationState(private val gamePath: String) { private var state: State private var surface: Surface? = null + private var surface2: Surface? = null init { // Starting state is stopped. diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 19e422324..30e8fbb23 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -204,6 +204,10 @@ void Config::ReadValues() { static_cast(sdl2_config->GetInteger( "Layout", "portrait_layout_option", static_cast(Settings::PortraitLayoutOption::PortraitTopFullWidth))); + Settings::values.secondary_screen_layout = + static_cast(sdl2_config->GetInteger( + "Layout", "secondary_screen_layout", + static_cast(Settings::SecondaryScreenLayout::None))); ReadSetting("Layout", Settings::values.custom_portrait_top_x); ReadSetting("Layout", Settings::values.custom_portrait_top_y); ReadSetting("Layout", Settings::values.custom_portrait_top_width); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5c802856e..cf9925746 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -251,6 +251,15 @@ custom_portrait_bottom_height = # 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent swap_screen = +# Secondary Screen Layout +# What the game should do if a secondary screen is connected physically or using +# Miracast / Chromecast screen mirroring +# 0 (default) - Use System Default (mirror) +# 1 - Show Top Screen Only +# 2 - Show Bottom Screen Only +# 3 - Show both screens side by side +secondary_screen_layout = + # Screen placement settings when using Cardboard VR (render3d = 4) # 30 - 100: Screen size as a percentage of the viewport. 85 (default) cardboard_screen_size = diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index b33c734f5..1d7220782 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -49,16 +49,17 @@ void EmuWindow_Android::OnFramebufferSizeChanged() { const int bigger{window_width > window_height ? window_width : window_height}; const int smaller{window_width < window_height ? window_width : window_height}; - if (is_portrait_mode) { + if (is_portrait_mode && !is_secondary) { UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode); } else { UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode); } } -EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface) : host_window{surface} { +EmuWindow_Android::EmuWindow_Android(ANativeWindow *surface, bool is_secondary) : EmuWindow{ + is_secondary}, host_window(surface) { LOG_DEBUG(Frontend, "Initializing EmuWindow_Android"); - + if (is_secondary) LOG_DEBUG(Frontend, "Initializing secondary window Android"); if (!surface) { LOG_CRITICAL(Frontend, "surface is nullptr"); return; diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index 4266fd1bb..41f7633e6 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include "core/frontend/emu_window.h" namespace Core { @@ -13,7 +14,7 @@ class System; class EmuWindow_Android : public Frontend::EmuWindow { public: - EmuWindow_Android(ANativeWindow* surface); + EmuWindow_Android(ANativeWindow* surface, bool is_secondary = false); ~EmuWindow_Android(); /// Called by the onSurfaceChanges() method to change the surface @@ -30,7 +31,10 @@ public: void DoneCurrent() override; virtual void TryPresenting() {} - + // EGL Context must be shared + // could probably use the existing + // SharedContext for this instead, this is maybe temporary + virtual EGLContext* GetEGLContext() {return NULL;} virtual void StopPresenting() {} protected: diff --git a/src/android/app/src/main/jni/emu_window/emu_window_gl.cpp b/src/android/app/src/main/jni/emu_window/emu_window_gl.cpp index 25db55bbe..4037fff54 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window_gl.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window_gl.cpp @@ -72,8 +72,8 @@ private: EGLContext egl_context{}; }; -EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface) - : EmuWindow_Android{surface}, system{system_} { +EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface, bool is_secondary, EGLContext* sharedContext) + : EmuWindow_Android{surface,is_secondary}, system{system_} { if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) { LOG_CRITICAL(Frontend, "eglGetDisplay() failed"); return; @@ -96,8 +96,9 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ if (eglQuerySurface(egl_display, egl_surface, EGL_HEIGHT, &window_height) != EGL_TRUE) { return; } - - if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data()); + if (sharedContext) { + egl_context = *sharedContext; + }else if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data()); egl_context == EGL_NO_CONTEXT) { LOG_CRITICAL(Frontend, "eglCreateContext() failed"); return; @@ -127,6 +128,10 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ OnFramebufferSizeChanged(); } +EGLContext* EmuWindow_Android_OpenGL::GetEGLContext() { + return &egl_context; +} + bool EmuWindow_Android_OpenGL::CreateWindowSurface() { if (!host_window) { return true; @@ -204,14 +209,15 @@ void EmuWindow_Android_OpenGL::TryPresenting() { return; } if (presenting_state == PresentingState::Initial) [[unlikely]] { - eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); presenting_state = PresentingState::Running; } - if (presenting_state != PresentingState::Running) [[unlikely]] { - return; - } +// if (presenting_state != PresentingState::Running) [[unlikely]] { +// return; +// } + eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0); - system.GPU().Renderer().TryPresent(0); + system.GPU().Renderer().TryPresent(100,is_secondary); eglSwapBuffers(egl_display, egl_surface); + } diff --git a/src/android/app/src/main/jni/emu_window/emu_window_gl.h b/src/android/app/src/main/jni/emu_window/emu_window_gl.h index a705174ac..c3f668483 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window_gl.h +++ b/src/android/app/src/main/jni/emu_window/emu_window_gl.h @@ -19,13 +19,13 @@ struct ANativeWindow; class EmuWindow_Android_OpenGL : public EmuWindow_Android { public: - EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface); + EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface, bool is_secondary, EGLContext* sharedContext = NULL); ~EmuWindow_Android_OpenGL() override = default; void TryPresenting() override; void StopPresenting() override; void PollEvents() override; - + EGLContext* GetEGLContext() override; std::unique_ptr CreateSharedContext() const override; private: diff --git a/src/android/app/src/main/jni/emu_window/emu_window_vk.cpp b/src/android/app/src/main/jni/emu_window/emu_window_vk.cpp index 238e1ae1a..f34957660 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window_vk.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window_vk.cpp @@ -24,8 +24,8 @@ private: }; EmuWindow_Android_Vulkan::EmuWindow_Android_Vulkan( - ANativeWindow* surface, std::shared_ptr driver_library_) - : EmuWindow_Android{surface}, driver_library{driver_library_} { + ANativeWindow* surface, std::shared_ptr driver_library_, bool is_secondary) + : EmuWindow_Android{surface,is_secondary}, driver_library{driver_library_} { CreateWindowSurface(); if (core_context = CreateSharedContext(); !core_context) { diff --git a/src/android/app/src/main/jni/emu_window/emu_window_vk.h b/src/android/app/src/main/jni/emu_window/emu_window_vk.h index fe54f9a36..60e2435a7 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window_vk.h +++ b/src/android/app/src/main/jni/emu_window/emu_window_vk.h @@ -11,7 +11,7 @@ struct ANativeWindow; class EmuWindow_Android_Vulkan : public EmuWindow_Android { public: EmuWindow_Android_Vulkan(ANativeWindow* surface, - std::shared_ptr driver_library); + std::shared_ptr driver_library, bool is_secondary); ~EmuWindow_Android_Vulkan() override = default; void PollEvents() override {} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index d4263c18a..99397938c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -16,11 +16,15 @@ #include #include "audio_core/dsp_interface.h" #include "common/arch.h" + #if CITRA_ARCH(arm64) + #include "common/aarch64/cpu_detect.h" + #elif CITRA_ARCH(x86_64) #include "common/x64/cpu_detect.h" #endif + #include "common/common_paths.h" #include "common/dynamic_library/dynamic_library.h" #include "common/file_util.h" @@ -44,12 +48,18 @@ #include "jni/camera/ndk_camera.h" #include "jni/camera/still_image_camera.h" #include "jni/config.h" + #ifdef ENABLE_OPENGL + #include "jni/emu_window/emu_window_gl.h" + #endif #ifdef ENABLE_VULKAN + #include "jni/emu_window/emu_window_vk.h" + #endif + #include "jni/id_cache.h" #include "jni/input_manager.h" #include "jni/ndk_motion.h" @@ -59,58 +69,63 @@ #include "video_core/renderer_base.h" #if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64) + #include + #endif namespace { -ANativeWindow* s_surf; + ANativeWindow *s_surf; + ANativeWindow *s_secondary_surface; + bool secondary_enabled = false; -std::shared_ptr vulkan_library{}; -std::unique_ptr window; + std::shared_ptr vulkan_library{}; + std::unique_ptr window; + std::unique_ptr second_window; -std::atomic stop_run{true}; -std::atomic pause_emulation{false}; + std::atomic stop_run{true}; + std::atomic pause_emulation{false}; -std::mutex paused_mutex; -std::mutex running_mutex; -std::condition_variable running_cv; + std::mutex paused_mutex; + std::mutex running_mutex; + std::condition_variable running_cv; } // Anonymous namespace static jobject ToJavaCoreError(Core::System::ResultStatus result) { - static const std::map CoreErrorNameMap{ - {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, - {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, - {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"}, - {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, + static const std::map CoreErrorNameMap{ + {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, + {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, + {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"}, + {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, }; const auto name = CoreErrorNameMap.count(result) ? CoreErrorNameMap.at(result) : "ErrorUnknown"; - JNIEnv* env = IDCache::GetEnvForThread(); + JNIEnv *env = IDCache::GetEnvForThread(); const jclass core_error_class = IDCache::GetCoreErrorClass(); return env->GetStaticObjectField( - core_error_class, env->GetStaticFieldID(core_error_class, name, - "Lorg/citra/citra_emu/NativeLibrary$CoreError;")); + core_error_class, env->GetStaticFieldID(core_error_class, name, + "Lorg/citra/citra_emu/NativeLibrary$CoreError;")); } -static bool HandleCoreError(Core::System::ResultStatus result, const std::string& details) { - JNIEnv* env = IDCache::GetEnvForThread(); +static bool HandleCoreError(Core::System::ResultStatus result, const std::string &details) { + JNIEnv *env = IDCache::GetEnvForThread(); return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(), ToJavaCoreError(result), env->NewStringUTF(details.c_str())) != JNI_FALSE; } static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) { - JNIEnv* env = IDCache::GetEnvForThread(); + JNIEnv *env = IDCache::GetEnvForThread(); env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(), IDCache::GetDiskCacheLoadProgress(), IDCache::GetJavaLoadCallbackStage(stage), static_cast(progress), static_cast(max)); } -static Camera::NDK::Factory* g_ndk_factory{}; +static Camera::NDK::Factory *g_ndk_factory{}; static void TryShutdown() { if (!window) { @@ -118,8 +133,10 @@ static void TryShutdown() { } window->DoneCurrent(); + if (second_window) second_window->DoneCurrent(); Core::System::GetInstance().Shutdown(); window.reset(); + if (second_window) second_window.reset(); InputManager::Shutdown(); MicroProfileShutdown(); } @@ -129,7 +146,7 @@ static bool CheckMicPermission() { IDCache::GetRequestMicPermission()); } -static Core::System::ResultStatus RunCitra(const std::string& filepath) { +static Core::System::ResultStatus RunCitra(const std::string &filepath) { // Citra core only supports a single running instance std::scoped_lock lock(running_mutex); @@ -142,33 +159,49 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { return Core::System::ResultStatus::ErrorLoader; } - Core::System& system{Core::System::GetInstance()}; + Core::System &system{Core::System::GetInstance()}; const auto graphics_api = Settings::values.graphics_api.GetValue(); switch (graphics_api) { #ifdef ENABLE_OPENGL - case Settings::GraphicsAPI::OpenGL: - window = std::make_unique(system, s_surf); - break; + case Settings::GraphicsAPI::OpenGL: + window = std::make_unique(system, s_surf, false); + if (secondary_enabled) { + EGLContext *c = window->GetEGLContext(); + second_window = std::make_unique(system, + s_secondary_surface, + true, c); + } + break; #endif #ifdef ENABLE_VULKAN - case Settings::GraphicsAPI::Vulkan: - window = std::make_unique(s_surf, vulkan_library); - break; + case Settings::GraphicsAPI::Vulkan: + window = std::make_unique(s_surf, vulkan_library, false); + if (secondary_enabled) + second_window = std::make_unique(s_secondary_surface, + vulkan_library, true); + break; #endif - default: - LOG_CRITICAL(Frontend, - "Unknown or unsupported graphics API {}, falling back to available default", - graphics_api); + default: + LOG_CRITICAL(Frontend, + "Unknown or unsupported graphics API {}, falling back to available default", + graphics_api); #ifdef ENABLE_OPENGL - window = std::make_unique(system, s_surf); + window = std::make_unique(system, s_surf, false); + if (secondary_enabled) { + EGLContext *c = window->GetEGLContext(); + second_window = std::make_unique(system, + s_secondary_surface, + true, c); + } #elif ENABLE_VULKAN - window = std::make_unique(s_surf, vulkan_library); + window = std::make_unique(s_surf, vulkan_library); + if (secondary_enabled) second_window = std::make_unique(s_secondary_surface, vulkan_library, true); #else -// TODO: Add a null renderer backend for this, perhaps. + // TODO: Add a null renderer backend for this, perhaps. #error "At least one renderer must be enabled." #endif - break; + break; } // Forces a config reload on game boot, if the user changed settings in the UI @@ -202,7 +235,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { InputManager::Init(); window->MakeCurrent(); - const Core::System::ResultStatus load_result{system.Load(*window, filepath)}; + const Core::System::ResultStatus load_result{ + system.Load(*window, filepath, second_window.get())}; if (load_result != Core::System::ResultStatus::Success) { return load_result; } @@ -249,18 +283,19 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { std::unique_lock pause_lock{paused_mutex}; running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; }); window->PollEvents(); + //if (second_window) second_window->PollEvents(); } } return Core::System::ResultStatus::Success; } -void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, - const std::string& custom_driver_name, - const std::string& file_redirect_dir) { +void InitializeGpuDriver(const std::string &hook_lib_dir, const std::string &custom_driver_dir, + const std::string &custom_driver_name, + const std::string &file_redirect_dir) { #if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64) - void* handle{}; - const char* file_redirect_dir_{}; + void *handle{}; + const char *file_redirect_dir_{}; int featureFlags{}; // Enable driver file redirection when renderer debugging is enabled. @@ -272,8 +307,8 @@ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& cus // Try to load a custom driver. if (custom_driver_name.size()) { handle = adrenotools_open_libvulkan( - RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(), - custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr); + RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(), + custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr); } // Try to load the system driver. @@ -288,7 +323,7 @@ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& cus extern "C" { -void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv *env, [[maybe_unused]] jobject obj, jobject surf) { s_surf = ANativeWindow_fromSurface(env, surf); @@ -298,15 +333,86 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, notify = window->OnSurfaceChanged(s_surf); } - auto& system = Core::System::GetInstance(); + auto &system = Core::System::GetInstance(); if (notify && system.IsPoweredOn()) { - system.GPU().Renderer().NotifySurfaceChanged(); + system.GPU().Renderer().NotifySurfaceChanged(false); } LOG_INFO(Frontend, "Surface changed"); } -void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv *env, + [[maybe_unused]] jobject obj, + jobject surf) { + auto &system = Core::System::GetInstance(); + s_secondary_surface = ANativeWindow_fromSurface(env, surf); + secondary_enabled = true; + bool notify = false; + if (!s_secondary_surface) { + // did not create the surface, so disable second screen + secondary_enabled = false; + if (system.IsPoweredOn()) { + system.GPU().Renderer().setSecondaryWindow(nullptr); + } + return; + } + if (second_window) { + //second window already created, so update it + notify = second_window->OnSurfaceChanged(s_secondary_surface); + } else if (system.IsPoweredOn() && window) { + // emulation running, window is new + // create a new window and set it + const auto graphics_api = Settings::values.graphics_api.GetValue(); + if (graphics_api == Settings::GraphicsAPI::OpenGL) { + EGLContext *c = window->GetEGLContext(); + second_window = std::make_unique(system, + s_secondary_surface,true, c); + }else{ + second_window = std::make_unique(s_secondary_surface, + vulkan_library, true); + } + system.GPU().Renderer().setSecondaryWindow(second_window.get()); + } + + if (notify && system.IsPoweredOn()) { + system.GPU().Renderer().NotifySurfaceChanged(true); + } + + LOG_INFO(Frontend, "Secondary Surface changed"); +} + + +void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceDestroyed(JNIEnv *env, + [[maybe_unused]] jobject obj) { + //auto &system = Core::System::GetInstance(); + secondary_enabled = false; + if (s_secondary_surface != nullptr) { + ANativeWindow_release(s_secondary_surface); + s_secondary_surface = nullptr; + } + + LOG_INFO(Frontend, "Secondary Surface Destroyed"); +} + +void Java_org_citra_citra_1emu_NativeLibrary_disableSecondaryScreen(JNIEnv *env, + [[maybe_unused]] jobject obj) { + auto &system = Core::System::GetInstance(); + secondary_enabled = false; + if (s_secondary_surface != nullptr) { + ANativeWindow_release(s_secondary_surface); + s_secondary_surface = nullptr; + } + if (system.IsPoweredOn()) { + system.GPU().Renderer().setSecondaryWindow(nullptr); + } + if (second_window) { + second_window.release(); + second_window = nullptr; + } + LOG_INFO(Frontend, "Secondary Window Disabled"); + +} +void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { if (s_surf != nullptr) { ANativeWindow_release(s_surf); @@ -317,7 +423,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] J } } -void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { if (stop_run || pause_emulation) { return; @@ -325,36 +431,39 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en if (window) { window->TryPresenting(); } + if (second_window) { + second_window->TryPresenting(); + } } void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver( - JNIEnv* env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir, - jstring custom_driver_name, jstring file_redirect_dir) { + JNIEnv *env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir, + jstring custom_driver_name, jstring file_redirect_dir) { InitializeGpuDriver(GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir), GetJString(env, custom_driver_name), GetJString(env, file_redirect_dir)); } -void Java_org_citra_citra_1emu_NativeLibrary_notifyOrientationChange([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_notifyOrientationChange([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jint layout_option, jint rotation, jboolean portrait) { Settings::values.layout_option = static_cast(layout_option); } -void Java_org_citra_citra_1emu_NativeLibrary_updateFramebuffer([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_updateFramebuffer([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jboolean is_portrait_mode) { - auto& system = Core::System::GetInstance(); + auto &system = Core::System::GetInstance(); if (system.IsPoweredOn()) { system.GPU().Renderer().UpdateCurrentFramebufferLayout(is_portrait_mode); } } -void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jboolean swap_screens, jint rotation) { Settings::values.swap_screen = swap_screens; - auto& system = Core::System::GetInstance(); + auto &system = Core::System::GetInstance(); if (system.IsPoweredOn()) { system.GPU().Renderer().UpdateCurrentFramebufferLayout(IsPortraitMode()); } @@ -362,14 +471,14 @@ void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv Camera::NDK::g_rotation = rotation; } -jboolean Java_org_citra_citra_1emu_NativeLibrary_areKeysAvailable([[maybe_unused]] JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_areKeysAvailable([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { HW::AES::InitKeys(); return HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) && HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2); } -jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env, +jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv *env, [[maybe_unused]] jobject obj, jint region) { const std::string path = Core::GetHomeMenuNcchPath(region); @@ -379,43 +488,44 @@ jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env, return ToJString(env, ""); } -void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv *env, [[maybe_unused]] jobject obj, jstring j_directory) { FileUtil::SetCurrentDir(GetJString(env, j_directory)); } jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths( - JNIEnv* env, [[maybe_unused]] jclass clazz) { + JNIEnv *env, [[maybe_unused]] jclass clazz) { std::vector games; const FileUtil::DirectoryEntryCallable ScanDir = - [&games, &ScanDir](u64*, const std::string& directory, const std::string& virtual_name) { - std::string path = directory + virtual_name; - if (FileUtil::IsDirectory(path)) { - path += '/'; - FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); - } else { - if (!FileUtil::Exists(path)) - return false; - auto loader = Loader::GetLoader(path); - if (loader) { - bool executable{}; - const Loader::ResultStatus result = loader->IsExecutable(executable); - if (Loader::ResultStatus::Success == result && executable) { - games.emplace_back(path); + [&games, &ScanDir](u64 *, const std::string &directory, + const std::string &virtual_name) { + std::string path = directory + virtual_name; + if (FileUtil::IsDirectory(path)) { + path += '/'; + FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); + } else { + if (!FileUtil::Exists(path)) + return false; + auto loader = Loader::GetLoader(path); + if (loader) { + bool executable{}; + const Loader::ResultStatus result = loader->IsExecutable(executable); + if (Loader::ResultStatus::Success == result && executable) { + games.emplace_back(path); + } } } - } - return true; - }; + return true; + }; ScanDir(nullptr, "", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + - "Nintendo " - "3DS/00000000000000000000000000000000/" - "00000000000000000000000000000000/title/00040000"); + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); ScanDir(nullptr, "", FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + - "00000000000000000000000000000000/title/00040010"); + "00000000000000000000000000000000/title/00040010"); jobjectArray jgames = env->NewObjectArray(static_cast(games.size()), env->FindClass("java/lang/String"), nullptr); for (jsize i = 0; i < games.size(); ++i) @@ -423,7 +533,7 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths( return jgames; } -jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env, +jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv *env, [[maybe_unused]] jobject obj, jint system_type, jint region) { @@ -431,11 +541,11 @@ jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env const std::vector titles = Core::GetSystemTitleIds(mode, region); jlongArray jTitles = env->NewLongArray(titles.size()); env->SetLongArrayRegion(jTitles, 0, titles.size(), - reinterpret_cast(titles.data())); + reinterpret_cast(titles.data())); return jTitles; } -jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv* env, +jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jlong title) { [[maybe_unused]] const auto title_id = static_cast(title); @@ -453,7 +563,7 @@ jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unu } jboolean JNICALL Java_org_citra_citra_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading( - JNIEnv* env, jobject instance) { + JNIEnv *env, jobject instance) { #ifdef CITRA_ARCH_arm64 // If the KGSL device exists custom drivers can be loaded using adrenotools return SupportsCustomDriver(); @@ -463,40 +573,41 @@ jboolean JNICALL Java_org_citra_citra_1emu_utils_GpuDriverHelper_supportsCustomD } // TODO(xperia64): ensure these cannot be called in an invalid state (e.g. after StopEmulation) -void Java_org_citra_citra_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { pause_emulation = false; running_cv.notify_all(); InputManager::NDKMotionHandler()->EnableSensors(); } -void Java_org_citra_citra_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { pause_emulation = true; InputManager::NDKMotionHandler()->DisableSensors(); } -void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { stop_run = true; pause_emulation = false; window->StopPresenting(); + if (second_window) second_window->StopPresenting(); running_cv.notify_all(); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { return static_cast(!stop_run); } -jlong Java_org_citra_citra_1emu_NativeLibrary_getRunningTitleId([[maybe_unused]] JNIEnv* env, +jlong Java_org_citra_citra_1emu_NativeLibrary_getRunningTitleId([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { u64 title_id{}; Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id); return static_cast(title_id); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device, jint j_button, jint action) { @@ -511,8 +622,9 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] } jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent( - [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device, - jint axis, jfloat x, jfloat y) { + [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, + [[maybe_unused]] jstring j_device, + jint axis, jfloat x, jfloat y) { // Clamp joystick movement to supported minimum and maximum // Citra uses an inverted y axis sent by the frontend x = std::clamp(x, -1.f, 1.f); @@ -530,27 +642,28 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent( } jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent( - [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device, - jint axis_id, jfloat axis_val) { + [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, + [[maybe_unused]] jstring j_device, + jint axis_id, jfloat axis_val) { return static_cast( - InputManager::ButtonHandler()->AnalogButtonEvent(axis_id, axis_val)); + InputManager::ButtonHandler()->AnalogButtonEvent(axis_id, axis_val)); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jfloat x, jfloat y, jboolean pressed) { return static_cast( - window->OnTouchEvent(static_cast(x + 0.5), static_cast(y + 0.5), pressed)); + window->OnTouchEvent(static_cast(x + 0.5), static_cast(y + 0.5), pressed)); } -void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jfloat x, jfloat y) { - window->OnTouchMoved((int)x, (int)y); + window->OnTouchMoved((int) x, (int) y); } -jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_unused]] jobject obj, +jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv *env, [[maybe_unused]] jobject obj, jstring j_filename) { std::string filepath = GetJString(env, j_filename); const auto loader = Loader::GetLoader(filepath); @@ -562,7 +675,7 @@ jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_un return static_cast(title_id); } -jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv *env, [[maybe_unused]] jobject obj, jstring path) { const std::string filepath = GetJString(env, path); @@ -578,19 +691,19 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv* env, return ((program_id >> 32) & 0xFFFFFFFF) == 0x00040010; } -void Java_org_citra_citra_1emu_NativeLibrary_createConfigFile([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_createConfigFile([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { Config{}; } -void Java_org_citra_citra_1emu_NativeLibrary_createLogFile([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_createLogFile([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { Common::Log::Initialize(); Common::Log::Start(); LOG_INFO(Frontend, "Logging backend initialised"); } -void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv *env, [[maybe_unused]] jobject obj, jstring j_path) { std::string_view path = env->GetStringUTFChars(j_path, 0); @@ -598,10 +711,10 @@ void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env, env->ReleaseStringUTFChars(j_path, path.data()); } -void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { Config{}; - Core::System& system{Core::System::GetInstance()}; + Core::System &system{Core::System::GetInstance()}; // Replace with game-specific settings if (system.IsPoweredOn()) { @@ -612,9 +725,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNI system.ApplySettings(); } -jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env, +jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv *env, [[maybe_unused]] jobject obj) { - auto& core = Core::System::GetInstance(); + auto &core = Core::System::GetInstance(); jdoubleArray j_stats = env->NewDoubleArray(4); if (core.IsPoweredOn()) { @@ -630,7 +743,7 @@ jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env, return j_stats; } -void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv *env, [[maybe_unused]] jobject obj, jstring j_path) { const std::string path = GetJString(env, j_path); @@ -647,19 +760,19 @@ void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* en } } -void Java_org_citra_citra_1emu_NativeLibrary_reloadCameraDevices([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_reloadCameraDevices([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { if (g_ndk_factory) { g_ndk_factory->ReloadCameraDevices(); } } -jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv* env, +jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv *env, [[maybe_unused]] jobject obj, jstring j_file) { std::string filepath = GetJString(env, j_file); - Core::System& system{Core::System::GetInstance()}; - Service::SM::ServiceManager& sm = system.ServiceManager(); + Core::System &system{Core::System::GetInstance()}; + Service::SM::ServiceManager &sm = system.ServiceManager(); auto nfc = sm.GetService("nfc:u"); if (nfc == nullptr) { return static_cast(false); @@ -668,10 +781,10 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv* env, return static_cast(nfc->LoadAmiibo(filepath)); } -void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { - Core::System& system{Core::System::GetInstance()}; - Service::SM::ServiceManager& sm = system.ServiceManager(); + Core::System &system{Core::System::GetInstance()}; + Service::SM::ServiceManager &sm = system.ServiceManager(); auto nfc = sm.GetService("nfc:u"); if (nfc == nullptr) { return; @@ -681,19 +794,20 @@ void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEn } JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_utils_CiaInstallWorker_installCIA( - JNIEnv* env, jobject jobj, jstring jpath) { + JNIEnv *env, jobject jobj, jstring jpath) { std::string path = GetJString(env, jpath); Service::AM::InstallStatus res = Service::AM::InstallCIA( - path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) { - env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(), - static_cast(file_size), static_cast(total_bytes_read)); - }); + path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) { + env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(), + static_cast(file_size), + static_cast(total_bytes_read)); + }); return IDCache::GetJavaCiaInstallStatus(res); } jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo( - JNIEnv* env, [[maybe_unused]] jobject obj) { + JNIEnv *env, [[maybe_unused]] jobject obj) { const jclass date_class = env->FindClass("java/util/Date"); const auto date_constructor = env->GetMethodID(date_class, "", "(J)V"); @@ -701,7 +815,7 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo( const auto slot_field = env->GetFieldID(savestate_info_class, "slot", "I"); const auto date_field = env->GetFieldID(savestate_info_class, "time", "Ljava/util/Date;"); - const Core::System& system{Core::System::GetInstance()}; + const Core::System &system{Core::System::GetInstance()}; if (!system.IsPoweredOn()) { return nullptr; } @@ -713,7 +827,8 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo( const auto savestates = Core::ListSaveStates(title_id, system.Movie().GetCurrentMovieID()); const jobjectArray array = - env->NewObjectArray(static_cast(savestates.size()), savestate_info_class, nullptr); + env->NewObjectArray(static_cast(savestates.size()), savestate_info_class, + nullptr); for (std::size_t i = 0; i < savestates.size(); ++i) { const jobject object = env->AllocObject(savestate_info_class); env->SetIntField(object, slot_field, static_cast(savestates[i].slot)); @@ -726,17 +841,17 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo( return array; } -void Java_org_citra_citra_1emu_NativeLibrary_saveState([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_saveState([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jint slot) { Core::System::GetInstance().SendSignal(Core::System::Signal::Save, slot); } -void Java_org_citra_citra_1emu_NativeLibrary_loadState([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_loadState([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj, jint slot) { Core::System::GetInstance().SendSignal(Core::System::Signal::Load, slot); } -void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv* env, +void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj) { LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); @@ -745,4 +860,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); } -} // extern "C" +} \ No newline at end of file diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 4159d9e4e..ceae4ea58 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -34,11 +34,25 @@ @string/emulation_screen_layout_custom + + @string/emulation_secondary_screen_default + @string/emulation_top_screen + @string/emulation_bottom_screen + @string/emulation_screen_layout_sidebyside + + 0 1 + + 0 + 1 + 2 + 3 + + @string/small_screen_position_top_right @string/small_screen_position_middle_right diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 5495a2a23..dad181c64 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -373,6 +373,7 @@ Open Cheats Landscape Screen Layout Portrait Screen Layout + Secondary Screen Layout Large Screen Portrait Single Screen @@ -380,6 +381,7 @@ Hybrid Screens Original Default + System Default (mirror) Custom Layout Small Screen Position Where should the small screen appear relative to the large one in Large Screen Layout? diff --git a/src/common/settings.cpp b/src/common/settings.cpp index c26771018..5a64ce315 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -113,6 +113,7 @@ void LogSettings() { } log_setting("Layout_LayoutOption", values.layout_option.GetValue()); log_setting("Layout_PortraitLayoutOption", values.portrait_layout_option.GetValue()); + log_setting("Layout_SecondScreenLayout",values.secondary_screen_layout.GetValue()); log_setting("Layout_SwapScreen", values.swap_screen.GetValue()); log_setting("Layout_UprightScreen", values.upright_screen.GetValue()); log_setting("Layout_LargeScreenProportion", values.large_screen_proportion.GetValue()); @@ -205,6 +206,7 @@ void RestoreGlobalState(bool is_powered_on) { values.delay_game_render_thread_us.SetGlobal(true); values.layout_option.SetGlobal(true); values.portrait_layout_option.SetGlobal(true); + values.secondary_screen_layout.SetGlobal(true); values.swap_screen.SetGlobal(true); values.upright_screen.SetGlobal(true); values.large_screen_proportion.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index e484c1cd2..560b2f888 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -53,6 +53,12 @@ enum class PortraitLayoutOption : u32 { PortraitCustomLayout, }; +enum class SecondaryScreenLayout : u32 { + None, + TopScreenOnly, + BottomScreenOnly, + SideBySide +}; /** Defines where the small screen will appear relative to the large screen * when in Large Screen mode */ @@ -503,6 +509,7 @@ struct Values { SwitchableSetting layout_option{LayoutOption::Default, "layout_option"}; SwitchableSetting swap_screen{false, "swap_screen"}; SwitchableSetting upright_screen{false, "upright_screen"}; + SwitchableSetting secondary_screen_layout{SecondaryScreenLayout::None, "secondary_screen_layout"}; SwitchableSetting large_screen_proportion{4.f, 1.f, 16.f, "large_screen_proportion"}; SwitchableSetting small_screen_position{SmallScreenPosition::BottomRight, diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index 6c2811cf0..ffcdcbe43 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -251,6 +251,9 @@ void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_po break; } } +#ifdef ANDROID + if (is_secondary) layout = Layout::AndroidSecondaryLayout(width,height); +#endif UpdateMinimumWindowSize(min_size); if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) { diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 7cf52a29c..df172b587 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -71,7 +71,7 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) || (Settings::values.screen_bottom_stretch.GetValue() && swapped); - const float window_aspect_ratio = static_cast(height) / width; + const float window_aspect_ratio = static_cast(height) / static_cast(width); if (stretched) { top_screen = {Settings::values.screen_top_leftright_padding.GetValue(), @@ -113,7 +113,7 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr FramebufferLayout res{width, height, true, true, {}, {}, !upright}; // Split the window into two parts. Give proportional width to the smaller screen // To do that, find the total emulation box and maximize that based on window size - const float window_aspect_ratio = static_cast(height) / width; + const float window_aspect_ratio = static_cast(height) / static_cast(width); float emulation_aspect_ratio; float large_height = @@ -297,6 +297,22 @@ FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary return SingleFrameLayout(width, height, is_secondary, upright); } +FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { + const Settings::SecondaryScreenLayout layout = Settings::values.secondary_screen_layout.GetValue(); + switch (layout) { + + case Settings::SecondaryScreenLayout::BottomScreenOnly: + return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue()); + case Settings::SecondaryScreenLayout::SideBySide: + return LargeFrameLayout(width,height,false,Settings::values.upright_screen.GetValue(),1.0f,Settings::SmallScreenPosition::MiddleRight); + case Settings::SecondaryScreenLayout::None: + // this should never happen, but if it does, somehow, send the top screen + case Settings::SecondaryScreenLayout::TopScreenOnly: + default: + return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); + } +} + FramebufferLayout CustomFrameLayout(u32 width, u32 height, bool is_swapped, bool is_portrait_mode) { ASSERT(width > 0); ASSERT(height > 0); diff --git a/src/core/frontend/framebuffer_layout.h b/src/core/frontend/framebuffer_layout.h index adcf26630..6088ca0d2 100644 --- a/src/core/frontend/framebuffer_layout.h +++ b/src/core/frontend/framebuffer_layout.h @@ -119,6 +119,16 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u */ FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary, bool upright); + +/** + * Method for constructing the secondary layout for Android, based on + * the appropriate setting. + * @param width Window framebuffer width in pixels + * @param height Window framebuffer height in pixels + */ +FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height); + + /** * Factory method for constructing a custom FramebufferLayout * @param width Window framebuffer width in pixels diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp index 16b59107a..a3ee517b5 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -35,7 +35,7 @@ void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) { window.UpdateCurrentFramebufferLayout(layout.width, layout.height, is_portrait_mode); }; update_layout(render_window); - if (secondary_window) { + if (secondary_window != nullptr) { update_layout(*secondary_window); } } @@ -67,4 +67,13 @@ void RendererBase::RequestScreenshot(void* data, std::function callb settings.screenshot_requested = true; } + Frontend::EmuWindow *RendererBase::getSecondaryWindow() const { + return secondary_window; + } + + void RendererBase::setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) { + secondary_window = secondaryWindow; + if (secondary_window) secondary_window->PollEvents(); + } + } // namespace VideoCore diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 55cb90d0b..cbd5fd244 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -64,7 +64,8 @@ public: virtual void Sync() {} /// This is called to notify the rendering backend of a surface change - virtual void NotifySurfaceChanged() {} + // if second == true then it is the second screen + virtual void NotifySurfaceChanged(bool second) {} /// Returns the resolution scale factor relative to the native 3DS screen resolution u32 GetResolutionScaleFactor(); @@ -110,7 +111,14 @@ protected: Core::System& system; RendererSettings settings; Frontend::EmuWindow& render_window; ///< Reference to the render window handle. - Frontend::EmuWindow* secondary_window; ///< Reference to the secondary render window handle. + Frontend::EmuWindow* secondary_window; +public: + Frontend::EmuWindow *getSecondaryWindow() const; + + virtual void setSecondaryWindow(Frontend::EmuWindow *secondaryWindow); + +protected: + ///< Reference to the secondary render window handle. f32 current_fps = 0.0f; ///< Current framerate, should be set by the renderer s32 current_frame = 0; ///< Current frame, should be set by the renderer }; diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 4f5dcb131..951b82005 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -87,6 +87,15 @@ RendererOpenGL::RendererOpenGL(Core::System& system, Pica::PicaCore& pica_, } RendererOpenGL::~RendererOpenGL() = default; +void RendererOpenGL::setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) { + if (secondaryWindow) { + secondary_window = secondaryWindow; + secondary_window->mailbox = std::make_unique(driver.HasDebugTool()); + }else { + secondary_window = nullptr; + // should I release something here? The mailbox?? + } +} void RendererOpenGL::SwapBuffers() { // Maintain the rasterizer's state as a priority @@ -96,7 +105,7 @@ void RendererOpenGL::SwapBuffers() { PrepareRendertarget(); RenderScreenshot(); - const auto& main_layout = render_window.GetFramebufferLayout(); + const auto &main_layout = render_window.GetFramebufferLayout(); RenderToMailbox(main_layout, render_window.mailbox, false); #ifndef ANDROID @@ -106,6 +115,15 @@ void RendererOpenGL::SwapBuffers() { RenderToMailbox(secondary_layout, secondary_window->mailbox, false); secondary_window->PollEvents(); } +#endif +#ifdef ANDROID + // on android, if secondary_window is defined at all + // it means we have a second display + if (secondary_window) { + const auto &secondary_layout = secondary_window->GetFramebufferLayout(); + RenderToMailbox(secondary_layout, secondary_window->mailbox, false); + secondary_window->PollEvents(); + } #endif if (frame_dumper.IsDumping()) { try { @@ -553,7 +571,7 @@ void RendererOpenGL::DrawSingleScreen(const ScreenInfo& screen_info, float x, fl state.texture_units[0].texture_2d = 0; state.texture_units[0].sampler = 0; state.Apply(); -} + } /** * Draws a single texture to the emulator window, rotating the texture to correct for the 3DS's LCD diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 7885a2f5b..c5b10242c 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -54,6 +54,7 @@ public: void PrepareVideoDumping() override; void CleanupVideoDumping() override; void Sync() override; + void setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) override; private: void InitOpenGLObjects(); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 6c95a22b7..616898d38 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -829,6 +829,16 @@ void RendererVulkan::SwapBuffers() { RenderToWindow(*second_window, secondary_layout, false); secondary_window->PollEvents(); } +#endif +#ifdef ANDROID + if (secondary_window) { + const auto &secondary_layout = secondary_window->GetFramebufferLayout(); + if (!second_window) { + second_window = std::make_unique(*secondary_window, instance, scheduler); + } + RenderToWindow(*second_window, secondary_layout, false); + secondary_window->PollEvents(); + } #endif rasterizer.TickFrame(); EndFrame(); @@ -1119,4 +1129,10 @@ bool RendererVulkan::TryRenderScreenshotWithHostMemory() { return true; } + void RendererVulkan::NotifySurfaceChanged(bool second) { + if (second && second_window) second_window->NotifySurfaceChanged(); + if (!second) main_window.NotifySurfaceChanged(); + + } + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index b52142e88..a35fb477b 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -74,9 +74,7 @@ public: return &rasterizer; } - void NotifySurfaceChanged() override { - main_window.NotifySurfaceChanged(); - } + void NotifySurfaceChanged(bool second) override; void SwapBuffers() override; void TryPresent(int timeout_ms, bool is_secondary) override {}