diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt index 65a393b99..2a6bf5f84 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt @@ -2,6 +2,9 @@ package org.kenjinx.android import android.annotation.SuppressLint import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log import android.view.SurfaceHolder import android.view.SurfaceView import androidx.compose.runtime.MutableState @@ -12,6 +15,7 @@ import kotlin.concurrent.thread @SuppressLint("ViewConstructor") class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { + private var isProgressHidden: Boolean = false private var progress: MutableState? = null private var progressValue: MutableState? = null @@ -27,19 +31,20 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su private var _isStarted: Boolean = false private val _nativeWindow: NativeWindow + private val mainHandler = Handler(Looper.getMainLooper()) + + // Stabilizer-State + private var stabilizerActive = false + var currentSurface: Long = -1 private set val currentWindowHandle: Long - get() { - return _nativeWindow.nativePointer - } + get() = _nativeWindow.nativePointer init { holder.addCallback(this) - _nativeWindow = NativeWindow(this) - mainViewModel.gameHost = this } @@ -47,45 +52,35 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su // no-op } - fun setProgress(info : String, progressVal: Float) { + fun setProgress(info: String, progressVal: Float) { showLoading?.apply { - progressValue?.apply { - this.value = progressVal - } - - progress?.apply { - this.value = info - } + progressValue?.apply { this.value = progressVal } + progress?.apply { this.value = info } } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - if (_isClosed) - return + if (_isClosed) return - if (_width != width || _height != height) { + val sizeChanged = (_width != width || _height != height) + + if (sizeChanged) { currentSurface = _nativeWindow.requeryWindowHandle() - _nativeWindow.swapInterval = 0 } _width = width _height = height + // Start renderer (if not already started) start(holder) - KenjinxNative.graphicsRendererSetSize( - width, - height - ) - - if (_isStarted) { - KenjinxNative.inputSetClientSize(width, height) - } + // Do not set size immediately → Stabilizer takes over + startStabilizedResize(expectedRotation = null) } override fun surfaceDestroyed(holder: SurfaceHolder) { - // no-op + // no-op (renderer lives in its own thread; close via close()) } fun close() { @@ -95,47 +90,42 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su KenjinxNative.uiHandlerSetResponse(false, "") - _updateThread?.join() - _renderingThreadWatcher?.join() + try { _updateThread?.join(200) } catch (_: Throwable) {} + try { _renderingThreadWatcher?.join(200) } catch (_: Throwable) {} } private fun start(surfaceHolder: SurfaceHolder) { - if (_isStarted) - return - + if (_isStarted) return _isStarted = true game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel + // Initialize input KenjinxNative.inputInitialize(width, height) val id = mainViewModel.physicalControllerManager?.connect() mainViewModel.motionSensorManager?.setControllerId(id ?: -1) - KenjinxNative.graphicsRendererSetSize( - surfaceHolder.surfaceFrame.width(), - surfaceHolder.surfaceFrame.height() - ) + // NO graphicsRendererSetSize here – we set it via the stabilizer! NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3) - _guestThread = thread(start = true) { + _guestThread = thread(start = true, name = "KenjinxGuest") { runGame() } - _updateThread = thread(start = true) { + _updateThread = thread(start = true, name = "KenjinxInput/Stats") { var c = 0 - val helper = NativeHelpers.instance while (_isStarted) { KenjinxNative.inputUpdate() Thread.sleep(1) c++ if (c >= 1000) { - if (progressValue?.value == -1f) + if (progressValue?.value == -1f) { progress?.apply { - this.value = - "Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}" + this.value = "Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game?.titleName ?: ""}" } + } c = 0 mainViewModel.updateStats( KenjinxNative.deviceGetGameFifo(), @@ -149,7 +139,6 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su private fun runGame() { KenjinxNative.graphicsRendererRunLoop() - game?.close() } @@ -161,17 +150,115 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su this.showLoading = showLoading this.progressValue = progressValue this.progress = progress - - showLoading?.apply { - showLoading.value = !isProgressHidden - } + showLoading?.apply { value = !isProgressHidden } } fun hideProgressIndicator() { isProgressHidden = true showLoading?.apply { - if (value == isProgressHidden) - value = !isProgressHidden + if (value == isProgressHidden) value = !isProgressHidden } } + + /** + * Sicheres Setzen der Renderer-/Input-Größe. + */ + @Synchronized + private fun safeSetSize(w: Int, h: Int) { + if (_isClosed) return + if (w <= 0 || h <= 0) return + try { + Log.d("GameHost", "safeSetSize: ${w}x$h (started=$_isStarted)") + KenjinxNative.graphicsRendererSetSize(w, h) + if (_isStarted) { + KenjinxNative.inputSetClientSize(w, h) + } + } catch (t: Throwable) { + Log.e("GameHost", "safeSetSize failed: ${t.message}", t) + } + } + + /** + * Wird von der Activity bei Rotations-/Layoutwechsel aufgerufen. + * Reicht die aktuelle Rotation durch, damit wir ggf. Breite/Höhe tauschen können. + */ + fun onOrientationOrSizeChanged(rotation: Int? = null) { + if (_isClosed) return + startStabilizedResize(rotation) + } + + /** + * Wartet kurz, bis das Surface seine finalen Maße nach der Drehung hat, + * prüft Plausibilität (Portrait/Landscape) und setzt erst dann die Größe. + */ + private fun startStabilizedResize(expectedRotation: Int?) { + if (_isClosed) return + + // Restart if already active + if (stabilizerActive) { + stabilizerActive = false + } + stabilizerActive = true + + var attempts = 0 + var stableCount = 0 + var lastW = -1 + var lastH = -1 + + val task = object : Runnable { + override fun run() { + if (!_isStarted || _isClosed) { + stabilizerActive = false + return + } + + // Prefer real frame size + var w = holder.surfaceFrame.width() + var h = holder.surfaceFrame.height() + + // Fallbacks + if (w <= 0 || h <= 0) { + w = width + h = height + } + + // If rotation is known: Force plausibility (Landscape ↔ Portrait) + expectedRotation?.let { rot -> + // ROTATION_90 (1) / ROTATION_270 (3) => Landscape + val landscape = (rot == 1 || rot == 3) + if (landscape && h > w) { + val t = w; w = h; h = t + } else if (!landscape && w > h) { + val t = w; w = h; h = t + } + } + + // Stability test + if (w == lastW && h == lastH && w > 0 && h > 0) { + stableCount++ + } else { + stableCount = 0 + lastW = w + lastH = h + } + + attempts++ + + // 2 consecutive identical measurements OR 20 attempts → apply + if ((stableCount >= 2 || attempts >= 20) && w > 0 && h > 0) { + Log.d("GameHost", "resize stabilized after $attempts ticks → ${w}x$h") + safeSetSize(w, h) + stabilizerActive = false + return + } + + // continue to pollen + if (stabilizerActive) { + mainHandler.postDelayed(this, 16) + } + } + } + + mainHandler.post(task) + } } diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt index ad4cdb579..95739695a 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt @@ -116,6 +116,11 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance { val text = NativeHelpers.instance.getStringJava(infoPtr) MainActivity.mainViewModel?.gameHost?.setProgress(text, progress) } + @JvmStatic + fun onSurfaceSizeChanged(width: Int, height: Int) { + // No-op: Placeholder. If you have a hook in C#/C++ (swapchain/viewport new), + // you can call it here. + } /** * Variant A (Pointer → Strings via NativeHelpers). diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt index 1a7c02a16..30dbad827 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt @@ -26,7 +26,6 @@ import org.kenjinx.android.viewmodels.MainViewModel import org.kenjinx.android.viewmodels.QuickSettings import org.kenjinx.android.viewmodels.GameModel import org.kenjinx.android.views.MainView -import androidx.core.net.toUri class MainActivity : BaseActivity() { private var physicalControllerManager: PhysicalControllerManager = @@ -78,47 +77,20 @@ class MainActivity : BaseActivity() { private external fun initVm() private fun initialize() { - if (_isInit) - return + if (_isInit) return val appPath: String = AppPath var quickSettings = QuickSettings(this) - KenjinxNative.loggingSetEnabled( - LogLevel.Info, - quickSettings.enableInfoLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Stub, - quickSettings.enableStubLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Warning, - quickSettings.enableWarningLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Error, - quickSettings.enableErrorLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.AccessLog, - quickSettings.enableFsAccessLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Guest, - quickSettings.enableGuestLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Trace, - quickSettings.enableTraceLogs - ) - KenjinxNative.loggingSetEnabled( - LogLevel.Debug, - quickSettings.enableDebugLogs - ) - KenjinxNative.loggingEnabledGraphicsLog( - quickSettings.enableGraphicsLogs - ) + KenjinxNative.loggingSetEnabled(LogLevel.Info, quickSettings.enableInfoLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Stub, quickSettings.enableStubLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Warning, quickSettings.enableWarningLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Error, quickSettings.enableErrorLogs) + KenjinxNative.loggingSetEnabled(LogLevel.AccessLog, quickSettings.enableFsAccessLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Guest, quickSettings.enableGuestLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Trace, quickSettings.enableTraceLogs) + KenjinxNative.loggingSetEnabled(LogLevel.Debug, quickSettings.enableDebugLogs) + KenjinxNative.loggingEnabledGraphicsLog(quickSettings.enableGraphicsLogs) _isInit = KenjinxNative.javaInitialize(appPath, JNIEnv.CURRENT) } @@ -143,7 +115,7 @@ class MainActivity : BaseActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - // --- Apply alignment + // Apply alignment applyOrientationPreference() WindowInsetsControllerCompat(window, window.decorView).let { controller -> @@ -151,7 +123,6 @@ class MainActivity : BaseActivity() { controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } - // >>> Important: Initialize UI handler (for software keyboard/dialog) uiHandler = UiHandler() mainViewModel = MainViewModel(this) @@ -194,23 +165,19 @@ class MainActivity : BaseActivity() { @SuppressLint("RestrictedApi") override fun dispatchKeyEvent(event: KeyEvent): Boolean { event.apply { - if (physicalControllerManager.onKeyEvent(this)) - return true + if (physicalControllerManager.onKeyEvent(this)) return true } return super.dispatchKeyEvent(event) } override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - ev?.apply { - physicalControllerManager.onMotionEvent(this) - } + ev?.apply { physicalControllerManager.onMotionEvent(this) } return super.dispatchGenericMotionEvent(ev) } override fun onStop() { super.onStop() isActive = false - if (isGameRunning) { mainViewModel?.performanceManager?.setTurboMode(false) } @@ -218,26 +185,23 @@ class MainActivity : BaseActivity() { override fun onResume() { super.onResume() - // --- Reapply alignment if necessary + // Reapply alignment if necessary applyOrientationPreference() handler.postDelayed(delayedHandleIntent, 10) isActive = true if (isGameRunning) { - if (QuickSettings(this).enableMotion) - motionSensorManager.register() + if (QuickSettings(this).enableMotion) motionSensorManager.register() } } override fun onPause() { super.onPause() isActive = false - if (isGameRunning) { mainViewModel?.performanceManager?.setTurboMode(false) } - motionSensorManager.unregister() } @@ -275,9 +239,7 @@ class MainActivity : BaseActivity() { val componentName = intent?.component val restartIntent = Intent.makeRestartActivityTask(componentName) - mainViewModel?.let { - it.performanceManager?.setTurboMode(false) - } + mainViewModel?.let { it.performanceManager?.setTurboMode(false) } startActivity(restartIntent) Runtime.getRuntime().exit(0) }