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.
This commit is contained in:
Tom Pratt 2026-04-28 17:37:40 +02:00
parent 371fa1a250
commit 1285cb2282
14 changed files with 407 additions and 218 deletions

View File

@ -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<String>, riivolution: Boolean)
external fun RunNetPlay(paths: Array<String>, riivolution: Boolean, bootSessionDataPointer: Long)
@JvmStatic
external fun ChangeDisc(path: String)

View File

@ -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<Unit>? = 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
}
}
}

View File

@ -6,7 +6,6 @@ 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
@ -29,14 +28,21 @@ 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
class NetplaySession(
private val onClosed: (NetplaySession) -> Unit,
) {
private var netPlayUICallbacksPointer: Long = nativeCreateUICallbacks()
private var netPlayClientPointer: Long = 0
@Keep
private var bootSessionDataPointer: Long = 0
private var sessionScope: CoroutineScope? = null
private val sessionScope = CoroutineScope(SupervisorJob())
@Volatile
var isClosing = false
private set
val isLaunching: Boolean
get() = bootSessionDataPointer != 0L
@ -93,85 +99,47 @@ object Netplay {
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<NetplayMessage>()) { acc, msg -> listOf(msg) + acc }
.onEach { _messages.tryEmit(it) }
.launchIn(scope)
.launchIn(sessionScope)
netPlayClientPointer = Join()
val isConnected = netPlayClientPointer != 0L && isClientConnected()
netPlayClientPointer = nativeJoin()
if (!isActive) {
releaseNetplayClient()
if (netPlayClientPointer == 0L || !isActive) {
closeBlocking()
return@withContext false
}
if (isConnected) {
return@withContext true
}
releaseNetplayClient()
false
true
}
suspend fun quit() = withContext(Dispatchers.IO) {
releaseNetplayClient()
}
fun sendMessage(message: String) = nativeSendMessage(message)
@OptIn(ExperimentalCoroutinesApi::class)
private fun releaseNetplayClient() {
sessionScope?.cancel()
sessionScope = null
fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
if (bootSessionDataPointer != 0L) {
ReleaseBootSessionData()
fun consumeBootSessionData(): Long {
return bootSessionDataPointer.also {
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
}
suspend fun close() = withContext(Dispatchers.IO) {
closeBlocking()
}
@JvmStatic
private external fun Join(): Long
@Synchronized
fun closeBlocking() {
if (isClosing) return
isClosing = true
sessionScope.cancel()
releaseNativeResources()
onClosed(this)
}
@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()
protected fun finalize() {
releaseNativeResources()
}
private fun mergeMessages(): Flow<NetplayMessage> = merge(
chatMessages.map { NetplayMessage.Chat(it) },
@ -180,10 +148,45 @@ object Netplay {
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
@JvmStatic
fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) {
this.bootSessionDataPointer = bootSessionDataPointer
_stopGame.flush()
@ -191,57 +194,47 @@ object Netplay {
}
@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<Player>) {
_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(
@ -258,7 +251,6 @@ object Netplay {
}
@Keep
@JvmStatic
fun onSetChunkedProgress(playerId: Int, progress: Long) {
val current = _saveTransferProgress.value
_saveTransferProgress.value = current?.copy(
@ -273,11 +265,9 @@ object Netplay {
}
@Keep
@JvmStatic
fun onHideChunkedProgressDialog() {
_saveTransferProgress.value = null
}
}
private fun <T> Channel<T>.flush() {

View File

@ -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>(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<String>(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 }
var errorForwarding: Job? = null
if (Netplay.join()) {
_showNetplayScreen.trySend(Unit)
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
}
}
}
_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()
}
class Factory(private val netplayManager: NetplayManager) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NetplaySetupViewModel(netplayManager) as T
}
}
}

View File

@ -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<Unit>(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 <T : ViewModel> create(modelClass: Class<T>): T {
return NetplayViewModel(session) as T
}
}
}

View File

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

View File

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

View File

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

View File

@ -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,16 +257,16 @@ jclass GetNetplayClass()
return s_netplay_class;
}
jfieldID GetNetPlayUICallbacksPointer()
{
return s_net_play_ui_callbacks_pointer;
}
jfieldID GetNetPlayClientPointer()
{
return s_net_play_client_pointer;
}
jfieldID GetNetplayBootSessionDataPointer()
{
return s_netplay_boot_session_data_pointer;
}
jmethodID GetNetplayOnBootGame()
{
return s_netplay_on_boot_game;
@ -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<jclass>(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 =

View File

@ -32,8 +32,8 @@ jclass GetGameFileCacheManagerClass();
jfieldID GetGameFileCacheManagerInstance();
jclass GetNetplayClass();
jfieldID GetNetPlayUICallbacksPointer();
jfieldID GetNetPlayClientPointer();
jfieldID GetNetplayBootSessionDataPointer();
jmethodID GetNetplayOnBootGame();
jmethodID GetNetplayOnStopGame();
jmethodID GetNetplayOnConnectionLost();

View File

@ -616,10 +616,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<BootSessionData>(reinterpret_cast<BootSessionData*>(
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer())));
auto boot_session_data = std::unique_ptr<BootSessionData>(
reinterpret_cast<BootSessionData*>(jBootSessionData));
if (!boot_session_data)
{
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), IDCache::GetDisplayToastMsg(),
@ -627,7 +627,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));
}

View File

@ -10,29 +10,54 @@
namespace NetPlay {
NetPlayUICallbacks::NetPlayUICallbacks(std::vector<std::shared_ptr<const UICommon::GameFile>> games)
: m_games(std::move(games))
NetPlayUICallbacks::NetPlayUICallbacks(jobject netplay_session,
std::vector<std::shared_ptr<const UICommon::GameFile>> 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<NetPlay::NetPlayClient*>(
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);
}
void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr<BootSessionData> boot_session_data) {
m_got_stop_request = false;
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(),
ToJString(env, filename), reinterpret_cast<jlong>(boot_session_data.release()));
jobject NetPlayUICallbacks::GetNetplaySessionLocalRef(JNIEnv* env) const
{
return env->NewLocalRef(m_netplay_session);
}
void NetPlayUICallbacks::BootGame(const std::string& filename,
std::unique_ptr<BootSessionData> boot_session_data)
{
m_got_stop_request = false;
JNIEnv* env = IDCache::GetEnvForThread();
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnBootGame(), ToJString(env, filename),
reinterpret_cast<jlong>(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,11 +81,18 @@ bool NetPlayUICallbacks::IsHosting() const { return false; }
void NetPlayUICallbacks::Update()
{
JNIEnv* env = IDCache::GetEnvForThread();
auto* client = reinterpret_cast<NetPlay::NetPlayClient*>(
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer()));
if (!client)
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
auto* client = reinterpret_cast<NetPlay::NetPlayClient*>(
env->GetLongField(netplay_session, IDCache::GetNetPlayClientPointer()));
if (!client)
{
env->DeleteLocalRef(netplay_session);
return;
}
const std::vector<const NetPlay::Player*> players = client->GetPlayers();
jobjectArray player_array =
@ -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<NetPlay::NetPlayClient*>(
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<jint>(buffer));
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnPadBufferChanged(),
static_cast<jint>(buffer));
env->DeleteLocalRef(netplay_session);
}
void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled)
{
JNIEnv* env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetNetplayClass(),
IDCache::GetNetplayOnHostInputAuthorityChanged(),
static_cast<jboolean>(enabled));
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnHostInputAuthorityChanged(),
static_cast<jboolean>(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<const int> players)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
jintArray j_players = env->NewIntArray(static_cast<jsize>(players.size()));
env->SetIntArrayRegion(j_players, 0, static_cast<jsize>(players.size()), players.data());
env->CallStaticVoidMethod(IDCache::GetNetplayClass(),
IDCache::GetNetplayOnShowChunkedProgressDialog(),
ToJString(env, title), static_cast<jlong>(data_size), j_players);
env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnShowChunkedProgressDialog(),
ToJString(env, title), static_cast<jlong>(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<jint>(pid), static_cast<jlong>(progress));
jobject netplay_session = GetNetplaySessionLocalRef(env);
if (!netplay_session)
return;
env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnSetChunkedProgress(),
static_cast<jint>(pid), static_cast<jlong>(progress));
env->DeleteLocalRef(netplay_session);
}
void NetPlayUICallbacks::SetHostWiiSyncData(std::vector<u64>, std::string) {}

View File

@ -5,6 +5,8 @@
#include <string>
#include <vector>
#include <jni.h>
#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<std::shared_ptr<const UICommon::GameFile>> games);
NetPlayUICallbacks(jobject netplay_session,
std::vector<std::shared_ptr<const UICommon::GameFile>> games);
~NetPlayUICallbacks() override;
void BootGame(const std::string& filename,
@ -59,6 +62,9 @@ public:
void SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder) override;
private:
jobject GetNetplaySessionLocalRef(JNIEnv* env) const;
jweak m_netplay_session;
std::vector<std::shared_ptr<const UICommon::GameFile>> m_games;
NetPlay::SyncIdentifier m_current_game_identifier;
std::string m_current_game_name;

View File

@ -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<NetPlay::NetPlayUICallbacks*>(
env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer()));
}
static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj)
{
return reinterpret_cast<NetPlay::NetPlayClient*>(
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer()));
env->GetLongField(obj, IDCache::GetNetPlayClientPointer()));
}
extern "C" {
JNIEXPORT jboolean JNICALL
Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass)
{
return static_cast<jboolean>(GetPointer(env)->IsConnected());
}
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<u32>(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<UICommon::GameFileCache*>(
@ -72,26 +62,58 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass
game_file_cache->ForEach(
[&games](const std::shared_ptr<const UICommon::GameFile>& game) { games.push_back(game); });
return reinterpret_cast<jlong>(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<jlong>(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)
{
auto* data = reinterpret_cast<BootSessionData*>(
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer()));
delete data;
env->SetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetplayBootSessionDataPointer(), 0);
delete reinterpret_cast<NetPlay::NetPlayUICallbacks*>(pointer);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass)
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseBootSessionData(JNIEnv*,
jobject,
jlong pointer)
{
delete GetPointer(env);
delete reinterpret_cast<BootSessionData*>(pointer);
}
JNIEXPORT void JNICALL
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClient(JNIEnv*, jobject,
jlong pointer)
{
delete reinterpret_cast<NetPlay::NetPlayClient*>(pointer);
}
} // extern "C"