Compare commits

...

23 commits

Author SHA1 Message Date
NovaChild
d547e7ea36
Merge 730af7d453 into 4867bb2e2b 2026-05-31 09:54:57 +03:00
RedBlackAka
4867bb2e2b
video_core: Change unimplemented gas stub behaviour for Vulkan (#2165) 2026-05-30 23:53:16 +02:00
OpenSauce04
730af7d453 Apply clang format 2026-05-24 13:01:30 +01:00
David Griswold
21fbf9f5ca Update framebuffer layout and renderers to correctly render hybrid mode on both primary and secondary displays 2026-05-24 12:59:01 +01:00
David Griswold
41b4e77206 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.
2026-05-24 12:59:01 +01:00
David Griswold
e3d1da145e first round of code fixes based on review 2026-05-24 12:59:01 +01:00
OpenSauce04
83fc47b45a Rename SecondaryDisplayLayout::ReversePrimary for consistency 2026-05-24 12:59:01 +01:00
OpenSauce04
8a6519e0d6 Rename menu_secondary_layout_reverse_primary for consistency 2026-05-24 12:59:01 +01:00
OpenSauce04
86bc2943c2 Rename "Opposite of Primary Display" option for brevity 2026-05-24 12:59:01 +01:00
OpenSauce04
bc2dbb502a Apply clang-format 2026-05-24 12:59:01 +01:00
David Griswold
0572930c70 update odin 2 bugfix to handle other languages 2026-05-24 12:59:01 +01:00
David Griswold
80f3fdb45b update default displayid behavior to exclude "Built" 2026-05-24 12:59:01 +01:00
David Griswold
3d76b60590 make secondary menu only appear if a secondary display is available 2026-05-24 12:59:01 +01:00
David Griswold
9dd3a8abc0 safety checks for crash prevention 2026-05-24 12:59:01 +01:00
David Griswold
f94b2ad5da updated secondary menu with functionality to switch external displays
 Conflicts:
	src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt
2026-05-24 12:59:01 +01:00
David Griswold
a603f261e7 added other secondary layouts 2026-05-24 12:59:01 +01:00
David Griswold
3c803af0f5 fix icon 2026-05-24 12:59:01 +01:00
David Griswold
6f5f6b2830 add quick menu option for secondary layout 2026-05-24 12:59:01 +01:00
David Griswold
f46596a9d8 Add a new Secondary Display Layout option on android that makes the secondary display honor swap button
# Conflicts:
#	src/core/frontend/framebuffer_layout.cpp
2026-05-24 12:59:01 +01:00
David Griswold
a28a0c3f6d added other secondary layouts 2026-05-24 12:59:01 +01:00
David Griswold
0fdd5781c6 fix icon 2026-05-24 12:59:01 +01:00
David Griswold
da0eebdf86 add quick menu option for secondary layout 2026-05-24 12:59:01 +01:00
David Griswold
52455587b5 Add a new Secondary Display Layout option on android that makes the secondary display honor swap button 2026-05-24 12:59:01 +01:00
24 changed files with 437 additions and 91 deletions

View file

@ -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}\",")

View file

@ -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()
}

View file

@ -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

View file

@ -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 {

View file

@ -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<Display>
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<Display> {
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()
}
})

View file

@ -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
}

View file

@ -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

View file

@ -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),

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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.");

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
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"/>
</vector>

View file

@ -32,6 +32,11 @@
android:icon="@drawable/ic_portrait_fit_screen"
android:title="@string/emulation_switch_portrait_layout" />
<item
android:id="@+id/menu_secondary_screen_layout"
android:icon="@drawable/ic_secondary_fit_screen"
android:title="@string/emulation_secondary_display_management" />
<item
android:id="@+id/menu_swap_screens"
android:icon="@drawable/ic_splitscreen"

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_enable_secondary_layout"
android:title="@string/emulation_secondary_display_enable"
android:checkable="true"
android:checked="true"/>
<item
android:id="@+id/menu_secondary_choose"
android:title="@string/emulation_select_secondary_display">
<menu>
<group
android:id="@+id/menu_secondary_management_display_group"
android:checkableBehavior="single">
</group>
</menu>
</item>
<item
android:title="@string/preferences_layout"
android:enabled="false"/>
<group
android:checkableBehavior="single"
android:id="@+id/menu_secondary_layout_group">
<item
android:id="@+id/menu_secondary_layout_reverse_primary"
android:title="@string/emulation_secondary_display_reverse_primary" />
<item
android:id="@+id/menu_secondary_layout_top"
android:title="@string/emulation_top_screen" />
<item
android:id="@+id/menu_secondary_layout_bottom"
android:title="@string/emulation_bottom_screen" />
<item
android:id="@+id/menu_secondary_layout_side_by_side"
android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_secondary_layout_original"
android:title="@string/emulation_screen_layout_original" />
<item
android:id="@+id/menu_secondary_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
<item
android:id="@+id/menu_secondary_layout_largescreen"
android:title="@string/emulation_screen_layout_largescreen" />
</group>
</menu>

View file

@ -36,10 +36,14 @@
</string-array>
<string-array name="secondaryLayouts">
<item>@string/emulation_secondary_display_default</item>
<item>@string/emulation_secondary_display_reverse_primary</item>
<item>@string/emulation_top_screen</item>
<item>@string/emulation_bottom_screen</item>
<item>@string/emulation_screen_layout_sidebyside</item>
<item>@string/emulation_screen_layout_original</item>
<item>@string/emulation_screen_layout_hybrid</item>
<item>@string/emulation_screen_layout_largescreen</item>
</string-array>
<integer-array name="portraitLayoutValues">
@ -49,10 +53,13 @@
</integer-array>
<integer-array name="secondaryLayoutValues">
<item>0</item>
<item>4</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>5</item>
<item>6</item>
<item>7</item>
</integer-array>
<string-array name="smallScreenPositions">

View file

@ -500,7 +500,11 @@
<string name="emulation_aspect_ratio">Aspect Ratio</string>
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
<string name="emulation_switch_portrait_layout">Portrait Screen Layout</string>
<string name="emulation_switch_secondary_layout">Secondary Display Screen Layout</string>
<string name="emulation_secondary_display_enable">Enable Secondary Display</string>
<string name="emulation_secondary_display_enable_description">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</string>
<string name="emulation_switch_secondary_layout">Secondary Display Layout</string>
<string name="emulation_secondary_display_management">Secondary Display</string>
<string name="emulation_select_secondary_display">Choose Display</string>
<string name="emulation_switch_secondary_layout_description">The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string>
@ -509,7 +513,8 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_secondary_display_default">System Default (mirror)</string>
<string name="emulation_secondary_display_default">None (system default)</string>
<string name="emulation_secondary_display_reverse_primary">Opposite Screen</string>
<string name="emulation_screen_layout_custom">Custom Layout</string>
<string name="bg_color">Background Color</string>
<string name="bg_color_description">The color which appears behind the screens during emulation, represented as an RGB value.</string>

View file

@ -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<bool> swap_screen{false, Keys::swap_screen};
SwitchableSetting<bool> upright_screen{false, Keys::upright_screen};
SwitchableSetting<SecondaryDisplayLayout> secondary_display_layout{
SecondaryDisplayLayout::None, Keys::secondary_display_layout};
SecondaryDisplayLayout::ReversePrimaryScreen, Keys::secondary_display_layout};
SwitchableSetting<std::vector<LayoutOption>> layouts_to_cycle{
{LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen,
LayoutOption::SideScreen,

View file

@ -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<u32> main = swapped ? res.bottom_screen : res.top_screen;
const Common::Rectangle<u32> small = swapped ? res.top_screen : res.bottom_screen;
res.additional_screen = Common::Rectangle<u32>{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());
}
}

View file

@ -32,13 +32,14 @@ struct FramebufferLayout {
bool bottom_screen_enabled;
Common::Rectangle<u32> top_screen;
Common::Rectangle<u32> 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<u32> additional_screen;
CardboardSettings cardboard;
/**

View file

@ -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);

View file

@ -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);

View file

@ -72,6 +72,7 @@ void FragmentModule::Generate() {
break;
case TexturingRegs::FogMode::Gas:
WriteGas();
// Return early due to unimplemented gas mode
return;
default:
break;
@ -196,7 +197,12 @@ void FragmentModule::WriteFog() {
void FragmentModule::WriteGas() {
// TODO: Implement me
LOG_CRITICAL(Render, "Unimplemented gas mode");
OpKill();
// Replace the output color with a transparent pixel,
// (just discarding the pixel causes graphical issues
// in some MH games).
OpStore(color_id, ConstF32(0.f, 0.f, 0.f, 0.f));
OpReturn();
OpFunctionEnd();
}