android: Implement secondary display support (#617)

* Enable the SecondScreenPresentation class

* Update everything to enable second screen on android under GL and Vulkan. Still some issues!

* Some attempts to enable surface changes

* OpenGL is working on surface change, vulkan still no

* release surfaces (also fixed vulkan?)

* added and enabled layout setting

* resolve merge conflict

* rearrange switch cases to satisfy linux compiler

* openGL is working!

* several vk changes to try to fix crashes

* maybe vulkan is working?

* removing unnecessary code attempts

* Simplified secondscreen for better performance

* vk_platform.cpp: Fixed build failure caused by bad rebase

* vk_present_window.h: Removed stray newline

* Applied clang-format

* bug fix for odin 2

* Applied clang-format

* Updated license headers

* Moved SecondScreen class to org.citra.citra_emu.display

* Various formatting and readability improvements

* Added brackets where previously absent for readability

* Additional readability improvement

* RendererVulkan::NotifySurfaceChanged: Simplified condition checking

* change all references to "secondary screen" to "secondary display" to limit confusion with top screen / bottom screen

* rename main_window to main_present_window and second_window to secondary_present_window

* Reverted accidentally downgraded compatibility list submodule

* Removed unnecessary log message

* Applied clang-format

* Added a description to the Secondary Display Screen Layout setting

* Added `_ptr` suffix to `secondary_present_window`

This distinguishes it as a pointer, as `main_present_window` isn't a pointer, so there could be confusion on whether to use `.` or `->`

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
David Griswold 2025-08-08 23:41:52 +03:00 committed by GitHub
parent 2697526f34
commit aca8b45664
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 431 additions and 82 deletions

View File

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

View File

@ -33,6 +33,7 @@ import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.SecondaryDisplay
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
@ -59,6 +60,7 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
private lateinit var secondaryDisplay: SecondaryDisplay;
private val emulationFragment: EmulationFragment
get() {
@ -73,10 +75,10 @@ class EmulationActivity : AppCompatActivity() {
requestWindowFeature(Window.FEATURE_NO_TITLE)
ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)
secondaryDisplay = SecondaryDisplay(this);
secondaryDisplay.updateDisplay();
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
@ -136,6 +138,11 @@ class EmulationActivity : AppCompatActivity() {
applyOrientationSettings() // Check for orientation settings changes on runtime
}
override fun onStop() {
secondaryDisplay.releasePresentation()
super.onStop()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
enableFullscreenImmersive()
@ -143,6 +150,7 @@ class EmulationActivity : AppCompatActivity() {
public override fun onRestart() {
super.onRestart()
secondaryDisplay.updateDisplay()
NativeLibrary.reloadCameraDevices()
}
@ -161,6 +169,9 @@ class EmulationActivity : AppCompatActivity() {
NativeLibrary.playTimeManagerStop()
isEmulationRunning = false
instance = null
secondaryDisplay.releasePresentation()
secondaryDisplay.releaseVD();
super.onDestroy()
}

View File

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

View File

@ -0,0 +1,113 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
import android.app.Presentation
import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.Bundle
import android.view.Display
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.IntSetting
class SecondaryDisplay(val context: Context) {
private var pres: SecondaryDisplayPresentation? = null
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val vd: VirtualDisplay
init {
val st = SurfaceTexture(0)
st.setDefaultBufferSize(1920, 1080)
val vdSurface = Surface(st)
vd = displayManager.createVirtualDisplay(
"HiddenDisplay",
1920,
1080,
320,
vdSurface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION
)
}
fun updateSurface() {
NativeLibrary.secondarySurfaceChanged(pres!!.getSurfaceHolder().surface)
}
fun destroySurface() {
NativeLibrary.secondarySurfaceDestroyed()
}
fun updateDisplay() {
// decide if we are going to the external display or the internal one
var display = getCustomerDisplay()
if (display == null ||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) {
display = vd.display
}
// if our presentation is already on the right display, ignore
if (pres?.display == display) return;
// otherwise, make a new presentation
releasePresentation()
pres = SecondaryDisplayPresentation(context, display!!, this)
pres?.show()
}
private fun getCustomerDisplay(): Display? {
val displays = displayManager.displays
// code taken from MelonDS dual screen - should fix odin 2 detection bug
return displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
.firstOrNull { it.displayId != Display.DEFAULT_DISPLAY && it.name != "Built-in Screen" && it.name != "HiddenDisplay"}
}
fun releasePresentation() {
pres?.dismiss()
pres = null
}
fun releaseVD() {
vd.release()
}
}
class SecondaryDisplayPresentation(
context: Context, display: Display, val parent: SecondaryDisplay
) : 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
) {
parent.updateSurface()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
parent.destroySurface()
}
})
setContentView(surfaceView) // Set SurfaceView as content
}
// Publicly accessible method to get the SurfaceHolder
fun getSurfaceHolder(): SurfaceHolder {
return surfaceView.holder
}
}

View File

@ -35,6 +35,7 @@ enum class IntSetting(
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
SCREEN_GAP("screen_gap",Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT("secondary_display_layout",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),

View File

@ -14,11 +14,8 @@ import android.os.Build
import android.text.TextUtils
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.math.min
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.display.PortraitScreenLayout
import org.citra.citra_emu.display.ScreenLayout
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
import org.citra.citra_emu.features.settings.model.AbstractSetting
@ -1111,6 +1108,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.SECONDARY_DISPLAY_LAYOUT,
R.string.emulation_switch_secondary_layout,
R.string.emulation_switch_secondary_layout_description,
R.array.secondaryLayouts,
R.array.secondaryLayoutValues,
IntSetting.SECONDARY_DISPLAY_LAYOUT.key,
IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.ASPECT_RATIO,

View File

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

View File

@ -288,6 +288,15 @@ swap_screen =
# 0 (default): Off, 1: On
expand_to_cutout_area =
# Secondary Display Layout
# What the game should do if a secondary display 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_display_layout =
# Screen placement settings when using Cardboard VR (render3d = 4)
# 30 - 100: Screen size as a percentage of the viewport. 85 (default)
cardboard_screen_size =

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -49,16 +49,16 @@ void EmuWindow_Android::OnFramebufferSizeChanged() {
const int bigger{window_width > window_height ? window_width : window_height};
const int smaller{window_width < window_height ? window_width : window_height};
if (is_portrait_mode) {
if (is_portrait_mode && !is_secondary) {
UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode);
} else {
UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode);
}
}
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface) : host_window{surface} {
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, bool is_secondary)
: EmuWindow{is_secondary}, host_window(surface) {
LOG_DEBUG(Frontend, "Initializing EmuWindow_Android");
if (!surface) {
LOG_CRITICAL(Frontend, "surface is nullptr");
return;

View File

@ -1,10 +1,11 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <vector>
#include <EGL/egl.h>
#include "core/frontend/emu_window.h"
namespace Core {
@ -13,7 +14,7 @@ class System;
class EmuWindow_Android : public Frontend::EmuWindow {
public:
EmuWindow_Android(ANativeWindow* surface);
EmuWindow_Android(ANativeWindow* surface, bool is_secondary = false);
~EmuWindow_Android();
/// Called by the onSurfaceChanges() method to change the surface
@ -30,7 +31,12 @@ public:
void DoneCurrent() override;
virtual void TryPresenting() {}
// EGL Context must be shared
// could probably use the existing
// SharedContext for this instead, this is maybe temporary
virtual EGLContext* GetEGLContext() {
return nullptr;
}
virtual void StopPresenting() {}
protected:

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -72,8 +72,9 @@ private:
EGLContext egl_context{};
};
EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface)
: EmuWindow_Android{surface}, system{system_} {
EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface,
bool is_secondary, EGLContext* sharedContext)
: EmuWindow_Android{surface, is_secondary}, system{system_} {
if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) {
LOG_CRITICAL(Frontend, "eglGetDisplay() failed");
return;
@ -96,9 +97,11 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ
if (eglQuerySurface(egl_display, egl_surface, EGL_HEIGHT, &window_height) != EGL_TRUE) {
return;
}
if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data());
egl_context == EGL_NO_CONTEXT) {
if (sharedContext) {
egl_context = *sharedContext;
} else if (egl_context =
eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data());
egl_context == EGL_NO_CONTEXT) {
LOG_CRITICAL(Frontend, "eglCreateContext() failed");
return;
}
@ -127,6 +130,10 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ
OnFramebufferSizeChanged();
}
EGLContext* EmuWindow_Android_OpenGL::GetEGLContext() {
return &egl_context;
}
bool EmuWindow_Android_OpenGL::CreateWindowSurface() {
if (!host_window) {
return true;
@ -204,14 +211,14 @@ void EmuWindow_Android_OpenGL::TryPresenting() {
return;
}
if (presenting_state == PresentingState::Initial) [[unlikely]] {
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
presenting_state = PresentingState::Running;
}
if (presenting_state != PresentingState::Running) [[unlikely]] {
return;
}
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0);
system.GPU().Renderer().TryPresent(0);
system.GPU().Renderer().TryPresent(100, is_secondary);
eglSwapBuffers(egl_display, egl_surface);
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -19,13 +19,14 @@ struct ANativeWindow;
class EmuWindow_Android_OpenGL : public EmuWindow_Android {
public:
EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface);
EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface, bool is_secondary,
EGLContext* sharedContext = NULL);
~EmuWindow_Android_OpenGL() override = default;
void TryPresenting() override;
void StopPresenting() override;
void PollEvents() override;
EGLContext* GetEGLContext() override;
std::unique_ptr<GraphicsContext> CreateSharedContext() const override;
private:

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -24,8 +24,9 @@ private:
};
EmuWindow_Android_Vulkan::EmuWindow_Android_Vulkan(
ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_)
: EmuWindow_Android{surface}, driver_library{driver_library_} {
ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_,
bool is_secondary)
: EmuWindow_Android{surface, is_secondary}, driver_library{driver_library_} {
CreateWindowSurface();
if (core_context = CreateSharedContext(); !core_context) {

View File

@ -1,4 +1,4 @@
// Copyright 2022 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -11,7 +11,8 @@ struct ANativeWindow;
class EmuWindow_Android_Vulkan : public EmuWindow_Android {
public:
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;
void PollEvents() override {}

View File

@ -16,11 +16,13 @@
#include <core/hle/service/cfg/cfg.h>
#include "audio_core/dsp_interface.h"
#include "common/arch.h"
#if CITRA_ARCH(arm64)
#include "common/aarch64/cpu_detect.h"
#elif CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h"
#endif
#include "common/common_paths.h"
#include "common/dynamic_library/dynamic_library.h"
#include "common/file_util.h"
@ -47,12 +49,18 @@
#include "jni/camera/ndk_camera.h"
#include "jni/camera/still_image_camera.h"
#include "jni/config.h"
#ifdef ENABLE_OPENGL
#include "jni/emu_window/emu_window_gl.h"
#endif
#ifdef ENABLE_VULKAN
#include "jni/emu_window/emu_window_vk.h"
#if CITRA_ARCH(arm64)
#include <adrenotools/driver.h>
#endif
#endif
#include "jni/id_cache.h"
#include "jni/input_manager.h"
#include "jni/ndk_motion.h"
@ -61,16 +69,14 @@
#include "video_core/gpu.h"
#include "video_core/renderer_base.h"
#if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64)
#include <adrenotools/driver.h>
#endif
namespace {
ANativeWindow* s_surf;
ANativeWindow* s_surface;
ANativeWindow* s_secondary_surface;
std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
std::unique_ptr<EmuWindow_Android> window;
std::unique_ptr<EmuWindow_Android> secondary_window;
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
jlong ptm_current_title_id = std::numeric_limits<jlong>::max(); // Arbitrary default value
@ -124,8 +130,17 @@ static void TryShutdown() {
}
window->DoneCurrent();
if (secondary_window) {
secondary_window->DoneCurrent();
}
Core::System::GetInstance().Shutdown();
window.reset();
if (secondary_window) {
secondary_window.reset();
}
InputManager::Shutdown();
MicroProfileShutdown();
}
@ -151,15 +166,21 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
Core::System& system{Core::System::GetInstance()};
const auto graphics_api = Settings::values.graphics_api.GetValue();
EGLContext* shared_context;
switch (graphics_api) {
#ifdef ENABLE_OPENGL
case Settings::GraphicsAPI::OpenGL:
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf);
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surface, false);
shared_context = window->GetEGLContext();
secondary_window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_secondary_surface,
true, shared_context);
break;
#endif
#ifdef ENABLE_VULKAN
case Settings::GraphicsAPI::Vulkan:
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surface, vulkan_library, false);
secondary_window =
std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface, vulkan_library, true);
break;
#endif
default:
@ -167,11 +188,17 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
"Unknown or unsupported graphics API {}, falling back to available default",
graphics_api);
#ifdef ENABLE_OPENGL
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf);
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surface, false);
shared_context = window->GetEGLContext();
secondary_window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_secondary_surface,
true, shared_context);
#elif ENABLE_VULKAN
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surface, vulkan_library);
secondary_window =
std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface, vulkan_library, true);
#else
// TODO: Add a null renderer backend for this, perhaps.
// TODO: Add a null renderer backend for this, perhaps.
#error "At least one renderer must be enabled."
#endif
break;
@ -208,7 +235,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
InputManager::Init();
window->MakeCurrent();
const Core::System::ResultStatus load_result{system.Load(*window, filepath)};
const Core::System::ResultStatus load_result{
system.Load(*window, filepath, secondary_window.get())};
if (load_result != Core::System::ResultStatus::Success) {
return load_result;
}
@ -262,6 +290,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
std::unique_lock pause_lock{paused_mutex};
running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; });
window->PollEvents();
// if (secondary_window) secondary_window->PollEvents();
}
}
@ -304,28 +333,68 @@ extern "C" {
void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
[[maybe_unused]] jobject obj,
jobject surf) {
s_surf = ANativeWindow_fromSurface(env, surf);
s_surface = ANativeWindow_fromSurface(env, surf);
bool notify = false;
if (window) {
notify = window->OnSurfaceChanged(s_surf);
notify = window->OnSurfaceChanged(s_surface);
}
auto& system = Core::System::GetInstance();
if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged();
system.GPU().Renderer().NotifySurfaceChanged(false);
}
LOG_INFO(Frontend, "Surface changed");
}
void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv* env,
[[maybe_unused]] jobject obj,
jobject surf) {
auto& system = Core::System::GetInstance();
if (s_secondary_surface) {
ANativeWindow_release(s_secondary_surface);
s_secondary_surface = nullptr;
}
s_secondary_surface = ANativeWindow_fromSurface(env, surf);
if (!s_secondary_surface) {
return;
}
bool notify = false;
if (secondary_window) {
// Second window already created, so update it
notify = secondary_window->OnSurfaceChanged(s_secondary_surface);
} else {
LOG_WARNING(Frontend,
"Second Window does not exist in native.cpp but surface changed. Ignoring.");
}
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) {
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_surfaceDestroyed([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj) {
if (s_surf != nullptr) {
ANativeWindow_release(s_surf);
s_surf = nullptr;
if (s_surface != nullptr) {
ANativeWindow_release(s_surface);
s_surface = nullptr;
if (window) {
window->OnSurfaceChanged(s_surf);
window->OnSurfaceChanged(s_surface);
}
}
}
@ -338,6 +407,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en
if (window) {
window->TryPresenting();
}
if (secondary_window) {
secondary_window->TryPresenting();
}
}
void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver(
@ -514,6 +586,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIE
stop_run = true;
pause_emulation = false;
window->StopPresenting();
if (secondary_window) {
secondary_window->StopPresenting();
}
running_cv.notify_all();
}

View File

@ -35,12 +35,26 @@
<item>@string/emulation_screen_layout_custom</item>
</string-array>
<string-array name="secondaryLayouts">
<item>@string/emulation_secondary_display_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">
<item>0</item>
<item>2</item>
<item>1</item>
</integer-array>
<integer-array name="secondaryLayoutValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</integer-array>
<string-array name="smallScreenPositions">
<item>@string/small_screen_position_top_right</item>
<item>@string/small_screen_position_middle_right</item>

View File

@ -437,6 +437,8 @@
<string name="emulation_aspect_ratio">Aspect Ratio</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_secondary_layout">Secondary Display Screen Layout</string>
<string name="emulation_switch_secondary_layout_description">The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string>
<string name="emulation_screen_layout_single">Single Screen</string>
@ -444,6 +446,7 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_secondary_display_default">System Default (mirror)</string>
<string name="emulation_screen_layout_custom">Custom Layout</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>

View File

@ -114,6 +114,7 @@ void LogSettings() {
}
log_setting("Layout_LayoutOption", values.layout_option.GetValue());
log_setting("Layout_PortraitLayoutOption", values.portrait_layout_option.GetValue());
log_setting("Layout_SecondaryDisplayLayout", values.secondary_display_layout.GetValue());
log_setting("Layout_SwapScreen", values.swap_screen.GetValue());
log_setting("Layout_UprightScreen", values.upright_screen.GetValue());
log_setting("Layout_ScreenGap", values.screen_gap.GetValue());
@ -207,6 +208,7 @@ void RestoreGlobalState(bool is_powered_on) {
values.delay_game_render_thread_us.SetGlobal(true);
values.layout_option.SetGlobal(true);
values.portrait_layout_option.SetGlobal(true);
values.secondary_display_layout.SetGlobal(true);
values.swap_screen.SetGlobal(true);
values.upright_screen.SetGlobal(true);
values.large_screen_proportion.SetGlobal(true);

View File

@ -54,6 +54,7 @@ enum class PortraitLayoutOption : u32 {
PortraitOriginal
};
enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide };
/** Defines where the small screen will appear relative to the large screen
* when in Large Screen mode
*/
@ -519,6 +520,8 @@ struct Values {
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"};
SwitchableSetting<bool> swap_screen{false, "swap_screen"};
SwitchableSetting<bool> upright_screen{false, "upright_screen"};
SwitchableSetting<SecondaryDisplayLayout> secondary_display_layout{SecondaryDisplayLayout::None,
"secondary_display_layout"};
SwitchableSetting<float, true> large_screen_proportion{4.f, 1.f, 16.f,
"large_screen_proportion"};
SwitchableSetting<int> screen_gap{0, "screen_gap"};

View File

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

View File

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -254,6 +254,9 @@ public:
bool is_portrait_mode = {});
std::unique_ptr<TextureMailbox> mailbox = nullptr;
bool isSecondary() const {
return is_secondary;
}
protected:
EmuWindow();

View File

@ -79,7 +79,7 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up
// TODO: This is kind of gross, make it platform agnostic. -OS
#ifdef ANDROID
const float window_aspect_ratio = static_cast<float>(height) / width;
const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
const auto aspect_ratio_setting = Settings::values.aspect_ratio.GetValue();
float emulation_aspect_ratio = (swapped) ? BOT_SCREEN_ASPECT_RATIO : TOP_SCREEN_ASPECT_RATIO;
@ -172,7 +172,7 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr
emulation_height = std::max(large_height, small_height);
}
const float window_aspect_ratio = static_cast<float>(height) / width;
const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
const float emulation_aspect_ratio = emulation_height / emulation_width;
Common::Rectangle<u32> screen_window_area{0, 0, width, height};
@ -281,7 +281,7 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u
// Split the window into two parts. Give 2.25x width to the main screen,
// and make a bar on the right side with 1x width top screen and 1.25x width bottom screen
// 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);
const float scale_factor = 2.25f;
float main_screen_aspect_ratio = TOP_SCREEN_ASPECT_RATIO;
@ -338,6 +338,24 @@ FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary
return SingleFrameLayout(width, height, is_secondary, upright);
}
FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) {
const Settings::SecondaryDisplayLayout layout =
Settings::values.secondary_display_layout.GetValue();
switch (layout) {
case Settings::SecondaryDisplayLayout::BottomScreenOnly:
return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::SideBySide:
return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(),
1.0f, Settings::SmallScreenPosition::MiddleRight);
case Settings::SecondaryDisplayLayout::None:
// this should never happen, but if it does, somehow, send the top screen
case Settings::SecondaryDisplayLayout::TopScreenOnly:
default:
return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue());
}
}
FramebufferLayout CustomFrameLayout(u32 width, u32 height, bool is_swapped, bool is_portrait_mode) {
ASSERT(width > 0);
ASSERT(height > 0);

View File

@ -133,6 +133,14 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u
*/
FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary, bool upright);
/**
* Method for constructing the secondary layout for Android, based on
* the appropriate setting.
* @param width Window framebuffer width in pixels
* @param height Window framebuffer height in pixels
*/
FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height);
/**
* Factory method for constructing a custom FramebufferLayout
* @param width Window framebuffer width in pixels

View File

@ -1,4 +1,4 @@
// Copyright 2015 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -35,7 +35,7 @@ void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) {
window.UpdateCurrentFramebufferLayout(layout.width, layout.height, is_portrait_mode);
};
update_layout(render_window);
if (secondary_window) {
if (secondary_window != nullptr) {
update_layout(*secondary_window);
}
}
@ -66,5 +66,4 @@ void RendererBase::RequestScreenshot(void* data, std::function<void(bool)> callb
settings.screenshot_framebuffer_layout = layout;
settings.screenshot_requested = true;
}
} // namespace VideoCore

View File

@ -61,7 +61,8 @@ public:
virtual void CleanupVideoDumping() {}
/// This is called to notify the rendering backend of a surface change
virtual void NotifySurfaceChanged() {}
// if second == true then it is the second screen
virtual void NotifySurfaceChanged(bool second) {}
/// Returns the resolution scale factor relative to the native 3DS screen resolution
u32 GetResolutionScaleFactor();
@ -106,10 +107,12 @@ public:
protected:
Core::System& system;
RendererSettings settings;
Frontend::EmuWindow& render_window; ///< Reference to the render window handle.
Frontend::EmuWindow* secondary_window; ///< Reference to the secondary render window handle.
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
Frontend::EmuWindow& render_window; /// Reference to the render window handle.
Frontend::EmuWindow* secondary_window; /// Reference to the secondary render window handle.
protected:
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
};
} // namespace VideoCore

View File

@ -100,7 +100,15 @@ void RendererOpenGL::SwapBuffers() {
const auto& main_layout = render_window.GetFramebufferLayout();
RenderToMailbox(main_layout, render_window.mailbox, false);
#ifndef ANDROID
#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();
}
#else
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
@ -108,6 +116,7 @@ void RendererOpenGL::SwapBuffers() {
secondary_window->PollEvents();
}
#endif
if (frame_dumper.IsDumping()) {
try {
RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true);

View File

@ -105,27 +105,33 @@ RendererVulkan::RendererVulkan(Core::System& system, Pica::PicaCore& pica_,
: RendererBase{system, window, secondary_window}, memory{system.Memory()}, pica{pica_},
instance{window, Settings::values.physical_device.GetValue()}, scheduler{instance},
renderpass_cache{instance, scheduler},
main_window{window, instance, scheduler, IsLowRefreshRate()},
main_present_window{window, instance, scheduler, IsLowRefreshRate()},
vertex_buffer{instance, scheduler, vk::BufferUsageFlagBits::eVertexBuffer,
VERTEX_BUFFER_SIZE},
update_queue{instance},
rasterizer{
memory, pica, system.CustomTexManager(), *this, render_window,
instance, scheduler, renderpass_cache, update_queue, main_window.ImageCount()},
update_queue{instance}, rasterizer{memory,
pica,
system.CustomTexManager(),
*this,
render_window,
instance,
scheduler,
renderpass_cache,
update_queue,
main_present_window.ImageCount()},
present_heap{instance, scheduler.GetMasterSemaphore(), PRESENT_BINDINGS, 32} {
CompileShaders();
BuildLayouts();
BuildPipelines();
if (secondary_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler,
IsLowRefreshRate());
secondary_present_window_ptr = std::make_unique<PresentWindow>(
*secondary_window, instance, scheduler, IsLowRefreshRate());
}
}
RendererVulkan::~RendererVulkan() {
vk::Device device = instance.GetDevice();
scheduler.Finish();
main_window.WaitPresent();
main_present_window.WaitPresent();
device.waitIdle();
device.destroyShaderModule(present_vertex_shader);
@ -177,7 +183,8 @@ void RendererVulkan::PrepareDraw(Frame* frame, const Layout::FramebufferLayout&
}
renderpass_cache.EndRendering();
scheduler.Record([this, layout, frame, present_set, renderpass = main_window.Renderpass(),
scheduler.Record([this, layout, frame, present_set,
renderpass = main_present_window.Renderpass(),
index = current_pipeline](vk::CommandBuffer cmdbuf) {
const vk::Viewport viewport = {
.x = 0.0f,
@ -434,7 +441,7 @@ void RendererVulkan::BuildPipelines() {
.pColorBlendState = &color_blending,
.pDynamicState = &dynamic_info,
.layout = *present_pipeline_layout,
.renderPass = main_window.Renderpass(),
.renderPass = main_present_window.Renderpass(),
};
const auto [result, pipeline] =
@ -887,19 +894,32 @@ void RendererVulkan::SwapBuffers() {
const Layout::FramebufferLayout& layout = render_window.GetFramebufferLayout();
PrepareRendertarget();
RenderScreenshot();
RenderToWindow(main_window, layout, false);
RenderToWindow(main_present_window, layout, false);
#ifndef ANDROID
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
if (!second_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler,
IsLowRefreshRate());
if (!secondary_present_window_ptr) {
secondary_present_window_ptr = std::make_unique<PresentWindow>(
*secondary_window, instance, scheduler, IsLowRefreshRate());
}
RenderToWindow(*second_window, secondary_layout, false);
RenderToWindow(*secondary_present_window_ptr, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
#ifdef ANDROID
if (secondary_window) {
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
if (!secondary_present_window_ptr) {
secondary_present_window_ptr = std::make_unique<PresentWindow>(
*secondary_window, instance, scheduler, IsLowRefreshRate());
}
RenderToWindow(*secondary_present_window_ptr, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
system.perf_stats->EndSwap();
rasterizer.TickFrame();
EndFrame();
@ -954,7 +974,7 @@ void RendererVulkan::RenderScreenshotWithStagingCopy() {
vk::Buffer staging_buffer{unsafe_buffer};
Frame frame{};
main_window.RecreateFrame(&frame, width, height);
main_present_window.RecreateFrame(&frame, width, height);
DrawScreens(&frame, layout, false);
@ -1127,7 +1147,7 @@ bool RendererVulkan::TryRenderScreenshotWithHostMemory() {
device.bindBufferMemory(imported_buffer.get(), imported_memory.get(), 0);
Frame frame{};
main_window.RecreateFrame(&frame, width, height);
main_present_window.RecreateFrame(&frame, width, height);
DrawScreens(&frame, layout, false);
@ -1190,4 +1210,14 @@ bool RendererVulkan::TryRenderScreenshotWithHostMemory() {
return true;
}
void RendererVulkan::NotifySurfaceChanged(bool is_second_window) {
if (is_second_window) {
if (secondary_present_window_ptr) {
secondary_present_window_ptr->NotifySurfaceChanged();
}
} else {
main_present_window.NotifySurfaceChanged();
}
}
} // namespace Vulkan

View File

@ -74,9 +74,7 @@ public:
return &rasterizer;
}
void NotifySurfaceChanged() override {
main_window.NotifySurfaceChanged();
}
void NotifySurfaceChanged(bool second) override;
void SwapBuffers() override;
void TryPresent(int timeout_ms, bool is_secondary) override {}
@ -117,11 +115,11 @@ private:
Instance instance;
Scheduler scheduler;
RenderManager renderpass_cache;
PresentWindow main_window;
PresentWindow main_present_window;
StreamBuffer vertex_buffer;
DescriptorUpdateQueue update_queue;
RasterizerVulkan rasterizer;
std::unique_ptr<PresentWindow> second_window;
std::unique_ptr<PresentWindow> secondary_present_window_ptr;
DescriptorHeap present_heap;
vk::UniquePipelineLayout present_pipeline_layout;