From abd324e98d735d836d8131cef91b58f3278bc6fa Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Tue, 12 May 2026 11:20:01 -0700 Subject: Make NetplaySession not a singleton Create a new NetplaySession each time we try to join a netplay game. Hold onto it in NetplayManager so its available to the different activities that need to access it. Close the session when backing out of the netplay UI. Some guardrails in case things go out of sync: creating a session closes the old one if it is still around for some reason, finalizer in NetplaySession to release native resources if not closed explicitly for some reason. Profiling done to ensure all kotlin and native objects are successfully cleared / garbage collected. --- .../org/dolphinemu/dolphinemu/NativeLibrary.kt | 2 +- .../dolphinemu/features/netplay/Netplay.kt | 285 --------------------- .../dolphinemu/features/netplay/NetplayManager.kt | 38 +++ .../dolphinemu/features/netplay/NetplaySession.kt | 277 ++++++++++++++++++++ .../netplay/model/NetplaySetupViewModel.kt | 52 +++- .../features/netplay/model/NetplayViewModel.kt | 47 ++-- .../features/netplay/ui/NetplayActivity.kt | 11 +- .../features/netplay/ui/NetplaySetupActivity.kt | 6 +- .../dolphinemu/fragments/EmulationFragment.kt | 15 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 41 +-- Source/Android/jni/AndroidCommon/IDCache.h | 2 +- Source/Android/jni/MainAndroid.cpp | 7 +- Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp | 152 ++++++++--- Source/Android/jni/NetPlay/NetPlayUICallbacks.h | 8 +- Source/Android/jni/NetPlay/Netplay.cpp | 92 ++++--- 15 files changed, 613 insertions(+), 422 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt (limited to 'Source/Android') diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index 2e2c295c7d..b52b7b4fa4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -346,7 +346,7 @@ object NativeLibrary { * Begins emulation for a netplay session, using the BootSessionData provided by the host. */ @JvmStatic - external fun RunNetPlay(paths: Array, riivolution: Boolean) + external fun RunNetPlay(paths: Array, riivolution: Boolean, bootSessionDataPointer: Long) @JvmStatic external fun ChangeDisc(path: String) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt deleted file mode 100644 index 1cec94f19d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2003 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.netplay - -import androidx.annotation.Keep -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.runningFold -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext -import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage -import org.dolphinemu.dolphinemu.features.netplay.model.Player -import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress - -object Netplay { - @Keep - private var netPlayClientPointer: Long = 0 - - @Keep - private var bootSessionDataPointer: Long = 0 - - private var sessionScope: CoroutineScope? = null - - val isLaunching: Boolean - get() = bootSessionDataPointer != 0L - - private val _launchGame = Channel(Channel.CONFLATED) - val launchGame = _launchGame.receiveAsFlow() - - private val _stopGame = Channel(Channel.CONFLATED) - val stopGame = _stopGame.receiveAsFlow() - - private val _connectionLost = Channel(Channel.CONFLATED) - val connectionLost = _connectionLost.receiveAsFlow() - - private val _connectionErrors = Channel(Channel.BUFFERED) - val connectionErrors = _connectionErrors.receiveAsFlow() - - private val _messages = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val messages = _messages.asSharedFlow() - - private val _players = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val players = _players.asSharedFlow().distinctUntilChanged() - - private val _chatMessages = MutableSharedFlow( - extraBufferCapacity = 32, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val chatMessages = _chatMessages.asSharedFlow() - - private val _game = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val game = _game.asSharedFlow() - - private val _hostInputAuthorityEnabled = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val hostInputAuthorityEnabled = _hostInputAuthorityEnabled.asSharedFlow() - - private val _padBuffer = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val padBuffer = _padBuffer.asSharedFlow() - - private val _saveTransferProgress = MutableStateFlow(null) - val saveTransferProgress = _saveTransferProgress.asStateFlow() - - suspend fun join(): Boolean = withContext(Dispatchers.IO) { - val scope = createSessionScope() - - // Gather all messages that should appear in the chat window. - mergeMessages() - .runningFold(emptyList()) { acc, msg -> listOf(msg) + acc } - .onEach { _messages.tryEmit(it) } - .launchIn(scope) - - netPlayClientPointer = Join() - val isConnected = netPlayClientPointer != 0L && isClientConnected() - - if (!isActive) { - releaseNetplayClient() - return@withContext false - } - - if (isConnected) { - return@withContext true - } - - releaseNetplayClient() - false - } - - suspend fun quit() = withContext(Dispatchers.IO) { - releaseNetplayClient() - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun releaseNetplayClient() { - sessionScope?.cancel() - sessionScope = null - - if (bootSessionDataPointer != 0L) { - ReleaseBootSessionData() - bootSessionDataPointer = 0 - } - - if (netPlayClientPointer != 0L) { - ReleaseNetplayClient() - netPlayClientPointer = 0 - } - - _launchGame.flush() - _stopGame.flush() - _connectionErrors.flush() - _players.resetReplayCache() - _messages.resetReplayCache() - _chatMessages.resetReplayCache() - _game.resetReplayCache() - _hostInputAuthorityEnabled.resetReplayCache() - _padBuffer.resetReplayCache() - _saveTransferProgress.value = null - } - - private fun createSessionScope(): CoroutineScope { - sessionScope?.cancel() - return CoroutineScope(SupervisorJob() + Dispatchers.IO).also { - sessionScope = it - } - } - - @JvmStatic - private external fun Join(): Long - - @JvmStatic - external fun isClientConnected(): Boolean - - @JvmStatic - external fun sendMessage(message: String) - - @JvmStatic - external fun adjustPadBufferSize(buffer: Int) - - @JvmStatic - private external fun ReleaseBootSessionData() - - @JvmStatic - private external fun ReleaseNetplayClient() - - private fun mergeMessages(): Flow = merge( - chatMessages.map { NetplayMessage.Chat(it) }, - game.map { NetplayMessage.GameChanged(it) }, - hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, - padBuffer.map { NetplayMessage.BufferChanged(it) }, - ) - - // NetPlayUI callbacks - - @Keep - @JvmStatic - fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { - this.bootSessionDataPointer = bootSessionDataPointer - _stopGame.flush() - _launchGame.trySend(gameFilePath) - } - - @Keep - @JvmStatic - fun onStopGame() { - _stopGame.trySend(Unit) - } - - @Keep - @JvmStatic - fun onConnectionLost() { - _connectionLost.trySend(Unit) - } - - @Keep - @JvmStatic - fun onConnectionError(message: String) { - _connectionErrors.trySend(message) - } - - @Keep - @JvmStatic - fun onUpdate(players: Array) { - _players.tryEmit(players.toList()) - } - - @Keep - @JvmStatic - fun onChatMessageReceived(message: String) { - _chatMessages.tryEmit(message) - } - - @Keep - @JvmStatic - fun onHostInputAuthorityChanged(enabled: Boolean) { - _hostInputAuthorityEnabled.tryEmit(enabled) - } - - @Keep - @JvmStatic - fun onGameChanged(game: String) { - _game.tryEmit(game) - } - - @Keep - @JvmStatic - fun onPadBufferChanged(buffer: Int) { - // Only for remote pad buffer settings. Ignore local max buffer changes. - if (_hostInputAuthorityEnabled.replayCache.firstOrNull() == true) return - _padBuffer.tryEmit(buffer) - } - - @Keep - @JvmStatic - fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { - val players = _players.replayCache.firstOrNull() - _saveTransferProgress.value = SaveTransferProgress( - title = title, - totalSize = dataSize, - playerProgresses = playerIds.map { playerId -> - SaveTransferProgress.PlayerProgress( - playerId = playerId, - name = players?.find { it.pid == playerId }?.name ?: "Invalid Player ID", - progress = 0, - ) - }, - ) - } - - @Keep - @JvmStatic - fun onSetChunkedProgress(playerId: Int, progress: Long) { - val current = _saveTransferProgress.value - _saveTransferProgress.value = current?.copy( - playerProgresses = current.playerProgresses.map { - if (it.playerId == playerId) { - it.copy(progress = progress) - } else { - it - } - } - ) - } - - @Keep - @JvmStatic - fun onHideChunkedProgressDialog() { - _saveTransferProgress.value = null - } - -} - -private fun Channel.flush() { - while (this.tryReceive().isSuccess) Unit -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt new file mode 100644 index 0000000000..eae44e65be --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt @@ -0,0 +1,38 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +object NetplayManager { + + private val mutex = Mutex() + + @Volatile + private var closeComplete: CompletableDeferred? = null + + @Volatile + var activeSession: NetplaySession? = null + private set + + suspend fun createSession(): NetplaySession = mutex.withLock { + closeComplete?.await() + + // Sessions should be closed by UI navigation, but just in case. + activeSession?.closeBlocking() + + closeComplete = CompletableDeferred() + + NetplaySession( + onClosed = { + activeSession = null + closeComplete?.complete(Unit) + } + ).also { + activeSession = it + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt new file mode 100644 index 0000000000..f47d9ab198 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -0,0 +1,277 @@ +// Copyright 2003 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.netplay + +import androidx.annotation.Keep +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage +import org.dolphinemu.dolphinemu.features.netplay.model.Player +import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress + +class NetplaySession( + private val onClosed: (NetplaySession) -> Unit, +) { + + private var netPlayUICallbacksPointer: Long = nativeCreateUICallbacks() + + private var netPlayClientPointer: Long = 0 + + private var bootSessionDataPointer: Long = 0 + + private val sessionScope = CoroutineScope(SupervisorJob()) + + @Volatile + var isClosed = false + private set + + val isLaunching: Boolean + get() = bootSessionDataPointer != 0L + + private val _launchGame = Channel(Channel.CONFLATED) + val launchGame = _launchGame.receiveAsFlow() + + private val _stopGame = Channel(Channel.CONFLATED) + val stopGame = _stopGame.receiveAsFlow() + + private val _connectionLost = Channel(Channel.CONFLATED) + val connectionLost = _connectionLost.receiveAsFlow() + + private val _connectionErrors = Channel(Channel.BUFFERED) + val connectionErrors = _connectionErrors.receiveAsFlow() + + private val _messages = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val messages = _messages.asSharedFlow() + + private val _players = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val players = _players.asSharedFlow().distinctUntilChanged() + + private val _chatMessages = MutableSharedFlow( + extraBufferCapacity = 32, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val chatMessages = _chatMessages.asSharedFlow() + + private val _game = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val game = _game.asSharedFlow() + + private val _hostInputAuthorityEnabled = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val hostInputAuthorityEnabled = _hostInputAuthorityEnabled.asSharedFlow() + + private val _padBuffer = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val padBuffer = _padBuffer.asSharedFlow() + + private val _saveTransferProgress = MutableStateFlow(null) + val saveTransferProgress = _saveTransferProgress.asStateFlow() + + suspend fun join(): Boolean = withContext(Dispatchers.IO) { + if (isClosed) throw IllegalStateException("Cannot join a closed session") + + mergeMessages() + .runningFold(emptyList()) { acc, msg -> listOf(msg) + acc } + .onEach { _messages.tryEmit(it) } + .launchIn(sessionScope) + + netPlayClientPointer = nativeJoin() + + if (netPlayClientPointer == 0L || !isActive) { + closeBlocking() + return@withContext false + } + + true + } + + fun sendMessage(message: String) = nativeSendMessage(message) + + fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + + fun consumeBootSessionData(): Long { + return bootSessionDataPointer.also { + bootSessionDataPointer = 0 + } + } + + suspend fun close() = withContext(Dispatchers.IO) { + closeBlocking() + } + + @Synchronized + fun closeBlocking() { + if (isClosed) return + isClosed = true + sessionScope.cancel() + releaseNativeResources() + onClosed(this) + } + + protected fun finalize() { + releaseNativeResources() + } + + private fun mergeMessages(): Flow = merge( + chatMessages.map { NetplayMessage.Chat(it) }, + game.map { NetplayMessage.GameChanged(it) }, + hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, + padBuffer.map { NetplayMessage.BufferChanged(it) }, + ) + + private fun releaseNativeResources() { + val currentBootSessionDataPointer = bootSessionDataPointer + if (currentBootSessionDataPointer != 0L) { + bootSessionDataPointer = 0 + nativeReleaseBootSessionData(currentBootSessionDataPointer) + } + + val currentNetPlayClientPointer = netPlayClientPointer + if (currentNetPlayClientPointer != 0L) { + netPlayClientPointer = 0 + nativeReleaseClient(currentNetPlayClientPointer) + } + + val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer + if (currentNetPlayUICallbacksPointer != 0L) { + netPlayUICallbacksPointer = 0 + nativeReleaseUICallbacks(currentNetPlayUICallbacksPointer) + } + } + + // JNI methods + + private external fun nativeCreateUICallbacks(): Long + + private external fun nativeJoin(): Long + + private external fun nativeSendMessage(message: String) + + private external fun nativeAdjustPadBufferSize(buffer: Int) + + private external fun nativeReleaseUICallbacks(pointer: Long) + + private external fun nativeReleaseClient(pointer: Long) + + private external fun nativeReleaseBootSessionData(pointer: Long) + + // NetPlayUI callbacks + + @Keep + fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { + this.bootSessionDataPointer = bootSessionDataPointer + _stopGame.flush() + _launchGame.trySend(gameFilePath) + } + + @Keep + fun onStopGame() { + _stopGame.trySend(Unit) + } + + @Keep + fun onConnectionLost() { + _connectionLost.trySend(Unit) + } + + @Keep + fun onConnectionError(message: String) { + _connectionErrors.trySend(message) + } + + @Keep + fun onUpdate(players: Array) { + _players.tryEmit(players.toList()) + } + + @Keep + fun onChatMessageReceived(message: String) { + _chatMessages.tryEmit(message) + } + + @Keep + fun onHostInputAuthorityChanged(enabled: Boolean) { + _hostInputAuthorityEnabled.tryEmit(enabled) + } + + @Keep + fun onGameChanged(game: String) { + _game.tryEmit(game) + } + + @Keep + fun onPadBufferChanged(buffer: Int) { + if (_hostInputAuthorityEnabled.replayCache.firstOrNull() == true) return + _padBuffer.tryEmit(buffer) + } + + @Keep + fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { + val players = _players.replayCache.firstOrNull() + _saveTransferProgress.value = SaveTransferProgress( + title = title, + totalSize = dataSize, + playerProgresses = playerIds.map { playerId -> + SaveTransferProgress.PlayerProgress( + playerId = playerId, + name = players?.find { it.pid == playerId }?.name ?: "Invalid Player ID", + progress = 0, + ) + }, + ) + } + + @Keep + fun onSetChunkedProgress(playerId: Int, progress: Long) { + val current = _saveTransferProgress.value + _saveTransferProgress.value = current?.copy( + playerProgresses = current.playerProgresses.map { + if (it.playerId == playerId) { + it.copy(progress = progress) + } else { + it + } + } + ) + } + + @Keep + fun onHideChunkedProgressDialog() { + _saveTransferProgress.value = null + } +} + +private fun Channel.flush() { + while (this.tryReceive().isSuccess) Unit +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index 2a04e41c35..a9b34d3147 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -3,22 +3,31 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting import org.dolphinemu.dolphinemu.services.GameFileCacheManager -class NetplaySetupViewModel : ViewModel() { +class NetplaySetupViewModel( + private val netplayManager: NetplayManager, +) : ViewModel() { + private val _connectionRole = MutableStateFlow(ConnectionRole.Connect) val connectionRole = _connectionRole.asStateFlow() @@ -45,7 +54,8 @@ class NetplaySetupViewModel : ViewModel() { private val _connecting = MutableStateFlow(false) val connecting = _connecting.asStateFlow() - val errors = Netplay.connectionErrors + private val _errors = MutableSharedFlow(extraBufferCapacity = 8) + val errors = _errors.asSharedFlow() init { GameFileCacheManager.startLoad() @@ -89,16 +99,42 @@ class NetplaySetupViewModel : ViewModel() { } fun connect() { + if (_connecting.value) return + _connecting.value = true viewModelScope.launch { - GameFileCacheManager.isLoading().asFlow().first { it == false } - - if (Netplay.join()) { - _showNetplayScreen.trySend(Unit) + var errorForwarding: Job? = null + + try { + GameFileCacheManager.isLoading().asFlow().first { it == false } + + val session = netplayManager.createSession() + errorForwarding = session.connectionErrors + .onEach { _errors.emit(it) } + .launchIn(this) + + if (session.join()) { + _showNetplayScreen.trySend(Unit) + } + } finally { + errorForwarding?.cancel() + _connecting.value = false } + } + } + + override fun onCleared() { + super.onCleared() + // There should not be an active session at this point but in case one was created + // but launching the Netplay screen failed, close it. + netplayManager.activeSession?.closeBlocking() + } - _connecting.value = false + class Factory(private val netplayManager: NetplayManager) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return NetplaySetupViewModel(netplayManager) as T } } } 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 66ad26aaec..96f9d7328b 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 @@ -3,51 +3,43 @@ package org.dolphinemu.dolphinemu.features.netplay.model import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplaySession import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig -class NetplayViewModel : ViewModel() { - val launchGame = Netplay.launchGame +class NetplayViewModel( + private val netplaySession: NetplaySession, +) : ViewModel() { - private val _goBack = Channel(CONFLATED) - val goBack = _goBack.receiveAsFlow() + val launchGame = netplaySession.launchGame - val connectionLost = Netplay.connectionLost + val connectionLost = netplaySession.connectionLost - val players = Netplay.players + val players = netplaySession.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - val messages = Netplay.messages + val messages = netplaySession.messages .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - val game = Netplay.game + val game = netplaySession.game .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") - val hostInputAuthority = Netplay.hostInputAuthorityEnabled + val hostInputAuthority = netplaySession.hostInputAuthorityEnabled .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) val maxBuffer = _maxBuffer.asStateFlow() - val saveTransferProgress = Netplay.saveTransferProgress - - init { - if (!Netplay.isClientConnected()) { - _goBack.trySend(Unit) - } - } + val saveTransferProgress = netplaySession.saveTransferProgress fun sendMessage(message: String) { val trimmedMessage = message.trim() @@ -55,20 +47,29 @@ class NetplayViewModel : ViewModel() { return } - Netplay.sendMessage(trimmedMessage) + netplaySession.sendMessage(trimmedMessage) } fun setMaxBuffer(buffer: Int) { _maxBuffer.value = buffer IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, buffer) - Netplay.adjustPadBufferSize(buffer) + netplaySession.adjustPadBufferSize(buffer) } @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() + // Closing the netplay session is a bit slow for the main thread so launch in + // GlobalScope and allow the activity and view model to finish immediately. GlobalScope.launch { - Netplay.quit() + netplaySession.close() + } + } + + class Factory(private val session: NetplaySession) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return NetplayViewModel(session) as T } } } 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 8a8f860825..f2acf50107 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 @@ -16,6 +16,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -29,11 +30,13 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) - val viewModel = ViewModelProvider(this)[NetplayViewModel::class.java] + val session = NetplayManager.activeSession + if (session == null) { + finish() + return + } - viewModel.goBack - .onEach { finish() } - .launchIn(lifecycleScope) + val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java] viewModel.launchGame .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt index 8058b3ad96..145cf7d635 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme @@ -28,7 +29,10 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider { enableEdgeToEdge() super.onCreate(savedInstanceState) - val viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java] + val viewModel = ViewModelProvider( + this, + NetplaySetupViewModel.Factory(NetplayManager) + )[NetplaySetupViewModel::class.java] viewModel.showNetplayScreen .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index 49331d8a4c..c989cdc780 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding -import org.dolphinemu.dolphinemu.features.netplay.Netplay +import org.dolphinemu.dolphinemu.features.netplay.NetplayManager import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.overlay.InputOverlay @@ -211,7 +211,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // Don't load temporary saves when launching Netplay, this path can trigger // when a game starts due to orientation changes caused by a mismatch in menu // vs emulation activity orientations. - if (loadPreviousTemporaryState && !Netplay.isLaunching) { + val netplaySession = NetplayManager.activeSession + if (loadPreviousTemporaryState && netplaySession?.isLaunching != true) { Log.debug("[EmulationFragment] Starting emulation thread from previous state.") val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" @@ -221,16 +222,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { if (launchSystemMenu) { Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") NativeLibrary.RunSystemMenu() - } else if (Netplay.isLaunching) { + } else if (netplaySession?.isLaunching == true) { Log.debug("[EmulationFragment] Starting emulation thread for Netplay.") val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" } lifecycleScope.launch { - Netplay.stopGame.first() + netplaySession.stopGame.first() stopEmulation() } - NativeLibrary.RunNetPlay(paths, riivolution) + NativeLibrary.RunNetPlay( + paths, + riivolution, + netplaySession.consumeBootSessionData() + ) } else { Log.debug("[EmulationFragment] Starting emulation thread.") val paths = requireNotNull(gamePaths) { diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 150cb8162e..2bee9e2138 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -29,8 +29,8 @@ static jclass s_game_file_cache_manager_class; static jfieldID s_game_file_cache_manager_instance; static jclass s_netplay_class; +static jfieldID s_net_play_ui_callbacks_pointer; static jfieldID s_net_play_client_pointer; -static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; static jmethodID s_netplay_on_connection_lost; @@ -257,14 +257,14 @@ jclass GetNetplayClass() return s_netplay_class; } -jfieldID GetNetPlayClientPointer() +jfieldID GetNetPlayUICallbacksPointer() { - return s_net_play_client_pointer; + return s_net_play_ui_callbacks_pointer; } -jfieldID GetNetplayBootSessionDataPointer() +jfieldID GetNetPlayClientPointer() { - return s_netplay_boot_session_data_pointer; + return s_net_play_client_pointer; } jmethodID GetNetplayOnBootGame() @@ -742,29 +742,30 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->DeleteLocalRef(game_file_cache_manager_class); const jclass netplay_class = - env->FindClass("org/dolphinemu/dolphinemu/features/netplay/Netplay"); + env->FindClass("org/dolphinemu/dolphinemu/features/netplay/NetplaySession"); s_netplay_class = reinterpret_cast(env->NewGlobalRef(netplay_class)); - s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J"); - s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); - s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); - s_netplay_on_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()V"); - s_netplay_on_connection_lost = env->GetStaticMethodID(netplay_class, "onConnectionLost", "()V"); - s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); + s_net_play_ui_callbacks_pointer = + env->GetFieldID(netplay_class, "netPlayUICallbacksPointer", "J"); + s_net_play_client_pointer = env->GetFieldID(netplay_class, "netPlayClientPointer", "J"); + s_netplay_on_boot_game = env->GetMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); + s_netplay_on_stop_game = env->GetMethodID(netplay_class, "onStopGame", "()V"); + s_netplay_on_connection_lost = env->GetMethodID(netplay_class, "onConnectionLost", "()V"); + s_netplay_on_connection_error = env->GetMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = - env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); + env->GetMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); s_netplay_on_host_input_authority_changed = - env->GetStaticMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); + env->GetMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); s_netplay_on_pad_buffer_changed = - env->GetStaticMethodID(netplay_class, "onPadBufferChanged", "(I)V"); + env->GetMethodID(netplay_class, "onPadBufferChanged", "(I)V"); s_netplay_on_chat_message_received = - env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); - s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); + env->GetMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); + s_netplay_update = env->GetMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); s_netplay_on_show_chunked_progress_dialog = - env->GetStaticMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); + env->GetMethodID(netplay_class, "onShowChunkedProgressDialog", "(Ljava/lang/String;J[I)V"); s_netplay_on_set_chunked_progress = - env->GetStaticMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); + env->GetMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = - env->GetStaticMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); + env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 4041b5060e..c5f0a45573 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -32,8 +32,8 @@ jclass GetGameFileCacheManagerClass(); jfieldID GetGameFileCacheManagerInstance(); jclass GetNetplayClass(); +jfieldID GetNetPlayUICallbacksPointer(); jfieldID GetNetPlayClientPointer(); -jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionLost(); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 13471a6cd0..0d29ad3908 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -618,10 +618,10 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2ZLjava_la } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( - JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution) + JNIEnv* env, jclass, jobjectArray jPaths, jboolean jRiivolution, jlong jBootSessionData) { - auto boot_session_data = std::unique_ptr(reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer()))); + auto boot_session_data = std::unique_ptr( + reinterpret_cast(jBootSessionData)); if (!boot_session_data) { env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetDisplayToastMsg(), @@ -629,7 +629,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RunNetPlay( env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetFinishEmulationActivity()); return; } - env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); Run(env, JStringArrayToVector(env, jPaths), jRiivolution, std::move(*boot_session_data)); } diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index fa013017dc..1f9e701edb 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -10,29 +10,54 @@ namespace NetPlay { -NetPlayUICallbacks::NetPlayUICallbacks(std::vector> games) - : m_games(std::move(games)) +NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session, + std::vector> games) + : m_netplay_session(IDCache::GetEnvForThread()->NewWeakGlobalRef(netplay_session)), + m_games(std::move(games)) { m_state_changed_hook = Core::AddOnStateChangedCallback([this](Core::State state) { if ((state == Core::State::Uninitialized || state == Core::State::Stopping) && !m_got_stop_request) { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (client) client->RequestStopGame(); + + env->DeleteLocalRef(netplay_session); } }); } -NetPlayUICallbacks::~NetPlayUICallbacks() = default; +NetPlayUICallbacks::~NetPlayUICallbacks() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->DeleteWeakGlobalRef(m_netplay_session); +} + +jobject NetPlayUICallbacks::GetNetplaySessionLocalRef(JNIEnv* env) const +{ + return env->NewLocalRef(m_netplay_session); +} + +void NetPlayUICallbacks::BootGame(const std::string& filename, + std::unique_ptr boot_session_data) +{ + m_got_stop_request = false; + + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; -void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr boot_session_data) { - m_got_stop_request = false; - JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(), - ToJString(env, filename), reinterpret_cast(boot_session_data.release())); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnBootGame(), ToJString(env, filename), + reinterpret_cast(boot_session_data.release())); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::StopGame() @@ -41,8 +66,14 @@ void NetPlayUICallbacks::StopGame() return; m_got_stop_request = true; + JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnStopGame()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnStopGame()); + env->DeleteLocalRef(netplay_session); } bool NetPlayUICallbacks::IsHosting() const { return false; } @@ -50,10 +81,17 @@ bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (!client) + { + env->DeleteLocalRef(netplay_session); return; + } const std::vector players = client->GetPlayers(); @@ -77,16 +115,21 @@ void NetPlayUICallbacks::Update() env->DeleteLocalRef(player_obj); } - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayUpdate(), player_array); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayUpdate(), player_array); env->DeleteLocalRef(player_array); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::AppendChat(const std::string& message) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnChatMessageReceived(), - ToJString(env, message)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnChatMessageReceived(), + ToJString(env, message)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, @@ -96,8 +139,13 @@ void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_ide m_current_game_name = netplay_name; JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnGameChanged(), - ToJString(env, netplay_name)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnGameChanged(), + ToJString(env, netplay_name)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} @@ -105,13 +153,19 @@ void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} void NetPlayUICallbacks::OnMsgStartGame() { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + auto* client = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer())); if (client) { if (const auto game = FindGameFile(m_current_game_identifier)) client->StartGame(game->GetFilePath()); } + + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnMsgStopGame() {} @@ -122,16 +176,25 @@ void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnPadBufferChanged(), - static_cast(buffer)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnPadBufferChanged(), + static_cast(buffer)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnHostInputAuthorityChanged(), - static_cast(enabled)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHostInputAuthorityChanged(), + static_cast(enabled)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} @@ -139,14 +202,24 @@ void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} void NetPlayUICallbacks::OnConnectionLost() { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionLost()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionLost()); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnConnectionError(const std::string& message) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionError(), - ToJString(env, message)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnConnectionError(), + ToJString(env, message)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {} @@ -191,29 +264,40 @@ void NetPlayUICallbacks::ShowChunkedProgressDialog(const std::string& title, u64 std::span players) { JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; jintArray j_players = env->NewIntArray(static_cast(players.size())); env->SetIntArrayRegion(j_players, 0, static_cast(players.size()), players.data()); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnShowChunkedProgressDialog(), - ToJString(env, title), static_cast(data_size), j_players); + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowChunkedProgressDialog(), + ToJString(env, title), static_cast(data_size), j_players); env->DeleteLocalRef(j_players); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::HideChunkedProgressDialog() { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnHideChunkedProgressDialog()); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHideChunkedProgressDialog()); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::SetChunkedProgress(int pid, u64 progress) { JNIEnv* env = IDCache::GetEnvForThread(); - env->CallStaticVoidMethod(IDCache::GetNetplayClass(), - IDCache::GetNetplayOnSetChunkedProgress(), - static_cast(pid), static_cast(progress)); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetChunkedProgress(), + static_cast(pid), static_cast(progress)); + env->DeleteLocalRef(netplay_session); } void NetPlayUICallbacks::SetHostWiiSyncData(std::vector, std::string) {} diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index 10c1b1ac22..57eec55230 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -5,6 +5,8 @@ #include #include +#include + #include "Common/HookableEvent.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" @@ -13,7 +15,8 @@ namespace NetPlay { class NetPlayUICallbacks : public NetPlay::NetPlayUI { public: - NetPlayUICallbacks(std::vector> games); + NetPlayUICallbacks(jobject netplay_session, + std::vector> games); ~NetPlayUICallbacks() override; void BootGame(const std::string& filename, @@ -59,6 +62,9 @@ public: void SetHostWiiSyncData(std::vector titles, std::string redirect_folder) override; private: + jobject GetNetplaySessionLocalRef(JNIEnv* env) const; + + jweak m_netplay_session; std::vector> m_games; NetPlay::SyncIdentifier m_current_game_identifier; std::string m_current_game_name; diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 62905d203c..61c2e77b9f 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -18,51 +18,41 @@ #include "jni/AndroidCommon/IDCache.h" #include "jni/NetPlay/NetPlayUICallbacks.h" -static NetPlay::NetPlayClient* GetPointer(JNIEnv* env) +static NetPlay::NetPlayUICallbacks* GetUICallbacksPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); } -extern "C" { - -JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass) +static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) { - return static_cast(GetPointer(env)->IsConnected()); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayClientPointer())); } +extern "C" { + JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_sendMessage(JNIEnv* env, jclass, - jstring jmessage) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSendMessage(JNIEnv* env, jobject obj, + jstring jmessage) { - if (auto* client = GetPointer(env)) + if (auto* client = GetClientPointer(env, obj)) client->SendChatMessage(GetJString(env, jmessage)); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_adjustPadBufferSize(JNIEnv* env, jclass, - jint buffer) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeAdjustPadBufferSize(JNIEnv* env, + jobject obj, + jint buffer) { - if (auto* client = GetPointer(env)) + if (auto* client = GetClientPointer(env, obj)) client->AdjustPadBufferSize(static_cast(buffer)); } JNIEXPORT jlong JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeCreateUICallbacks(JNIEnv* env, + jobject obj) { - const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); - const bool is_traversal = traversal_choice == "traversal"; - - std::string host_ip; - host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : - Config::Get(Config::NETPLAY_ADDRESS); - - const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); - const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); - const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); - const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); - jobject jgame_file_cache = env->GetStaticObjectField( IDCache::GetGameFileCacheManagerClass(), IDCache::GetGameFileCacheManagerInstance()); auto* game_file_cache = reinterpret_cast( @@ -72,26 +62,58 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass game_file_cache->ForEach( [&games](const std::shared_ptr& game) { games.push_back(game); }); + return reinterpret_cast(new NetPlay::NetPlayUICallbacks(obj, std::move(games))); +} + +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv* env, jobject obj) +{ + auto* ui = GetUICallbacksPointer(env, obj); + + const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); + const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); + const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); + + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + const std::string host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : + Config::Get(Config::NETPLAY_ADDRESS); + const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + auto* client = new NetPlay::NetPlayClient( - host_ip, host_port, new NetPlay::NetPlayUICallbacks(std::move(games)), nickname, + host_ip, host_port, ui, nickname, NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port}); + if (!client->IsConnected()) + { + delete client; + return 0; + } + return reinterpret_cast(client); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseBootSessionData(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, + jobject, + jlong pointer) +{ + delete reinterpret_cast(pointer); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseBootSessionData(JNIEnv*, + jobject, + jlong pointer) { - auto* data = reinterpret_cast( - env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer())); - delete data; - env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0); + delete reinterpret_cast(pointer); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass) +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClient(JNIEnv*, jobject, + jlong pointer) { - delete GetPointer(env); + delete reinterpret_cast(pointer); } } // extern "C" -- cgit v1.2.3