From 52455587b5eec94119065d02db1bbb5801581280 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 14:12:10 +0300 Subject: [PATCH 01/21] Add a new Secondary Display Layout option on android that makes the secondary display honor swap button --- .../src/main/java/org/citra/citra_emu/display/ScreenLayout.kt | 4 +++- src/android/app/src/main/res/values/arrays.xml | 3 +++ src/android/app/src/main/res/values/strings.xml | 1 + src/common/settings.h | 2 +- src/core/frontend/framebuffer_layout.cpp | 3 ++- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt index c46dcadd8..5d299ed0e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt @@ -56,7 +56,9 @@ enum class SecondaryDisplayLayout(val int: Int) { NONE(0), TOP_SCREEN(1), BOTTOM_SCREEN(2), - SIDE_BY_SIDE(3); + SIDE_BY_SIDE(3), + + REVERSE_PRIMARY(4); companion object { fun from(int: Int): SecondaryDisplayLayout { diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 2a08cd546..29845f486 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -37,9 +37,11 @@ @string/emulation_secondary_display_default + @string/emulation_secondary_display_reverse_primary @string/emulation_top_screen @string/emulation_bottom_screen @string/emulation_screen_layout_sidebyside + @@ -50,6 +52,7 @@ 0 + 4 1 2 3 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index a94611efc..015de4797 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -510,6 +510,7 @@ Original Default System Default (mirror) + Opposite of Primary Display Custom Layout Background Color The color which appears behind the screens during emulation, represented as an RGB value. diff --git a/src/common/settings.h b/src/common/settings.h index 4196557d1..345b2d1cd 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -57,7 +57,7 @@ enum class PortraitLayoutOption : u32 { PortraitOriginal }; -enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide }; +enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide, ReversePrimary }; /** Defines where the small screen will appear relative to the large screen * when in Large Screen mode */ diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 918c1454f..7bd731966 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -305,7 +305,8 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { - + case Settings::SecondaryDisplayLayout::ReversePrimary: + return SingleFrameLayout(width,height,! Settings::values.swap_screen,Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::BottomScreenOnly: return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::SideBySide: From da0eebdf861b9fb4230d6558795cd7b233f1aeee Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 21:29:37 +0300 Subject: [PATCH 02/21] add quick menu option for secondary layout --- .../citra_emu/display/ScreenAdjustmentUtil.kt | 7 +++ .../citra_emu/fragments/EmulationFragment.kt | 61 +++++++++++++++++++ .../res/drawable/ic_secondary_fit_screen.xml | 13 ++++ .../app/src/main/res/menu/menu_in_game.xml | 5 ++ .../res/menu/menu_secondary_screen_layout.xml | 28 +++++++++ 5 files changed, 114 insertions(+) create mode 100644 src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml create mode 100644 src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index e63960fa8..cf18a175c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -71,6 +71,13 @@ class ScreenAdjustmentUtil( NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) } + fun changeSecondaryOrientation(layoutOption: Int) { + IntSetting.SECONDARY_DISPLAY_LAYOUT.int = layoutOption + settings.saveSetting(IntSetting.SECONDARY_DISPLAY_LAYOUT,SettingsFile.FILE_NAME_CONFIG) + NativeLibrary.reloadSettings() + NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) + } + fun changeActivityOrientation(orientationOption: Int) { val activity = context as? Activity ?: return IntSetting.ORIENTATION_OPTION.int = orientationOption diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index e1c1fc076..9d2945cf9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -67,6 +67,7 @@ import org.citra.citra_emu.databinding.FragmentEmulationBinding import org.citra.citra_emu.display.PortraitScreenLayout import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.display.ScreenLayout +import org.citra.citra_emu.display.SecondaryDisplayLayout 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.Settings @@ -336,6 +337,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram true } + R.id.menu_secondary_screen_layout -> { + showSecondaryScreenLayoutMenu() + true + } + R.id.menu_swap_screens -> { screenAdjustmentUtil.swapScreen() true @@ -1039,6 +1045,61 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.show() } + private fun showSecondaryScreenLayoutMenu() { + val popupMenu = PopupMenu( + requireContext(), + binding.inGameMenu.findViewById(R.id.menu_secondary_screen_layout) + ) + + popupMenu.menuInflater.inflate(R.menu.menu_secondary_screen_layout, popupMenu.menu) + + val layoutOptionMenuItem = when (IntSetting.SECONDARY_DISPLAY_LAYOUT.int) { + SecondaryDisplayLayout.NONE.int -> + R.id.menu_secondary_layout_none + SecondaryDisplayLayout.REVERSE_PRIMARY.int -> + R.id.menu_secondary_layout_reverse_primary + SecondaryDisplayLayout.TOP_SCREEN.int -> + R.id.menu_secondary_layout_top + SecondaryDisplayLayout.BOTTOM_SCREEN.int -> + R.id.menu_secondary_layout_bottom + else -> + R.id.menu_secondary_layout_side_by_side + + } + + popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true) + + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_secondary_layout_none -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int) + true + } + + R.id.menu_secondary_layout_reverse_primary -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.REVERSE_PRIMARY.int) + true + } + R.id.menu_secondary_layout_top -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.TOP_SCREEN.int) + true + } + R.id.menu_secondary_layout_bottom -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.BOTTOM_SCREEN.int) + true + } + R.id.menu_secondary_layout_side_by_side -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.SIDE_BY_SIDE.int) + true + } + + + else -> true + } + } + + popupMenu.show() + } private fun editControlsPlacement() { if (binding.surfaceInputOverlay.isInEditMode) { binding.doneControlConfig.visibility = View.GONE diff --git a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml new file mode 100644 index 000000000..3f7d5ac06 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index 950ab6fc8..410c1ddff 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -32,6 +32,11 @@ android:icon="@drawable/ic_portrait_fit_screen" android:title="@string/emulation_switch_portrait_layout" /> + + + + + + + + + + + + + + + + + + + From 0fdd5781c66d8f5fa62a5d1afdd3687ec629115b Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 22:02:08 +0300 Subject: [PATCH 03/21] fix icon --- .../src/main/res/drawable/ic_secondary_fit_screen.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml index 3f7d5ac06..184e6be4c 100644 --- a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml +++ b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml @@ -3,11 +3,10 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorOnSurface"> - + android:fillColor="@android:color/black" + android:fillType="evenOdd" + android:pathData="M17 4h3c1.1 0 2 0.9 2 2v2h-2V6h-3ZM4 8V6h3V4H4C2.9 4 2 4.9 2 6v2z m16 8v2h-3v2h3c1.1 0 2-0.9 2-2v-2ZM7 18H4v-2H2v2c0 1.1 0.9 2 2 2h3ZM18 8H6v8h12z M12.6 10.23q-0.07-0.17-0.21-0.28-0.13-0.12-0.26-0.14-0.42 0-0.77 0.23-0.35 0.22-0.64 0.53-0.13 0.1-0.29 0.1-0.16 0-0.27-0.1-0.1-0.12-0.1-0.3 0-0.15 0.1-0.3 0.18-0.19 0.39-0.37 0.21-0.2 0.45-0.34 0.24-0.14 0.5-0.23 0.25-0.09 0.52-0.09 0.31 0 0.58 0.14 0.27 0.12 0.47 0.36 0.2 0.24 0.31 0.56 0.12 0.33 0.12 0.72 0 0.12-0.03 0.26-0.1 0.44-0.33 0.72-0.21 0.28-0.48 0.48-0.26 0.2-0.55 0.36-0.28 0.15-0.53 0.35-0.24 0.2-0.42 0.47-0.19 0.27-0.25 0.7h1.84q0.11 0 0.17-0.03l0.09-0.06 0.1-0.12q0.08-0.1 0.12-0.11l0.1-0.04 0.12-0.01q0.18 0 0.28 0.12 0.1 0.12 0.1 0.28 0 0.11-0.02 0.17l-0.04 0.08q-0.18 0.26-0.43 0.42-0.25 0.15-0.56 0.15h-2.3q-0.38-0.03-0.42-0.45 0.03-0.32 0.1-0.62 0.05-0.3 0.16-0.59 0.1-0.28 0.27-0.53 0.16-0.25 0.41-0.45 0.22-0.17 0.51-0.33 0.3-0.15 0.55-0.34 0.26-0.19 0.45-0.44 0.18-0.25 0.18-0.6 0-0.1-0.04-0.21l-0.05-0.12z"/> \ No newline at end of file From a28a0c3f6d61750a4d90e409c458678d5f34cf38 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 27 Oct 2025 09:18:56 +0300 Subject: [PATCH 04/21] added other secondary layouts --- .../citra/citra_emu/display/ScreenLayout.kt | 7 ++++-- .../citra_emu/fragments/EmulationFragment.kt | 18 +++++++++++++++ .../res/menu/menu_secondary_screen_layout.xml | 9 ++++++++ .../app/src/main/res/values/arrays.xml | 6 +++++ .../app/src/main/res/values/strings.xml | 2 +- src/common/settings.h | 2 +- src/core/frontend/framebuffer_layout.cpp | 22 ++++++++++++++----- 7 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt index 5d299ed0e..c705c2104 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt @@ -57,8 +57,11 @@ enum class SecondaryDisplayLayout(val int: Int) { TOP_SCREEN(1), BOTTOM_SCREEN(2), SIDE_BY_SIDE(3), - - REVERSE_PRIMARY(4); + REVERSE_PRIMARY(4), + ORIGINAL(5), + HYBRID(6), + LARGE_SCREEN(7) + ; companion object { fun from(int: Int): SecondaryDisplayLayout { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 9d2945cf9..5a440e395 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -1062,6 +1062,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram R.id.menu_secondary_layout_top SecondaryDisplayLayout.BOTTOM_SCREEN.int -> R.id.menu_secondary_layout_bottom + SecondaryDisplayLayout.HYBRID.int -> + R.id.menu_secondary_layout_hybrid + SecondaryDisplayLayout.LARGE_SCREEN.int -> + R.id.menu_secondary_layout_largescreen + SecondaryDisplayLayout.ORIGINAL.int -> + R.id.menu_secondary_layout_original else -> R.id.menu_secondary_layout_side_by_side @@ -1092,6 +1098,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.SIDE_BY_SIDE.int) true } + R.id.menu_secondary_layout_hybrid -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.HYBRID.int) + true + } + R.id.menu_secondary_layout_original -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.ORIGINAL.int) + true + } + R.id.menu_secondary_layout_largescreen -> { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.LARGE_SCREEN.int) + true + } else -> true diff --git a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml index 5e609e315..69d6c66dc 100644 --- a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml +++ b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml @@ -22,6 +22,15 @@ + + + diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 29845f486..0f5dfbedb 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -41,6 +41,9 @@ @string/emulation_top_screen @string/emulation_bottom_screen @string/emulation_screen_layout_sidebyside + @string/emulation_screen_layout_original + @string/emulation_screen_layout_hybrid + @string/emulation_screen_layout_largescreen @@ -56,6 +59,9 @@ 1 2 3 + 5 + 6 + 7 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 015de4797..2c265c79b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -500,7 +500,7 @@ Aspect Ratio Landscape Screen Layout Portrait Screen Layout - Secondary Display Screen Layout + Secondary Display Layout The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast) Large Screen Portrait diff --git a/src/common/settings.h b/src/common/settings.h index 345b2d1cd..61656e01c 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -57,7 +57,7 @@ enum class PortraitLayoutOption : u32 { PortraitOriginal }; -enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide, ReversePrimary }; +enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide, ReversePrimary, Original, Hybrid, LargeScreen }; /** Defines where the small screen will appear relative to the large screen * when in Large Screen mode */ diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 7bd731966..f87c1fdf7 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -305,18 +305,30 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { - case Settings::SecondaryDisplayLayout::ReversePrimary: - return SingleFrameLayout(width,height,! Settings::values.swap_screen,Settings::values.upright_screen.GetValue()); + case Settings::SecondaryDisplayLayout::TopScreenOnly: + return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); + 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::LargeScreen: + return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(), + Settings::values.large_screen_proportion.GetValue(), + Settings::values.small_screen_position.GetValue()); + case Settings::SecondaryDisplayLayout::Original: + return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(), + 1.0f, Settings::SmallScreenPosition::BelowLarge); + case Settings::SecondaryDisplayLayout::Hybrid: + return HybridScreenLayout(width, height, false, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::None: - // this should never happen, but if it does, somehow, send the top screen - case Settings::SecondaryDisplayLayout::TopScreenOnly: + // this should never happen - if "none" is set this method shouldn't run - but if it does, + // somehow, use ReversePrimary + case Settings::SecondaryDisplayLayout::ReversePrimary: default: - return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); + return SingleFrameLayout(width, height, !Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue()); } } From f46596a9d88aa7720010aaac687e12d7739875de Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 14:12:10 +0300 Subject: [PATCH 05/21] Add a new Secondary Display Layout option on android that makes the secondary display honor swap button # Conflicts: # src/core/frontend/framebuffer_layout.cpp --- src/android/app/src/main/res/values/arrays.xml | 1 - src/core/frontend/framebuffer_layout.cpp | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 0f5dfbedb..8efc480b6 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -44,7 +44,6 @@ @string/emulation_screen_layout_original @string/emulation_screen_layout_hybrid @string/emulation_screen_layout_largescreen - diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index f87c1fdf7..f7710510f 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -305,6 +305,8 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { + case Settings::SecondaryDisplayLayout::ReversePrimary: + return SingleFrameLayout(width,height,! Settings::values.swap_screen,Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::TopScreenOnly: return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); From 6f5f6b2830172a7c79d011f0643d08307048cdb0 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 21:29:37 +0300 Subject: [PATCH 06/21] add quick menu option for secondary layout --- .../src/main/res/drawable/ic_secondary_fit_screen.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml index 184e6be4c..3f7d5ac06 100644 --- a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml +++ b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml @@ -3,10 +3,11 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorOnSurface"> + android:viewportHeight="24"> + android:fillColor="#FF000000" + android:pathData="M17 4h3c1.1 0 2 0.9 2 2v2h-2V6h-3ZM4 8V6h3V4H4C2.9 4 2 4.9 2 6v2z m16 8v2h-3v2h3c1.1 0 2-0.9 2-2v-2ZM7 18H4v-2H2v2c0 1.1 0.9 2 2 2h3ZM18 8H6v8h12z"/> + \ No newline at end of file From 3c803af0f5189676bb85f815a875982e1d77899e Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 22:02:08 +0300 Subject: [PATCH 07/21] fix icon --- .../src/main/res/drawable/ic_secondary_fit_screen.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml index 3f7d5ac06..184e6be4c 100644 --- a/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml +++ b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml @@ -3,11 +3,10 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorOnSurface"> - + android:fillColor="@android:color/black" + android:fillType="evenOdd" + android:pathData="M17 4h3c1.1 0 2 0.9 2 2v2h-2V6h-3ZM4 8V6h3V4H4C2.9 4 2 4.9 2 6v2z m16 8v2h-3v2h3c1.1 0 2-0.9 2-2v-2ZM7 18H4v-2H2v2c0 1.1 0.9 2 2 2h3ZM18 8H6v8h12z M12.6 10.23q-0.07-0.17-0.21-0.28-0.13-0.12-0.26-0.14-0.42 0-0.77 0.23-0.35 0.22-0.64 0.53-0.13 0.1-0.29 0.1-0.16 0-0.27-0.1-0.1-0.12-0.1-0.3 0-0.15 0.1-0.3 0.18-0.19 0.39-0.37 0.21-0.2 0.45-0.34 0.24-0.14 0.5-0.23 0.25-0.09 0.52-0.09 0.31 0 0.58 0.14 0.27 0.12 0.47 0.36 0.2 0.24 0.31 0.56 0.12 0.33 0.12 0.72 0 0.12-0.03 0.26-0.1 0.44-0.33 0.72-0.21 0.28-0.48 0.48-0.26 0.2-0.55 0.36-0.28 0.15-0.53 0.35-0.24 0.2-0.42 0.47-0.19 0.27-0.25 0.7h1.84q0.11 0 0.17-0.03l0.09-0.06 0.1-0.12q0.08-0.1 0.12-0.11l0.1-0.04 0.12-0.01q0.18 0 0.28 0.12 0.1 0.12 0.1 0.28 0 0.11-0.02 0.17l-0.04 0.08q-0.18 0.26-0.43 0.42-0.25 0.15-0.56 0.15h-2.3q-0.38-0.03-0.42-0.45 0.03-0.32 0.1-0.62 0.05-0.3 0.16-0.59 0.1-0.28 0.27-0.53 0.16-0.25 0.41-0.45 0.22-0.17 0.51-0.33 0.3-0.15 0.55-0.34 0.26-0.19 0.45-0.44 0.18-0.25 0.18-0.6 0-0.1-0.04-0.21l-0.05-0.12z"/> \ No newline at end of file From a603f261e7ceb192c9d849e68d9b6edccf69e472 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 27 Oct 2025 09:18:56 +0300 Subject: [PATCH 08/21] added other secondary layouts --- src/android/app/src/main/res/values/arrays.xml | 1 + src/core/frontend/framebuffer_layout.cpp | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 8efc480b6..0f5dfbedb 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -44,6 +44,7 @@ @string/emulation_screen_layout_original @string/emulation_screen_layout_hybrid @string/emulation_screen_layout_largescreen + diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index f7710510f..303661059 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -305,9 +305,7 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { - case Settings::SecondaryDisplayLayout::ReversePrimary: - return SingleFrameLayout(width,height,! Settings::values.swap_screen,Settings::values.upright_screen.GetValue()); - case Settings::SecondaryDisplayLayout::TopScreenOnly: + case Settings::SecondaryDisplayLayout::TopScreenOnly: return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::BottomScreenOnly: From f94b2ad5da4b7983987b2aab077ca389db34d657 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 11 Nov 2025 22:05:43 +0300 Subject: [PATCH 09/21] updated secondary menu with functionality to switch external displays  Conflicts:  src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt --- .../citra_emu/activities/EmulationActivity.kt | 2 +- .../citra_emu/display/SecondaryDisplay.kt | 51 ++++--- .../citra_emu/fragments/EmulationFragment.kt | 136 ++++++++++++++---- src/android/app/src/main/jni/config.cpp | 2 +- .../src/main/jni/emu_window/emu_window.cpp | 17 +-- src/android/app/src/main/jni/native.cpp | 5 + .../app/src/main/res/menu/menu_in_game.xml | 2 +- .../res/menu/menu_secondary_screen_layout.xml | 30 +++- .../app/src/main/res/values/strings.xml | 5 +- 9 files changed, 183 insertions(+), 67 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 957b98611..7cdcee349 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -64,7 +64,7 @@ class EmulationActivity : AppCompatActivity() { private lateinit var binding: ActivityEmulationBinding private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var hotkeyUtility: HotkeyUtility - private lateinit var secondaryDisplay: SecondaryDisplay + lateinit var secondaryDisplay: SecondaryDisplay private val onShutdown = Runnable { if (intent.getBooleanExtra("launched_from_shortcut", false)) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index d09daab41..a58e6bece 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -6,21 +6,28 @@ 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.Build import android.os.Bundle +import android.util.Log import android.view.Display import android.view.MotionEvent +import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import android.view.WindowManager import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.display.SecondaryDisplayLayout import org.citra.citra_emu.NativeLibrary class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { private var pres: SecondaryDisplayPresentation? = null private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val vd: VirtualDisplay + var preferredDisplayId = -1 + var currentDisplayId = -1 init { vd = displayManager.createVirtualDisplay( @@ -42,24 +49,26 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { NativeLibrary.secondarySurfaceDestroyed() } - private fun getExternalDisplay(context: Context): Display? { - val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val currentDisplayId = context.display.displayId + fun getSecondaryDisplays(context: Context): List { + val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val currentDisplayId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.display.displayId + } else { + @Suppress("DEPRECATION") + (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .defaultDisplay.displayId + } val displays = dm.displays val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); - val extDisplays = displays.filter { + return displays.filter { val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId } - val isNotDefaultOrPresentable = it.displayId != Display.DEFAULT_DISPLAY || isPresentable - isNotDefaultOrPresentable && + val isNotDefaultOrPresentable = it != null && it.displayId != Display.DEFAULT_DISPLAY || isPresentable + isNotDefaultOrPresentable && it.displayId != currentDisplayId && it.name != "HiddenDisplay" && it.state != Display.STATE_OFF && it.isValid } - // if there is a display called Built-In Display or Built-In Screen, prioritize the OTHER screen - val selected = extDisplays.firstOrNull { ! it.name.contains("Built",true) } - ?: extDisplays.firstOrNull() - return selected } fun updateDisplay() { @@ -67,12 +76,20 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { if (context is android.app.Activity && (context.isFinishing || context.isDestroyed)) { return } + val displays = getSecondaryDisplays(context) + val display = if (displays.isEmpty() || + IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int + ) { + currentDisplayId = -1 + vd.display + } else if (preferredDisplayId >=0 && displays.any { it.displayId == preferredDisplayId }) { - // decide if we are going to the external display or the internal one - var display = getExternalDisplay(context) - if (display == null || - IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) { - display = vd.display + currentDisplayId = preferredDisplayId + displays.first { it.displayId == preferredDisplayId } + } else { + //TODO: re-enable the filter of "built-in displays" odin style to pick default + currentDisplayId = displays[0].displayId + displays[0] } // if our presentation is already on the right display, ignore @@ -137,16 +154,18 @@ class SecondaryDisplayPresentation( surfaceView = SurfaceView(context) surfaceView.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { - + Log.d("SecondaryDisplay", "Surface created") } override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { + Log.d("SecondaryDisplay", "Surface changed: ${width}x${height}") parent.updateSurface() } override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.d("SecondaryDisplay", "Surface destroyed") parent.destroySurface() } }) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 5a440e395..d706b9140 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -70,7 +70,6 @@ import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.SecondaryDisplayLayout 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.Settings import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile @@ -108,8 +107,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private val settingsViewModel: SettingsViewModel by viewModels() private val settings get() = settingsViewModel.settings - private val onPause = Runnable{ togglePause() } - private val onShutdown = Runnable{ emulationState.stop() } + private val onPause = Runnable { togglePause() } + private val onShutdown = Runnable { emulationState.stop() } // Only used if a game is passed through intent on google play variant private var gameFd: Int? = null @@ -185,7 +184,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram retainInstance = true emulationState = EmulationState(game.path) emulationActivity = requireActivity() as EmulationActivity - screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) + screenAdjustmentUtil = + ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) EmulationLifecycleUtil.addPauseResumeHook(onPause) EmulationLifecycleUtil.addShutdownHook(onShutdown) } @@ -630,17 +630,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } add(text).setEnabled(enableClick).setOnMenuItemClickListener { - if(isSaving) { + if (isSaving) { NativeLibrary.saveState(slot) - Toast.makeText(context, + Toast.makeText( + context, getString(R.string.saving), - Toast.LENGTH_SHORT).show() + Toast.LENGTH_SHORT + ).show() } else { NativeLibrary.loadState(slot) binding.drawerLayout.close() - Toast.makeText(context, + Toast.makeText( + context, getString(R.string.loading), - Toast.LENGTH_SHORT).show() + Toast.LENGTH_SHORT + ).show() } true } @@ -649,9 +653,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram savestates?.forEach { var enableClick = true - val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) { + val text = if (it.slot == NativeLibrary.QUICKSAVE_SLOT) { getString(R.string.emulation_occupied_quicksave_slot, it.time) - } else{ + } else { getString(R.string.emulation_occupied_state_slot, it.slot, it.time) } popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick) @@ -733,8 +737,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } R.id.menu_performance_overlay_show -> { - BooleanSetting.PERF_OVERLAY_ENABLE.boolean = !BooleanSetting.PERF_OVERLAY_ENABLE.boolean - settings.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, SettingsFile.FILE_NAME_CONFIG) + BooleanSetting.PERF_OVERLAY_ENABLE.boolean = + !BooleanSetting.PERF_OVERLAY_ENABLE.boolean + settings.saveSetting( + BooleanSetting.PERF_OVERLAY_ENABLE, + SettingsFile.FILE_NAME_CONFIG + ) updateShowPerformanceOverlay() true } @@ -1005,10 +1013,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) { PortraitScreenLayout.TOP_FULL_WIDTH.int -> R.id.menu_portrait_layout_top_full + PortraitScreenLayout.ORIGINAL.int -> R.id.menu_portrait_layout_original + PortraitScreenLayout.CUSTOM_PORTRAIT_LAYOUT.int -> R.id.menu_portrait_layout_custom + else -> R.id.menu_portrait_layout_top_full @@ -1050,35 +1061,82 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram requireContext(), binding.inGameMenu.findViewById(R.id.menu_secondary_screen_layout) ) - popupMenu.menuInflater.inflate(R.menu.menu_secondary_screen_layout, popupMenu.menu) - val layoutOptionMenuItem = when (IntSetting.SECONDARY_DISPLAY_LAYOUT.int) { - SecondaryDisplayLayout.NONE.int -> - R.id.menu_secondary_layout_none + var selectedLayout = IntSetting.SECONDARY_DISPLAY_LAYOUT.int + val chooserMenu = popupMenu.menu.findItem(R.id.menu_secondary_choose) + val enableSecondaryCheckbox = popupMenu.menu.findItem(R.id.menu_secondary_layout_none) + chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group) + val displays = + emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity) + + if (selectedLayout == SecondaryDisplayLayout.NONE.int) { + enableSecondaryCheckbox.isChecked = false + chooserMenu.isVisible = false + popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, false) + selectedLayout = SecondaryDisplayLayout.REVERSE_PRIMARY.int + } else { + popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, true) + chooserMenu.isVisible = (displays.size > 1) + } + val layoutOptionMenuItem = when (selectedLayout) { + SecondaryDisplayLayout.NONE.int -> { + R.id.menu_secondary_layout_reverse_primary + } + SecondaryDisplayLayout.REVERSE_PRIMARY.int -> R.id.menu_secondary_layout_reverse_primary + SecondaryDisplayLayout.TOP_SCREEN.int -> R.id.menu_secondary_layout_top + SecondaryDisplayLayout.BOTTOM_SCREEN.int -> R.id.menu_secondary_layout_bottom + SecondaryDisplayLayout.HYBRID.int -> R.id.menu_secondary_layout_hybrid + SecondaryDisplayLayout.LARGE_SCREEN.int -> R.id.menu_secondary_layout_largescreen + SecondaryDisplayLayout.ORIGINAL.int -> R.id.menu_secondary_layout_original + else -> R.id.menu_secondary_layout_side_by_side - } + popupMenu.menu.findItem(layoutOptionMenuItem).isChecked = true - popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true) + if (displays.size > 1 && selectedLayout != SecondaryDisplayLayout.NONE.int) { + val current = emulationActivity.secondaryDisplay.currentDisplayId + chooserMenu.isVisible = true + displays.forEachIndexed { index, display -> + chooserMenu?.subMenu?.add( + R.id.menu_secondary_management_display_group, + display.displayId, + index, + "Display ${display.displayId} - ${display.name}" + )?.apply { + isChecked = (display.displayId == current) + } + } + chooserMenu.subMenu?.setGroupCheckable( + R.id.menu_secondary_management_display_group, + true, + true + ) + } popupMenu.setOnMenuItemClickListener { when (it.itemId) { R.id.menu_secondary_layout_none -> { - screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int) + if (!it.isChecked) { + screenAdjustmentUtil.changeSecondaryOrientation(selectedLayout) + } else { + screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int) + } + emulationActivity.secondaryDisplay.updateDisplay() + showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors true } @@ -1086,38 +1144,52 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.REVERSE_PRIMARY.int) true } + R.id.menu_secondary_layout_top -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.TOP_SCREEN.int) true } + R.id.menu_secondary_layout_bottom -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.BOTTOM_SCREEN.int) true } + R.id.menu_secondary_layout_side_by_side -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.SIDE_BY_SIDE.int) true } + R.id.menu_secondary_layout_hybrid -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.HYBRID.int) true } + R.id.menu_secondary_layout_original -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.ORIGINAL.int) true } + R.id.menu_secondary_layout_largescreen -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.LARGE_SCREEN.int) true } + R.id.menu_secondary_choose -> { + true + } - else -> true + else -> { + // display ID selection + emulationActivity.secondaryDisplay.preferredDisplayId = it.itemId + emulationActivity.secondaryDisplay.updateDisplay() + true + } } } - popupMenu.show() } + private fun editControlsPlacement() { if (binding.surfaceInputOverlay.isInEditMode) { binding.doneControlConfig.visibility = View.GONE @@ -1174,7 +1246,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram slider.valueFrom = 0f slider.value = preferences.getInt(target, 50).toFloat() textValue.setText((slider.value + 50).toInt().toString()) - textValue.addTextChangedListener( object : TextWatcher { + textValue.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { val value = s.toString().toIntOrNull() if (value == null || value < 50 || value > 150) { @@ -1184,6 +1256,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram slider.value = value.toFloat() - 50 } } + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} }) @@ -1224,7 +1297,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram slider.value = preferences.getInt("controlOpacity", 50).toFloat() textValue.setText(slider.value.toInt().toString()) - textValue.addTextChangedListener( object : TextWatcher { + textValue.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { val value = s.toString().toIntOrNull() if (value == null || value < slider.valueFrom || value > slider.valueTo) { @@ -1234,6 +1307,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram slider.value = value.toFloat() } } + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} }) @@ -1242,11 +1316,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram slider.addOnChangeListener { _: Slider, value: Float, _: Boolean -> if (textValue.text.toString() != slider.value.toInt().toString()) { - textValue.setText(slider.value.toInt().toString()) - textValue.setSelection(textValue.length()) - setControlOpacity(slider.value.toInt()) - } + textValue.setText(slider.value.toInt().toString()) + textValue.setSelection(textValue.length()) + setControlOpacity(slider.value.toInt()) } + } textInput.suffixText = "%" } @@ -1431,7 +1505,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } private fun updateStatsPosition(position: Int) { - val params = binding.performanceOverlayShowText.layoutParams as CoordinatorLayout.LayoutParams + val params = + binding.performanceOverlayShowText.layoutParams as CoordinatorLayout.LayoutParams val padding = (20 * resources.displayMetrics.density).toInt() // 20dp params.setMargins(padding, 0, padding, 0) @@ -1466,7 +1541,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private fun getBatteryTemperature(): Float { try { - val batteryIntent = requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + val batteryIntent = + requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) // Temperature in tenths of a degree Celsius val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0 // Convert to degrees Celsius diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 967166e12..2c481bb8e 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -157,7 +157,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.turbo_limit); // Workaround to map Android setting for enabling the frame limiter to the format Citra expects if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) { - ReadSetting("Renderer", Settings::values.frame_limit); + ReadSetting("Renderer", Settings::values.frame_limit); } else { Settings::values.frame_limit = 0; } diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index 881e54ece..c00086a79 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -18,10 +18,13 @@ #include "video_core/renderer_base.h" bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - if (render_window == surface) { + int w = ANativeWindow_getWidth(surface); + int h = ANativeWindow_getHeight(surface); + if (render_window == surface && w == window_width && h == window_height) { return false; } - + window_width = w; + window_height = h; render_window = surface; window_info.type = Frontend::WindowSystemType::Android; window_info.render_surface = surface; @@ -48,15 +51,9 @@ void EmuWindow_Android::OnTouchMoved(int x, int y) { } void EmuWindow_Android::OnFramebufferSizeChanged() { - const bool is_portrait_mode{IsPortraitMode()}; + const bool is_portrait_mode = IsPortraitMode() && !is_secondary; - 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 && !is_secondary) { - UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode); - } else { - UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode); - } + UpdateCurrentFramebufferLayout(window_width,window_height,is_portrait_mode); } EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, bool is_secondary) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 42ac888d2..9888e5356 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -391,6 +391,11 @@ void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv* env if (secondary_window) { // Second window already created, so update it notify = secondary_window->OnSurfaceChanged(s_secondary_surface); + + // Log the dimensions for debugging + int32_t width = ANativeWindow_getWidth(s_secondary_surface); + int32_t height = ANativeWindow_getHeight(s_secondary_surface); + LOG_INFO(Frontend, "Secondary Surface changed to {}x{}", width, height); } else { LOG_WARNING(Frontend, "Second Window does not exist in native.cpp but surface changed. Ignoring."); diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index 410c1ddff..3fe422a13 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -35,7 +35,7 @@ + android:title="@string/emulation_secondary_display_management" /> - - - - - + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 2c265c79b..373d72223 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -500,7 +500,10 @@ Aspect Ratio Landscape Screen Layout Portrait Screen Layout + Enable Secondary Display Secondary Display Layout + Secondary Display + Choose Display The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast) Large Screen Portrait @@ -509,7 +512,7 @@ Hybrid Screens Original Default - System Default (mirror) + None (system default) Opposite of Primary Display Custom Layout Background Color From 9dd3a8abc0966f4afdce82cc8a83054c259cd3bd Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 11 Nov 2025 23:02:21 +0300 Subject: [PATCH 10/21] safety checks for crash prevention --- .../java/org/citra/citra_emu/display/SecondaryDisplay.kt | 7 ++++++- src/android/app/src/main/jni/emu_window/emu_window.cpp | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index a58e6bece..13d347688 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -42,7 +42,12 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { } fun updateSurface() { - NativeLibrary.secondarySurfaceChanged(pres!!.getSurfaceHolder().surface) + val surface = pres?.getSurfaceHolder()?.surface + if (surface != null && surface.isValid) { + NativeLibrary.secondarySurfaceChanged(surface) + } else { + Log.w("SecondaryDisplay", "Attempted to update null or invalid surface") + } } fun destroySurface() { diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index c00086a79..b7c2f4e42 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -18,8 +18,8 @@ #include "video_core/renderer_base.h" bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - int w = ANativeWindow_getWidth(surface); - int h = ANativeWindow_getHeight(surface); + int w = surface== NULL ? 0 : ANativeWindow_getWidth(surface); + int h = surface== NULL ? 0 : ANativeWindow_getHeight(surface); if (render_window == surface && w == window_width && h == window_height) { return false; } From 3d76b60590cc01edbd3c02f89f3857d09393a196 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 13 Nov 2025 12:21:20 +0300 Subject: [PATCH 11/21] make secondary menu only appear if a secondary display is available --- .../java/org/citra/citra_emu/fragments/EmulationFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index d706b9140..eda824b8d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -196,6 +196,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram savedInstanceState: Bundle? ): View { _binding = FragmentEmulationBinding.inflate(inflater) + binding.inGameMenu.menu.findItem(R.id.menu_secondary_screen_layout).isVisible = + emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity).isNotEmpty() binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = CitraApplication.appContext.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT From 80f3fdb45b521113c55b3fa9090b38f79a1b19b0 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 22 Jan 2026 11:41:37 +0300 Subject: [PATCH 12/21] update default displayid behavior to exclude "Built" --- .../java/org/citra/citra_emu/display/SecondaryDisplay.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index 13d347688..e5b0db0c5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -88,13 +88,12 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { currentDisplayId = -1 vd.display } else if (preferredDisplayId >=0 && displays.any { it.displayId == preferredDisplayId }) { - currentDisplayId = preferredDisplayId displays.first { it.displayId == preferredDisplayId } } else { - //TODO: re-enable the filter of "built-in displays" odin style to pick default - currentDisplayId = displays[0].displayId - displays[0] + // prioritize a display without the word "Built" in name if it exists + currentDisplayId = displays.firstOrNull{!it.name.contains("Built",true)}?.displayId ?: displays[0].displayId + displays.first{ it.displayId == currentDisplayId } } // if our presentation is already on the right display, ignore From 0572930c70dfe0858b491c11eece2e4fb77425d3 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 2 Feb 2026 10:38:23 +0300 Subject: [PATCH 13/21] update odin 2 bugfix to handle other languages --- .../java/org/citra/citra_emu/display/SecondaryDisplay.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index e5b0db0c5..07b1cb403 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -91,8 +91,12 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { currentDisplayId = preferredDisplayId displays.first { it.displayId == preferredDisplayId } } else { - // prioritize a display without the word "Built" in name if it exists - currentDisplayId = displays.firstOrNull{!it.name.contains("Built",true)}?.displayId ?: displays[0].displayId + val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val default = dm.displays.first {it.displayId == Display.DEFAULT_DISPLAY} + // prioritize displays that have a different name from the default display, as + // some devices such as the Odin 2 create a permanent virtual display with the same + // name as the default display that should be skipped in most cases + currentDisplayId = displays.firstOrNull{it.name != default.name && !it.name.contains("Built",true)}?.displayId ?: displays[0].displayId displays.first{ it.displayId == currentDisplayId } } From bc2dbb502a51befc1e4fbe9bcb850584eeee4d38 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sun, 26 Apr 2026 16:58:01 +0100 Subject: [PATCH 14/21] Apply clang-format --- src/android/app/src/main/jni/config.cpp | 2 +- .../app/src/main/jni/emu_window/emu_window.cpp | 6 +++--- src/common/settings.h | 11 ++++++++++- src/core/frontend/framebuffer_layout.cpp | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 2c481bb8e..967166e12 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -157,7 +157,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.turbo_limit); // Workaround to map Android setting for enabling the frame limiter to the format Citra expects if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) { - ReadSetting("Renderer", Settings::values.frame_limit); + ReadSetting("Renderer", Settings::values.frame_limit); } else { Settings::values.frame_limit = 0; } diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index b7c2f4e42..fed800c52 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -18,8 +18,8 @@ #include "video_core/renderer_base.h" bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - int w = surface== NULL ? 0 : ANativeWindow_getWidth(surface); - int h = surface== NULL ? 0 : ANativeWindow_getHeight(surface); + int w = surface == NULL ? 0 : ANativeWindow_getWidth(surface); + int h = surface == NULL ? 0 : ANativeWindow_getHeight(surface); if (render_window == surface && w == window_width && h == window_height) { return false; } @@ -53,7 +53,7 @@ void EmuWindow_Android::OnTouchMoved(int x, int y) { void EmuWindow_Android::OnFramebufferSizeChanged() { const bool is_portrait_mode = IsPortraitMode() && !is_secondary; - UpdateCurrentFramebufferLayout(window_width,window_height,is_portrait_mode); + UpdateCurrentFramebufferLayout(window_width, window_height, is_portrait_mode); } EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, bool is_secondary) diff --git a/src/common/settings.h b/src/common/settings.h index 61656e01c..e98254e97 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -57,7 +57,16 @@ enum class PortraitLayoutOption : u32 { PortraitOriginal }; -enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide, ReversePrimary, Original, Hybrid, LargeScreen }; +enum class SecondaryDisplayLayout : u32 { + None, + TopScreenOnly, + BottomScreenOnly, + SideBySide, + ReversePrimary, + Original, + Hybrid, + LargeScreen +}; /** Defines where the small screen will appear relative to the large screen * when in Large Screen mode */ diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 303661059..f87c1fdf7 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -305,7 +305,7 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { - case Settings::SecondaryDisplayLayout::TopScreenOnly: + case Settings::SecondaryDisplayLayout::TopScreenOnly: return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::BottomScreenOnly: From 86bc2943c2dd5507a5eb84173308104224ed8f61 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sun, 26 Apr 2026 18:00:37 +0100 Subject: [PATCH 15/21] Rename "Opposite of Primary Display" option for brevity --- .../app/src/main/res/menu/menu_secondary_screen_layout.xml | 2 +- src/android/app/src/main/res/values/arrays.xml | 2 +- src/android/app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml index 01f571061..3fc2cbf6f 100644 --- a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml +++ b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml @@ -25,7 +25,7 @@ android:id="@+id/menu_secondary_layout_group"> + android:title="@string/emulation_secondary_display_opposite" /> @string/emulation_secondary_display_default - @string/emulation_secondary_display_reverse_primary + @string/emulation_secondary_display_opposite @string/emulation_top_screen @string/emulation_bottom_screen @string/emulation_screen_layout_sidebyside diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 373d72223..7c35ed749 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -513,7 +513,7 @@ Original Default None (system default) - Opposite of Primary Display + Opposite Screen Custom Layout Background Color The color which appears behind the screens during emulation, represented as an RGB value. From 8a6519e0d6450b65e0eecd5f46c83d8e63413d19 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sun, 26 Apr 2026 18:20:38 +0100 Subject: [PATCH 16/21] Rename menu_secondary_layout_reverse_primary for consistency --- .../java/org/citra/citra_emu/fragments/EmulationFragment.kt | 6 +++--- .../app/src/main/res/menu/menu_secondary_screen_layout.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index eda824b8d..6cef15987 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -1083,11 +1083,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } val layoutOptionMenuItem = when (selectedLayout) { SecondaryDisplayLayout.NONE.int -> { - R.id.menu_secondary_layout_reverse_primary + R.id.menu_secondary_layout_opposite } SecondaryDisplayLayout.REVERSE_PRIMARY.int -> - R.id.menu_secondary_layout_reverse_primary + R.id.menu_secondary_layout_opposite SecondaryDisplayLayout.TOP_SCREEN.int -> R.id.menu_secondary_layout_top @@ -1142,7 +1142,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram true } - R.id.menu_secondary_layout_reverse_primary -> { + R.id.menu_secondary_layout_opposite -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.REVERSE_PRIMARY.int) true } diff --git a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml index 3fc2cbf6f..148a1405d 100644 --- a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml +++ b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml @@ -24,7 +24,7 @@ android:checkableBehavior="single" android:id="@+id/menu_secondary_layout_group"> Date: Sun, 26 Apr 2026 18:25:18 +0100 Subject: [PATCH 17/21] Rename SecondaryDisplayLayout::ReversePrimary for consistency --- src/common/settings.h | 2 +- src/core/frontend/framebuffer_layout.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index e98254e97..f4b29ee10 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -62,7 +62,7 @@ enum class SecondaryDisplayLayout : u32 { TopScreenOnly, BottomScreenOnly, SideBySide, - ReversePrimary, + OppositeScreenOnly, Original, Hybrid, LargeScreen diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index f87c1fdf7..52ac1cfd9 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -324,8 +324,8 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { return HybridScreenLayout(width, height, false, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::None: // this should never happen - if "none" is set this method shouldn't run - but if it does, - // somehow, use ReversePrimary - case Settings::SecondaryDisplayLayout::ReversePrimary: + // somehow, use OppositeScreenOnly + case Settings::SecondaryDisplayLayout::OppositeScreenOnly: default: return SingleFrameLayout(width, height, !Settings::values.swap_screen.GetValue(), Settings::values.upright_screen.GetValue()); From e3d1da145eb7e7877a129a8d162774934bf3ab2f Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 28 Apr 2026 14:46:30 +0300 Subject: [PATCH 18/21] first round of code fixes based on review --- .../citra_emu/activities/EmulationActivity.kt | 14 +++--- .../citra_emu/display/SecondaryDisplay.kt | 49 ++++++++++--------- .../citra_emu/fragments/EmulationFragment.kt | 30 ++++++------ .../src/main/jni/emu_window/emu_window.cpp | 14 ++---- 4 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 7cdcee349..0991a8ecd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -64,7 +64,7 @@ class EmulationActivity : AppCompatActivity() { private lateinit var binding: ActivityEmulationBinding private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var hotkeyUtility: HotkeyUtility - lateinit var secondaryDisplay: SecondaryDisplay + lateinit var secondaryDisplayManager: SecondaryDisplay private val onShutdown = Runnable { if (intent.getBooleanExtra("launched_from_shortcut", false)) { @@ -102,8 +102,8 @@ class EmulationActivity : AppCompatActivity() { super.onCreate(savedInstanceState) - secondaryDisplay = SecondaryDisplay(this) - secondaryDisplay.updateDisplay() + secondaryDisplayManager = SecondaryDisplay(this) + secondaryDisplayManager.updateDisplay() binding = ActivityEmulationBinding.inflate(layoutInflater) hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this) @@ -188,7 +188,7 @@ class EmulationActivity : AppCompatActivity() { } override fun onStop() { - secondaryDisplay.releasePresentation() + secondaryDisplayManager.releasePresentation() super.onStop() } @@ -199,7 +199,7 @@ class EmulationActivity : AppCompatActivity() { public override fun onRestart() { super.onRestart() - secondaryDisplay.updateDisplay() + secondaryDisplayManager.updateDisplay() NativeLibrary.reloadCameraDevices() } @@ -222,8 +222,8 @@ class EmulationActivity : AppCompatActivity() { NativeLibrary.playTimeManagerStop() isEmulationRunning = false instance = null - secondaryDisplay.releasePresentation() - secondaryDisplay.releaseVD() + secondaryDisplayManager.releasePresentation() + secondaryDisplayManager.releaseVD() super.onDestroy() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index 07b1cb403..5796cb4b0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -6,21 +6,18 @@ 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.Build import android.os.Bundle -import android.util.Log import android.view.Display import android.view.MotionEvent -import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import android.view.WindowManager import org.citra.citra_emu.features.settings.model.IntSetting -import org.citra.citra_emu.display.SecondaryDisplayLayout import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.utils.Log class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { private var pres: SecondaryDisplayPresentation? = null @@ -29,6 +26,9 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { var preferredDisplayId = -1 var currentDisplayId = -1 + val availableDisplays: List + get() = getSecondaryDisplays() + init { vd = displayManager.createVirtualDisplay( "HiddenDisplay", @@ -46,7 +46,7 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { if (surface != null && surface.isValid) { NativeLibrary.secondarySurfaceChanged(surface) } else { - Log.w("SecondaryDisplay", "Attempted to update null or invalid surface") + Log.warning("SecondaryDisplay Attempted to update null or invalid surface") } } @@ -54,7 +54,7 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { NativeLibrary.secondarySurfaceDestroyed() } - fun getSecondaryDisplays(context: Context): List { + private fun getSecondaryDisplays(): List { val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val currentDisplayId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { context.display.displayId @@ -67,12 +67,13 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); return displays.filter { val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId } - val isNotDefaultOrPresentable = it != null && it.displayId != Display.DEFAULT_DISPLAY || isPresentable - isNotDefaultOrPresentable && - it.displayId != currentDisplayId && - it.name != "HiddenDisplay" && - it.state != Display.STATE_OFF && - it.isValid + val isNotDefaultOrPresentable = (it != null && it.displayId != Display.DEFAULT_DISPLAY) || isPresentable + + isNotDefaultOrPresentable && + it.displayId != currentDisplayId && + it.name != "HiddenDisplay" && + it.state != Display.STATE_OFF && + it.isValid } } @@ -81,33 +82,35 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { if (context is android.app.Activity && (context.isFinishing || context.isDestroyed)) { return } - val displays = getSecondaryDisplays(context) - val display = if (displays.isEmpty() || + + val displayToUse = if (availableDisplays.isEmpty() || IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int ) { currentDisplayId = -1 vd.display - } else if (preferredDisplayId >=0 && displays.any { it.displayId == preferredDisplayId }) { + } else if (preferredDisplayId >=0 && availableDisplays.any { it.displayId == preferredDisplayId }) { currentDisplayId = preferredDisplayId - displays.first { it.displayId == preferredDisplayId } + availableDisplays.first { it.displayId == preferredDisplayId } } else { val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val default = dm.displays.first {it.displayId == Display.DEFAULT_DISPLAY} // prioritize displays that have a different name from the default display, as // some devices such as the Odin 2 create a permanent virtual display with the same // name as the default display that should be skipped in most cases - currentDisplayId = displays.firstOrNull{it.name != default.name && !it.name.contains("Built",true)}?.displayId ?: displays[0].displayId - displays.first{ it.displayId == currentDisplayId } + currentDisplayId = availableDisplays.firstOrNull{ + it.name != default.name && !it.name.contains("Built",true)}?.displayId ?: + availableDisplays[0].displayId + availableDisplays.first{ it.displayId == currentDisplayId } } // if our presentation is already on the right display, ignore - if (pres?.display == display) return + if (pres?.display == displayToUse) return // otherwise, make a new presentation releasePresentation() try { - pres = SecondaryDisplayPresentation(context, display!!, this) + pres = SecondaryDisplayPresentation(context, displayToUse!!, this) pres?.show() } // catch BadTokenException and InvalidDisplayException, @@ -162,18 +165,18 @@ class SecondaryDisplayPresentation( surfaceView = SurfaceView(context) surfaceView.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { - Log.d("SecondaryDisplay", "Surface created") + Log.debug("SecondaryDisplay Surface created") } override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { - Log.d("SecondaryDisplay", "Surface changed: ${width}x${height}") + Log.debug("SecondaryDisplay Surface changed: ${width}x${height}") parent.updateSurface() } override fun surfaceDestroyed(holder: SurfaceHolder) { - Log.d("SecondaryDisplay", "Surface destroyed") + Log.debug("SecondaryDisplay Surface destroyed") parent.destroySurface() } }) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 6cef15987..d3e6546fc 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -78,7 +78,6 @@ import org.citra.citra_emu.utils.BuildUtil import org.citra.citra_emu.utils.DirectoryInitialization import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState import org.citra.citra_emu.utils.EmulationMenuSettings -import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameIconUtils import org.citra.citra_emu.utils.EmulationLifecycleUtil @@ -93,7 +92,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private lateinit var emulationState: EmulationState private var perfStatsUpdater: Runnable? = null - private lateinit var emulationActivity: EmulationActivity + private val emulationActivity: EmulationActivity + get() = (requireActivity() as EmulationActivity) private var _binding: FragmentEmulationBinding? = null private val binding get() = _binding!! @@ -116,8 +116,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram override fun onAttach(context: Context) { super.onAttach(context) if (context is EmulationActivity) { - emulationActivity = context - NativeLibrary.setEmulationActivity(context) + NativeLibrary.setEmulationActivity(context) } else { throw IllegalStateException("EmulationFragment must have EmulationActivity parent") } @@ -183,7 +182,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram // So this fragment doesn't restart on configuration changes; i.e. rotation. retainInstance = true emulationState = EmulationState(game.path) - emulationActivity = requireActivity() as EmulationActivity screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) EmulationLifecycleUtil.addPauseResumeHook(onPause) @@ -197,7 +195,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram ): View { _binding = FragmentEmulationBinding.inflate(inflater) binding.inGameMenu.menu.findItem(R.id.menu_secondary_screen_layout).isVisible = - emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity).isNotEmpty() + emulationActivity.secondaryDisplayManager.availableDisplays.isNotEmpty() binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = CitraApplication.appContext.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT @@ -1070,7 +1068,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram val enableSecondaryCheckbox = popupMenu.menu.findItem(R.id.menu_secondary_layout_none) chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group) val displays = - emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity) + emulationActivity.secondaryDisplayManager.availableDisplays if (selectedLayout == SecondaryDisplayLayout.NONE.int) { enableSecondaryCheckbox.isChecked = false @@ -1082,9 +1080,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram chooserMenu.isVisible = (displays.size > 1) } val layoutOptionMenuItem = when (selectedLayout) { - SecondaryDisplayLayout.NONE.int -> { + SecondaryDisplayLayout.NONE.int -> R.id.menu_secondary_layout_opposite - } SecondaryDisplayLayout.REVERSE_PRIMARY.int -> R.id.menu_secondary_layout_opposite @@ -1108,9 +1105,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram R.id.menu_secondary_layout_side_by_side } popupMenu.menu.findItem(layoutOptionMenuItem).isChecked = true - + // Add the available secondary displays to the display chooser list + // Use the display ID as the menu ID - since generated menu IDs are all > 1,000,000 this + // *should* result in unique ids if (displays.size > 1 && selectedLayout != SecondaryDisplayLayout.NONE.int) { - val current = emulationActivity.secondaryDisplay.currentDisplayId + val current = emulationActivity.secondaryDisplayManager.currentDisplayId chooserMenu.isVisible = true displays.forEachIndexed { index, display -> chooserMenu?.subMenu?.add( @@ -1137,7 +1136,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } else { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int) } - emulationActivity.secondaryDisplay.updateDisplay() + emulationActivity.secondaryDisplayManager.updateDisplay() showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors true } @@ -1183,8 +1182,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram else -> { // display ID selection - emulationActivity.secondaryDisplay.preferredDisplayId = it.itemId - emulationActivity.secondaryDisplay.updateDisplay() + // If we are clicking on a menu item that isn't one of the options above, it must + // be one of the dynamically generated menu items added to the secondary display + // choice list. + emulationActivity.secondaryDisplayManager.preferredDisplayId = it.itemId + emulationActivity.secondaryDisplayManager.updateDisplay() true } } diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index fed800c52..942e04e66 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -18,20 +18,16 @@ #include "video_core/renderer_base.h" bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - int w = surface == NULL ? 0 : ANativeWindow_getWidth(surface); - int h = surface == NULL ? 0 : ANativeWindow_getHeight(surface); - if (render_window == surface && w == window_width && h == window_height) { + int temp_width = surface == nullptr ? 0 : ANativeWindow_getWidth(surface); + int temp_height = surface == nullptr ? 0 : ANativeWindow_getHeight(surface); + if (render_window == surface && temp_width == window_width && temp_height == window_height) { return false; } - window_width = w; - window_height = h; + window_width = temp_width; + window_height = temp_height; render_window = surface; window_info.type = Frontend::WindowSystemType::Android; window_info.render_surface = surface; - if (surface != nullptr) { - window_width = ANativeWindow_getWidth(surface); - window_height = ANativeWindow_getHeight(surface); - } StopPresenting(); OnFramebufferSizeChanged(); return true; From 41b4e772060873066068bf67ea0c063963e3609b Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sat, 2 May 2026 11:46:13 +0300 Subject: [PATCH 19/21] Added android-side enable_secondary_display boolean setting, replacing secondary_display_layout = none on the android side. Exposed it in Layout settings, and secondary display layout is now only selectable if it is enabled. Support for the old option is still in the code, but should no longer be selectable. Also renamed opposite to reverse_primary in a few other places. --- CMakeModules/GenerateSettingKeys.cmake | 1 + .../citra_emu/display/ScreenAdjustmentUtil.kt | 11 +++++++++++ .../citra/citra_emu/display/ScreenLayout.kt | 2 ++ .../citra_emu/display/SecondaryDisplay.kt | 6 +++++- .../features/settings/SettingKeys.kt | 1 + .../features/settings/model/BooleanSetting.kt | 1 + .../features/settings/model/IntSetting.kt | 2 +- .../settings/ui/SettingsFragmentPresenter.kt | 12 +++++++++++- .../citra_emu/fragments/EmulationFragment.kt | 19 ++++++++++--------- src/android/app/src/main/jni/default_ini.h | 4 ++++ .../res/menu/menu_secondary_screen_layout.xml | 6 +++--- .../app/src/main/res/values/arrays.xml | 4 +--- .../app/src/main/res/values/strings.xml | 3 ++- src/common/settings.h | 4 ++-- src/core/frontend/framebuffer_layout.cpp | 4 ++-- 15 files changed, 57 insertions(+), 23 deletions(-) diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 7aff65db5..8054e9afc 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -238,6 +238,7 @@ if (ANDROID) "android_hide_images" "screen_orientation" "performance_overlay_position" + "enable_secondary_display" ) string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY}) set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",") diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index cf18a175c..d717e87e6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -78,6 +78,17 @@ class ScreenAdjustmentUtil( NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) } + fun enableSecondaryDisplay(layoutOption: Int) { + BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = true + settings.saveSetting(BooleanSetting.ENABLE_SECONDARY_DISPLAY, SettingsFile.FILE_NAME_CONFIG) + changeSecondaryOrientation(layoutOption) + } + + fun disableSecondaryDisplay() { + BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = false + settings.saveSetting(BooleanSetting.ENABLE_SECONDARY_DISPLAY, SettingsFile.FILE_NAME_CONFIG) + } + fun changeActivityOrientation(orientationOption: Int) { val activity = context as? Activity ?: return IntSetting.ORIENTATION_OPTION.int = orientationOption diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt index c705c2104..9e72f3894 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt @@ -53,6 +53,8 @@ enum class PortraitScreenLayout(val int: Int) { enum class SecondaryDisplayLayout(val int: Int) { // These must match what is defined in src/common/settings.h + // NONE is no longer selectable in the interface, having been replaced with + // the boolean ENABLE_SECONDARY_DISPLAY setting, but is left here for backwards compatibility NONE(0), TOP_SCREEN(1), BOTTOM_SCREEN(2), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index 5796cb4b0..0a3eee316 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -17,6 +17,7 @@ import android.view.SurfaceView import android.view.WindowManager import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.utils.Log class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { @@ -84,7 +85,10 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { } val displayToUse = if (availableDisplays.isEmpty() || - IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int + // Theoretically, the NONE option is no longer selectable, but + // I am leaving this in for backwards compatibility + IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int || + ! BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean ) { currentDisplayId = -1 vd.display diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt index 56ffb6789..f9ff306f8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt @@ -141,4 +141,5 @@ object SettingKeys { external fun android_hide_images(): String external fun screen_orientation(): String external fun performance_overlay_position(): String + external fun enable_secondary_display(): String } \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt index 0e88dacf3..cda0a5f2f 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -59,6 +59,7 @@ enum class BooleanSetting( ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false), APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true), USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false), + ENABLE_SECONDARY_DISPLAY(SettingKeys.enable_secondary_display(), Settings.SECTION_LAYOUT, true), SIMULATE_3DS_GPU_TIMINGS(SettingKeys.simulate_3ds_gpu_timings(), Settings.SECTION_RENDERER, true); override var boolean: Boolean = defaultValue diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index eb1a880a5..5eb8944d5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -37,7 +37,7 @@ enum class IntSetting( LANDSCAPE_BOTTOM_HEIGHT(SettingKeys.custom_bottom_height(),Settings.SECTION_LAYOUT,480), SCREEN_GAP(SettingKeys.screen_gap(),Settings.SECTION_LAYOUT,0), PORTRAIT_SCREEN_LAYOUT(SettingKeys.portrait_layout_option(),Settings.SECTION_LAYOUT,0), - SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,0), + SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,4), PORTRAIT_TOP_X(SettingKeys.custom_portrait_top_x(),Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_Y(SettingKeys.custom_portrait_top_y(),Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_WIDTH(SettingKeys.custom_portrait_top_width(),Settings.SECTION_LAYOUT,800), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 94bfe78a9..ad3f9cb8c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1201,6 +1201,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue ) ) + add ( + SwitchSetting( + BooleanSetting.ENABLE_SECONDARY_DISPLAY, + R.string.emulation_secondary_display_enable, + R.string.emulation_secondary_display_enable_description, + BooleanSetting.ENABLE_SECONDARY_DISPLAY.key, + BooleanSetting.ENABLE_SECONDARY_DISPLAY.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.SECONDARY_DISPLAY_LAYOUT, @@ -1209,7 +1218,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.array.secondaryLayouts, R.array.secondaryLayoutValues, IntSetting.SECONDARY_DISPLAY_LAYOUT.key, - IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue + IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue, + BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean ) ) add( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index d3e6546fc..979e5131b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -1065,26 +1065,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram var selectedLayout = IntSetting.SECONDARY_DISPLAY_LAYOUT.int val chooserMenu = popupMenu.menu.findItem(R.id.menu_secondary_choose) - val enableSecondaryCheckbox = popupMenu.menu.findItem(R.id.menu_secondary_layout_none) + val enableSecondaryCheckbox = popupMenu.menu.findItem(R.id.menu_enable_secondary_layout) chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group) val displays = emulationActivity.secondaryDisplayManager.availableDisplays - if (selectedLayout == SecondaryDisplayLayout.NONE.int) { + if (selectedLayout == SecondaryDisplayLayout.NONE.int || !BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean) { + BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = false enableSecondaryCheckbox.isChecked = false chooserMenu.isVisible = false popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, false) - selectedLayout = SecondaryDisplayLayout.REVERSE_PRIMARY.int + } else { popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, true) chooserMenu.isVisible = (displays.size > 1) } val layoutOptionMenuItem = when (selectedLayout) { SecondaryDisplayLayout.NONE.int -> - R.id.menu_secondary_layout_opposite + R.id.menu_secondary_layout_reverse_primary SecondaryDisplayLayout.REVERSE_PRIMARY.int -> - R.id.menu_secondary_layout_opposite + R.id.menu_secondary_layout_reverse_primary SecondaryDisplayLayout.TOP_SCREEN.int -> R.id.menu_secondary_layout_top @@ -1130,18 +1131,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.setOnMenuItemClickListener { when (it.itemId) { - R.id.menu_secondary_layout_none -> { + R.id.menu_enable_secondary_layout -> { if (!it.isChecked) { - screenAdjustmentUtil.changeSecondaryOrientation(selectedLayout) + screenAdjustmentUtil.enableSecondaryDisplay(selectedLayout) } else { - screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int) + screenAdjustmentUtil.disableSecondaryDisplay() } emulationActivity.secondaryDisplayManager.updateDisplay() showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors true } - R.id.menu_secondary_layout_opposite -> { + R.id.menu_secondary_layout_reverse_primary -> { screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.REVERSE_PRIMARY.int) true } diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 6864d04b0..cc3d91ec5 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -354,6 +354,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( # 0 (default): Off, 1: On )") DECLARE_KEY(expand_to_cutout_area) BOOST_HANA_STRING(R"( +# Allows Azahar to use externally connected displays +# 0: Off, 1: On (default) +)") DECLARE_KEY(enable_secondary_display) BOOST_HANA_STRING(R"( + # Secondary Display Layout # What the game should do if a secondary display is connected physically or using # Miracast / Chromecast screen mirroring diff --git a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml index 148a1405d..0ed57b51d 100644 --- a/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml +++ b/src/android/app/src/main/res/menu/menu_secondary_screen_layout.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -24,8 +24,8 @@ android:checkableBehavior="single" android:id="@+id/menu_secondary_layout_group"> + android:id="@+id/menu_secondary_layout_reverse_primary" + android:title="@string/emulation_secondary_display_reverse_primary" /> - @string/emulation_secondary_display_default - @string/emulation_secondary_display_opposite + @string/emulation_secondary_display_reverse_primary @string/emulation_top_screen @string/emulation_bottom_screen @string/emulation_screen_layout_sidebyside @@ -54,7 +53,6 @@ - 0 4 1 2 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 7c35ed749..c326fdb39 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -501,6 +501,7 @@ Landscape Screen Layout Portrait Screen Layout Enable Secondary Display + If disabled, Azahar will let Android manage connected displays. If this is enabled and multiple displays are connected, you can select which one Azahar will use in the Emulation Quick Menu Secondary Display Layout Secondary Display Choose Display @@ -513,7 +514,7 @@ Original Default None (system default) - Opposite Screen + Opposite Screen Custom Layout Background Color The color which appears behind the screens during emulation, represented as an RGB value. diff --git a/src/common/settings.h b/src/common/settings.h index f4b29ee10..8de6ea457 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -62,7 +62,7 @@ enum class SecondaryDisplayLayout : u32 { TopScreenOnly, BottomScreenOnly, SideBySide, - OppositeScreenOnly, + ReversePrimaryScreen, Original, Hybrid, LargeScreen @@ -558,7 +558,7 @@ struct Values { SwitchableSetting swap_screen{false, Keys::swap_screen}; SwitchableSetting upright_screen{false, Keys::upright_screen}; SwitchableSetting secondary_display_layout{ - SecondaryDisplayLayout::None, Keys::secondary_display_layout}; + SecondaryDisplayLayout::ReversePrimaryScreen, Keys::secondary_display_layout}; SwitchableSetting> layouts_to_cycle{ {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, LayoutOption::SideScreen, diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 52ac1cfd9..948886aff 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -324,8 +324,8 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { return HybridScreenLayout(width, height, false, Settings::values.upright_screen.GetValue()); case Settings::SecondaryDisplayLayout::None: // this should never happen - if "none" is set this method shouldn't run - but if it does, - // somehow, use OppositeScreenOnly - case Settings::SecondaryDisplayLayout::OppositeScreenOnly: + // somehow, use ReversePrimaryScreen + case Settings::SecondaryDisplayLayout::ReversePrimaryScreen: default: return SingleFrameLayout(width, height, !Settings::values.swap_screen.GetValue(), Settings::values.upright_screen.GetValue()); From 21fbf9f5caeb82026ef99adec26769f345813533 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sun, 10 May 2026 08:54:58 +0300 Subject: [PATCH 20/21] Update framebuffer layout and renderers to correctly render hybrid mode on both primary and secondary displays --- src/core/frontend/framebuffer_layout.cpp | 9 +++++---- src/core/frontend/framebuffer_layout.h | 7 ++++--- src/video_core/renderer_opengl/renderer_opengl.cpp | 2 +- src/video_core/renderer_vulkan/renderer_vulkan.cpp | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 948886aff..994d03fa3 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -275,18 +275,19 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u std::swap(width, height); } - // 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 + // use Large Screen layout with these specific ratios to get two of the pieces const float scale_factor = swapped ? 2.25 : 1.8; const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight : Settings::SmallScreenPosition::BottomRight; - FramebufferLayout res = LargeFrameLayout(width, height, swapped, upright, scale_factor, pos); + // always pass false as the upright value here, as it is being handled here not there + FramebufferLayout res = LargeFrameLayout(width, height, swapped, false, scale_factor, pos); const Common::Rectangle main = swapped ? res.bottom_screen : res.top_screen; const Common::Rectangle small = swapped ? res.top_screen : res.bottom_screen; res.additional_screen = Common::Rectangle{small.left, swapped ? small.bottom : main.top, small.right, swapped ? main.bottom : small.top}; + res.additional_screen_is_bottom = swapped; res.additional_screen_enabled = true; + res.is_rotated = !upright; if (upright) { return reverseLayout(res); } else { diff --git a/src/core/frontend/framebuffer_layout.h b/src/core/frontend/framebuffer_layout.h index 1d15e2c89..766929bff 100644 --- a/src/core/frontend/framebuffer_layout.h +++ b/src/core/frontend/framebuffer_layout.h @@ -32,13 +32,14 @@ struct FramebufferLayout { bool bottom_screen_enabled; Common::Rectangle top_screen; Common::Rectangle bottom_screen; + // is_rotated is true when the screen is in landscape mode - not sure why! bool is_rotated = true; - bool is_portrait = false; - bool additional_screen_enabled; + bool additional_screen_enabled = false; + // top_opacity is currently not used but could be used in the future float top_opacity = 1.0f; float bottom_opacity = 1.0f; + bool additional_screen_is_bottom = false; Common::Rectangle additional_screen; - CardboardSettings cardboard; /** diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index c8cb6c000..b675b5df4 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -706,7 +706,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f if (layout.additional_screen_enabled) { const auto& additional_screen = layout.additional_screen; - if (!Settings::values.swap_screen.GetValue()) { + if (!layout.additional_screen_is_bottom) { DrawTopScreen(layout, additional_screen); } else { DrawBottomScreen(layout, additional_screen); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 0a25c2036..1281f4a97 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -1037,7 +1037,7 @@ void RendererVulkan::DrawScreens(Frame* frame, const Layout::FramebufferLayout& if (layout.additional_screen_enabled) { const auto& additional_screen = layout.additional_screen; - if (!Settings::values.swap_screen.GetValue()) { + if (!layout.additional_screen_is_bottom) { DrawTopScreen(layout, additional_screen); } else { DrawBottomScreen(layout, additional_screen); From 730af7d45335bfc831d27d9f9bb0591759ebdd1f Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sun, 24 May 2026 13:01:30 +0100 Subject: [PATCH 21/21] Apply clang format --- src/common/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/settings.h b/src/common/settings.h index 8de6ea457..13ec9dddc 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -558,7 +558,7 @@ struct Values { SwitchableSetting swap_screen{false, Keys::swap_screen}; SwitchableSetting upright_screen{false, Keys::upright_screen}; SwitchableSetting secondary_display_layout{ - SecondaryDisplayLayout::ReversePrimaryScreen, Keys::secondary_display_layout}; + SecondaryDisplayLayout::ReversePrimaryScreen, Keys::secondary_display_layout}; SwitchableSetting> layouts_to_cycle{ {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, LayoutOption::SideScreen,