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

@ -49,3 +49,17 @@ enum class PortraitScreenLayout(val int: Int) {
} }
} }
} }
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,15 +69,20 @@
#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};
@ -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();
} }
@ -148,12 +165,21 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
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);
if (secondary_enabled) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,
true, c);
}
break; 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);
if (secondary_enabled)
second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface,
vulkan_library, true);
break; break;
#endif #endif
default: default:
@ -161,9 +187,16 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
"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."
@ -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,6 +283,7 @@ 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();
} }
} }
@ -300,12 +335,83 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
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_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, 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) {
@ -325,6 +431,9 @@ 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(
@ -389,7 +498,8 @@ 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,
const std::string &virtual_name) {
std::string path = directory + virtual_name; std::string path = directory + virtual_name;
if (FileUtil::IsDirectory(path)) { if (FileUtil::IsDirectory(path)) {
path += '/'; path += '/';
@ -481,6 +591,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIE
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();
} }
@ -511,7 +622,8 @@ 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,
[[maybe_unused]] jstring j_device,
jint axis, jfloat x, jfloat y) { 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
@ -530,7 +642,8 @@ 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,
[[maybe_unused]] jstring j_device,
jint axis_id, jfloat axis_val) { 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));
@ -686,7 +799,8 @@ JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_utils_CiaInstallWorker_insta
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);
@ -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));
@ -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
@ -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 {

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