godot/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt

/**************************************************************************/
/*  VkThread.kt                                                           */
/**************************************************************************/
/*                         This file is part of:                          */
/*                             GODOT ENGINE                               */
/*                        https://godotengine.org                         */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
/*                                                                        */
/* Permission is hereby granted, free of charge, to any person obtaining  */
/* a copy of this software and associated documentation files (the        */
/* "Software"), to deal in the Software without restriction, including    */
/* without limitation the rights to use, copy, modify, merge, publish,    */
/* distribute, sublicense, and/or sell copies of the Software, and to     */
/* permit persons to whom the Software is furnished to do so, subject to  */
/* the following conditions:                                              */
/*                                                                        */
/* The above copyright notice and this permission notice shall be         */
/* included in all copies or substantial portions of the Software.        */
/*                                                                        */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
/**************************************************************************/

@file:JvmName("VkThread")
package org.godotengine.godot.vulkan

import android.util.Log
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

/**
 * Thread implementation for the [VkSurfaceView] onto which the vulkan logic is ran.
 *
 * The implementation is modeled after [android.opengl.GLSurfaceView]'s GLThread.
 */
internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vkRenderer: VkRenderer) : Thread(TAG) {
	companion object {
		private val TAG = VkThread::class.java.simpleName
	}

	/**
	 * Used to run events scheduled on the thread.
	 */
	private val eventQueue = ArrayList<Runnable>()

	/**
	 * Used to synchronize interaction with other threads (e.g: main thread).
	 */
	private val lock = ReentrantLock()
	private val lockCondition = lock.newCondition()

	private var shouldExit = false
	private var exited = false
	private var rendererInitialized = false
	private var rendererResumed = false
	private var resumed = false
	private var surfaceChanged = false
	private var hasSurface = false
	private var width = 0
	private var height = 0

	/**
	 * Determine when drawing can occur on the thread. This usually occurs after the
	 * [android.view.Surface] is available, the app is in a resumed state.
	 */
	private val readyToDraw
		get() = hasSurface && resumed

	private fun threadExiting() {
		lock.withLock {
			Log.d(TAG, "Exiting render thread")
			vkRenderer.onRenderThreadExiting()

			exited = true
			lockCondition.signalAll()
		}
	}

	/**
	 * Queue an event on the [VkThread].
	 */
	fun queueEvent(event: Runnable) {
		lock.withLock {
			eventQueue.add(event)
			lockCondition.signalAll()
		}
	}

	/**
	 * Request the thread to exit and block until it's done.
	 */
	fun requestExitAndWait() {
		lock.withLock {
			shouldExit = true
			lockCondition.signalAll()
			while (!exited) {
				try {
					Log.i(TAG, "Waiting on exit for $name")
					lockCondition.await()
				} catch (ex: InterruptedException) {
					currentThread().interrupt()
				}
			}
		}
	}

	/**
	 * Invoked when the app resumes.
	 */
	fun onResume() {
		lock.withLock {
			resumed = true
			lockCondition.signalAll()
		}
	}

	/**
	 * Invoked when the app pauses.
	 */
	fun onPause() {
		lock.withLock {
			resumed = false
			lockCondition.signalAll()
		}
	}

	/**
	 * Invoked when the [android.view.Surface] has been created.
	 */
	fun onSurfaceCreated() {
		// This is a no op because surface creation will always be followed by surfaceChanged()
		// which provide all the needed information.
	}

	/**
	 * Invoked following structural updates to [android.view.Surface].
	 */
	fun onSurfaceChanged(width: Int, height: Int) {
		lock.withLock {
			hasSurface = true
			surfaceChanged = true
			this.width = width
			this.height = height

			lockCondition.signalAll()
		}
	}

	/**
	 * Invoked when the [android.view.Surface] is no longer available.
	 */
	fun onSurfaceDestroyed() {
		lock.withLock {
			hasSurface = false
			lockCondition.signalAll()
		}
	}

	/**
	 * Thread loop modeled after [android.opengl.GLSurfaceView]'s GLThread.
	 */
	override fun run() {
		try {
			while (true) {
				var event: Runnable? = null
				lock.withLock {
					while (true) {
						// Code path for exiting the thread loop.
						if (shouldExit) {
							return
						}

						// Check for events and execute them outside of the loop if found to avoid
						// blocking the thread lifecycle by holding onto the lock.
						if (eventQueue.isNotEmpty()) {
							event = eventQueue.removeAt(0)
							break
						}

						if (readyToDraw) {
							if (!rendererResumed) {
								rendererResumed = true
								vkRenderer.onVkResume()

								if (!rendererInitialized) {
									rendererInitialized = true
									vkRenderer.onVkSurfaceCreated(vkSurfaceView.holder.surface)
								}
							}

							if (surfaceChanged) {
								vkRenderer.onVkSurfaceChanged(vkSurfaceView.holder.surface, width, height)
								surfaceChanged = false
							}

							// Break out of the loop so drawing can occur without holding onto the lock.
							break
						} else if (rendererResumed) {
							// If we aren't ready to draw but are resumed, that means we either lost a surface
							// or the app was paused.
							rendererResumed = false
							vkRenderer.onVkPause()
						}
						// We only reach this state if we are not ready to draw and have no queued events, so
						// we wait.
						// On state change, the thread will be awoken using the [lock] and [lockCondition], and
						// we will resume execution.
						lockCondition.await()
					}
				}

				// Run queued event.
				if (event != null) {
					event?.run()
					continue
				}

				// Draw only when there no more queued events.
				vkRenderer.onVkDrawFrame()
			}
		} catch (ex: InterruptedException) {
			Log.i(TAG, "InterruptedException", ex)
		} catch (ex: IllegalStateException) {
			Log.i(TAG, "IllegalStateException", ex)
		} finally {
			threadExiting()
		}
	}
}