diff options
| -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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.fragments; | ||
| 2 | |||
| 3 | import android.content.Context; | ||
| 4 | import android.content.IntentFilter; | ||
| 5 | import android.content.SharedPreferences; | ||
| 6 | import android.graphics.Color; | ||
| 7 | import android.os.Bundle; | ||
| 8 | import android.os.Handler; | ||
| 9 | import android.preference.PreferenceManager; | ||
| 10 | import android.view.Choreographer; | ||
| 11 | import android.view.LayoutInflater; | ||
| 12 | import android.view.Surface; | ||
| 13 | import android.view.SurfaceHolder; | ||
| 14 | import android.view.SurfaceView; | ||
| 15 | import android.view.View; | ||
| 16 | import android.view.ViewGroup; | ||
| 17 | import android.widget.Button; | ||
| 18 | import android.widget.TextView; | ||
| 19 | import android.widget.Toast; | ||
| 20 | |||
| 21 | import androidx.annotation.NonNull; | ||
| 22 | import androidx.fragment.app.Fragment; | ||
| 23 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | ||
| 24 | |||
| 25 | import org.yuzu.yuzu_emu.NativeLibrary; | ||
| 26 | import org.yuzu.yuzu_emu.R; | ||
| 27 | import org.yuzu.yuzu_emu.activities.EmulationActivity; | ||
| 28 | import org.yuzu.yuzu_emu.overlay.InputOverlay; | ||
| 29 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization; | ||
| 30 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState; | ||
| 31 | import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver; | ||
| 32 | import org.yuzu.yuzu_emu.utils.Log; | ||
| 33 | |||
| 34 | public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { | ||
| 35 | private static final String KEY_GAMEPATH = "gamepath"; | ||
| 36 | |||
| 37 | private static final Handler perfStatsUpdateHandler = new Handler(); | ||
| 38 | |||
| 39 | private SharedPreferences mPreferences; | ||
| 40 | |||
| 41 | private InputOverlay mInputOverlay; | ||
| 42 | |||
| 43 | private EmulationState mEmulationState; | ||
| 44 | |||
| 45 | private DirectoryStateReceiver directoryStateReceiver; | ||
| 46 | |||
| 47 | private EmulationActivity activity; | ||
| 48 | |||
| 49 | private TextView mPerfStats; | ||
| 50 | |||
| 51 | private Runnable perfStatsUpdater; | ||
| 52 | |||
| 53 | public static EmulationFragment newInstance(String gamePath) { | ||
| 54 | Bundle args = new Bundle(); | ||
| 55 | args.putString(KEY_GAMEPATH, gamePath); | ||
| 56 | |||
| 57 | EmulationFragment fragment = new EmulationFragment(); | ||
| 58 | fragment.setArguments(args); | ||
| 59 | return fragment; | ||
| 60 | } | ||
| 61 | |||
| 62 | @Override | ||
| 63 | public void onAttach(@NonNull Context context) { | ||
| 64 | super.onAttach(context); | ||
| 65 | |||
| 66 | if (context instanceof EmulationActivity) { | ||
| 67 | activity = (EmulationActivity) context; | ||
| 68 | NativeLibrary.setEmulationActivity((EmulationActivity) context); | ||
| 69 | } else { | ||
| 70 | throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | /** | ||
| 75 | * Initialize anything that doesn't depend on the layout / views in here. | ||
| 76 | */ | ||
| 77 | @Override | ||
| 78 | public void onCreate(Bundle savedInstanceState) { | ||
| 79 | super.onCreate(savedInstanceState); | ||
| 80 | |||
| 81 | // So this fragment doesn't restart on configuration changes; i.e. rotation. | ||
| 82 | setRetainInstance(true); | ||
| 83 | |||
| 84 | mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||
| 85 | |||
| 86 | String gamePath = getArguments().getString(KEY_GAMEPATH); | ||
| 87 | mEmulationState = new EmulationState(gamePath); | ||
| 88 | } | ||
| 89 | |||
| 90 | /** | ||
| 91 | * Initialize the UI and start emulation in here. | ||
| 92 | */ | ||
| 93 | @Override | ||
| 94 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||
| 95 | View contents = inflater.inflate(R.layout.fragment_emulation, container, false); | ||
| 96 | |||
| 97 | SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); | ||
| 98 | surfaceView.getHolder().addCallback(this); | ||
| 99 | |||
| 100 | mInputOverlay = contents.findViewById(R.id.surface_input_overlay); | ||
| 101 | mPerfStats = contents.findViewById(R.id.show_fps_text); | ||
| 102 | mPerfStats.setTextColor(Color.YELLOW); | ||
| 103 | |||
| 104 | Button doneButton = contents.findViewById(R.id.done_control_config); | ||
| 105 | if (doneButton != null) { | ||
| 106 | doneButton.setOnClickListener(v -> stopConfiguringControls()); | ||
| 107 | } | ||
| 108 | |||
| 109 | // Setup overlay. | ||
| 110 | resetInputOverlay(); | ||
| 111 | updateShowFpsOverlay(); | ||
| 112 | |||
| 113 | // The new Surface created here will get passed to the native code via onSurfaceChanged. | ||
| 114 | return contents; | ||
| 115 | } | ||
| 116 | |||
| 117 | @Override | ||
| 118 | public void onResume() { | ||
| 119 | super.onResume(); | ||
| 120 | Choreographer.getInstance().postFrameCallback(this); | ||
| 121 | if (DirectoryInitialization.areDirectoriesReady()) { | ||
| 122 | mEmulationState.run(activity.isActivityRecreated()); | ||
| 123 | } else { | ||
| 124 | setupDirectoriesThenStartEmulation(); | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | @Override | ||
| 129 | public void onPause() { | ||
| 130 | if (directoryStateReceiver != null) { | ||
| 131 | LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); | ||
| 132 | directoryStateReceiver = null; | ||
| 133 | } | ||
| 134 | |||
| 135 | if (mEmulationState.isRunning()) { | ||
| 136 | mEmulationState.pause(); | ||
| 137 | } | ||
| 138 | |||
| 139 | Choreographer.getInstance().removeFrameCallback(this); | ||
| 140 | super.onPause(); | ||
| 141 | } | ||
| 142 | |||
| 143 | @Override | ||
| 144 | public void onDetach() { | ||
| 145 | NativeLibrary.clearEmulationActivity(); | ||
| 146 | super.onDetach(); | ||
| 147 | } | ||
| 148 | |||
| 149 | private void setupDirectoriesThenStartEmulation() { | ||
| 150 | IntentFilter statusIntentFilter = new IntentFilter( | ||
| 151 | DirectoryInitialization.BROADCAST_ACTION); | ||
| 152 | |||
| 153 | directoryStateReceiver = | ||
| 154 | new DirectoryStateReceiver(directoryInitializationState -> | ||
| 155 | { | ||
| 156 | if (directoryInitializationState == | ||
| 157 | DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { | ||
| 158 | mEmulationState.run(activity.isActivityRecreated()); | ||
| 159 | } else if (directoryInitializationState == | ||
| 160 | DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { | ||
| 161 | Toast.makeText(getContext(), R.string.external_storage_not_mounted, | ||
| 162 | Toast.LENGTH_SHORT) | ||
| 163 | .show(); | ||
| 164 | } | ||
| 165 | }); | ||
| 166 | |||
| 167 | // Registers the DirectoryStateReceiver and its intent filters | ||
| 168 | LocalBroadcastManager.getInstance(getActivity()).registerReceiver( | ||
| 169 | directoryStateReceiver, | ||
| 170 | statusIntentFilter); | ||
| 171 | DirectoryInitialization.start(getActivity()); | ||
| 172 | } | ||
| 173 | |||
| 174 | public void refreshInputOverlay() { | ||
| 175 | mInputOverlay.refreshControls(); | ||
| 176 | } | ||
| 177 | |||
| 178 | public void resetInputOverlay() { | ||
| 179 | // Reset button scale | ||
| 180 | SharedPreferences.Editor editor = mPreferences.edit(); | ||
| 181 | editor.putInt("controlScale", 50); | ||
| 182 | editor.apply(); | ||
| 183 | |||
| 184 | mInputOverlay.resetButtonPlacement(); | ||
| 185 | } | ||
| 186 | |||
| 187 | public void updateShowFpsOverlay() { | ||
| 188 | if (true) { | ||
| 189 | final int SYSTEM_FPS = 0; | ||
| 190 | final int FPS = 1; | ||
| 191 | final int FRAMETIME = 2; | ||
| 192 | final int SPEED = 3; | ||
| 193 | |||
| 194 | perfStatsUpdater = () -> | ||
| 195 | { | ||
| 196 | final double[] perfStats = NativeLibrary.GetPerfStats(); | ||
| 197 | if (perfStats[FPS] > 0) { | ||
| 198 | mPerfStats.setText(String.format("FPS: %.1f", perfStats[FPS])); | ||
| 199 | } | ||
| 200 | |||
| 201 | perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 100); | ||
| 202 | }; | ||
| 203 | perfStatsUpdateHandler.post(perfStatsUpdater); | ||
| 204 | |||
| 205 | mPerfStats.setVisibility(View.VISIBLE); | ||
| 206 | } else { | ||
| 207 | if (perfStatsUpdater != null) { | ||
| 208 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); | ||
| 209 | } | ||
| 210 | |||
| 211 | mPerfStats.setVisibility(View.GONE); | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | @Override | ||
| 216 | public void surfaceCreated(SurfaceHolder holder) { | ||
| 217 | // We purposely don't do anything here. | ||
| 218 | // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. | ||
| 219 | } | ||
| 220 | |||
| 221 | @Override | ||
| 222 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | ||
| 223 | Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); | ||
| 224 | mEmulationState.newSurface(holder.getSurface()); | ||
| 225 | } | ||
| 226 | |||
| 227 | @Override | ||
| 228 | public void surfaceDestroyed(SurfaceHolder holder) { | ||
| 229 | mEmulationState.clearSurface(); | ||
| 230 | } | ||
| 231 | |||
| 232 | @Override | ||
| 233 | public void doFrame(long frameTimeNanos) { | ||
| 234 | Choreographer.getInstance().postFrameCallback(this); | ||
| 235 | NativeLibrary.DoFrame(); | ||
| 236 | } | ||
| 237 | |||
| 238 | public void stopEmulation() { | ||
| 239 | mEmulationState.stop(); | ||
| 240 | } | ||
| 241 | |||
| 242 | public void startConfiguringControls() { | ||
| 243 | getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); | ||
| 244 | mInputOverlay.setIsInEditMode(true); | ||
| 245 | } | ||
| 246 | |||
| 247 | public void stopConfiguringControls() { | ||
| 248 | getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); | ||
| 249 | mInputOverlay.setIsInEditMode(false); | ||
| 250 | } | ||
| 251 | |||
| 252 | public boolean isConfiguringControls() { | ||
| 253 | return mInputOverlay.isInEditMode(); | ||
| 254 | } | ||
| 255 | |||
| 256 | private static class EmulationState { | ||
| 257 | private final String mGamePath; | ||
| 258 | private State state; | ||
| 259 | private Surface mSurface; | ||
| 260 | private boolean mRunWhenSurfaceIsValid; | ||
| 261 | |||
| 262 | EmulationState(String gamePath) { | ||
| 263 | mGamePath = gamePath; | ||
| 264 | // Starting state is stopped. | ||
| 265 | state = State.STOPPED; | ||
| 266 | } | ||
| 267 | |||
| 268 | public synchronized boolean isStopped() { | ||
| 269 | return state == State.STOPPED; | ||
| 270 | } | ||
| 271 | |||
| 272 | // Getters for the current state | ||
| 273 | |||
| 274 | public synchronized boolean isPaused() { | ||
| 275 | return state == State.PAUSED; | ||
| 276 | } | ||
| 277 | |||
| 278 | public synchronized boolean isRunning() { | ||
| 279 | return state == State.RUNNING; | ||
| 280 | } | ||
| 281 | |||
| 282 | public synchronized void stop() { | ||
| 283 | if (state != State.STOPPED) { | ||
| 284 | Log.debug("[EmulationFragment] Stopping emulation."); | ||
| 285 | state = State.STOPPED; | ||
| 286 | NativeLibrary.StopEmulation(); | ||
| 287 | } else { | ||
| 288 | Log.warning("[EmulationFragment] Stop called while already stopped."); | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | // State changing methods | ||
| 293 | |||
| 294 | public synchronized void pause() { | ||
| 295 | if (state != State.PAUSED) { | ||
| 296 | state = State.PAUSED; | ||
| 297 | Log.debug("[EmulationFragment] Pausing emulation."); | ||
| 298 | |||
| 299 | // Release the surface before pausing, since emulation has to be running for that. | ||
| 300 | NativeLibrary.SurfaceDestroyed(); | ||
| 301 | NativeLibrary.PauseEmulation(); | ||
| 302 | } else { | ||
| 303 | Log.warning("[EmulationFragment] Pause called while already paused."); | ||
| 304 | } | ||
| 305 | } | ||
| 306 | |||
| 307 | public synchronized void run(boolean isActivityRecreated) { | ||
| 308 | if (isActivityRecreated) { | ||
| 309 | if (NativeLibrary.IsRunning()) { | ||
| 310 | state = State.PAUSED; | ||
| 311 | } | ||
| 312 | } else { | ||
| 313 | Log.debug("[EmulationFragment] activity resumed or fresh start"); | ||
| 314 | } | ||
| 315 | |||
| 316 | // If the surface is set, run now. Otherwise, wait for it to get set. | ||
| 317 | if (mSurface != null) { | ||
| 318 | runWithValidSurface(); | ||
| 319 | } else { | ||
| 320 | mRunWhenSurfaceIsValid = true; | ||
| 321 | } | ||
| 322 | } | ||
| 323 | |||
| 324 | // Surface callbacks | ||
| 325 | public synchronized void newSurface(Surface surface) { | ||
| 326 | mSurface = surface; | ||
| 327 | if (mRunWhenSurfaceIsValid) { | ||
| 328 | runWithValidSurface(); | ||
| 329 | } | ||
| 330 | } | ||
| 331 | |||
| 332 | public synchronized void clearSurface() { | ||
| 333 | if (mSurface == null) { | ||
| 334 | Log.warning("[EmulationFragment] clearSurface called, but surface already null."); | ||
| 335 | } else { | ||
| 336 | mSurface = null; | ||
| 337 | Log.debug("[EmulationFragment] Surface destroyed."); | ||
| 338 | |||
| 339 | if (state == State.RUNNING) { | ||
| 340 | NativeLibrary.SurfaceDestroyed(); | ||
| 341 | state = State.PAUSED; | ||
| 342 | } else if (state == State.PAUSED) { | ||
| 343 | Log.warning("[EmulationFragment] Surface cleared while emulation paused."); | ||
| 344 | } else { | ||
| 345 | Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); | ||
| 346 | } | ||
| 347 | } | ||
| 348 | } | ||
| 349 | |||
| 350 | private void runWithValidSurface() { | ||
| 351 | mRunWhenSurfaceIsValid = false; | ||
| 352 | if (state == State.STOPPED) { | ||
| 353 | NativeLibrary.SurfaceChanged(mSurface); | ||
| 354 | Thread mEmulationThread = new Thread(() -> | ||
| 355 | { | ||
| 356 | Log.debug("[EmulationFragment] Starting emulation thread."); | ||
| 357 | NativeLibrary.Run(mGamePath); | ||
| 358 | }, "NativeEmulation"); | ||
| 359 | mEmulationThread.start(); | ||
| 360 | |||
| 361 | } else if (state == State.PAUSED) { | ||
| 362 | Log.debug("[EmulationFragment] Resuming emulation."); | ||
| 363 | NativeLibrary.SurfaceChanged(mSurface); | ||
| 364 | NativeLibrary.UnPauseEmulation(); | ||
| 365 | } else { | ||
| 366 | Log.debug("[EmulationFragment] Bug, run called while already running."); | ||
| 367 | } | ||
| 368 | state = State.RUNNING; | ||
| 369 | } | ||
| 370 | |||
| 371 | private enum State { | ||
| 372 | STOPPED, RUNNING, PAUSED | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } | ||
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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.fragments | ||
| 2 | |||
| 3 | import android.content.Context | ||
| 4 | import android.content.IntentFilter | ||
| 5 | import android.content.SharedPreferences | ||
| 6 | import android.graphics.Color | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.os.Handler | ||
| 9 | import android.view.* | ||
| 10 | import android.widget.Button | ||
| 11 | import android.widget.TextView | ||
| 12 | import android.widget.Toast | ||
| 13 | import androidx.fragment.app.Fragment | ||
| 14 | import androidx.localbroadcastmanager.content.LocalBroadcastManager | ||
| 15 | import androidx.preference.PreferenceManager | ||
| 16 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 17 | import org.yuzu.yuzu_emu.R | ||
| 18 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 19 | import org.yuzu.yuzu_emu.activities.EmulationActivity | ||
| 20 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 21 | import org.yuzu.yuzu_emu.overlay.InputOverlay | ||
| 22 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 23 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState | ||
| 24 | import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver | ||
| 25 | import org.yuzu.yuzu_emu.utils.Log | ||
| 26 | |||
| 27 | class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback { | ||
| 28 | private lateinit var preferences: SharedPreferences | ||
| 29 | private var inputOverlay: InputOverlay? = null | ||
| 30 | private lateinit var emulationState: EmulationState | ||
| 31 | private var directoryStateReceiver: DirectoryStateReceiver? = null | ||
| 32 | private var emulationActivity: EmulationActivity? = null | ||
| 33 | private lateinit var perfStats: TextView | ||
| 34 | private var perfStatsUpdater: (() -> Unit)? = null | ||
| 35 | |||
| 36 | override fun onAttach(context: Context) { | ||
| 37 | super.onAttach(context) | ||
| 38 | if (context is EmulationActivity) { | ||
| 39 | emulationActivity = context | ||
| 40 | NativeLibrary.setEmulationActivity(context) | ||
| 41 | } else { | ||
| 42 | throw IllegalStateException("EmulationFragment must have EmulationActivity parent") | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Initialize anything that doesn't depend on the layout / views in here. | ||
| 48 | */ | ||
| 49 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 50 | super.onCreate(savedInstanceState) | ||
| 51 | |||
| 52 | // So this fragment doesn't restart on configuration changes; i.e. rotation. | ||
| 53 | retainInstance = true | ||
| 54 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||
| 55 | val gamePath = requireArguments().getString(KEY_GAMEPATH) | ||
| 56 | emulationState = EmulationState(gamePath) | ||
| 57 | } | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Initialize the UI and start emulation in here. | ||
| 61 | */ | ||
| 62 | override fun onCreateView( | ||
| 63 | inflater: LayoutInflater, | ||
| 64 | container: ViewGroup?, | ||
| 65 | savedInstanceState: Bundle? | ||
| 66 | ): View? { | ||
| 67 | val contents = inflater.inflate(R.layout.fragment_emulation, container, false) | ||
| 68 | val surfaceView = contents.findViewById<SurfaceView>(R.id.surface_emulation) | ||
| 69 | surfaceView.holder.addCallback(this) | ||
| 70 | inputOverlay = contents.findViewById(R.id.surface_input_overlay) | ||
| 71 | perfStats = contents.findViewById(R.id.show_fps_text) | ||
| 72 | perfStats.setTextColor(Color.YELLOW) | ||
| 73 | val doneButton = contents.findViewById<Button>(R.id.done_control_config) | ||
| 74 | doneButton?.setOnClickListener { stopConfiguringControls() } | ||
| 75 | |||
| 76 | // Setup overlay. | ||
| 77 | resetInputOverlay() | ||
| 78 | updateShowFpsOverlay() | ||
| 79 | |||
| 80 | // The new Surface created here will get passed to the native code via onSurfaceChanged. | ||
| 81 | return contents | ||
| 82 | } | ||
| 83 | |||
| 84 | override fun onResume() { | ||
| 85 | super.onResume() | ||
| 86 | Choreographer.getInstance().postFrameCallback(this) | ||
| 87 | if (DirectoryInitialization.areDirectoriesReady()) { | ||
| 88 | emulationState.run(emulationActivity!!.isActivityRecreated) | ||
| 89 | } else { | ||
| 90 | setupDirectoriesThenStartEmulation() | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | override fun onPause() { | ||
| 95 | if (directoryStateReceiver != null) { | ||
| 96 | LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver( | ||
| 97 | directoryStateReceiver!! | ||
| 98 | ) | ||
| 99 | directoryStateReceiver = null | ||
| 100 | } | ||
| 101 | if (emulationState.isRunning) { | ||
| 102 | emulationState.pause() | ||
| 103 | } | ||
| 104 | Choreographer.getInstance().removeFrameCallback(this) | ||
| 105 | super.onPause() | ||
| 106 | } | ||
| 107 | |||
| 108 | override fun onDetach() { | ||
| 109 | NativeLibrary.clearEmulationActivity() | ||
| 110 | super.onDetach() | ||
| 111 | } | ||
| 112 | |||
| 113 | private fun setupDirectoriesThenStartEmulation() { | ||
| 114 | val statusIntentFilter = IntentFilter( | ||
| 115 | DirectoryInitialization.BROADCAST_ACTION | ||
| 116 | ) | ||
| 117 | directoryStateReceiver = | ||
| 118 | DirectoryStateReceiver { directoryInitializationState: DirectoryInitializationState -> | ||
| 119 | if (directoryInitializationState == | ||
| 120 | DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED | ||
| 121 | ) { | ||
| 122 | emulationState.run(emulationActivity!!.isActivityRecreated) | ||
| 123 | } else if (directoryInitializationState == | ||
| 124 | DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE | ||
| 125 | ) { | ||
| 126 | Toast.makeText( | ||
| 127 | context, | ||
| 128 | R.string.external_storage_not_mounted, | ||
| 129 | Toast.LENGTH_SHORT | ||
| 130 | ) | ||
| 131 | .show() | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | // Registers the DirectoryStateReceiver and its intent filters | ||
| 136 | LocalBroadcastManager.getInstance(requireActivity()).registerReceiver( | ||
| 137 | directoryStateReceiver!!, | ||
| 138 | statusIntentFilter | ||
| 139 | ) | ||
| 140 | DirectoryInitialization.start(requireContext()) | ||
| 141 | } | ||
| 142 | |||
| 143 | fun refreshInputOverlay() { | ||
| 144 | inputOverlay!!.refreshControls() | ||
| 145 | } | ||
| 146 | |||
| 147 | fun resetInputOverlay() { | ||
| 148 | // Reset button scale | ||
| 149 | preferences.edit() | ||
| 150 | .putInt(Settings.PREF_CONTROL_SCALE, 50) | ||
| 151 | .apply() | ||
| 152 | inputOverlay!!.resetButtonPlacement() | ||
| 153 | } | ||
| 154 | |||
| 155 | private fun updateShowFpsOverlay() { | ||
| 156 | // TODO: Create a setting so that this actually works... | ||
| 157 | if (true) { | ||
| 158 | val SYSTEM_FPS = 0 | ||
| 159 | val FPS = 1 | ||
| 160 | val FRAMETIME = 2 | ||
| 161 | val SPEED = 3 | ||
| 162 | perfStatsUpdater = { | ||
| 163 | val perfStats = NativeLibrary.GetPerfStats() | ||
| 164 | if (perfStats[FPS] > 0) { | ||
| 165 | this.perfStats.text = String.format("FPS: %.1f", perfStats[FPS]) | ||
| 166 | } | ||
| 167 | perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100) | ||
| 168 | } | ||
| 169 | perfStatsUpdateHandler.post(perfStatsUpdater!!) | ||
| 170 | perfStats.visibility = View.VISIBLE | ||
| 171 | } else { | ||
| 172 | if (perfStatsUpdater != null) { | ||
| 173 | perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) | ||
| 174 | } | ||
| 175 | perfStats.visibility = View.GONE | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | override fun surfaceCreated(holder: SurfaceHolder) { | ||
| 180 | // We purposely don't do anything here. | ||
| 181 | // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. | ||
| 182 | } | ||
| 183 | |||
| 184 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { | ||
| 185 | Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height) | ||
| 186 | emulationState.newSurface(holder.surface) | ||
| 187 | } | ||
| 188 | |||
| 189 | override fun surfaceDestroyed(holder: SurfaceHolder) { | ||
| 190 | emulationState.clearSurface() | ||
| 191 | } | ||
| 192 | |||
| 193 | override fun doFrame(frameTimeNanos: Long) { | ||
| 194 | Choreographer.getInstance().postFrameCallback(this) | ||
| 195 | NativeLibrary.DoFrame() | ||
| 196 | } | ||
| 197 | |||
| 198 | fun stopEmulation() { | ||
| 199 | emulationState.stop() | ||
| 200 | } | ||
| 201 | |||
| 202 | fun startConfiguringControls() { | ||
| 203 | requireView().findViewById<View>(R.id.done_control_config).visibility = | ||
| 204 | View.VISIBLE | ||
| 205 | inputOverlay!!.setIsInEditMode(true) | ||
| 206 | } | ||
| 207 | |||
| 208 | fun stopConfiguringControls() { | ||
| 209 | requireView().findViewById<View>(R.id.done_control_config).visibility = View.GONE | ||
| 210 | inputOverlay!!.setIsInEditMode(false) | ||
| 211 | } | ||
| 212 | |||
| 213 | val isConfiguringControls: Boolean | ||
| 214 | get() = inputOverlay!!.isInEditMode | ||
| 215 | |||
| 216 | private class EmulationState(private val mGamePath: String?) { | ||
| 217 | private var state: State | ||
| 218 | private var surface: Surface? = null | ||
| 219 | private var runWhenSurfaceIsValid = false | ||
| 220 | |||
| 221 | init { | ||
| 222 | // Starting state is stopped. | ||
| 223 | state = State.STOPPED | ||
| 224 | } | ||
| 225 | |||
| 226 | @get:Synchronized | ||
| 227 | val isStopped: Boolean | ||
| 228 | get() = state == State.STOPPED | ||
| 229 | |||
| 230 | // Getters for the current state | ||
| 231 | @get:Synchronized | ||
| 232 | val isPaused: Boolean | ||
| 233 | get() = state == State.PAUSED | ||
| 234 | |||
| 235 | @get:Synchronized | ||
| 236 | val isRunning: Boolean | ||
| 237 | get() = state == State.RUNNING | ||
| 238 | |||
| 239 | @Synchronized | ||
| 240 | fun stop() { | ||
| 241 | if (state != State.STOPPED) { | ||
| 242 | Log.debug("[EmulationFragment] Stopping emulation.") | ||
| 243 | state = State.STOPPED | ||
| 244 | NativeLibrary.StopEmulation() | ||
| 245 | } else { | ||
| 246 | Log.warning("[EmulationFragment] Stop called while already stopped.") | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | // State changing methods | ||
| 251 | @Synchronized | ||
| 252 | fun pause() { | ||
| 253 | if (state != State.PAUSED) { | ||
| 254 | state = State.PAUSED | ||
| 255 | Log.debug("[EmulationFragment] Pausing emulation.") | ||
| 256 | |||
| 257 | // Release the surface before pausing, since emulation has to be running for that. | ||
| 258 | NativeLibrary.SurfaceDestroyed() | ||
| 259 | NativeLibrary.PauseEmulation() | ||
| 260 | } else { | ||
| 261 | Log.warning("[EmulationFragment] Pause called while already paused.") | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 265 | @Synchronized | ||
| 266 | fun run(isActivityRecreated: Boolean) { | ||
| 267 | if (isActivityRecreated) { | ||
| 268 | if (NativeLibrary.IsRunning()) { | ||
| 269 | state = State.PAUSED | ||
| 270 | } | ||
| 271 | } else { | ||
| 272 | Log.debug("[EmulationFragment] activity resumed or fresh start") | ||
| 273 | } | ||
| 274 | |||
| 275 | // If the surface is set, run now. Otherwise, wait for it to get set. | ||
| 276 | if (surface != null) { | ||
| 277 | runWithValidSurface() | ||
| 278 | } else { | ||
| 279 | runWhenSurfaceIsValid = true | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | // Surface callbacks | ||
| 284 | @Synchronized | ||
| 285 | fun newSurface(surface: Surface?) { | ||
| 286 | this.surface = surface | ||
| 287 | if (runWhenSurfaceIsValid) { | ||
| 288 | runWithValidSurface() | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | @Synchronized | ||
| 293 | fun clearSurface() { | ||
| 294 | if (surface == null) { | ||
| 295 | Log.warning("[EmulationFragment] clearSurface called, but surface already null.") | ||
| 296 | } else { | ||
| 297 | surface = null | ||
| 298 | Log.debug("[EmulationFragment] Surface destroyed.") | ||
| 299 | when (state) { | ||
| 300 | State.RUNNING -> { | ||
| 301 | NativeLibrary.SurfaceDestroyed() | ||
| 302 | state = State.PAUSED | ||
| 303 | } | ||
| 304 | State.PAUSED -> Log.warning("[EmulationFragment] Surface cleared while emulation paused.") | ||
| 305 | else -> Log.warning("[EmulationFragment] Surface cleared while emulation stopped.") | ||
| 306 | } | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | private fun runWithValidSurface() { | ||
| 311 | runWhenSurfaceIsValid = false | ||
| 312 | when (state) { | ||
| 313 | State.STOPPED -> { | ||
| 314 | NativeLibrary.SurfaceChanged(surface) | ||
| 315 | val mEmulationThread = Thread({ | ||
| 316 | Log.debug("[EmulationFragment] Starting emulation thread.") | ||
| 317 | NativeLibrary.Run(mGamePath) | ||
| 318 | }, "NativeEmulation") | ||
| 319 | mEmulationThread.start() | ||
| 320 | } | ||
| 321 | State.PAUSED -> { | ||
| 322 | Log.debug("[EmulationFragment] Resuming emulation.") | ||
| 323 | NativeLibrary.SurfaceChanged(surface) | ||
| 324 | NativeLibrary.UnPauseEmulation() | ||
| 325 | } | ||
| 326 | else -> Log.debug("[EmulationFragment] Bug, run called while already running.") | ||
| 327 | } | ||
| 328 | state = State.RUNNING | ||
| 329 | } | ||
| 330 | |||
| 331 | private enum class State { | ||
| 332 | STOPPED, RUNNING, PAUSED | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | companion object { | ||
| 337 | private const val KEY_GAMEPATH = "gamepath" | ||
| 338 | private val perfStatsUpdateHandler = Handler() | ||
| 339 | |||
| 340 | fun newInstance(gamePath: String?): EmulationFragment { | ||
| 341 | val args = Bundle() | ||
| 342 | args.putString(KEY_GAMEPATH, gamePath) | ||
| 343 | val fragment = EmulationFragment() | ||
| 344 | fragment.arguments = args | ||
| 345 | return fragment | ||
| 346 | } | ||
| 347 | } | ||
| 348 | } | ||