From 8a6d597decf1f86982dfd58e0401bc8d2d2e55b5 Mon Sep 17 00:00:00 2001 From: NovaChild Date: Sun, 14 Jun 2026 11:37:32 -0500 Subject: [PATCH] android: Secondary Layout and Menu Improvements (#1385) * Add a new Secondary Display Layout option on android that makes the secondary display honor swap button * add quick menu option for secondary layout * fix icon * added other secondary layouts * Add a new Secondary Display Layout option on android that makes the secondary display honor swap button # Conflicts: # src/core/frontend/framebuffer_layout.cpp * add quick menu option for secondary layout * fix icon * added other secondary layouts * updated secondary menu with functionality to switch external displays  Conflicts:  src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt * safety checks for crash prevention * make secondary menu only appear if a secondary display is available * update default displayid behavior to exclude "Built" * update odin 2 bugfix to handle other languages * Apply clang-format * Rename "Opposite of Primary Display" option for brevity * Rename menu_secondary_layout_reverse_primary for consistency * Rename SecondaryDisplayLayout::ReversePrimary for consistency * first round of code fixes based on review * 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. * Update framebuffer layout and renderers to correctly render hybrid mode on both primary and secondary displays * Apply clang format * Re-apply terminology changes which were undone by a recent commit * Removed unused string emulation_secondary_display_default --------- Co-authored-by: OpenSauce04 --- CMakeModules/GenerateSettingKeys.cmake | 1 + .../citra_emu/activities/EmulationActivity.kt | 14 +- .../citra_emu/display/ScreenAdjustmentUtil.kt | 18 ++ .../citra/citra_emu/display/ScreenLayout.kt | 9 +- .../citra_emu/display/SecondaryDisplay.kt | 78 +++++-- .../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 | 212 +++++++++++++++--- src/android/app/src/main/jni/default_ini.h | 4 + .../src/main/jni/emu_window/emu_window.cpp | 21 +- src/android/app/src/main/jni/native.cpp | 5 + .../res/drawable/ic_secondary_fit_screen.xml | 12 + .../app/src/main/res/menu/menu_in_game.xml | 5 + .../res/menu/menu_secondary_screen_layout.xml | 53 +++++ .../app/src/main/res/values/arrays.xml | 11 +- .../app/src/main/res/values/strings.xml | 8 +- src/common/settings.h | 13 +- src/core/frontend/framebuffer_layout.cpp | 28 ++- src/core/frontend/framebuffer_layout.h | 7 +- .../renderer_opengl/renderer_opengl.cpp | 2 +- .../renderer_vulkan/renderer_vulkan.cpp | 2 +- 23 files changed, 429 insertions(+), 90 deletions(-) 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/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/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 957b98611..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 - private 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/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index e63960fa8..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 @@ -71,6 +71,24 @@ 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 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 c46dcadd8..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,10 +53,17 @@ 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), - SIDE_BY_SIDE(3); + SIDE_BY_SIDE(3), + 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/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index d09daab41..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 @@ -8,6 +8,7 @@ import android.app.Presentation import android.content.Context import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay +import android.os.Build import android.os.Bundle import android.view.Display import android.view.MotionEvent @@ -16,11 +17,18 @@ 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 { 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 + + val availableDisplays: List + get() = getSecondaryDisplays() init { vd = displayManager.createVirtualDisplay( @@ -35,31 +43,39 @@ 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.warning("SecondaryDisplay Attempted to update null or invalid surface") + } } fun destroySurface() { NativeLibrary.secondarySurfaceDestroyed() } - private fun getExternalDisplay(context: Context): Display? { - val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - val currentDisplayId = context.display.displayId + 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 + } 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 + val isNotDefaultOrPresentable = (it != null && it.displayId != Display.DEFAULT_DISPLAY) || isPresentable + isNotDefaultOrPresentable && - it.displayId != currentDisplayId && - it.name != "HiddenDisplay" && - it.state != Display.STATE_OFF && - it.isValid + 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() { @@ -68,21 +84,37 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { return } - // 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 + val displayToUse = if (availableDisplays.isEmpty() || + // 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 + } else if (preferredDisplayId >=0 && availableDisplays.any { it.displayId == preferredDisplayId }) { + currentDisplayId = 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 = 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, @@ -137,16 +169,18 @@ class SecondaryDisplayPresentation( surfaceView = SurfaceView(context) surfaceView.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { - + Log.debug("SecondaryDisplay Surface created") } override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { + Log.debug("SecondaryDisplay Surface changed: ${width}x${height}") parent.updateSurface() } override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.debug("SecondaryDisplay Surface destroyed") parent.destroySurface() } }) 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 e1c1fc076..a4e0edc8b 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,9 +67,9 @@ 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 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 @@ -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!! @@ -107,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 @@ -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,8 +182,8 @@ 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) + screenAdjustmentUtil = + ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) EmulationLifecycleUtil.addPauseResumeHook(onPause) EmulationLifecycleUtil.addShutdownHook(onShutdown) } @@ -195,6 +194,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.secondaryDisplayManager.availableDisplays.isNotEmpty() binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = CitraApplication.appContext.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT @@ -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 @@ -624,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 } @@ -643,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) @@ -727,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 } @@ -999,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 @@ -1039,6 +1056,145 @@ 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) + + 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_enable_secondary_layout) + chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group) + val displays = + emulationActivity.secondaryDisplayManager.availableDisplays + + 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) + + } 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 + + SecondaryDisplayLayout.REVERSE_PRIMARY.int -> + R.id.menu_secondary_layout_opposite + + 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 + // 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.secondaryDisplayManager.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_enable_secondary_layout -> { + if (!it.isChecked) { + screenAdjustmentUtil.enableSecondaryDisplay(selectedLayout) + } else { + screenAdjustmentUtil.disableSecondaryDisplay() + } + emulationActivity.secondaryDisplayManager.updateDisplay() + showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors + true + } + + R.id.menu_secondary_layout_opposite -> { + 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 -> { + // display ID selection + // 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 + } + } + } + popupMenu.show() + } + private fun editControlsPlacement() { if (binding.surfaceInputOverlay.isInEditMode) { binding.doneControlConfig.visibility = View.GONE @@ -1095,7 +1251,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) { @@ -1105,6 +1261,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) {} }) @@ -1145,7 +1302,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) { @@ -1155,6 +1312,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) {} }) @@ -1163,11 +1321,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 = "%" } @@ -1352,7 +1510,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) @@ -1387,7 +1546,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/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/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index 881e54ece..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,17 +18,16 @@ #include "video_core/renderer_base.h" bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - if (render_window == surface) { + 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 = 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; @@ -48,15 +47,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/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..184e6be4c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_secondary_fit_screen.xml @@ -0,0 +1,12 @@ + + + + \ 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..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 @@ -32,6 +32,11 @@ android:icon="@drawable/ic_portrait_fit_screen" android:title="@string/emulation_switch_portrait_layout" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 2a08cd546..867e0379a 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -36,10 +36,14 @@ - @string/emulation_secondary_display_default + @string/emulation_secondary_display_opposite @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 + @@ -49,10 +53,13 @@ - 0 + 4 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 a94611efc..412c3c134 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -500,7 +500,11 @@ Aspect Ratio Landscape Screen Layout Portrait Screen Layout - Secondary Display 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 The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast) Large Screen Portrait @@ -509,7 +513,7 @@ Hybrid Screens Original Default - System Default (mirror) + 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 4196557d1..462d79736 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 }; +enum class SecondaryDisplayLayout : u32 { + None, + TopScreenOnly, + BottomScreenOnly, + SideBySide, + OppositeScreenOnly, + Original, + Hybrid, + LargeScreen +}; /** Defines where the small screen will appear relative to the large screen * when in Large Screen mode */ @@ -549,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::OppositeScreenOnly, 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 918c1454f..0c397118a 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 { @@ -305,17 +306,30 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) { const Settings::SecondaryDisplayLayout layout = Settings::values.secondary_display_layout.GetValue(); switch (layout) { + 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 OppositeScreenOnly + case Settings::SecondaryDisplayLayout::OppositeScreenOnly: 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()); } } 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);