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..979e5131b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -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_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 + // 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_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 + } + + 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..c4ff113c4 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_reverse_primary @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..c326fdb39 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,8 @@ Hybrid Screens Original Default - System Default (mirror) + None (system default) + 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..13ec9dddc 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, + ReversePrimaryScreen, + 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::ReversePrimaryScreen, Keys::secondary_display_layout}; SwitchableSetting> layouts_to_cycle{ {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, LayoutOption::SideScreen, diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 918c1454f..994d03fa3 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -275,18 +275,19 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u std::swap(width, height); } - // Split the window into two parts. Give 2.25x width to the main screen, - // and make a bar on the right side with 1x width top screen and 1.25x width bottom screen - // To do that, find the total emulation box and maximize that based on window size + // use Large Screen layout with these specific ratios to get two of the pieces const float scale_factor = swapped ? 2.25 : 1.8; const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight : Settings::SmallScreenPosition::BottomRight; - FramebufferLayout res = LargeFrameLayout(width, height, swapped, upright, scale_factor, pos); + // always pass false as the upright value here, as it is being handled here not there + FramebufferLayout res = LargeFrameLayout(width, height, swapped, false, scale_factor, pos); const Common::Rectangle main = swapped ? res.bottom_screen : res.top_screen; const Common::Rectangle small = swapped ? res.top_screen : res.bottom_screen; res.additional_screen = Common::Rectangle{small.left, swapped ? small.bottom : main.top, small.right, swapped ? main.bottom : small.top}; + res.additional_screen_is_bottom = swapped; res.additional_screen_enabled = true; + res.is_rotated = !upright; if (upright) { return reverseLayout(res); } else { @@ -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 ReversePrimaryScreen + case Settings::SecondaryDisplayLayout::ReversePrimaryScreen: 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);