diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java | 375 | ||||
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt | 348 |
2 files changed, 348 insertions, 375 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java deleted file mode 100644 index 2a2b0f68b..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java +++ /dev/null @@ -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 - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt new file mode 100644 index 000000000..77964b88c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -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 + } + } +} |