summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-12 16:12:35 -0700
committerTom Pratt <tom.pratt@outlook.com>2026-07-19 20:30:53 +0200
commit6877e436fbf1511bea91eeef569b94d0354771fc (patch)
tree38a0d36ed18eb318b3176ad216b91449d6d229e4 /Source/Android
parentb6d8bc299ee7d03496d624b2e6b9a18d70522435 (diff)
Android: Dual core warning when hosting netplay
Using dual core during netplay seems sufficiently detrimental that its worth showing this warning message and the option to turn dual core off when starting netplay.
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt30
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt100
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt3
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml3
5 files changed, 133 insertions, 6 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
index adea1a13a5..0764448824 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
@@ -11,15 +11,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
+import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
@@ -104,18 +105,37 @@ class NetplayViewModel(
private val _notAllPlayersHaveGame = Channel<Unit>(Channel.CONFLATED)
val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow()
+ private val _dualCoreWarning = Channel<Unit>(Channel.CONFLATED)
+ val dualCoreWarning = _dualCoreWarning.receiveAsFlow()
+
fun startGame() {
- if (netplaySession.doAllPlayersHaveGame()) {
- netplaySession.startGame()
- } else {
+ if (!netplaySession.doAllPlayersHaveGame()) {
_notAllPlayersHaveGame.trySend(Unit)
+ return
+ }
+
+ if (BooleanSetting.MAIN_CPU_THREAD.boolean &&
+ !BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.boolean
+ ) {
+ _dualCoreWarning.trySend(Unit)
+ return
}
+
+ confirmStartGame()
}
fun confirmStartGame() {
netplaySession.startGame()
}
+ fun setDualCoreEnabled(enabled: Boolean) {
+ BooleanSetting.MAIN_CPU_THREAD.setBoolean(NativeConfig.LAYER_BASE, enabled)
+ }
+
+ fun skipDualCoreWarning() {
+ BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.setBoolean(NativeConfig.LAYER_BASE, true)
+ }
+
fun sendMessage(message: String) {
val trimmedMessage = message.trim()
if (trimmedMessage.isEmpty()) {
@@ -176,12 +196,14 @@ class NetplayViewModel(
JoinInfoType.EXTERNAL to JoinAddress.Loading,
)
}
+
is TraversalState.Connected -> {
_joinAddresses.value += mapOf(
JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode),
JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress),
)
}
+
is TraversalState.Failure -> {
_joinAddresses.value += mapOf(
JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry),
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
index eb9cecec20..28e0e4ca16 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
@@ -58,6 +58,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
onGameSelected = viewModel::changeGame,
gameFiles = viewModel.gameFiles.collectAsState().value,
notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame,
+ dualCoreWarning = viewModel.dualCoreWarning,
+ onSetDualCoreEnabled = viewModel::setDualCoreEnabled,
+ onSkipDualCoreWarning = viewModel::skipDualCoreWarning,
onConfirmStartGame = viewModel::confirmStartGame,
players = viewModel.players.collectAsState().value,
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
index 789a3cef17..7df8ea35ee 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
@@ -5,6 +5,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui
import android.content.Intent
import android.content.res.Configuration
import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -37,6 +38,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Share
@@ -59,6 +61,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -72,7 +75,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@@ -87,7 +93,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.window.core.layout.WindowSizeClass
import coil.compose.AsyncImage
@@ -129,6 +134,9 @@ fun NetplayScreen(
onGameSelected: (GameFile) -> Unit,
gameFiles: List<GameFile>,
notAllPlayersHaveGame: Flow<Unit>,
+ dualCoreWarning: Flow<Unit>,
+ onSetDualCoreEnabled: (Boolean) -> Unit,
+ onSkipDualCoreWarning: () -> Unit,
onConfirmStartGame: () -> Unit,
hostInputAuthorityEnabled: Boolean,
networkMode: NetworkMode,
@@ -252,6 +260,11 @@ fun NetplayScreen(
notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true }
}
+ var showDualCoreWarning by rememberSaveable { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ dualCoreWarning.collect { showDualCoreWarning = true }
+ }
+
var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) }
if (saveTransferProgress == null) {
dismissSaveTransferProgressDialog = false
@@ -323,6 +336,21 @@ fun NetplayScreen(
onDismissRequest = { showNotAllPlayersHaveGame = false },
)
}
+
+ showDualCoreWarning -> {
+ DualCoreWarningDialog(
+ onSetDualCoreEnabled = onSetDualCoreEnabled,
+ onStartGame = {
+ showDualCoreWarning = false
+ onConfirmStartGame()
+ },
+ onSkipWarning = {
+ showDualCoreWarning = false
+ onSkipDualCoreWarning()
+ },
+ onDismiss = { showDualCoreWarning = false },
+ )
+ }
}
}
}
@@ -1274,6 +1302,73 @@ private fun GameDigestPlayerRow(
}
@Composable
+private fun DualCoreWarningDialog(
+ onSetDualCoreEnabled: (Boolean) -> Unit,
+ onStartGame: () -> Unit,
+ onSkipWarning: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ var dualCoreEnabled by rememberSaveable { mutableStateOf(true) }
+ AlertDialog(
+ title = { Text(stringResource(R.string.netplay_dual_core_warning_title)) },
+ text = {
+ Column {
+ Text(
+ text = stringResource(R.string.netplay_dual_core_warning_message),
+ )
+ val ripplePadding = 12.dp
+ Spacer(Modifier.height(12.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .layout { measurable, constraints ->
+ // Enlarge the ripple area beyond the dialog's padding
+ val hPx = ripplePadding.roundToPx()
+ val placeable = measurable.measure(
+ constraints.copy(
+ minWidth = constraints.maxWidth + hPx * 2,
+ maxWidth = constraints.maxWidth + hPx * 2
+ )
+ )
+ layout(constraints.maxWidth, placeable.height) {
+ placeable.place(-hPx, 0)
+ }
+ }
+ .clip(MaterialTheme.shapes.small)
+ .clickable {
+ dualCoreEnabled = !dualCoreEnabled
+ onSetDualCoreEnabled(dualCoreEnabled)
+ }
+ .padding(ripplePadding),
+ ) {
+ Text(
+ text = stringResource(R.string.dual_core),
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(1f),
+ )
+ Switch(
+ checked = dualCoreEnabled,
+ onCheckedChange = null,
+ )
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onStartGame) {
+ Text(stringResource(R.string.netplay_start))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onSkipWarning) {
+ Text(stringResource(R.string.netplay_dont_warn_again))
+ }
+ },
+ onDismissRequest = onDismiss,
+ )
+}
+
+@Composable
private fun NetplayMessage.color(): Color {
val isDark = isSystemInDarkTheme()
return when (this) {
@@ -1361,6 +1456,9 @@ private fun PreviewNetplayScreen() {
onGameSelected = {},
gameFiles = emptyList(),
notAllPlayersHaveGame = emptyFlow(),
+ dualCoreWarning = emptyFlow(),
+ onSetDualCoreEnabled = {},
+ onSkipDualCoreWarning = {},
onConfirmStartGame = {},
hostInputAuthorityEnabled = true,
networkMode = NetworkMode.HOST_INPUT_AUTHORITY,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
index 68c859082c..47f05f14ca 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
@@ -947,7 +947,8 @@ enum class BooleanSetting(
"ProgressEnabled",
false
),
- NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false);
+ NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false),
+ NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false);
override val isOverridden: Boolean
get() = NativeConfig.isOverridden(file, section, key)
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 9d97169d95..a6419ec83b 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -1012,6 +1012,9 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="netplay_start">Start</string>
<string name="netplay_start_warning_title">Warning</string>
<string name="netplay_start_warning_not_all_players_have_game">Not all players have the game. Do you really want to start?</string>
+ <string name="netplay_dual_core_warning_title">Dual Core Enabled</string>
+ <string name="netplay_dual_core_warning_message">Dual core during netplay is not recommended. It can cause performance issues and prevent certain games from booting.</string>
+ <string name="netplay_dont_warn_again">Don\'t warn again</string>
<string name="netplay_chat_label">Chat</string>
<string name="netplay_chat_send">Send</string>
<string name="netplay_message_game_changed">Game changed to %1$s</string>