From 693d486ddbce839fe3dd3be41f3af81e55ec74dd Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 14:12:10 +0300 Subject: [PATCH 01/13] 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 750677fd5..ca54ed6ab 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -477,6 +477,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 90922bcff..a31c62581 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 820978ec62a4989d95cb0fe88ad8f54eedd749ca Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 21:29:37 +0300 Subject: [PATCH 02/13] 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 35899ce7f..07d3e65b5 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 @@ -333,6 +334,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 @@ -1033,6 +1039,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 cc9ab7a557665211b9d0fcd4dfeba6ab650046a5 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 22:02:08 +0300 Subject: [PATCH 03/13] 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 6cc31376330ae8fde84f7b2c53fab3c3ef16d90b Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 27 Oct 2025 09:18:56 +0300 Subject: [PATCH 04/13] 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 07d3e65b5..04898179c 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 @@ -1056,6 +1056,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 @@ -1086,6 +1092,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 ca54ed6ab..a62e0c836 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -467,7 +467,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 a31c62581..8f7079d5f 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 2531740920681281b080c6e9ec6cd3e546cf353f Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 14:12:10 +0300 Subject: [PATCH 05/13] 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 c0f32db83e5bf78693adf73370e7b941c0f5862d Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 21:29:37 +0300 Subject: [PATCH 06/13] 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 39124f6cfebcc9fd4d22c6c5642afd8c50aba969 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Wed, 17 Sep 2025 22:02:08 +0300 Subject: [PATCH 07/13] 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 7e78498b33bcedc8acf3fca69410ec400eb575a5 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 27 Oct 2025 09:18:56 +0300 Subject: [PATCH 08/13] 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 d5c95ac3d87ec31ee17d294ddee6ce389787c1fa Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 11 Nov 2025 22:05:43 +0300 Subject: [PATCH 09/13] 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 5f5215876..669b8b315 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 @@ -63,7 +63,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 04898179c..3494e0bbe 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 @@ -182,7 +181,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) } @@ -624,17 +624,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 } @@ -643,9 +647,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) @@ -727,8 +731,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 } @@ -999,10 +1007,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 @@ -1044,35 +1055,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 } @@ -1080,38 +1138,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 @@ -1168,7 +1240,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) { @@ -1178,6 +1250,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) {} }) @@ -1218,7 +1291,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) { @@ -1228,6 +1301,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) {} }) @@ -1236,11 +1310,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 = "%" } @@ -1425,7 +1499,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) @@ -1460,7 +1535,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 e0fa260c5..f425b39a2 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 26cd2da56..df2096e75 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; @@ -47,15 +50,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 f4a30610c..c95dd2edb 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 a62e0c836..1e5478df0 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -467,7 +467,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 @@ -476,7 +479,7 @@ Hybrid Screens Original Default - System Default (mirror) + None (system default) Opposite of Primary Display Custom Layout Background Color From 592237a32b233b1dca6db921eb2978d3406da58a Mon Sep 17 00:00:00 2001 From: David Griswold Date: Tue, 11 Nov 2025 23:02:21 +0300 Subject: [PATCH 10/13] 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 df2096e75..548fac315 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 a00355076f7c59b84322a4cd4d7a9becbe99cc18 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 13 Nov 2025 12:21:20 +0300 Subject: [PATCH 11/13] 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 3494e0bbe..65afadee3 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 @@ -193,6 +193,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 9dead4925c45a318da0834d4961e8f6937e74b1f Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 22 Jan 2026 11:41:37 +0300 Subject: [PATCH 12/13] 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 b73aa12adad244b41e9439c409ed88505f5bef20 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Mon, 2 Feb 2026 10:38:23 +0300 Subject: [PATCH 13/13] 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 } }