fixed screen stretch when switching between landscape and portrait mode

This commit is contained in:
BeZide93 2025-09-06 18:42:39 -05:00 committed by KeatonTheBot
parent 316dc3a9b3
commit 8418025b82
3 changed files with 156 additions and 102 deletions

View file

@ -2,6 +2,9 @@ package org.kenjinx.android
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -12,6 +15,7 @@ import kotlin.concurrent.thread
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context),
SurfaceHolder.Callback { SurfaceHolder.Callback {
private var isProgressHidden: Boolean = false private var isProgressHidden: Boolean = false
private var progress: MutableState<String>? = null private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null private var progressValue: MutableState<Float>? = null
@ -27,19 +31,20 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
private var _isStarted: Boolean = false private var _isStarted: Boolean = false
private val _nativeWindow: NativeWindow private val _nativeWindow: NativeWindow
private val mainHandler = Handler(Looper.getMainLooper())
// Stabilizer-State
private var stabilizerActive = false
var currentSurface: Long = -1 var currentSurface: Long = -1
private set private set
val currentWindowHandle: Long val currentWindowHandle: Long
get() { get() = _nativeWindow.nativePointer
return _nativeWindow.nativePointer
}
init { init {
holder.addCallback(this) holder.addCallback(this)
_nativeWindow = NativeWindow(this) _nativeWindow = NativeWindow(this)
mainViewModel.gameHost = this mainViewModel.gameHost = this
} }
@ -47,45 +52,35 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
// no-op // no-op
} }
fun setProgress(info : String, progressVal: Float) { fun setProgress(info: String, progressVal: Float) {
showLoading?.apply { showLoading?.apply {
progressValue?.apply { progressValue?.apply { this.value = progressVal }
this.value = progressVal progress?.apply { this.value = info }
}
progress?.apply {
this.value = info
}
} }
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (_isClosed) if (_isClosed) return
return
if (_width != width || _height != height) { val sizeChanged = (_width != width || _height != height)
if (sizeChanged) {
currentSurface = _nativeWindow.requeryWindowHandle() currentSurface = _nativeWindow.requeryWindowHandle()
_nativeWindow.swapInterval = 0 _nativeWindow.swapInterval = 0
} }
_width = width _width = width
_height = height _height = height
// Start renderer (if not already started)
start(holder) start(holder)
KenjinxNative.graphicsRendererSetSize( // Do not set size immediately → Stabilizer takes over
width, startStabilizedResize(expectedRotation = null)
height
)
if (_isStarted) {
KenjinxNative.inputSetClientSize(width, height)
}
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
// no-op // no-op (renderer lives in its own thread; close via close())
} }
fun close() { fun close() {
@ -95,46 +90,41 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
KenjinxNative.uiHandlerSetResponse(false, "") KenjinxNative.uiHandlerSetResponse(false, "")
_updateThread?.join() try { _updateThread?.join(200) } catch (_: Throwable) {}
_renderingThreadWatcher?.join() try { _renderingThreadWatcher?.join(200) } catch (_: Throwable) {}
} }
private fun start(surfaceHolder: SurfaceHolder) { private fun start(surfaceHolder: SurfaceHolder) {
if (_isStarted) if (_isStarted) return
return
_isStarted = true _isStarted = true
game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel
// Initialize input
KenjinxNative.inputInitialize(width, height) KenjinxNative.inputInitialize(width, height)
val id = mainViewModel.physicalControllerManager?.connect() val id = mainViewModel.physicalControllerManager?.connect()
mainViewModel.motionSensorManager?.setControllerId(id ?: -1) mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
KenjinxNative.graphicsRendererSetSize( // NO graphicsRendererSetSize here we set it via the stabilizer!
surfaceHolder.surfaceFrame.width(),
surfaceHolder.surfaceFrame.height()
)
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3) NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
_guestThread = thread(start = true) { _guestThread = thread(start = true, name = "KenjinxGuest") {
runGame() runGame()
} }
_updateThread = thread(start = true) { _updateThread = thread(start = true, name = "KenjinxInput/Stats") {
var c = 0 var c = 0
val helper = NativeHelpers.instance
while (_isStarted) { while (_isStarted) {
KenjinxNative.inputUpdate() KenjinxNative.inputUpdate()
Thread.sleep(1) Thread.sleep(1)
c++ c++
if (c >= 1000) { if (c >= 1000) {
if (progressValue?.value == -1f) if (progressValue?.value == -1f) {
progress?.apply { progress?.apply {
this.value = this.value = "Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game?.titleName ?: ""}"
"Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}" }
} }
c = 0 c = 0
mainViewModel.updateStats( mainViewModel.updateStats(
@ -149,7 +139,6 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
private fun runGame() { private fun runGame() {
KenjinxNative.graphicsRendererRunLoop() KenjinxNative.graphicsRendererRunLoop()
game?.close() game?.close()
} }
@ -161,17 +150,115 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
this.showLoading = showLoading this.showLoading = showLoading
this.progressValue = progressValue this.progressValue = progressValue
this.progress = progress this.progress = progress
showLoading?.apply { value = !isProgressHidden }
showLoading?.apply {
showLoading.value = !isProgressHidden
}
} }
fun hideProgressIndicator() { fun hideProgressIndicator() {
isProgressHidden = true isProgressHidden = true
showLoading?.apply { showLoading?.apply {
if (value == isProgressHidden) if (value == isProgressHidden) 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)
}
} }

View file

@ -116,6 +116,11 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
val text = NativeHelpers.instance.getStringJava(infoPtr) val text = NativeHelpers.instance.getStringJava(infoPtr)
MainActivity.mainViewModel?.gameHost?.setProgress(text, progress) 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). * Variant A (Pointer Strings via NativeHelpers).

View file

@ -26,7 +26,6 @@ import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings import org.kenjinx.android.viewmodels.QuickSettings
import org.kenjinx.android.viewmodels.GameModel import org.kenjinx.android.viewmodels.GameModel
import org.kenjinx.android.views.MainView import org.kenjinx.android.views.MainView
import androidx.core.net.toUri
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager = private var physicalControllerManager: PhysicalControllerManager =
@ -78,47 +77,20 @@ class MainActivity : BaseActivity() {
private external fun initVm() private external fun initVm()
private fun initialize() { private fun initialize() {
if (_isInit) if (_isInit) return
return
val appPath: String = AppPath val appPath: String = AppPath
var quickSettings = QuickSettings(this) var quickSettings = QuickSettings(this)
KenjinxNative.loggingSetEnabled( KenjinxNative.loggingSetEnabled(LogLevel.Info, quickSettings.enableInfoLogs)
LogLevel.Info, KenjinxNative.loggingSetEnabled(LogLevel.Stub, quickSettings.enableStubLogs)
quickSettings.enableInfoLogs KenjinxNative.loggingSetEnabled(LogLevel.Warning, quickSettings.enableWarningLogs)
) KenjinxNative.loggingSetEnabled(LogLevel.Error, quickSettings.enableErrorLogs)
KenjinxNative.loggingSetEnabled( KenjinxNative.loggingSetEnabled(LogLevel.AccessLog, quickSettings.enableFsAccessLogs)
LogLevel.Stub, KenjinxNative.loggingSetEnabled(LogLevel.Guest, quickSettings.enableGuestLogs)
quickSettings.enableStubLogs KenjinxNative.loggingSetEnabled(LogLevel.Trace, quickSettings.enableTraceLogs)
) KenjinxNative.loggingSetEnabled(LogLevel.Debug, quickSettings.enableDebugLogs)
KenjinxNative.loggingSetEnabled( KenjinxNative.loggingEnabledGraphicsLog(quickSettings.enableGraphicsLogs)
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) _isInit = KenjinxNative.javaInitialize(appPath, JNIEnv.CURRENT)
} }
@ -143,7 +115,7 @@ class MainActivity : BaseActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// --- Apply alignment // Apply alignment
applyOrientationPreference() applyOrientationPreference()
WindowInsetsControllerCompat(window, window.decorView).let { controller -> WindowInsetsControllerCompat(window, window.decorView).let { controller ->
@ -151,7 +123,6 @@ class MainActivity : BaseActivity() {
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} }
// >>> Important: Initialize UI handler (for software keyboard/dialog)
uiHandler = UiHandler() uiHandler = UiHandler()
mainViewModel = MainViewModel(this) mainViewModel = MainViewModel(this)
@ -194,23 +165,19 @@ class MainActivity : BaseActivity() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
event.apply { event.apply {
if (physicalControllerManager.onKeyEvent(this)) if (physicalControllerManager.onKeyEvent(this)) return true
return true
} }
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply { ev?.apply { physicalControllerManager.onMotionEvent(this) }
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev) return super.dispatchGenericMotionEvent(ev)
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
isActive = false isActive = false
if (isGameRunning) { if (isGameRunning) {
mainViewModel?.performanceManager?.setTurboMode(false) mainViewModel?.performanceManager?.setTurboMode(false)
} }
@ -218,26 +185,23 @@ class MainActivity : BaseActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// --- Reapply alignment if necessary // Reapply alignment if necessary
applyOrientationPreference() applyOrientationPreference()
handler.postDelayed(delayedHandleIntent, 10) handler.postDelayed(delayedHandleIntent, 10)
isActive = true isActive = true
if (isGameRunning) { if (isGameRunning) {
if (QuickSettings(this).enableMotion) if (QuickSettings(this).enableMotion) motionSensorManager.register()
motionSensorManager.register()
} }
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
isActive = false isActive = false
if (isGameRunning) { if (isGameRunning) {
mainViewModel?.performanceManager?.setTurboMode(false) mainViewModel?.performanceManager?.setTurboMode(false)
} }
motionSensorManager.unregister() motionSensorManager.unregister()
} }
@ -275,9 +239,7 @@ class MainActivity : BaseActivity() {
val componentName = intent?.component val componentName = intent?.component
val restartIntent = Intent.makeRestartActivityTask(componentName) val restartIntent = Intent.makeRestartActivityTask(componentName)
mainViewModel?.let { mainViewModel?.let { it.performanceManager?.setTurboMode(false) }
it.performanceManager?.setTurboMode(false)
}
startActivity(restartIntent) startActivity(restartIntent)
Runtime.getRuntime().exit(0) Runtime.getRuntime().exit(0)
} }