Fix the cleanup logic for the Android render thread

On Android the exit logic goes through `Godot#onDestroy()` who attempts to cleanup the engine using the following code:

```
runOnRenderThread {
	GodotLib.ondestroy()
	forceQuit()
}
```

The issue however is that by the time we ran this code, the render thread has already been paused (but not yet destroyed), and thus `GodotLib.ondestroy()` and `forceQuit()` which are scheduled on the render thread are not executed.

To address this, we instead explicitly request the render thread to exit and block until it does. As part of it exit logic, the render thread has been updated to properly destroy and clean the native instance of the Godot engine, resolving the issue.
This commit is contained in:
Fredia Huya-Kouadio 2024-07-22 17:51:45 -07:00
parent 91eb688e17
commit 4d0da74014
15 changed files with 136 additions and 37 deletions

View File

@ -203,7 +203,14 @@ open class GodotEditor : GodotActivity() {
}
if (editorWindowInfo.windowClassName == javaClass.name) {
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
ProcessPhoenix.triggerRebirth(this, newInstance)
val godot = godot
if (godot != null) {
godot.destroyAndKillProcess {
ProcessPhoenix.triggerRebirth(this, newInstance)
}
} else {
ProcessPhoenix.triggerRebirth(this, newInstance)
}
} else {
Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
newInstance.putExtra(EXTRA_NEW_LAUNCH, true)

View File

@ -73,6 +73,7 @@ import java.io.InputStream
import java.lang.Exception
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.atomic.AtomicReference
/**
* Core component used to interface with the native layer of the engine.
@ -127,6 +128,11 @@ class Godot(private val context: Context) : SensorEventListener {
val netUtils = GodotNetUtils(context)
private val commandLineFileParser = CommandLineFileParser()
/**
* Task to run when the engine terminates.
*/
private val runOnTerminate = AtomicReference<Runnable>()
/**
* Tracks whether [onCreate] was completed successfully.
*/
@ -577,10 +583,7 @@ class Godot(private val context: Context) : SensorEventListener {
plugin.onMainDestroy()
}
runOnRenderThread {
GodotLib.ondestroy()
forceQuit()
}
renderView?.onActivityDestroyed()
}
/**
@ -663,6 +666,15 @@ class Godot(private val context: Context) : SensorEventListener {
primaryHost?.onGodotMainLoopStarted()
}
/**
* Invoked on the render thread when the engine is about to terminate.
*/
@Keep
private fun onGodotTerminating() {
Log.v(TAG, "OnGodotTerminating")
runOnTerminate.get()?.run()
}
private fun restart() {
primaryHost?.onGodotRestartRequested(this)
}
@ -798,8 +810,28 @@ class Godot(private val context: Context) : SensorEventListener {
mClipboard.setPrimaryClip(clip)
}
fun forceQuit() {
forceQuit(0)
/**
* Destroys the Godot Engine and kill the process it's running in.
*/
@JvmOverloads
fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
val host = primaryHost
val activity = host?.activity
if (host == null || activity == null) {
// Run the destroyRunnable right away as we are about to force quit.
destroyRunnable?.run()
// Fallback to force quit
forceQuit(0)
return
}
// Store the destroyRunnable so it can be run when the engine is terminating
runOnTerminate.set(destroyRunnable)
runOnUiThread {
onDestroy(host)
}
}
@Keep
@ -814,11 +846,7 @@ class Godot(private val context: Context) : SensorEventListener {
} ?: return false
}
fun onBackPressed(host: GodotHost) {
if (host != primaryHost) {
return
}
fun onBackPressed() {
var shouldQuit = true
for (plugin in pluginRegistry.allPlugins) {
if (plugin.onMainBackPressed()) {

View File

@ -85,12 +85,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
protected open fun getGodotAppLayout() = R.layout.godot_app_layout
override fun onDestroy() {
Log.v(TAG, "Destroying Godot app...")
Log.v(TAG, "Destroying GodotActivity $this...")
super.onDestroy()
godotFragment?.let {
terminateGodotInstance(it.godot)
}
}
override fun onGodotForceQuit(instance: Godot) {

View File

@ -187,7 +187,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
final Activity activity = getActivity();
mCurrentIntent = activity.getIntent();
godot = new Godot(requireContext());
if (parentHost != null) {
godot = parentHost.getGodot();
}
if (godot == null) {
godot = new Godot(requireContext());
}
performEngineInitialization();
BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
}
@ -209,7 +214,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
final String errorMessage = TextUtils.isEmpty(e.getMessage())
? getString(R.string.error_engine_setup_message)
: e.getMessage();
godot.alert(errorMessage, getString(R.string.text_error_title), godot::forceQuit);
godot.alert(errorMessage, getString(R.string.text_error_title), godot::destroyAndKillProcess);
} catch (IllegalArgumentException ignored) {
final Activity activity = getActivity();
Intent notifierIntent = new Intent(activity, activity.getClass());
@ -325,7 +330,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
}
public void onBackPressed() {
godot.onBackPressed(this);
godot.onBackPressed();
}
/**

View File

@ -42,7 +42,6 @@ import org.godotengine.godot.xr.regular.RegularContextFactory;
import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -77,7 +76,7 @@ import java.io.InputStream;
* that matches it exactly (with regards to red/green/blue/alpha channels
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
*/
public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler inputHandler;
@ -140,9 +139,14 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
resumeGLThread();
}
@Override
public void onActivityDestroyed() {
requestRenderThreadExitAndWait();
}
@Override
public void onBackPressed() {
godot.onBackPressed(host);
godot.onBackPressed();
}
@Override

View File

@ -44,6 +44,9 @@ public interface GodotRenderView {
*/
void startRenderer();
/**
* Queues a runnable to be run on the rendering thread.
*/
void queueOnRenderThread(Runnable event);
void onActivityPaused();
@ -54,6 +57,8 @@ public interface GodotRenderView {
void onActivityStarted();
void onActivityDestroyed();
void onBackPressed();
GodotInputHandler getInputHandler();

View File

@ -50,7 +50,7 @@ import androidx.annotation.Keep;
import java.io.InputStream;
public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler mInputHandler;
@ -118,9 +118,14 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
});
}
@Override
public void onActivityDestroyed() {
requestRenderThreadExitAndWait();
}
@Override
public void onBackPressed() {
godot.onBackPressed(host);
godot.onBackPressed();
}
@Override

View File

@ -595,6 +595,15 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
protected final void resumeGLThread() {
mGLThread.onResume();
}
/**
* Requests the render thread to exit and block until it does.
*/
protected final void requestRenderThreadExitAndWait() {
if (mGLThread != null) {
mGLThread.requestExitAndWait();
}
}
// -- GODOT end --
/**
@ -783,6 +792,11 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
* @return true if the buffers should be swapped, false otherwise.
*/
boolean onDrawFrame(GL10 gl);
/**
* Invoked when the render thread is in the process of shutting down.
*/
void onRenderThreadExiting();
// -- GODOT end --
}
@ -1621,6 +1635,12 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
* clean-up everything...
*/
synchronized (sGLThreadManager) {
Log.d("GLThread", "Exiting render thread");
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
view.mRenderer.onRenderThreadExiting();
}
stopEglSurfaceLocked();
stopEglContextLocked();
}

View File

@ -34,6 +34,8 @@ import org.godotengine.godot.GodotLib;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
import android.util.Log;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
@ -41,6 +43,8 @@ import javax.microedition.khronos.opengles.GL10;
* Godot's GL renderer implementation.
*/
public class GodotRenderer implements GLSurfaceView.Renderer {
private final String TAG = GodotRenderer.class.getSimpleName();
private final GodotPluginRegistry pluginRegistry;
private boolean activityJustResumed = false;
@ -62,6 +66,12 @@ public class GodotRenderer implements GLSurfaceView.Renderer {
return swapBuffers;
}
@Override
public void onRenderThreadExiting() {
Log.d(TAG, "Destroying Godot Engine");
GodotLib.ondestroy();
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
GodotLib.resize(null, width, height);
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {

View File

@ -31,11 +31,9 @@
@file:JvmName("VkRenderer")
package org.godotengine.godot.vulkan
import android.util.Log
import android.view.Surface
import org.godotengine.godot.Godot
import org.godotengine.godot.GodotLib
import org.godotengine.godot.plugin.GodotPlugin
import org.godotengine.godot.plugin.GodotPluginRegistry
/**
@ -52,6 +50,11 @@ import org.godotengine.godot.plugin.GodotPluginRegistry
* @see [VkSurfaceView.startRenderer]
*/
internal class VkRenderer {
companion object {
private val TAG = VkRenderer::class.java.simpleName
}
private val pluginRegistry: GodotPluginRegistry = GodotPluginRegistry.getPluginRegistry()
/**
@ -101,8 +104,10 @@ internal class VkRenderer {
}
/**
* Called when the rendering thread is destroyed and used as signal to tear down the Vulkan logic.
* Invoked when the render thread is in the process of shutting down.
*/
fun onVkDestroy() {
fun onRenderThreadExiting() {
Log.d(TAG, "Destroying Godot Engine")
GodotLib.ondestroy()
}
}

View File

@ -113,12 +113,10 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf
}
/**
* Tear down the rendering thread.
*
* Must not be called before a [VkRenderer] has been set.
* Requests the render thread to exit and block until it does.
*/
fun onDestroy() {
vkThread.blockingExit()
fun requestRenderThreadExitAndWait() {
vkThread.requestExitAndWait()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

View File

@ -75,6 +75,9 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
private fun threadExiting() {
lock.withLock {
Log.d(TAG, "Exiting render thread")
vkRenderer.onRenderThreadExiting()
exited = true
lockCondition.signalAll()
}
@ -93,7 +96,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
/**
* Request the thread to exit and block until it's done.
*/
fun blockingExit() {
fun requestExitAndWait() {
lock.withLock {
shouldExit = true
lockCondition.signalAll()
@ -171,7 +174,6 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
while (true) {
// Code path for exiting the thread loop.
if (shouldExit) {
vkRenderer.onVkDestroy()
return
}

View File

@ -114,6 +114,7 @@ static void _terminate(JNIEnv *env, bool p_restart = false) {
NetSocketAndroid::terminate();
if (godot_java) {
godot_java->on_godot_terminating(env);
if (!restart_on_cleanup) {
if (p_restart) {
godot_java->restart(env);

View File

@ -76,6 +76,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
_on_godot_terminating = p_env->GetMethodID(godot_class, "onGodotTerminating", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
_begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V");
@ -136,6 +137,16 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
}
}
void GodotJavaWrapper::on_godot_terminating(JNIEnv *p_env) {
if (_on_godot_terminating) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
ERR_FAIL_NULL(p_env);
p_env->CallVoidMethod(godot_instance, _on_godot_terminating);
}
}
void GodotJavaWrapper::restart(JNIEnv *p_env) {
if (_restart) {
if (p_env == nullptr) {

View File

@ -68,6 +68,7 @@ private:
jmethodID _get_input_fallback_mapping = nullptr;
jmethodID _on_godot_setup_completed = nullptr;
jmethodID _on_godot_main_loop_started = nullptr;
jmethodID _on_godot_terminating = nullptr;
jmethodID _create_new_godot_instance = nullptr;
jmethodID _get_render_view = nullptr;
jmethodID _begin_benchmark_measure = nullptr;
@ -85,6 +86,7 @@ public:
void on_godot_setup_completed(JNIEnv *p_env = nullptr);
void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
void on_godot_terminating(JNIEnv *p_env = nullptr);
void restart(JNIEnv *p_env = nullptr);
bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0);
void set_keep_screen_on(bool p_enabled);