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.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<String>? = 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 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,46 +90,41 @@ 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(
@ -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)
}
}

View file

@ -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).

View file

@ -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)
}