Android: when the surface is lost, pause the game and allow coming back to it (#358)

This commit is contained in:
NullVector 2026-10-05 22:36:31 +10:30 committed by GitHub
parent 007d540b29
commit 3ae90631bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 522 additions and 89 deletions

View File

@ -14,8 +14,10 @@
android:theme="@style/Theme.NxEmu.Splash">
<activity
android:name="org.nxemu.ui.main.MainActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.NxEmu.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -34,6 +36,7 @@
android:name="org.nxemu.ui.emulation.EmulationActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize|density"
android:exported="false"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.NxEmu" />
<activity

View File

@ -49,7 +49,14 @@ object NativeLibrary {
external fun emulationSurfaceReady(surface: android.view.Surface, pixelRatio: Float, romPath: String): Boolean
external fun emulationSurfaceDestroyed()
external fun emulationStopped()
external fun surfaceChanged(surface: android.view.Surface)
external fun pauseEmulation()
external fun unpauseEmulation()
external fun isPaused(): Boolean
external fun getAppletCaptureBuffer(): ByteArray
external fun getAppletCaptureWidth(): Int
external fun getAppletCaptureHeight(): Int
/** Used from native [yuzu_common/fs/fs_android.cpp] via RegisterCallbacks. */
@JvmStatic

View File

@ -1,6 +1,7 @@
package org.nxemu.ui.emulation
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.PixelFormat
@ -50,6 +51,11 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
Thread(r, "nxemu-emu").apply { isDaemon = true }
}
private val nativeSurfaceSessionOpen = AtomicBoolean(false)
private var explicitExit = false
private var leavingApp = false
private lateinit var pausedFrameImage: ImageView
private val captureInFlight = AtomicBoolean(false)
private val pausedFrameListener = { updatePauseLabel() }
private val perfStatsHandler = Handler(Looper.getMainLooper())
private var perfStatsUpdater: Runnable? = null
private lateinit var loadingIndicator: View
@ -61,6 +67,8 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
private lateinit var showDeviceText: TextView
private lateinit var surfaceInputOverlay: InputOverlay
private lateinit var drawerLayout: DrawerLayout
private lateinit var pauseItem: TextView
private lateinit var pausedIcon: ImageView
private lateinit var lockDrawerItem: TextView
private lateinit var overlayAppVersion: String
private lateinit var overlayPhoneModel: String
@ -102,6 +110,18 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
surfaceInputOverlay.setZOrderMediaOverlay(true)
surfaceInputOverlay.holder.setFormat(PixelFormat.TRANSLUCENT)
drawerLayout = findViewById(R.id.drawer_layout)
pauseItem = findViewById(R.id.menu_pause)
pausedIcon = findViewById(R.id.paused_icon)
pausedFrameImage = findViewById(R.id.paused_frame_image)
frameListener = pausedFrameListener
pausedIcon.setOnClickListener {
if (NativeLibrary.isPaused()) {
resumeAfterBackground.set(false)
NativeLibrary.unpauseEmulation()
clearPausedFrame()
updatePauseLabel()
}
}
lockDrawerItem = findViewById(R.id.menu_lock_drawer)
setupInGameDrawer()
updateInputOverlayLayout()
@ -141,8 +161,22 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}
override fun onUserLeaveHint() {
leavingApp = true
super.onUserLeaveHint()
}
override fun onResume() {
super.onResume()
leavingApp = false
val surface = findViewById<SurfaceView>(R.id.emulation_surface).holder.surface
if (surface != null && surface.isValid) {
unpauseIfBackgrounded()
}
updatePauseLabel()
if (NativeLibrary.isPaused() && pausedFrame == null) {
capturePausedFrame()
}
InputHandler.updateControllerData()
if (::surfaceInputOverlay.isInitialized) {
updateInputOverlayLayout()
@ -150,6 +184,22 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}
override fun onPause() {
releaseDrawerStick()
val leftApp = leavingApp
if (!NativeLibrary.isPaused()) {
NativeLibrary.pauseEmulation()
if (!leftApp) {
resumeAfterBackground.set(true)
}
}
if (leftApp) {
capturePausedFrame()
}
leavingApp = false
super.onPause()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
return super.dispatchKeyEvent(event)
@ -195,6 +245,7 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun onDrawerOpened(drawerView: View) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
updatePauseLabel()
drawerMenuItems().first().requestFocus()
}
@ -206,14 +257,84 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}
})
pauseItem.setOnClickListener { togglePause() }
lockDrawerItem.setOnClickListener { toggleDrawerLock() }
findViewById<View>(R.id.menu_exit).setOnClickListener {
drawerLayout.closeDrawers()
explicitExit = true
activeGamePath = null
clearPausedFrame()
finish()
}
updatePauseLabel()
updateLockDrawerLabel()
}
private fun togglePause() {
if (NativeLibrary.isPaused()) {
NativeLibrary.unpauseEmulation()
clearPausedFrame()
} else {
NativeLibrary.pauseEmulation()
capturePausedFrame()
}
updatePauseLabel()
}
private fun updatePauseLabel() {
val paused = NativeLibrary.isPaused()
pauseItem.text = getString(
if (paused) R.string.emulation_unpause else R.string.emulation_pause,
)
if (::pausedIcon.isInitialized) {
pausedIcon.visibility = if (paused) View.VISIBLE else View.GONE
}
if (::pausedFrameImage.isInitialized) {
val frame = if (paused) pausedFrame else null
pausedFrameImage.setImageBitmap(frame)
pausedFrameImage.visibility = if (frame != null) View.VISIBLE else View.GONE
}
}
private fun capturePausedFrame() {
if (!captureInFlight.compareAndSet(false, true)) {
return
}
executor.execute {
try {
val width = NativeLibrary.getAppletCaptureWidth()
val height = NativeLibrary.getAppletCaptureHeight()
val frame = NativeLibrary.getAppletCaptureBuffer()
val expected = width * height * 4
if (width <= 0 || height <= 0 || frame.size < expected) {
return@execute
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(frame, 0, expected))
val previous = pausedFrame
pausedFrame = bitmap
Handler(Looper.getMainLooper()).post {
frameListener?.invoke()
if (previous != null && previous !== pausedFrame) {
previous.recycle()
}
}
} finally {
captureInFlight.set(false)
}
}
}
private fun clearPausedFrame() {
val previous = pausedFrame
pausedFrame = null
if (::pausedFrameImage.isInitialized) {
pausedFrameImage.setImageDrawable(null)
pausedFrameImage.visibility = View.GONE
}
previous?.recycle()
}
private fun releaseOverlayTouches() {
val event = MotionEvent.obtain(
SystemClock.uptimeMillis(),
@ -253,6 +374,7 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
private fun drawerMenuItems(): List<View> {
return listOf(
pauseItem,
lockDrawerItem,
findViewById(R.id.menu_exit),
)
@ -468,45 +590,43 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
if (!nativeSurfaceSessionOpen.compareAndSet(false, true)) {
Log.w(TAG, "Ignoring extra surfaceCreated")
override fun surfaceCreated(holder: SurfaceHolder) = Unit
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (width <= 0 || height <= 0) {
return
}
val path = intent.getStringExtra(EXTRA_GAME_PATH)
if (path.isNullOrEmpty()) {
Log.e(TAG, "Missing EXTRA_GAME_PATH")
nativeSurfaceSessionOpen.set(false)
finish()
return
}
val ratio = resources.displayMetrics.density
val surface = holder.surface
val ratio = resources.displayMetrics.density
executor.execute {
try {
if (!NativeLibrary.emulationSurfaceReady(surface, ratio, path)) {
Log.e(TAG, "emulationSurfaceReady returned false")
nativeSurfaceSessionOpen.set(false)
runOnUiThread { finish() }
if (!nativeSurfaceSessionOpen.get()) {
if (!NativeLibrary.emulationSurfaceReady(surface, ratio, path)) {
Log.e(TAG, "emulationSurfaceReady returned false")
runOnUiThread { finish() }
} else {
nativeSurfaceSessionOpen.set(true)
activeGamePath = path
InputHandler.updateControllerData()
unpauseIfBackgrounded()
runOnUiThread { updatePauseLabel() }
}
} else {
NativeLibrary.surfaceChanged(surface)
unpauseIfBackgrounded()
runOnUiThread { updatePauseLabel() }
}
} catch (e: Throwable) {
Log.e(TAG, "emulationSurfaceReady failed", e)
nativeSurfaceSessionOpen.set(false)
runOnUiThread { finish() }
}
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (!nativeSurfaceSessionOpen.get() || width <= 0 || height <= 0) {
return
}
val surface = holder.surface
executor.execute {
try {
NativeLibrary.surfaceChanged(surface)
} catch (e: Throwable) {
Log.e(TAG, "surfaceChanged failed", e)
if (!nativeSurfaceSessionOpen.get()) {
runOnUiThread { finish() }
}
}
}
}
@ -514,27 +634,25 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) {
try {
executor.submit {
try {
NativeLibrary.emulationSurfaceDestroyed()
} finally {
nativeSurfaceSessionOpen.set(false)
}
NativeLibrary.emulationSurfaceDestroyed()
}.get(120, TimeUnit.SECONDS)
} catch (e: Exception) {
Log.e(TAG, "emulationSurfaceDestroyed failed", e)
nativeSurfaceSessionOpen.set(false)
}
}
override fun onDestroy() {
if (frameListener === pausedFrameListener) {
frameListener = null
}
NativeLibrary.removeSettingChangedListener(settingChangedListener)
stopPerfOverlay()
stopAnimatedDrawables()
if (nativeSurfaceSessionOpen.get()) {
if (explicitExit && nativeSurfaceSessionOpen.get()) {
try {
executor.submit {
try {
NativeLibrary.emulationSurfaceDestroyed()
NativeLibrary.emulationStopped()
} finally {
nativeSurfaceSessionOpen.set(false)
}
@ -555,7 +673,21 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
super.onDestroy()
}
private fun unpauseIfBackgrounded() {
if (resumeAfterBackground.compareAndSet(true, false)) {
NativeLibrary.unpauseEmulation()
runOnUiThread {
clearPausedFrame()
updatePauseLabel()
}
}
}
companion object {
private val resumeAfterBackground = AtomicBoolean(false)
var activeGamePath: String? = null
var pausedFrame: Bitmap? = null
var frameListener: (() -> Unit)? = null
const val EXTRA_GAME_PATH = "org.nxemu.EXTRA_GAME_PATH"
private const val TAG = "NxEmu-Emulation"
private const val DRAWER_REPEAT_MS = 220L

View File

@ -1,5 +1,6 @@
package org.nxemu.ui.main
import android.content.Context
import android.content.Intent
import android.hardware.input.InputManager
import android.net.Uri
@ -20,7 +21,10 @@ import org.json.JSONArray
import org.json.JSONObject
import org.nxemu.NXUISetting
import org.nxemu.NativeLibrary
import org.nxemu.features.input.NativeInput
import org.nxemu.features.input.model.NativeButton
import org.nxemu.ui.emulation.EmulationActivity
import org.nxemu.ui.settings.ControllerSettings
import org.nxemu.ui.settings.InputMappingSession
import org.nxemu.ui.settings.OverlayLayoutActivity
import org.nxemu.utils.InputHandler
@ -143,15 +147,62 @@ open class MainActivity : ComponentActivity() {
},
)
NativeLibrary.addSettingChangedListener(settingChangedForwarder)
bootstrapControllerOnFirstRun()
val page = if (this is SettingsActivity) "settings.html" else "index.html"
webView.loadUrl("file:///android_asset/$page")
setContentView(webView)
ThemeHelper.applySystemBars(this)
}
private fun bootstrapControllerOnFirstRun() {
if (InputHandler.getDevices().isEmpty()) {
return
}
if (NativeInput.getButtonParam(0, NativeButton.A).get("engine", "") != "keyboard") {
return
}
InputHandler.updateControllerData()
ControllerSettings.autoMap(this, playerIndex = 0, index = 0)
if (NativeInput.getButtonParam(0, NativeButton.A).get("engine", "") == "keyboard") {
return
}
NativeLibrary.setSettingBool(NXUISetting.ShowInputOverlay, false)
NativeLibrary.saveSettings()
}
override fun onResume() {
super.onResume()
emulationLaunchPending = false
InputHandler.updateControllerData()
inputManager()?.registerInputDeviceListener(inputDeviceListener, null)
refreshConnectedControllers()
resumeRunningGame()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
resumeRunningGame()
}
private fun resumeRunningGame() {
if (this is SettingsActivity) {
return
}
val path = EmulationActivity.activeGamePath ?: return
startActivity(
Intent(this, EmulationActivity::class.java).apply {
putExtra(EmulationActivity.EXTRA_GAME_PATH, path)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
)
}
override fun onPause() {
inputMapping.dismiss()
inputManager()?.unregisterInputDeviceListener(inputDeviceListener)
releaseBrowserStick()
super.onPause()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@ -260,6 +311,14 @@ open class MainActivity : ComponentActivity() {
}
webView.evaluateJavascript("if(typeof moveGameSelection==='function'){$moves}", null)
}
private fun releaseBrowserStick() {
stickX = 0
stickY = 0
if (::webView.isInitialized) {
webView.removeCallbacks(stickRepeat)
}
}
fun beginControllerMap(playerIndex: Int, mapId: String, title: String, filterIndex: Int) {
runOnUiThread {
inputMapping.begin(playerIndex, mapId, title, filterIndex)
@ -280,6 +339,10 @@ open class MainActivity : ComponentActivity() {
}
}
private fun inputManager(): InputManager? {
return getSystemService(Context.INPUT_SERVICE) as? InputManager
}
private fun refreshConnectedControllers() {
if (!::webView.isInitialized || isDestroyed) {
return

View File

@ -10,6 +10,7 @@ AndroidRenderWindow::~AndroidRenderWindow()
void AndroidRenderWindow::AttachSurface(ANativeWindow * native_window, float pixel_ratio)
{
std::lock_guard lock(m_mutex);
m_present = native_window != nullptr;
m_pixel_ratio = pixel_ratio > 0.f ? pixel_ratio : 1.f;
if (m_native_window == native_window)
{
@ -23,9 +24,16 @@ void AndroidRenderWindow::AttachSurface(ANativeWindow * native_window, float pix
m_native_window = native_window;
}
void AndroidRenderWindow::SuppressPresentation()
{
std::lock_guard lock(m_mutex);
m_present = false;
}
void AndroidRenderWindow::ClearSurface()
{
std::lock_guard lock(m_mutex);
m_present = false;
if (m_native_window != nullptr)
{
ANativeWindow_release(m_native_window);
@ -37,6 +45,10 @@ void AndroidRenderWindow::ClearSurface()
void * AndroidRenderWindow::RenderSurface() const
{
std::lock_guard lock(m_mutex);
if (!m_present)
{
return nullptr;
}
return m_native_window;
}

View File

@ -12,6 +12,7 @@ public:
~AndroidRenderWindow();
void AttachSurface(ANativeWindow * native_window, float pixel_ratio);
void SuppressPresentation();
void ClearSurface();
void * RenderSurface() const override;
@ -21,4 +22,5 @@ private:
mutable std::mutex m_mutex;
ANativeWindow * m_native_window = nullptr;
float m_pixel_ratio = 1.0f;
bool m_present = false;
};

View File

@ -118,7 +118,6 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con
if (native_window == nullptr)
{
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: no native window");
RestoreStubModulesLocked();
return false;
}
@ -131,7 +130,6 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con
if (!m_system_modules->IsValid())
{
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: SystemModules invalid after Setup");
RestoreStubModulesLocked();
return false;
}
@ -147,20 +145,61 @@ bool EmulationSession::Run(ANativeWindow * native_window, float pixel_ratio, con
ISystemloader & loader = m_system_modules->Modules().Systemloader();
if (!loader.LoadRom(rom_path.c_str(), 0, -1, ApplicationLaunchType::FrontendInitiated))
{
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "Run: LoadRom failed");
RestoreStubModulesLocked();
return false;
}
__android_log_print(ANDROID_LOG_INFO, kLogTag, "Run: LoadRom ok");
return true;
}
void EmulationSession::Stop()
{
std::lock_guard lock(m_mutex);
SettingsStore::GetInstance().SetBool(NXCoreSetting::EmulationRunning, false);
RestoreStubModulesLocked();
}
void EmulationSession::SetPaused(bool paused)
{
std::lock_guard lock(m_mutex);
if (!m_system_modules || !m_system_modules->IsValid())
{
return;
}
IOperatingSystem & os = m_system_modules->Modules().OperatingSystem();
if (!os.IsPoweredOn())
{
return;
}
os.SetEmulationPaused(paused);
}
bool EmulationSession::IsPaused()
{
std::lock_guard lock(m_mutex);
if (!m_system_modules || !m_system_modules->IsValid())
{
return false;
}
return m_system_modules->Modules().OperatingSystem().IsEmulationPaused();
}
void EmulationSession::SetNativeWindow(ANativeWindow * native_window)
{
std::lock_guard lock(m_mutex);
m_render_window.AttachSurface(native_window, m_pixel_ratio);
m_native_window = native_window;
}
void EmulationSession::SurfaceDestroyed()
{
std::lock_guard lock(m_mutex);
SettingsStore::GetInstance().SetBool(NXCoreSetting::EmulationRunning, false);
RestoreStubModulesLocked();
const bool keep_running = m_system_modules && m_system_modules->IsValid() &&
SettingsStore::GetInstance().GetBool(NXCoreSetting::EmulationRunning);
m_render_window.SuppressPresentation();
if (keep_running)
{
m_system_modules->Modules().Video().NotifyWindowChanged();
}
}
void EmulationSession::SurfaceChanged()
@ -174,15 +213,57 @@ void EmulationSession::SurfaceChanged()
{
return;
}
IVideo & video = m_system_modules->Modules().Video();
video.NotifyWindowChanged();
const int native_w = ANativeWindow_getWidth(m_native_window);
const int native_h = ANativeWindow_getHeight(m_native_window);
if (native_w > 0 && native_h > 0)
{
m_system_modules->Modules().Video().UpdateFramebufferLayout(static_cast<uint32_t>(native_w),
static_cast<uint32_t>(native_h));
video.UpdateFramebufferLayout(static_cast<uint32_t>(native_w), static_cast<uint32_t>(native_h));
}
}
bool EmulationSession::ReadAppletCapture(std::vector<uint8_t> & out, uint32_t & width, uint32_t & height)
{
IVideo * video = nullptr;
{
std::lock_guard lock(m_mutex);
if (!m_system_modules || !m_system_modules->IsValid())
{
return false;
}
video = &m_system_modules->Modules().Video();
}
uint32_t capture_width = 0;
uint32_t capture_height = 0;
const uint32_t size = video->GetLinearAppletCaptureBuffer(nullptr, 0, &capture_width, &capture_height);
if (size == 0 || capture_width == 0 || capture_height == 0)
{
return false;
}
out.resize(size);
video->GetLinearAppletCaptureBuffer(out.data(), size, &capture_width, &capture_height);
width = capture_width;
height = capture_height;
return width * height * 4 <= out.size();
}
void EmulationSession::AppletCaptureSize(uint32_t & width, uint32_t & height)
{
width = 0;
height = 0;
IVideo * video = nullptr;
{
std::lock_guard lock(m_mutex);
if (!m_system_modules || !m_system_modules->IsValid())
{
return;
}
video = &m_system_modules->Modules().Video();
}
video->GetLinearAppletCaptureBuffer(nullptr, 0, &width, &height);
}
ANativeWindow * EmulationSession::NativeWindow() const
{
std::lock_guard lock(m_mutex);

View File

@ -1,9 +1,11 @@
#pragma once
#include <array>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include <nxemu-core/modules/system_modules.h>
#include <nxemu-module-spec/operating_system.h>
@ -31,8 +33,14 @@ public:
int GetStyleIndex(int player_index);
bool Run(ANativeWindow * native_window, float pixel_ratio, const std::string & rom_path);
void Stop();
void SetPaused(bool paused);
bool IsPaused();
void SetNativeWindow(ANativeWindow * native_window);
void SurfaceDestroyed();
void SurfaceChanged();
bool ReadAppletCapture(std::vector<uint8_t> & out, uint32_t & width, uint32_t & height);
void AppletCaptureSize(uint32_t & width, uint32_t & height);
ANativeWindow * NativeWindow() const;
float PixelRatio() const;

View File

@ -3,7 +3,9 @@
#include <android/native_window_jni.h>
#include <jni.h>
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include <common/json.h>
#include <nxemu-core/app_init.h>
@ -162,6 +164,12 @@ Java_org_nxemu_NativeLibrary_emulationSurfaceDestroyed(JNIEnv * /*env*/, jclass
EmulationSession::GetInstance().SurfaceDestroyed();
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_NativeLibrary_emulationStopped(JNIEnv * /*env*/, jclass /*clazz*/)
{
EmulationSession::GetInstance().Stop();
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_NativeLibrary_surfaceChanged(JNIEnv * env, jclass /*clazz*/, jobject jsurface)
{
@ -176,13 +184,7 @@ Java_org_nxemu_NativeLibrary_surfaceChanged(JNIEnv * env, jclass /*clazz*/, jobj
return;
}
EmulationSession & session = EmulationSession::GetInstance();
if (session.NativeWindow() == nw)
{
ANativeWindow_release(nw);
session.SurfaceChanged();
return;
}
ANativeWindow_release(nw);
session.SetNativeWindow(nw);
session.SurfaceChanged();
}
@ -215,40 +217,56 @@ Java_org_nxemu_NativeLibrary_getAppVersion(JNIEnv * env, jclass /*clazz*/)
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_features_input_NativeInput_onTouchPressed(JNIEnv * /*env*/, jobject /*obj*/,
jint /*fingerId*/, jfloat /*x*/, jfloat /*y*/)
Java_org_nxemu_NativeLibrary_pauseEmulation(JNIEnv * /*env*/, jclass /*clazz*/)
{
EmulationSession::GetInstance().SetPaused(true);
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_features_input_NativeInput_onTouchMoved(JNIEnv * /*env*/, jobject /*obj*/,
jint /*fingerId*/, jfloat /*x*/, jfloat /*y*/)
Java_org_nxemu_NativeLibrary_unpauseEmulation(JNIEnv * /*env*/, jclass /*clazz*/)
{
EmulationSession::GetInstance().SetPaused(false);
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_features_input_NativeInput_onTouchReleased(JNIEnv * /*env*/, jobject /*obj*/,
jint /*fingerId*/)
extern "C" JNIEXPORT jboolean JNICALL
Java_org_nxemu_NativeLibrary_isPaused(JNIEnv * /*env*/, jclass /*clazz*/)
{
return EmulationSession::GetInstance().IsPaused() ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_features_input_NativeInput_onOverlayButtonEventImpl(JNIEnv * /*env*/, jobject /*obj*/,
jint port, jint buttonId, jint action)
extern "C" JNIEXPORT jbyteArray JNICALL
Java_org_nxemu_NativeLibrary_getAppletCaptureBuffer(JNIEnv * env, jclass /*clazz*/)
{
EmulationSession::GetInstance().SetOverlayButton(port, buttonId, action == 1);
}
extern "C" JNIEXPORT void JNICALL
Java_org_nxemu_features_input_NativeInput_onOverlayJoystickEventImpl(JNIEnv * /*env*/, jobject /*obj*/,
jint port, jint stickId, jfloat x, jfloat y)
{
EmulationSession::GetInstance().SetOverlayJoystick(port, stickId, x, y);
std::vector<uint8_t> bytes;
uint32_t width = 0;
uint32_t height = 0;
if (!EmulationSession::GetInstance().ReadAppletCapture(bytes, width, height) || bytes.empty())
{
return env->NewByteArray(0);
}
jbyteArray buffer = env->NewByteArray(static_cast<jsize>(bytes.size()));
if (buffer == nullptr)
{
return env->NewByteArray(0);
}
env->SetByteArrayRegion(buffer, 0, static_cast<jsize>(bytes.size()), reinterpret_cast<const jbyte *>(bytes.data()));
return buffer;
}
extern "C" JNIEXPORT jint JNICALL
Java_org_nxemu_features_input_NativeInput_getStyleIndexImpl(JNIEnv * /*env*/, jobject /*obj*/,
jint playerIndex)
Java_org_nxemu_NativeLibrary_getAppletCaptureWidth(JNIEnv * /*env*/, jclass /*clazz*/)
{
return EmulationSession::GetInstance().GetStyleIndex(playerIndex);
uint32_t width = 0;
uint32_t height = 0;
EmulationSession::GetInstance().AppletCaptureSize(width, height);
return static_cast<jint>(width);
}
extern "C" JNIEXPORT jint JNICALL
Java_org_nxemu_NativeLibrary_getAppletCaptureHeight(JNIEnv * /*env*/, jclass /*clazz*/)
{
uint32_t width = 0;
uint32_t height = 0;
EmulationSession::GetInstance().AppletCaptureSize(width, height);
return static_cast<jint>(height);
}

View File

@ -450,6 +450,12 @@ JNIEXPORT jintArray JNICALL Java_org_nxemu_features_input_NativeInput_getSupport
return result;
}
JNIEXPORT jint JNICALL Java_org_nxemu_features_input_NativeInput_getStyleIndexImpl(
JNIEnv * /*env*/, jobject /*obj*/, jint j_player_index)
{
return EmulationSession::GetInstance().GetStyleIndex(j_player_index);
}
JNIEXPORT void JNICALL Java_org_nxemu_features_input_NativeInput_setStyleIndexImpl(JNIEnv * /*env*/, jobject /*obj*/, jint j_player_index, jint j_style_index)
{
SystemModules * modules = EmulationSession::GetInstance().Modules();

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#E6FFFFFF" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8,5v14l11,-7z" />
</vector>

View File

@ -143,6 +143,27 @@
android:textSize="12sp"
android:visibility="gone" />
</FrameLayout>
<ImageView
android:id="@+id/paused_frame_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:visibility="gone" />
<ImageView
android:id="@+id/paused_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/circle_white"
android:clickable="true"
android:contentDescription="@string/emulation_unpause"
android:focusable="false"
android:padding="14dp"
android:src="@drawable/ic_play"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
@ -156,6 +177,21 @@
android:focusable="true"
android:orientation="vertical"
android:paddingTop="24dp">
<TextView
android:id="@+id/menu_pause"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/in_game_menu_item"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/emulation_pause"
android:textColor="@android:color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/menu_lock_drawer"
android:layout_width="match_parent"

View File

@ -19,6 +19,8 @@
<string name="button_map_description">Press a button or move a trigger/stick</string>
<string name="emulation_done">Done</string>
<string name="emulation_touch_overlay_reset">Reset overlay</string>
<string name="emulation_pause">Pause emulation</string>
<string name="emulation_unpause">Unpause emulation</string>
<string name="emulation_exit">Exit emulation</string>
<string name="lock_drawer">Lock drawer</string>
<string name="unlock_drawer">Unlock drawer</string>

View File

@ -26,7 +26,7 @@
enum
{
MODULE_LOADER_SPECS_VERSION = 0x0130,
MODULE_VIDEO_SPECS_VERSION = 0x0121,
MODULE_VIDEO_SPECS_VERSION = 0x0122,
MODULE_CPU_SPECS_VERSION = 0x011d,
MODULE_OPERATING_SYSTEM_SPECS_VERSION = 0x0124,
};

View File

@ -196,6 +196,8 @@ nxinterface IVideo
virtual void ClearCdmaInstance(uint32_t id) = 0;
virtual uint32_t GetAppletCaptureBuffer(uint8_t * out, uint32_t out_size) = 0;
virtual void InvalidateMemory(const uint8_t * pointer, uint64_t size) = 0;
virtual void NotifyWindowChanged() = 0;
virtual uint32_t GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height) = 0;
};
EXPORT IVideo * CALL CreateVideo(IRenderWindow & renderWindow, ISystemModules & modules);

View File

@ -323,6 +323,11 @@ RenderWindow::RenderWindow(IRenderWindow & renderWindow) :
UpdateCurrentFramebufferLayout(640, 480);
}
void RenderWindow::RefreshRenderSurface()
{
window_info.render_surface = m_renderWindow.RenderSurface();
}
void RenderWindow::OnFrameDisplayed()
{
if (!m_firstFrame)

View File

@ -11,6 +11,8 @@ class RenderWindow :
public:
RenderWindow(IRenderWindow & renderWindow);
void RefreshRenderSurface();
// EmuWindow
void OnFrameDisplayed();
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const;

View File

@ -11,6 +11,7 @@
#include "yuzu_video_core/rasterizer_interface.h"
#include <nxemu-core/settings/identifiers.h>
#include "yuzu_video_core/shader_notify.h"
#include "yuzu_video_core/textures/decoders.h"
#include "yuzu_common/polyfill_thread.h"
#include "yuzu_common/settings.h"
#include "nxemu-video/video_settings.h"
@ -290,9 +291,43 @@ void VideoManager::RequestComposite(VideoFramebufferConfig * layers, uint32_t la
void VideoManager::UpdateFramebufferLayout(uint32_t width, uint32_t height)
{
impl->m_emuWindow->RefreshRenderSurface();
impl->m_emuWindow->UpdateCurrentFramebufferLayout(width, height);
}
uint32_t VideoManager::GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height)
{
using namespace VideoCore::Capture;
constexpr uint32_t linear_size = LinearWidth * LinearHeight * BytesPerPixel;
if (out_width != nullptr)
{
*out_width = LinearWidth;
}
if (out_height != nullptr)
{
*out_height = LinearHeight;
}
if (out == nullptr || out_size < linear_size)
{
return linear_size;
}
std::vector<uint8_t> tiled(TiledSize);
GetAppletCaptureBuffer(tiled.data(), static_cast<uint32_t>(tiled.size()));
std::vector<uint8_t> linear(linear_size);
Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight, LinearDepth, BlockHeight, BlockDepth);
std::memcpy(out, linear.data(), linear_size);
return linear_size;
}
void VideoManager::NotifyWindowChanged()
{
if (impl->m_emuWindow)
{
impl->m_emuWindow->RefreshRenderSurface();
}
}
IChannelState * VideoManager::AllocateChannel()
{
return std::make_unique<IChannelStatePtr>(*impl->m_gpuCore, impl->m_memoryManagerRegistry, std::move(impl->m_gpuCore->AllocateChannel())).release();

View File

@ -51,6 +51,8 @@ public:
void ClearCdmaInstance(uint32_t id) override;
uint32_t GetAppletCaptureBuffer(uint8_t * out, uint32_t out_size) override;
void InvalidateMemory(const uint8_t * pointer, uint64_t size) override;
void NotifyWindowChanged() override;
uint32_t GetLinearAppletCaptureBuffer(uint8_t * out, uint32_t out_size, uint32_t * out_width, uint32_t * out_height) override;
private:
VideoManager() = delete;

View File

@ -99,7 +99,8 @@ PresentManager::PresentManager(const vk::Instance& instance_,
memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_},
surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(),
swapchain.GetImageViewFormat())},
use_present_thread{videoSettings.async_presentation} {
use_present_thread{videoSettings.async_presentation},
presented_window{render_window.GetWindowInfo().render_surface} {
SetImageCount();
auto& dld = device.GetLogical();
@ -296,25 +297,25 @@ void PresentManager::SetImageCount() {
}
void PresentManager::CopyToSwapchain(Frame* frame) {
bool requires_recreation = false;
const void* window = render_window.GetWindowInfo().render_surface;
if (window == nullptr) {
presented_window = nullptr;
return;
}
while (true) {
try {
// Recreate surface and swapchain if needed.
if (requires_recreation) {
surface = CreateSurface(instance, render_window.GetWindowInfo());
RecreateSwapchain(frame);
}
// Draw to swapchain.
return CopyToSwapchainImpl(frame);
} catch (const vk::Exception& except) {
if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR) {
throw;
}
requires_recreation = true;
try {
if (window != presented_window) {
surface = CreateSurface(instance, render_window.GetWindowInfo());
presented_window = window;
RecreateSwapchain(frame);
}
CopyToSwapchainImpl(frame);
} catch (const vk::Exception& except) {
if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR &&
except.GetResult() != VK_ERROR_INITIALIZATION_FAILED) {
throw;
}
presented_window = nullptr;
}
}

View File

@ -86,6 +86,7 @@ private:
bool blit_supported;
bool use_present_thread;
std::size_t image_count{};
const void * presented_window = nullptr;
};
} // namespace Vulkan