android: Convert EmulationFragment to Kotlin

This commit is contained in:
Charles Lombardo 2023-03-08 10:41:29 -05:00 committed by bunnei
parent 0e4256651a
commit 66079923ae
2 changed files with 348 additions and 375 deletions

View File

@ -1,375 +0,0 @@
package org.yuzu.yuzu_emu.fragments;
import android.content.Context;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.view.Choreographer;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.yuzu.yuzu_emu.NativeLibrary;
import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.overlay.InputOverlay;
import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState;
import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver;
import org.yuzu.yuzu_emu.utils.Log;
public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback {
private static final String KEY_GAMEPATH = "gamepath";
private static final Handler perfStatsUpdateHandler = new Handler();
private SharedPreferences mPreferences;
private InputOverlay mInputOverlay;
private EmulationState mEmulationState;
private DirectoryStateReceiver directoryStateReceiver;
private EmulationActivity activity;
private TextView mPerfStats;
private Runnable perfStatsUpdater;
public static EmulationFragment newInstance(String gamePath) {
Bundle args = new Bundle();
args.putString(KEY_GAMEPATH, gamePath);
EmulationFragment fragment = new EmulationFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof EmulationActivity) {
activity = (EmulationActivity) context;
NativeLibrary.setEmulationActivity((EmulationActivity) context);
} else {
throw new IllegalStateException("EmulationFragment must have EmulationActivity parent");
}
}
/**
* Initialize anything that doesn't depend on the layout / views in here.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// So this fragment doesn't restart on configuration changes; i.e. rotation.
setRetainInstance(true);
mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
String gamePath = getArguments().getString(KEY_GAMEPATH);
mEmulationState = new EmulationState(gamePath);
}
/**
* Initialize the UI and start emulation in here.
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation);
surfaceView.getHolder().addCallback(this);
mInputOverlay = contents.findViewById(R.id.surface_input_overlay);
mPerfStats = contents.findViewById(R.id.show_fps_text);
mPerfStats.setTextColor(Color.YELLOW);
Button doneButton = contents.findViewById(R.id.done_control_config);
if (doneButton != null) {
doneButton.setOnClickListener(v -> stopConfiguringControls());
}
// Setup overlay.
resetInputOverlay();
updateShowFpsOverlay();
// The new Surface created here will get passed to the native code via onSurfaceChanged.
return contents;
}
@Override
public void onResume() {
super.onResume();
Choreographer.getInstance().postFrameCallback(this);
if (DirectoryInitialization.areDirectoriesReady()) {
mEmulationState.run(activity.isActivityRecreated());
} else {
setupDirectoriesThenStartEmulation();
}
}
@Override
public void onPause() {
if (directoryStateReceiver != null) {
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver);
directoryStateReceiver = null;
}
if (mEmulationState.isRunning()) {
mEmulationState.pause();
}
Choreographer.getInstance().removeFrameCallback(this);
super.onPause();
}
@Override
public void onDetach() {
NativeLibrary.clearEmulationActivity();
super.onDetach();
}
private void setupDirectoriesThenStartEmulation() {
IntentFilter statusIntentFilter = new IntentFilter(
DirectoryInitialization.BROADCAST_ACTION);
directoryStateReceiver =
new DirectoryStateReceiver(directoryInitializationState ->
{
if (directoryInitializationState ==
DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
mEmulationState.run(activity.isActivityRecreated());
} else if (directoryInitializationState ==
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
Toast.LENGTH_SHORT)
.show();
}
});
// Registers the DirectoryStateReceiver and its intent filters
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
directoryStateReceiver,
statusIntentFilter);
DirectoryInitialization.start(getActivity());
}
public void refreshInputOverlay() {
mInputOverlay.refreshControls();
}
public void resetInputOverlay() {
// Reset button scale
SharedPreferences.Editor editor = mPreferences.edit();
editor.putInt("controlScale", 50);
editor.apply();
mInputOverlay.resetButtonPlacement();
}
public void updateShowFpsOverlay() {
if (true) {
final int SYSTEM_FPS = 0;
final int FPS = 1;
final int FRAMETIME = 2;
final int SPEED = 3;
perfStatsUpdater = () ->
{
final double[] perfStats = NativeLibrary.GetPerfStats();
if (perfStats[FPS] > 0) {
mPerfStats.setText(String.format("FPS: %.1f", perfStats[FPS]));
}
perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 100);
};
perfStatsUpdateHandler.post(perfStatsUpdater);
mPerfStats.setVisibility(View.VISIBLE);
} else {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater);
}
mPerfStats.setVisibility(View.GONE);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// We purposely don't do anything here.
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height);
mEmulationState.newSurface(holder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mEmulationState.clearSurface();
}
@Override
public void doFrame(long frameTimeNanos) {
Choreographer.getInstance().postFrameCallback(this);
NativeLibrary.DoFrame();
}
public void stopEmulation() {
mEmulationState.stop();
}
public void startConfiguringControls() {
getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE);
mInputOverlay.setIsInEditMode(true);
}
public void stopConfiguringControls() {
getView().findViewById(R.id.done_control_config).setVisibility(View.GONE);
mInputOverlay.setIsInEditMode(false);
}
public boolean isConfiguringControls() {
return mInputOverlay.isInEditMode();
}
private static class EmulationState {
private final String mGamePath;
private State state;
private Surface mSurface;
private boolean mRunWhenSurfaceIsValid;
EmulationState(String gamePath) {
mGamePath = gamePath;
// Starting state is stopped.
state = State.STOPPED;
}
public synchronized boolean isStopped() {
return state == State.STOPPED;
}
// Getters for the current state
public synchronized boolean isPaused() {
return state == State.PAUSED;
}
public synchronized boolean isRunning() {
return state == State.RUNNING;
}
public synchronized void stop() {
if (state != State.STOPPED) {
Log.debug("[EmulationFragment] Stopping emulation.");
state = State.STOPPED;
NativeLibrary.StopEmulation();
} else {
Log.warning("[EmulationFragment] Stop called while already stopped.");
}
}
// State changing methods
public synchronized void pause() {
if (state != State.PAUSED) {
state = State.PAUSED;
Log.debug("[EmulationFragment] Pausing emulation.");
// Release the surface before pausing, since emulation has to be running for that.
NativeLibrary.SurfaceDestroyed();
NativeLibrary.PauseEmulation();
} else {
Log.warning("[EmulationFragment] Pause called while already paused.");
}
}
public synchronized void run(boolean isActivityRecreated) {
if (isActivityRecreated) {
if (NativeLibrary.IsRunning()) {
state = State.PAUSED;
}
} else {
Log.debug("[EmulationFragment] activity resumed or fresh start");
}
// If the surface is set, run now. Otherwise, wait for it to get set.
if (mSurface != null) {
runWithValidSurface();
} else {
mRunWhenSurfaceIsValid = true;
}
}
// Surface callbacks
public synchronized void newSurface(Surface surface) {
mSurface = surface;
if (mRunWhenSurfaceIsValid) {
runWithValidSurface();
}
}
public synchronized void clearSurface() {
if (mSurface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.");
} else {
mSurface = null;
Log.debug("[EmulationFragment] Surface destroyed.");
if (state == State.RUNNING) {
NativeLibrary.SurfaceDestroyed();
state = State.PAUSED;
} else if (state == State.PAUSED) {
Log.warning("[EmulationFragment] Surface cleared while emulation paused.");
} else {
Log.warning("[EmulationFragment] Surface cleared while emulation stopped.");
}
}
}
private void runWithValidSurface() {
mRunWhenSurfaceIsValid = false;
if (state == State.STOPPED) {
NativeLibrary.SurfaceChanged(mSurface);
Thread mEmulationThread = new Thread(() ->
{
Log.debug("[EmulationFragment] Starting emulation thread.");
NativeLibrary.Run(mGamePath);
}, "NativeEmulation");
mEmulationThread.start();
} else if (state == State.PAUSED) {
Log.debug("[EmulationFragment] Resuming emulation.");
NativeLibrary.SurfaceChanged(mSurface);
NativeLibrary.UnPauseEmulation();
} else {
Log.debug("[EmulationFragment] Bug, run called while already running.");
}
state = State.RUNNING;
}
private enum State {
STOPPED, RUNNING, PAUSED
}
}
}

View File

@ -0,0 +1,348 @@
package org.yuzu.yuzu_emu.fragments
import android.content.Context
import android.content.IntentFilter
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState
import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver
import org.yuzu.yuzu_emu.utils.Log
class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
private lateinit var preferences: SharedPreferences
private var inputOverlay: InputOverlay? = null
private lateinit var emulationState: EmulationState
private var directoryStateReceiver: DirectoryStateReceiver? = null
private var emulationActivity: EmulationActivity? = null
private lateinit var perfStats: TextView
private var perfStatsUpdater: (() -> Unit)? = null
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is EmulationActivity) {
emulationActivity = context
NativeLibrary.setEmulationActivity(context)
} else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
}
}
/**
* Initialize anything that doesn't depend on the layout / views in here.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
val gamePath = requireArguments().getString(KEY_GAMEPATH)
emulationState = EmulationState(gamePath)
}
/**
* Initialize the UI and start emulation in here.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val contents = inflater.inflate(R.layout.fragment_emulation, container, false)
val surfaceView = contents.findViewById<SurfaceView>(R.id.surface_emulation)
surfaceView.holder.addCallback(this)
inputOverlay = contents.findViewById(R.id.surface_input_overlay)
perfStats = contents.findViewById(R.id.show_fps_text)
perfStats.setTextColor(Color.YELLOW)
val doneButton = contents.findViewById<Button>(R.id.done_control_config)
doneButton?.setOnClickListener { stopConfiguringControls() }
// Setup overlay.
resetInputOverlay()
updateShowFpsOverlay()
// The new Surface created here will get passed to the native code via onSurfaceChanged.
return contents
}
override fun onResume() {
super.onResume()
Choreographer.getInstance().postFrameCallback(this)
if (DirectoryInitialization.areDirectoriesReady()) {
emulationState.run(emulationActivity!!.isActivityRecreated)
} else {
setupDirectoriesThenStartEmulation()
}
}
override fun onPause() {
if (directoryStateReceiver != null) {
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(
directoryStateReceiver!!
)
directoryStateReceiver = null
}
if (emulationState.isRunning) {
emulationState.pause()
}
Choreographer.getInstance().removeFrameCallback(this)
super.onPause()
}
override fun onDetach() {
NativeLibrary.clearEmulationActivity()
super.onDetach()
}
private fun setupDirectoriesThenStartEmulation() {
val statusIntentFilter = IntentFilter(
DirectoryInitialization.BROADCAST_ACTION
)
directoryStateReceiver =
DirectoryStateReceiver { directoryInitializationState: DirectoryInitializationState ->
if (directoryInitializationState ==
DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED
) {
emulationState.run(emulationActivity!!.isActivityRecreated)
} else if (directoryInitializationState ==
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE
) {
Toast.makeText(
context,
R.string.external_storage_not_mounted,
Toast.LENGTH_SHORT
)
.show()
}
}
// Registers the DirectoryStateReceiver and its intent filters
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(
directoryStateReceiver!!,
statusIntentFilter
)
DirectoryInitialization.start(requireContext())
}
fun refreshInputOverlay() {
inputOverlay!!.refreshControls()
}
fun resetInputOverlay() {
// Reset button scale
preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, 50)
.apply()
inputOverlay!!.resetButtonPlacement()
}
private fun updateShowFpsOverlay() {
// TODO: Create a setting so that this actually works...
if (true) {
val SYSTEM_FPS = 0
val FPS = 1
val FRAMETIME = 2
val SPEED = 3
perfStatsUpdater = {
val perfStats = NativeLibrary.GetPerfStats()
if (perfStats[FPS] > 0) {
this.perfStats.text = String.format("FPS: %.1f", perfStats[FPS])
}
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
}
perfStatsUpdateHandler.post(perfStatsUpdater!!)
perfStats.visibility = View.VISIBLE
} else {
if (perfStatsUpdater != null) {
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
}
perfStats.visibility = View.GONE
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
// We purposely don't do anything here.
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height)
emulationState.newSurface(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
emulationState.clearSurface()
}
override fun doFrame(frameTimeNanos: Long) {
Choreographer.getInstance().postFrameCallback(this)
NativeLibrary.DoFrame()
}
fun stopEmulation() {
emulationState.stop()
}
fun startConfiguringControls() {
requireView().findViewById<View>(R.id.done_control_config).visibility =
View.VISIBLE
inputOverlay!!.setIsInEditMode(true)
}
fun stopConfiguringControls() {
requireView().findViewById<View>(R.id.done_control_config).visibility = View.GONE
inputOverlay!!.setIsInEditMode(false)
}
val isConfiguringControls: Boolean
get() = inputOverlay!!.isInEditMode
private class EmulationState(private val mGamePath: String?) {
private var state: State
private var surface: Surface? = null
private var runWhenSurfaceIsValid = false
init {
// Starting state is stopped.
state = State.STOPPED
}
@get:Synchronized
val isStopped: Boolean
get() = state == State.STOPPED
// Getters for the current state
@get:Synchronized
val isPaused: Boolean
get() = state == State.PAUSED
@get:Synchronized
val isRunning: Boolean
get() = state == State.RUNNING
@Synchronized
fun stop() {
if (state != State.STOPPED) {
Log.debug("[EmulationFragment] Stopping emulation.")
state = State.STOPPED
NativeLibrary.StopEmulation()
} else {
Log.warning("[EmulationFragment] Stop called while already stopped.")
}
}
// State changing methods
@Synchronized
fun pause() {
if (state != State.PAUSED) {
state = State.PAUSED
Log.debug("[EmulationFragment] Pausing emulation.")
// Release the surface before pausing, since emulation has to be running for that.
NativeLibrary.SurfaceDestroyed()
NativeLibrary.PauseEmulation()
} else {
Log.warning("[EmulationFragment] Pause called while already paused.")
}
}
@Synchronized
fun run(isActivityRecreated: Boolean) {
if (isActivityRecreated) {
if (NativeLibrary.IsRunning()) {
state = State.PAUSED
}
} else {
Log.debug("[EmulationFragment] activity resumed or fresh start")
}
// If the surface is set, run now. Otherwise, wait for it to get set.
if (surface != null) {
runWithValidSurface()
} else {
runWhenSurfaceIsValid = true
}
}
// Surface callbacks
@Synchronized
fun newSurface(surface: Surface?) {
this.surface = surface
if (runWhenSurfaceIsValid) {
runWithValidSurface()
}
}
@Synchronized
fun clearSurface() {
if (surface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
} else {
surface = null
Log.debug("[EmulationFragment] Surface destroyed.")
when (state) {
State.RUNNING -> {
NativeLibrary.SurfaceDestroyed()
state = State.PAUSED
}
State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.")
else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.")
}
}
}
private fun runWithValidSurface() {
runWhenSurfaceIsValid = false
when (state) {
State.STOPPED -> {
NativeLibrary.SurfaceChanged(surface)
val mEmulationThread = Thread({
Log.debug("[EmulationFragment] Starting emulation thread.")
NativeLibrary.Run(mGamePath)
}, "NativeEmulation")
mEmulationThread.start()
}
State.PAUSED -> {
Log.debug("[EmulationFragment] Resuming emulation.")
NativeLibrary.SurfaceChanged(surface)
NativeLibrary.UnPauseEmulation()
}
else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
}
state = State.RUNNING
}
private enum class State {
STOPPED, RUNNING, PAUSED
}
}
companion object {
private const val KEY_GAMEPATH = "gamepath"
private val perfStatsUpdateHandler = Handler()
fun newInstance(gamePath: String?): EmulationFragment {
val args = Bundle()
args.putString(KEY_GAMEPATH, gamePath)
val fragment = EmulationFragment()
fragment.arguments = args
return fragment
}
}
}