Merge 3929d448156e75c74c145e9984d8d4a0642bbe9d into 26ce7e4f2844a445bf77b4b14977d62e6434df08

This commit is contained in:
David Griswold 2025-03-11 20:51:49 +00:00 committed by GitHub
commit ac5b74685c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 533 additions and 162 deletions

View File

@ -125,6 +125,10 @@ object NativeLibrary {
external fun surfaceDestroyed() external fun surfaceDestroyed()
external fun doFrame() external fun doFrame()
//Second window
external fun secondarySurfaceChanged(secondary_surface: Surface)
external fun secondarySurfaceDestroyed()
external fun disableSecondaryScreen()
/** /**
* Unpauses emulation from a paused state. * Unpauses emulation from a paused state.
*/ */

View File

@ -4,14 +4,18 @@
package org.citra.citra_emu.activities package org.citra.citra_emu.activities
import SecondScreenPresentation
import android.Manifest.permission import android.Manifest.permission
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Presentation
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.hardware.display.DisplayManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Display
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent 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.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil 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.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.IntSetting
@ -56,6 +61,34 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility 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 private val emulationFragment: EmulationFragment
get() { get() {
@ -68,10 +101,9 @@ class EmulationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this) ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings() settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
updatePresentation();
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings) screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
@ -117,6 +149,11 @@ class EmulationActivity : AppCompatActivity() {
applyOrientationSettings() // Check for orientation settings changes on runtime applyOrientationSettings() // Check for orientation settings changes on runtime
} }
override fun onStop() {
releasePresentation()
super.onStop()
}
override fun onWindowFocusChanged(hasFocus: Boolean) { override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus) super.onWindowFocusChanged(hasFocus)
enableFullscreenImmersive() enableFullscreenImmersive()
@ -124,6 +161,7 @@ class EmulationActivity : AppCompatActivity() {
public override fun onRestart() { public override fun onRestart() {
super.onRestart() super.onRestart()
updatePresentation()
NativeLibrary.reloadCameraDevices() NativeLibrary.reloadCameraDevices()
} }
@ -141,6 +179,7 @@ class EmulationActivity : AppCompatActivity() {
EmulationLifecycleUtil.clear() EmulationLifecycleUtil.clear()
isEmulationRunning = false isEmulationRunning = false
instance = null instance = null
releasePresentation()
super.onDestroy() super.onDestroy()
} }

View File

@ -48,4 +48,18 @@ enum class PortraitScreenLayout(val int: Int) {
return entries.firstOrNull { it.int == int } ?: TOP_FULL_WIDTH 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
}
}
} }

View File

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

View File

@ -34,6 +34,7 @@ enum class IntSetting(
LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640), LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480), LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0), 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_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y("custom_portrait_top_y",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), PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),

View File

@ -962,6 +962,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue 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( add(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.SMALL_SCREEN_POSITION, IntSetting.SMALL_SCREEN_POSITION,

View File

@ -272,6 +272,9 @@ object SettingsFile {
val settings = section.settings val settings = section.settings
val keySet: Set<String> = settings.keys val keySet: Set<String> = settings.keys
for (key in keySet) { for (key in keySet) {
if (key.equals("secondary_screen_layout")) {
println("hi");
}
val setting = settings[key] val setting = settings[key]
parser.put(header, setting!!.key, setting.valueAsString) parser.put(header, setting!!.key, setting.valueAsString)
} }

View File

@ -5,9 +5,12 @@
package org.citra.citra_emu.fragments package org.citra.citra_emu.fragments
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Presentation
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.hardware.display.DisplayManager
import android.media.MediaRouter
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -16,6 +19,7 @@ import android.os.SystemClock
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.Choreographer import android.view.Choreographer
import android.view.Display
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.Surface import android.view.Surface
@ -26,6 +30,7 @@ import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -79,6 +84,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private lateinit var emulationState: EmulationState private lateinit var emulationState: EmulationState
private var perfStatsUpdater: Runnable? = null private var perfStatsUpdater: Runnable? = null
private lateinit var emulationActivity: EmulationActivity private lateinit var emulationActivity: EmulationActivity
private var _binding: FragmentEmulationBinding? = null private var _binding: FragmentEmulationBinding? = null
@ -146,6 +152,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true retainInstance = true
emulationState = EmulationState(game.path) emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings) screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() }) EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() }) EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
@ -1214,6 +1221,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private class EmulationState(private val gamePath: String) { private class EmulationState(private val gamePath: String) {
private var state: State private var state: State
private var surface: Surface? = null private var surface: Surface? = null
private var surface2: Surface? = null
init { init {
// Starting state is stopped. // Starting state is stopped.

View File

@ -204,6 +204,10 @@ void Config::ReadValues() {
static_cast<Settings::PortraitLayoutOption>(sdl2_config->GetInteger( static_cast<Settings::PortraitLayoutOption>(sdl2_config->GetInteger(
"Layout", "portrait_layout_option", "Layout", "portrait_layout_option",
static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth))); static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth)));
Settings::values.secondary_screen_layout =
static_cast<Settings::SecondaryScreenLayout>(sdl2_config->GetInteger(
"Layout", "secondary_screen_layout",
static_cast<int>(Settings::SecondaryScreenLayout::None)));
ReadSetting("Layout", Settings::values.custom_portrait_top_x); ReadSetting("Layout", Settings::values.custom_portrait_top_x);
ReadSetting("Layout", Settings::values.custom_portrait_top_y); ReadSetting("Layout", Settings::values.custom_portrait_top_y);
ReadSetting("Layout", Settings::values.custom_portrait_top_width); ReadSetting("Layout", Settings::values.custom_portrait_top_width);

View File

@ -251,6 +251,15 @@ custom_portrait_bottom_height =
# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent # 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent
swap_screen = 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) # Screen placement settings when using Cardboard VR (render3d = 4)
# 30 - 100: Screen size as a percentage of the viewport. 85 (default) # 30 - 100: Screen size as a percentage of the viewport. 85 (default)
cardboard_screen_size = cardboard_screen_size =

View File

@ -49,16 +49,17 @@ void EmuWindow_Android::OnFramebufferSizeChanged() {
const int bigger{window_width > window_height ? window_width : window_height}; const int bigger{window_width > window_height ? window_width : window_height};
const int smaller{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); UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode);
} else { } else {
UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode); 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"); LOG_DEBUG(Frontend, "Initializing EmuWindow_Android");
if (is_secondary) LOG_DEBUG(Frontend, "Initializing secondary window Android");
if (!surface) { if (!surface) {
LOG_CRITICAL(Frontend, "surface is nullptr"); LOG_CRITICAL(Frontend, "surface is nullptr");
return; return;

View File

@ -5,6 +5,7 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include <EGL/egl.h>
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
namespace Core { namespace Core {
@ -13,7 +14,7 @@ class System;
class EmuWindow_Android : public Frontend::EmuWindow { class EmuWindow_Android : public Frontend::EmuWindow {
public: public:
EmuWindow_Android(ANativeWindow* surface); EmuWindow_Android(ANativeWindow* surface, bool is_secondary = false);
~EmuWindow_Android(); ~EmuWindow_Android();
/// Called by the onSurfaceChanges() method to change the surface /// Called by the onSurfaceChanges() method to change the surface
@ -30,7 +31,10 @@ public:
void DoneCurrent() override; void DoneCurrent() override;
virtual void TryPresenting() {} 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() {} virtual void StopPresenting() {}
protected: protected:

View File

@ -72,8 +72,8 @@ private:
EGLContext egl_context{}; EGLContext egl_context{};
}; };
EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface) EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface, bool is_secondary, EGLContext* sharedContext)
: EmuWindow_Android{surface}, system{system_} { : EmuWindow_Android{surface,is_secondary}, system{system_} {
if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) { if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) {
LOG_CRITICAL(Frontend, "eglGetDisplay() failed"); LOG_CRITICAL(Frontend, "eglGetDisplay() failed");
return; 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) { if (eglQuerySurface(egl_display, egl_surface, EGL_HEIGHT, &window_height) != EGL_TRUE) {
return; return;
} }
if (sharedContext) {
if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data()); egl_context = *sharedContext;
}else if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data());
egl_context == EGL_NO_CONTEXT) { egl_context == EGL_NO_CONTEXT) {
LOG_CRITICAL(Frontend, "eglCreateContext() failed"); LOG_CRITICAL(Frontend, "eglCreateContext() failed");
return; return;
@ -127,6 +128,10 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ
OnFramebufferSizeChanged(); OnFramebufferSizeChanged();
} }
EGLContext* EmuWindow_Android_OpenGL::GetEGLContext() {
return &egl_context;
}
bool EmuWindow_Android_OpenGL::CreateWindowSurface() { bool EmuWindow_Android_OpenGL::CreateWindowSurface() {
if (!host_window) { if (!host_window) {
return true; return true;
@ -204,14 +209,15 @@ void EmuWindow_Android_OpenGL::TryPresenting() {
return; return;
} }
if (presenting_state == PresentingState::Initial) [[unlikely]] { if (presenting_state == PresentingState::Initial) [[unlikely]] {
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
presenting_state = PresentingState::Running; presenting_state = PresentingState::Running;
} }
if (presenting_state != PresentingState::Running) [[unlikely]] { // if (presenting_state != PresentingState::Running) [[unlikely]] {
return; // 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); 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); eglSwapBuffers(egl_display, egl_surface);
} }

View File

@ -19,13 +19,13 @@ struct ANativeWindow;
class EmuWindow_Android_OpenGL : public EmuWindow_Android { class EmuWindow_Android_OpenGL : public EmuWindow_Android {
public: 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; ~EmuWindow_Android_OpenGL() override = default;
void TryPresenting() override; void TryPresenting() override;
void StopPresenting() override; void StopPresenting() override;
void PollEvents() override; void PollEvents() override;
EGLContext* GetEGLContext() override;
std::unique_ptr<GraphicsContext> CreateSharedContext() const override; std::unique_ptr<GraphicsContext> CreateSharedContext() const override;
private: private:

View File

@ -24,8 +24,8 @@ private:
}; };
EmuWindow_Android_Vulkan::EmuWindow_Android_Vulkan( EmuWindow_Android_Vulkan::EmuWindow_Android_Vulkan(
ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_) ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_, bool is_secondary)
: EmuWindow_Android{surface}, driver_library{driver_library_} { : EmuWindow_Android{surface,is_secondary}, driver_library{driver_library_} {
CreateWindowSurface(); CreateWindowSurface();
if (core_context = CreateSharedContext(); !core_context) { if (core_context = CreateSharedContext(); !core_context) {

View File

@ -11,7 +11,7 @@ struct ANativeWindow;
class EmuWindow_Android_Vulkan : public EmuWindow_Android { class EmuWindow_Android_Vulkan : public EmuWindow_Android {
public: public:
EmuWindow_Android_Vulkan(ANativeWindow* surface, EmuWindow_Android_Vulkan(ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library); std::shared_ptr<Common::DynamicLibrary> driver_library, bool is_secondary);
~EmuWindow_Android_Vulkan() override = default; ~EmuWindow_Android_Vulkan() override = default;
void PollEvents() override {} void PollEvents() override {}

View File

@ -16,11 +16,15 @@
#include <core/hle/service/cfg/cfg.h> #include <core/hle/service/cfg/cfg.h>
#include "audio_core/dsp_interface.h" #include "audio_core/dsp_interface.h"
#include "common/arch.h" #include "common/arch.h"
#if CITRA_ARCH(arm64) #if CITRA_ARCH(arm64)
#include "common/aarch64/cpu_detect.h" #include "common/aarch64/cpu_detect.h"
#elif CITRA_ARCH(x86_64) #elif CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h" #include "common/x64/cpu_detect.h"
#endif #endif
#include "common/common_paths.h" #include "common/common_paths.h"
#include "common/dynamic_library/dynamic_library.h" #include "common/dynamic_library/dynamic_library.h"
#include "common/file_util.h" #include "common/file_util.h"
@ -44,12 +48,18 @@
#include "jni/camera/ndk_camera.h" #include "jni/camera/ndk_camera.h"
#include "jni/camera/still_image_camera.h" #include "jni/camera/still_image_camera.h"
#include "jni/config.h" #include "jni/config.h"
#ifdef ENABLE_OPENGL #ifdef ENABLE_OPENGL
#include "jni/emu_window/emu_window_gl.h" #include "jni/emu_window/emu_window_gl.h"
#endif #endif
#ifdef ENABLE_VULKAN #ifdef ENABLE_VULKAN
#include "jni/emu_window/emu_window_vk.h" #include "jni/emu_window/emu_window_vk.h"
#endif #endif
#include "jni/id_cache.h" #include "jni/id_cache.h"
#include "jni/input_manager.h" #include "jni/input_manager.h"
#include "jni/ndk_motion.h" #include "jni/ndk_motion.h"
@ -59,58 +69,63 @@
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64) #if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64)
#include <adrenotools/driver.h> #include <adrenotools/driver.h>
#endif #endif
namespace { namespace {
ANativeWindow* s_surf; ANativeWindow *s_surf;
ANativeWindow *s_secondary_surface;
bool secondary_enabled = false;
std::shared_ptr<Common::DynamicLibrary> vulkan_library{}; std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
std::unique_ptr<EmuWindow_Android> window; std::unique_ptr<EmuWindow_Android> window;
std::unique_ptr<EmuWindow_Android> second_window;
std::atomic<bool> stop_run{true}; std::atomic<bool> stop_run{true};
std::atomic<bool> pause_emulation{false}; std::atomic<bool> pause_emulation{false};
std::mutex paused_mutex; std::mutex paused_mutex;
std::mutex running_mutex; std::mutex running_mutex;
std::condition_variable running_cv; std::condition_variable running_cv;
} // Anonymous namespace } // Anonymous namespace
static jobject ToJavaCoreError(Core::System::ResultStatus result) { static jobject ToJavaCoreError(Core::System::ResultStatus result) {
static const std::map<Core::System::ResultStatus, const char*> CoreErrorNameMap{ static const std::map<Core::System::ResultStatus, const char *> CoreErrorNameMap{
{Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"},
{Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"},
{Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"}, {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"},
{Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"},
}; };
const auto name = CoreErrorNameMap.count(result) ? CoreErrorNameMap.at(result) : "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(); const jclass core_error_class = IDCache::GetCoreErrorClass();
return env->GetStaticObjectField( return env->GetStaticObjectField(
core_error_class, env->GetStaticFieldID(core_error_class, name, core_error_class, env->GetStaticFieldID(core_error_class, name,
"Lorg/citra/citra_emu/NativeLibrary$CoreError;")); "Lorg/citra/citra_emu/NativeLibrary$CoreError;"));
} }
static bool HandleCoreError(Core::System::ResultStatus result, const std::string& details) { static bool HandleCoreError(Core::System::ResultStatus result, const std::string &details) {
JNIEnv* env = IDCache::GetEnvForThread(); JNIEnv *env = IDCache::GetEnvForThread();
return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(), return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(),
ToJavaCoreError(result), ToJavaCoreError(result),
env->NewStringUTF(details.c_str())) != JNI_FALSE; env->NewStringUTF(details.c_str())) != JNI_FALSE;
} }
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) { static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
JNIEnv* env = IDCache::GetEnvForThread(); JNIEnv *env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(), env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
IDCache::GetDiskCacheLoadProgress(), IDCache::GetDiskCacheLoadProgress(),
IDCache::GetJavaLoadCallbackStage(stage), static_cast<jint>(progress), IDCache::GetJavaLoadCallbackStage(stage), static_cast<jint>(progress),
static_cast<jint>(max)); static_cast<jint>(max));
} }
static Camera::NDK::Factory* g_ndk_factory{}; static Camera::NDK::Factory *g_ndk_factory{};
static void TryShutdown() { static void TryShutdown() {
if (!window) { if (!window) {
@ -118,8 +133,10 @@ static void TryShutdown() {
} }
window->DoneCurrent(); window->DoneCurrent();
if (second_window) second_window->DoneCurrent();
Core::System::GetInstance().Shutdown(); Core::System::GetInstance().Shutdown();
window.reset(); window.reset();
if (second_window) second_window.reset();
InputManager::Shutdown(); InputManager::Shutdown();
MicroProfileShutdown(); MicroProfileShutdown();
} }
@ -129,7 +146,7 @@ static bool CheckMicPermission() {
IDCache::GetRequestMicPermission()); 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 // Citra core only supports a single running instance
std::scoped_lock lock(running_mutex); std::scoped_lock lock(running_mutex);
@ -142,33 +159,49 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
return Core::System::ResultStatus::ErrorLoader; 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(); const auto graphics_api = Settings::values.graphics_api.GetValue();
switch (graphics_api) { switch (graphics_api) {
#ifdef ENABLE_OPENGL #ifdef ENABLE_OPENGL
case Settings::GraphicsAPI::OpenGL: case Settings::GraphicsAPI::OpenGL:
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf); window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf, false);
break; if (secondary_enabled) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,
true, c);
}
break;
#endif #endif
#ifdef ENABLE_VULKAN #ifdef ENABLE_VULKAN
case Settings::GraphicsAPI::Vulkan: case Settings::GraphicsAPI::Vulkan:
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library); window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library, false);
break; if (secondary_enabled)
second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface,
vulkan_library, true);
break;
#endif #endif
default: default:
LOG_CRITICAL(Frontend, LOG_CRITICAL(Frontend,
"Unknown or unsupported graphics API {}, falling back to available default", "Unknown or unsupported graphics API {}, falling back to available default",
graphics_api); graphics_api);
#ifdef ENABLE_OPENGL #ifdef ENABLE_OPENGL
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf); window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf, false);
if (secondary_enabled) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,
true, c);
}
#elif ENABLE_VULKAN #elif ENABLE_VULKAN
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library); window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
if (secondary_enabled) second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface, vulkan_library, true);
#else #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." #error "At least one renderer must be enabled."
#endif #endif
break; break;
} }
// Forces a config reload on game boot, if the user changed settings in the UI // 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(); InputManager::Init();
window->MakeCurrent(); 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) { if (load_result != Core::System::ResultStatus::Success) {
return load_result; return load_result;
} }
@ -249,18 +283,19 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
std::unique_lock pause_lock{paused_mutex}; std::unique_lock pause_lock{paused_mutex};
running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; }); running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; });
window->PollEvents(); window->PollEvents();
//if (second_window) second_window->PollEvents();
} }
} }
return Core::System::ResultStatus::Success; return Core::System::ResultStatus::Success;
} }
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, void InitializeGpuDriver(const std::string &hook_lib_dir, const std::string &custom_driver_dir,
const std::string& custom_driver_name, const std::string &custom_driver_name,
const std::string& file_redirect_dir) { const std::string &file_redirect_dir) {
#if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64) #if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64)
void* handle{}; void *handle{};
const char* file_redirect_dir_{}; const char *file_redirect_dir_{};
int featureFlags{}; int featureFlags{};
// Enable driver file redirection when renderer debugging is enabled. // 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. // Try to load a custom driver.
if (custom_driver_name.size()) { if (custom_driver_name.size()) {
handle = adrenotools_open_libvulkan( handle = adrenotools_open_libvulkan(
RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(), 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); custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
} }
// Try to load the system driver. // Try to load the system driver.
@ -288,7 +323,7 @@ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& cus
extern "C" { 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, [[maybe_unused]] jobject obj,
jobject surf) { jobject surf) {
s_surf = ANativeWindow_fromSurface(env, 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); notify = window->OnSurfaceChanged(s_surf);
} }
auto& system = Core::System::GetInstance(); auto &system = Core::System::GetInstance();
if (notify && system.IsPoweredOn()) { if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged(); system.GPU().Renderer().NotifySurfaceChanged(false);
} }
LOG_INFO(Frontend, "Surface changed"); 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<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,true, c);
}else{
second_window = std::make_unique<EmuWindow_Android_Vulkan>(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) { [[maybe_unused]] jobject obj) {
if (s_surf != nullptr) { if (s_surf != nullptr) {
ANativeWindow_release(s_surf); 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) { [[maybe_unused]] jobject obj) {
if (stop_run || pause_emulation) { if (stop_run || pause_emulation) {
return; return;
@ -325,36 +431,39 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en
if (window) { if (window) {
window->TryPresenting(); window->TryPresenting();
} }
if (second_window) {
second_window->TryPresenting();
}
} }
void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver( void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver(
JNIEnv* env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir, JNIEnv *env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir,
jstring custom_driver_name, jstring file_redirect_dir) { jstring custom_driver_name, jstring file_redirect_dir) {
InitializeGpuDriver(GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir), InitializeGpuDriver(GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir),
GetJString(env, custom_driver_name), GetJString(env, file_redirect_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, [[maybe_unused]] jobject obj,
jint layout_option, jint layout_option,
jint rotation, jint rotation,
jboolean portrait) { jboolean portrait) {
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layout_option); Settings::values.layout_option = static_cast<Settings::LayoutOption>(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, [[maybe_unused]] jobject obj,
jboolean is_portrait_mode) { jboolean is_portrait_mode) {
auto& system = Core::System::GetInstance(); auto &system = Core::System::GetInstance();
if (system.IsPoweredOn()) { if (system.IsPoweredOn()) {
system.GPU().Renderer().UpdateCurrentFramebufferLayout(is_portrait_mode); 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, [[maybe_unused]] jobject obj,
jboolean swap_screens, jint rotation) { jboolean swap_screens, jint rotation) {
Settings::values.swap_screen = swap_screens; Settings::values.swap_screen = swap_screens;
auto& system = Core::System::GetInstance(); auto &system = Core::System::GetInstance();
if (system.IsPoweredOn()) { if (system.IsPoweredOn()) {
system.GPU().Renderer().UpdateCurrentFramebufferLayout(IsPortraitMode()); 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; 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) { [[maybe_unused]] jobject obj) {
HW::AES::InitKeys(); HW::AES::InitKeys();
return HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) && return HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) &&
HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2); 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, [[maybe_unused]] jobject obj,
jint region) { jint region) {
const std::string path = Core::GetHomeMenuNcchPath(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, ""); 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, [[maybe_unused]] jobject obj,
jstring j_directory) { jstring j_directory) {
FileUtil::SetCurrentDir(GetJString(env, j_directory)); FileUtil::SetCurrentDir(GetJString(env, j_directory));
} }
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths( jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths(
JNIEnv* env, [[maybe_unused]] jclass clazz) { JNIEnv *env, [[maybe_unused]] jclass clazz) {
std::vector<std::string> games; std::vector<std::string> games;
const FileUtil::DirectoryEntryCallable ScanDir = const FileUtil::DirectoryEntryCallable ScanDir =
[&games, &ScanDir](u64*, const std::string& directory, const std::string& virtual_name) { [&games, &ScanDir](u64 *, const std::string &directory,
std::string path = directory + virtual_name; const std::string &virtual_name) {
if (FileUtil::IsDirectory(path)) { std::string path = directory + virtual_name;
path += '/'; if (FileUtil::IsDirectory(path)) {
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); path += '/';
} else { FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
if (!FileUtil::Exists(path)) } else {
return false; if (!FileUtil::Exists(path))
auto loader = Loader::GetLoader(path); return false;
if (loader) { auto loader = Loader::GetLoader(path);
bool executable{}; if (loader) {
const Loader::ResultStatus result = loader->IsExecutable(executable); bool executable{};
if (Loader::ResultStatus::Success == result && executable) { const Loader::ResultStatus result = loader->IsExecutable(executable);
games.emplace_back(path); if (Loader::ResultStatus::Success == result && executable) {
games.emplace_back(path);
}
} }
} }
} return true;
return true; };
};
ScanDir(nullptr, "", ScanDir(nullptr, "",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
"Nintendo " "Nintendo "
"3DS/00000000000000000000000000000000/" "3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000"); "00000000000000000000000000000000/title/00040000");
ScanDir(nullptr, "", ScanDir(nullptr, "",
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
"00000000000000000000000000000000/title/00040010"); "00000000000000000000000000000000/title/00040010");
jobjectArray jgames = env->NewObjectArray(static_cast<jsize>(games.size()), jobjectArray jgames = env->NewObjectArray(static_cast<jsize>(games.size()),
env->FindClass("java/lang/String"), nullptr); env->FindClass("java/lang/String"), nullptr);
for (jsize i = 0; i < games.size(); ++i) for (jsize i = 0; i < games.size(); ++i)
@ -423,7 +533,7 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths(
return jgames; 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, [[maybe_unused]] jobject obj,
jint system_type, jint system_type,
jint region) { jint region) {
@ -431,11 +541,11 @@ jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env
const std::vector<u64> titles = Core::GetSystemTitleIds(mode, region); const std::vector<u64> titles = Core::GetSystemTitleIds(mode, region);
jlongArray jTitles = env->NewLongArray(titles.size()); jlongArray jTitles = env->NewLongArray(titles.size());
env->SetLongArrayRegion(jTitles, 0, titles.size(), env->SetLongArrayRegion(jTitles, 0, titles.size(),
reinterpret_cast<const jlong*>(titles.data())); reinterpret_cast<const jlong *>(titles.data()));
return jTitles; 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, [[maybe_unused]] jobject obj,
jlong title) { jlong title) {
[[maybe_unused]] const auto title_id = static_cast<u64>(title); [[maybe_unused]] const auto title_id = static_cast<u64>(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( jboolean JNICALL Java_org_citra_citra_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading(
JNIEnv* env, jobject instance) { JNIEnv *env, jobject instance) {
#ifdef CITRA_ARCH_arm64 #ifdef CITRA_ARCH_arm64
// If the KGSL device exists custom drivers can be loaded using adrenotools // If the KGSL device exists custom drivers can be loaded using adrenotools
return SupportsCustomDriver(); 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) // 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) { [[maybe_unused]] jobject obj) {
pause_emulation = false; pause_emulation = false;
running_cv.notify_all(); running_cv.notify_all();
InputManager::NDKMotionHandler()->EnableSensors(); 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) { [[maybe_unused]] jobject obj) {
pause_emulation = true; pause_emulation = true;
InputManager::NDKMotionHandler()->DisableSensors(); 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) { [[maybe_unused]] jobject obj) {
stop_run = true; stop_run = true;
pause_emulation = false; pause_emulation = false;
window->StopPresenting(); window->StopPresenting();
if (second_window) second_window->StopPresenting();
running_cv.notify_all(); 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) { [[maybe_unused]] jobject obj) {
return static_cast<jboolean>(!stop_run); return static_cast<jboolean>(!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) { [[maybe_unused]] jobject obj) {
u64 title_id{}; u64 title_id{};
Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id); Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id);
return static_cast<jlong>(title_id); return static_cast<jlong>(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]] jobject obj,
[[maybe_unused]] jstring j_device, [[maybe_unused]] jstring j_device,
jint j_button, jint action) { 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( jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device, [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj,
jint axis, jfloat x, jfloat y) { [[maybe_unused]] jstring j_device,
jint axis, jfloat x, jfloat y) {
// Clamp joystick movement to supported minimum and maximum // Clamp joystick movement to supported minimum and maximum
// Citra uses an inverted y axis sent by the frontend // Citra uses an inverted y axis sent by the frontend
x = std::clamp(x, -1.f, 1.f); 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( jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device, [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj,
jint axis_id, jfloat axis_val) { [[maybe_unused]] jstring j_device,
jint axis_id, jfloat axis_val) {
return static_cast<jboolean>( return static_cast<jboolean>(
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, [[maybe_unused]] jobject obj,
jfloat x, jfloat y, jfloat x, jfloat y,
jboolean pressed) { jboolean pressed) {
return static_cast<jboolean>( return static_cast<jboolean>(
window->OnTouchEvent(static_cast<int>(x + 0.5), static_cast<int>(y + 0.5), pressed)); window->OnTouchEvent(static_cast<int>(x + 0.5), static_cast<int>(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, [[maybe_unused]] jobject obj, jfloat x,
jfloat y) { 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) { jstring j_filename) {
std::string filepath = GetJString(env, j_filename); std::string filepath = GetJString(env, j_filename);
const auto loader = Loader::GetLoader(filepath); 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<jlong>(title_id); return static_cast<jlong>(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, [[maybe_unused]] jobject obj,
jstring path) { jstring path) {
const std::string filepath = GetJString(env, 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; 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) { [[maybe_unused]] jobject obj) {
Config{}; 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) { [[maybe_unused]] jobject obj) {
Common::Log::Initialize(); Common::Log::Initialize();
Common::Log::Start(); Common::Log::Start();
LOG_INFO(Frontend, "Logging backend initialised"); 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, [[maybe_unused]] jobject obj,
jstring j_path) { jstring j_path) {
std::string_view path = env->GetStringUTFChars(j_path, 0); 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()); 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) { [[maybe_unused]] jobject obj) {
Config{}; Config{};
Core::System& system{Core::System::GetInstance()}; Core::System &system{Core::System::GetInstance()};
// Replace with game-specific settings // Replace with game-specific settings
if (system.IsPoweredOn()) { if (system.IsPoweredOn()) {
@ -612,9 +725,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNI
system.ApplySettings(); 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) { [[maybe_unused]] jobject obj) {
auto& core = Core::System::GetInstance(); auto &core = Core::System::GetInstance();
jdoubleArray j_stats = env->NewDoubleArray(4); jdoubleArray j_stats = env->NewDoubleArray(4);
if (core.IsPoweredOn()) { if (core.IsPoweredOn()) {
@ -630,7 +743,7 @@ jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env,
return j_stats; 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, [[maybe_unused]] jobject obj,
jstring j_path) { jstring j_path) {
const std::string path = GetJString(env, 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) { [[maybe_unused]] jobject obj) {
if (g_ndk_factory) { if (g_ndk_factory) {
g_ndk_factory->ReloadCameraDevices(); 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, [[maybe_unused]] jobject obj,
jstring j_file) { jstring j_file) {
std::string filepath = GetJString(env, j_file); std::string filepath = GetJString(env, j_file);
Core::System& system{Core::System::GetInstance()}; Core::System &system{Core::System::GetInstance()};
Service::SM::ServiceManager& sm = system.ServiceManager(); Service::SM::ServiceManager &sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u"); auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (nfc == nullptr) { if (nfc == nullptr) {
return static_cast<jboolean>(false); return static_cast<jboolean>(false);
@ -668,10 +781,10 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv* env,
return static_cast<jboolean>(nfc->LoadAmiibo(filepath)); return static_cast<jboolean>(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) { [[maybe_unused]] jobject obj) {
Core::System& system{Core::System::GetInstance()}; Core::System &system{Core::System::GetInstance()};
Service::SM::ServiceManager& sm = system.ServiceManager(); Service::SM::ServiceManager &sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u"); auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (nfc == nullptr) { if (nfc == nullptr) {
return; 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( 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); std::string path = GetJString(env, jpath);
Service::AM::InstallStatus res = Service::AM::InstallCIA( Service::AM::InstallStatus res = Service::AM::InstallCIA(
path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) { path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) {
env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(), env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(),
static_cast<jint>(file_size), static_cast<jint>(total_bytes_read)); static_cast<jint>(file_size),
}); static_cast<jint>(total_bytes_read));
});
return IDCache::GetJavaCiaInstallStatus(res); return IDCache::GetJavaCiaInstallStatus(res);
} }
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo( 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 jclass date_class = env->FindClass("java/util/Date");
const auto date_constructor = env->GetMethodID(date_class, "<init>", "(J)V"); const auto date_constructor = env->GetMethodID(date_class, "<init>", "(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 slot_field = env->GetFieldID(savestate_info_class, "slot", "I");
const auto date_field = env->GetFieldID(savestate_info_class, "time", "Ljava/util/Date;"); 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()) { if (!system.IsPoweredOn()) {
return nullptr; 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 auto savestates = Core::ListSaveStates(title_id, system.Movie().GetCurrentMovieID());
const jobjectArray array = const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(savestates.size()), savestate_info_class, nullptr); env->NewObjectArray(static_cast<jsize>(savestates.size()), savestate_info_class,
nullptr);
for (std::size_t i = 0; i < savestates.size(); ++i) { for (std::size_t i = 0; i < savestates.size(); ++i) {
const jobject object = env->AllocObject(savestate_info_class); const jobject object = env->AllocObject(savestate_info_class);
env->SetIntField(object, slot_field, static_cast<jint>(savestates[i].slot)); env->SetIntField(object, slot_field, static_cast<jint>(savestates[i].slot));
@ -726,17 +841,17 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo(
return array; 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) { [[maybe_unused]] jobject obj, jint slot) {
Core::System::GetInstance().SendSignal(Core::System::Signal::Save, 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) { [[maybe_unused]] jobject obj, jint slot) {
Core::System::GetInstance().SendSignal(Core::System::Signal::Load, 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) { [[maybe_unused]] jobject obj) {
LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
Common::g_scm_desc); 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()); LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
} }
} // extern "C" }

View File

@ -34,11 +34,25 @@
<item>@string/emulation_screen_layout_custom</item> <item>@string/emulation_screen_layout_custom</item>
</string-array> </string-array>
<string-array name="secondaryLayouts">
<item>@string/emulation_secondary_screen_default</item>
<item>@string/emulation_top_screen</item>
<item>@string/emulation_bottom_screen</item>
<item>@string/emulation_screen_layout_sidebyside</item>
</string-array>
<integer-array name="portraitLayoutValues"> <integer-array name="portraitLayoutValues">
<item>0</item> <item>0</item>
<item>1</item> <item>1</item>
</integer-array> </integer-array>
<integer-array name="secondaryLayoutValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</integer-array>
<string-array name="smallScreenPositions"> <string-array name="smallScreenPositions">
<item>@string/small_screen_position_top_right</item> <item>@string/small_screen_position_top_right</item>
<item>@string/small_screen_position_middle_right</item> <item>@string/small_screen_position_middle_right</item>

View File

@ -373,6 +373,7 @@
<string name="emulation_open_cheats">Open Cheats</string> <string name="emulation_open_cheats">Open Cheats</string>
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string> <string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
<string name="emulation_switch_portrait_layout">Portrait Screen Layout</string> <string name="emulation_switch_portrait_layout">Portrait Screen Layout</string>
<string name="emulation_switch_secondary_layout">Secondary Screen Layout</string>
<string name="emulation_screen_layout_largescreen">Large Screen</string> <string name="emulation_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string> <string name="emulation_screen_layout_portrait">Portrait</string>
<string name="emulation_screen_layout_single">Single Screen</string> <string name="emulation_screen_layout_single">Single Screen</string>
@ -380,6 +381,7 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string> <string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</string> <string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string> <string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_secondary_screen_default">System Default (mirror)</string>
<string name="emulation_screen_layout_custom">Custom Layout</string> <string name="emulation_screen_layout_custom">Custom Layout</string>
<string name="emulation_small_screen_position">Small Screen Position</string> <string name="emulation_small_screen_position">Small Screen Position</string>
<string name="small_screen_position_description">Where should the small screen appear relative to the large one in Large Screen Layout?</string> <string name="small_screen_position_description">Where should the small screen appear relative to the large one in Large Screen Layout?</string>

View File

@ -113,6 +113,7 @@ void LogSettings() {
} }
log_setting("Layout_LayoutOption", values.layout_option.GetValue()); log_setting("Layout_LayoutOption", values.layout_option.GetValue());
log_setting("Layout_PortraitLayoutOption", values.portrait_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_SwapScreen", values.swap_screen.GetValue());
log_setting("Layout_UprightScreen", values.upright_screen.GetValue()); log_setting("Layout_UprightScreen", values.upright_screen.GetValue());
log_setting("Layout_LargeScreenProportion", values.large_screen_proportion.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.delay_game_render_thread_us.SetGlobal(true);
values.layout_option.SetGlobal(true); values.layout_option.SetGlobal(true);
values.portrait_layout_option.SetGlobal(true); values.portrait_layout_option.SetGlobal(true);
values.secondary_screen_layout.SetGlobal(true);
values.swap_screen.SetGlobal(true); values.swap_screen.SetGlobal(true);
values.upright_screen.SetGlobal(true); values.upright_screen.SetGlobal(true);
values.large_screen_proportion.SetGlobal(true); values.large_screen_proportion.SetGlobal(true);

View File

@ -53,6 +53,12 @@ enum class PortraitLayoutOption : u32 {
PortraitCustomLayout, PortraitCustomLayout,
}; };
enum class SecondaryScreenLayout : u32 {
None,
TopScreenOnly,
BottomScreenOnly,
SideBySide
};
/** Defines where the small screen will appear relative to the large screen /** Defines where the small screen will appear relative to the large screen
* when in Large Screen mode * when in Large Screen mode
*/ */
@ -503,6 +509,7 @@ struct Values {
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"}; SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"};
SwitchableSetting<bool> swap_screen{false, "swap_screen"}; SwitchableSetting<bool> swap_screen{false, "swap_screen"};
SwitchableSetting<bool> upright_screen{false, "upright_screen"}; SwitchableSetting<bool> upright_screen{false, "upright_screen"};
SwitchableSetting<SecondaryScreenLayout> secondary_screen_layout{SecondaryScreenLayout::None, "secondary_screen_layout"};
SwitchableSetting<float, true> large_screen_proportion{4.f, 1.f, 16.f, SwitchableSetting<float, true> large_screen_proportion{4.f, 1.f, 16.f,
"large_screen_proportion"}; "large_screen_proportion"};
SwitchableSetting<SmallScreenPosition> small_screen_position{SmallScreenPosition::BottomRight, SwitchableSetting<SmallScreenPosition> small_screen_position{SmallScreenPosition::BottomRight,

View File

@ -251,6 +251,9 @@ void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_po
break; break;
} }
} }
#ifdef ANDROID
if (is_secondary) layout = Layout::AndroidSecondaryLayout(width,height);
#endif
UpdateMinimumWindowSize(min_size); UpdateMinimumWindowSize(min_size);
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) { if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) {

View File

@ -71,7 +71,7 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up
const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) || const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) ||
(Settings::values.screen_bottom_stretch.GetValue() && swapped); (Settings::values.screen_bottom_stretch.GetValue() && swapped);
const float window_aspect_ratio = static_cast<float>(height) / width; const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
if (stretched) { if (stretched) {
top_screen = {Settings::values.screen_top_leftright_padding.GetValue(), 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}; FramebufferLayout res{width, height, true, true, {}, {}, !upright};
// Split the window into two parts. Give proportional width to the smaller screen // 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 // To do that, find the total emulation box and maximize that based on window size
const float window_aspect_ratio = static_cast<float>(height) / width; const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
float emulation_aspect_ratio; float emulation_aspect_ratio;
float large_height = float large_height =
@ -297,6 +297,22 @@ FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary
return SingleFrameLayout(width, height, is_secondary, upright); 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) { FramebufferLayout CustomFrameLayout(u32 width, u32 height, bool is_swapped, bool is_portrait_mode) {
ASSERT(width > 0); ASSERT(width > 0);
ASSERT(height > 0); ASSERT(height > 0);

View File

@ -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); 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 * Factory method for constructing a custom FramebufferLayout
* @param width Window framebuffer width in pixels * @param width Window framebuffer width in pixels

View File

@ -35,7 +35,7 @@ void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) {
window.UpdateCurrentFramebufferLayout(layout.width, layout.height, is_portrait_mode); window.UpdateCurrentFramebufferLayout(layout.width, layout.height, is_portrait_mode);
}; };
update_layout(render_window); update_layout(render_window);
if (secondary_window) { if (secondary_window != nullptr) {
update_layout(*secondary_window); update_layout(*secondary_window);
} }
} }
@ -67,4 +67,13 @@ void RendererBase::RequestScreenshot(void* data, std::function<void(bool)> callb
settings.screenshot_requested = true; 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 } // namespace VideoCore

View File

@ -64,7 +64,8 @@ public:
virtual void Sync() {} virtual void Sync() {}
/// This is called to notify the rendering backend of a surface change /// 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 /// Returns the resolution scale factor relative to the native 3DS screen resolution
u32 GetResolutionScaleFactor(); u32 GetResolutionScaleFactor();
@ -110,7 +111,14 @@ protected:
Core::System& system; Core::System& system;
RendererSettings settings; RendererSettings settings;
Frontend::EmuWindow& render_window; ///< Reference to the render window handle. 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 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 s32 current_frame = 0; ///< Current frame, should be set by the renderer
}; };

View File

@ -87,6 +87,15 @@ RendererOpenGL::RendererOpenGL(Core::System& system, Pica::PicaCore& pica_,
} }
RendererOpenGL::~RendererOpenGL() = default; RendererOpenGL::~RendererOpenGL() = default;
void RendererOpenGL::setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) {
if (secondaryWindow) {
secondary_window = secondaryWindow;
secondary_window->mailbox = std::make_unique<OGLTextureMailbox>(driver.HasDebugTool());
}else {
secondary_window = nullptr;
// should I release something here? The mailbox??
}
}
void RendererOpenGL::SwapBuffers() { void RendererOpenGL::SwapBuffers() {
// Maintain the rasterizer's state as a priority // Maintain the rasterizer's state as a priority
@ -96,7 +105,7 @@ void RendererOpenGL::SwapBuffers() {
PrepareRendertarget(); PrepareRendertarget();
RenderScreenshot(); RenderScreenshot();
const auto& main_layout = render_window.GetFramebufferLayout(); const auto &main_layout = render_window.GetFramebufferLayout();
RenderToMailbox(main_layout, render_window.mailbox, false); RenderToMailbox(main_layout, render_window.mailbox, false);
#ifndef ANDROID #ifndef ANDROID
@ -106,6 +115,15 @@ void RendererOpenGL::SwapBuffers() {
RenderToMailbox(secondary_layout, secondary_window->mailbox, false); RenderToMailbox(secondary_layout, secondary_window->mailbox, false);
secondary_window->PollEvents(); 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 #endif
if (frame_dumper.IsDumping()) { if (frame_dumper.IsDumping()) {
try { 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].texture_2d = 0;
state.texture_units[0].sampler = 0; state.texture_units[0].sampler = 0;
state.Apply(); state.Apply();
} }
/** /**
* Draws a single texture to the emulator window, rotating the texture to correct for the 3DS's LCD * Draws a single texture to the emulator window, rotating the texture to correct for the 3DS's LCD

View File

@ -54,6 +54,7 @@ public:
void PrepareVideoDumping() override; void PrepareVideoDumping() override;
void CleanupVideoDumping() override; void CleanupVideoDumping() override;
void Sync() override; void Sync() override;
void setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) override;
private: private:
void InitOpenGLObjects(); void InitOpenGLObjects();

View File

@ -829,6 +829,16 @@ void RendererVulkan::SwapBuffers() {
RenderToWindow(*second_window, secondary_layout, false); RenderToWindow(*second_window, secondary_layout, false);
secondary_window->PollEvents(); secondary_window->PollEvents();
} }
#endif
#ifdef ANDROID
if (secondary_window) {
const auto &secondary_layout = secondary_window->GetFramebufferLayout();
if (!second_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler);
}
RenderToWindow(*second_window, secondary_layout, false);
secondary_window->PollEvents();
}
#endif #endif
rasterizer.TickFrame(); rasterizer.TickFrame();
EndFrame(); EndFrame();
@ -1119,4 +1129,10 @@ bool RendererVulkan::TryRenderScreenshotWithHostMemory() {
return true; return true;
} }
void RendererVulkan::NotifySurfaceChanged(bool second) {
if (second && second_window) second_window->NotifySurfaceChanged();
if (!second) main_window.NotifySurfaceChanged();
}
} // namespace Vulkan } // namespace Vulkan

View File

@ -74,9 +74,7 @@ public:
return &rasterizer; return &rasterizer;
} }
void NotifySurfaceChanged() override { void NotifySurfaceChanged(bool second) override;
main_window.NotifySurfaceChanged();
}
void SwapBuffers() override; void SwapBuffers() override;
void TryPresent(int timeout_ms, bool is_secondary) override {} void TryPresent(int timeout_ms, bool is_secondary) override {}