mirror of
https://git.ryujinx.app/kenji-nx/ryujinx.git
synced 2025-12-14 07:37:04 +00:00
sensor landscape fix
This commit is contained in:
parent
09bc10b15d
commit
c42663115b
6 changed files with 356 additions and 42 deletions
|
|
@ -34,7 +34,8 @@
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="unspecified"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:theme="@style/Theme.KenjinxAndroid">
|
android:theme="@style/Theme.KenjinxAndroid">
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||||
// Stabilizer-State
|
// Stabilizer-State
|
||||||
private var stabilizerActive = false
|
private var stabilizerActive = false
|
||||||
|
|
||||||
|
// last known Android rotation (0,1,2,3)
|
||||||
|
private var lastRotation: Int? = null
|
||||||
|
|
||||||
var currentSurface: Long = -1
|
var currentSurface: Long = -1
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -66,8 +69,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||||
val sizeChanged = (_width != width || _height != height)
|
val sizeChanged = (_width != width || _height != height)
|
||||||
|
|
||||||
if (sizeChanged) {
|
if (sizeChanged) {
|
||||||
|
// Requery Surface / Window handle and report to C#
|
||||||
currentSurface = _nativeWindow.requeryWindowHandle()
|
currentSurface = _nativeWindow.requeryWindowHandle()
|
||||||
_nativeWindow.swapInterval = 0
|
_nativeWindow.swapInterval = 0
|
||||||
|
try { KenjinxNative.deviceSetWindowHandle(currentWindowHandle) } catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_width = width
|
_width = width
|
||||||
|
|
@ -77,7 +82,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||||
start(holder)
|
start(holder)
|
||||||
|
|
||||||
// Do not set size immediately → Stabilizer takes over
|
// Do not set size immediately → Stabilizer takes over
|
||||||
startStabilizedResize(expectedRotation = null)
|
startStabilizedResize(expectedRotation = lastRotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
|
@ -107,11 +112,27 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||||
val id = mainViewModel.physicalControllerManager?.connect()
|
val id = mainViewModel.physicalControllerManager?.connect()
|
||||||
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
|
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
|
||||||
|
|
||||||
// NO graphicsRendererSetSize here – we set it via the stabilizer!
|
// ❌ Removed: initial "flip" at 270° (caused 90° lock at start right)
|
||||||
|
// NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
// ✅ Correct: Report current Android rotation directly to the native site
|
||||||
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
|
val currentRot = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
mainViewModel.activity.display?.rotation
|
||||||
|
} else {
|
||||||
|
TODO("VERSION.SDK_INT < R")
|
||||||
}
|
}
|
||||||
|
lastRotation = currentRot
|
||||||
|
try {
|
||||||
|
KenjinxNative.setSurfaceRotationByAndroidRotation(currentRot)
|
||||||
|
// Pass the window handle for safety reasons (if Surface has just been refreshed)
|
||||||
|
try { KenjinxNative.deviceSetWindowHandle(currentWindowHandle) } catch (_: Throwable) {}
|
||||||
|
// Swapchain/Viewport “knock”: set identical size again
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
try { KenjinxNative.resizeRendererAndInput(width, height) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
|
||||||
|
// NO graphicsRendererSetSize here – we set it via the stabilizer!
|
||||||
|
|
||||||
_guestThread = thread(start = true, name = "KenjinxGuest") {
|
_guestThread = thread(start = true, name = "KenjinxGuest") {
|
||||||
runGame()
|
runGame()
|
||||||
|
|
@ -182,17 +203,42 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wird von der Activity bei Rotations-/Layoutwechsel aufgerufen.
|
* Called on the activity when the rotation/layout changes.
|
||||||
* Reicht die aktuelle Rotation durch, damit wir ggf. Breite/Höhe tauschen können.
|
* Detects 90°↔270° and immediately forces a NativeWindow query.
|
||||||
*/
|
*/
|
||||||
fun onOrientationOrSizeChanged(rotation: Int? = null) {
|
fun onOrientationOrSizeChanged(rotation: Int? = null) {
|
||||||
if (_isClosed) return
|
if (_isClosed) return
|
||||||
|
|
||||||
|
val old = lastRotation
|
||||||
|
lastRotation = rotation
|
||||||
|
|
||||||
|
val isSideFlip = (old == 1 && rotation == 3) || (old == 3 && rotation == 1)
|
||||||
|
|
||||||
|
if (isSideFlip) {
|
||||||
|
// 1) Report NativeRotation
|
||||||
|
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rotation) } catch (_: Throwable) {}
|
||||||
|
|
||||||
|
// 2) Requery NativeWindow immediately (forces real rebind) + window handle to C#
|
||||||
|
try {
|
||||||
|
currentSurface = _nativeWindow.requeryWindowHandle()
|
||||||
|
_nativeWindow.swapInterval = 0
|
||||||
|
try { KenjinxNative.deviceSetWindowHandle(currentWindowHandle) } catch (_: Throwable) {}
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
|
||||||
|
// 3) Swapchain/Viewport directly “knock”, set identical size again
|
||||||
|
val w = if (holder.surfaceFrame.width() > 0) holder.surfaceFrame.width() else width
|
||||||
|
val h = if (holder.surfaceFrame.height() > 0) holder.surfaceFrame.height() else height
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
try { KenjinxNative.resizeRendererAndInput(w, h) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startStabilizedResize(rotation)
|
startStabilizedResize(rotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wartet kurz, bis das Surface seine finalen Maße nach der Drehung hat,
|
* Wait a moment until the surface has its final dimensions after rotation,
|
||||||
* prüft Plausibilität (Portrait/Landscape) und setzt erst dann die Größe.
|
* checks plausibility (portrait/landscape) and only then sets the size.
|
||||||
*/
|
*/
|
||||||
private fun startStabilizedResize(expectedRotation: Int?) {
|
private fun startStabilizedResize(expectedRotation: Int?) {
|
||||||
if (_isClosed) return
|
if (_isClosed) return
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.sun.jna.Library
|
||||||
import com.sun.jna.Native
|
import com.sun.jna.Native
|
||||||
import org.kenjinx.android.viewmodels.GameInfo
|
import org.kenjinx.android.viewmodels.GameInfo
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import android.view.Surface
|
||||||
|
|
||||||
interface KenjinxNativeJna : Library {
|
interface KenjinxNativeJna : Library {
|
||||||
fun deviceInitialize(
|
fun deviceInitialize(
|
||||||
|
|
@ -87,6 +88,12 @@ interface KenjinxNativeJna : Library {
|
||||||
fun userGetAllUsers(): Array<String>
|
fun userGetAllUsers(): Array<String>
|
||||||
fun deviceGetDlcContentList(path: String, titleId: Long): Array<String>
|
fun deviceGetDlcContentList(path: String, titleId: Long): Array<String>
|
||||||
fun loggingEnabledGraphicsLog(enabled: Boolean)
|
fun loggingEnabledGraphicsLog(enabled: Boolean)
|
||||||
|
// Surface rotation (0/90/180/270 degrees)
|
||||||
|
fun deviceSetSurfaceRotation(degrees: Int)
|
||||||
|
// (optional alias): compact resize shortcut
|
||||||
|
fun deviceResize(width: Int, height: Int)
|
||||||
|
// Set window handle after each query
|
||||||
|
fun deviceSetWindowHandle(handle: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
val jnaInstance: KenjinxNativeJna = Native.load(
|
val jnaInstance: KenjinxNativeJna = Native.load(
|
||||||
|
|
@ -112,14 +119,34 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun updateProgress(infoPtr: Long, progress: Float) {
|
fun updateProgress(infoPtr: Long, progress: Float) {
|
||||||
// Get string from native pointer and push into progress overlay
|
|
||||||
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
|
@JvmStatic
|
||||||
fun onSurfaceSizeChanged(width: Int, height: Int) {
|
fun onSurfaceSizeChanged(width: Int, height: Int) {
|
||||||
// No-op: Placeholder. If you have a hook in C#/C++ (swapchain/viewport new),
|
// No-Op: Placeholder – Hook if needed.
|
||||||
// you can call it here.
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setSurfaceRotationByAndroidRotation(androidRotation: Int?) {
|
||||||
|
val degrees = when (androidRotation) {
|
||||||
|
Surface.ROTATION_0 -> 0
|
||||||
|
Surface.ROTATION_90 -> 90
|
||||||
|
Surface.ROTATION_180 -> 180
|
||||||
|
Surface.ROTATION_270 -> 270
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
try { deviceSetSurfaceRotation(degrees) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun resizeRendererAndInput(width: Int, height: Int) {
|
||||||
|
try {
|
||||||
|
// Alternatively: deviceResize(width, height)
|
||||||
|
graphicsRendererSetSize(width, height)
|
||||||
|
inputSetClientSize(width, height)
|
||||||
|
} catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -159,8 +186,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant B (strings directly). Used by newer JNI/Interop paths.
|
* Variant B (strings directly). Used by newer JNI/interop paths.
|
||||||
* Signature exactly matches the C# call in AndroidUIHandler.cs / Interop.UpdateUiHandler(...).
|
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun uiHandlerUpdate(
|
fun uiHandlerUpdate(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -20,6 +22,7 @@ import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.sun.jna.JNIEnv
|
import com.sun.jna.JNIEnv
|
||||||
import org.kenjinx.android.ui.theme.KenjinxAndroidTheme
|
import org.kenjinx.android.ui.theme.KenjinxAndroidTheme
|
||||||
|
|
@ -27,6 +30,12 @@ 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 java.io.File
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.hardware.display.DisplayManager
|
||||||
|
import android.view.Surface
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
private var physicalControllerManager: PhysicalControllerManager =
|
private var physicalControllerManager: PhysicalControllerManager =
|
||||||
|
|
@ -34,17 +43,105 @@ class MainActivity : BaseActivity() {
|
||||||
private lateinit var motionSensorManager: MotionSensorManager
|
private lateinit var motionSensorManager: MotionSensorManager
|
||||||
private var _isInit: Boolean = false
|
private var _isInit: Boolean = false
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private val delayedHandleIntent = object : Runnable {
|
private val delayedHandleIntent = object : Runnable { override fun run() { handleIntent() } }
|
||||||
override fun run() {
|
|
||||||
handleIntent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var storedIntent: Intent = Intent()
|
var storedIntent: Intent = Intent()
|
||||||
var isGameRunning = false
|
var isGameRunning = false
|
||||||
var isActive = false
|
var isActive = false
|
||||||
var storageHelper: SimpleStorageHelper? = null
|
var storageHelper: SimpleStorageHelper? = null
|
||||||
lateinit var uiHandler: UiHandler
|
lateinit var uiHandler: UiHandler
|
||||||
|
|
||||||
|
// Display Rotation + Orientation Handling
|
||||||
|
private lateinit var displayManager: DisplayManager
|
||||||
|
private var lastKnownRotation: Int? = null
|
||||||
|
private var pulsingOrientation = false
|
||||||
|
|
||||||
|
private val TAG_ROT = "RotationDebug"
|
||||||
|
|
||||||
|
private val displayListener = object : DisplayManager.DisplayListener {
|
||||||
|
override fun onDisplayAdded(displayId: Int) {}
|
||||||
|
override fun onDisplayRemoved(displayId: Int) {}
|
||||||
|
override fun onDisplayChanged(displayId: Int) {
|
||||||
|
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
display?.displayId != displayId
|
||||||
|
} else {
|
||||||
|
TODO("VERSION.SDK_INT < R")
|
||||||
|
}
|
||||||
|
) return
|
||||||
|
val rot = display?.rotation
|
||||||
|
if (rot == lastKnownRotation) return
|
||||||
|
|
||||||
|
Log.d(TAG_ROT, "onDisplayChanged: display.rotation=$rot → ${deg(rot)}°")
|
||||||
|
|
||||||
|
val pref = QuickSettings(this@MainActivity).orientationPreference
|
||||||
|
val old = lastKnownRotation
|
||||||
|
lastKnownRotation = rot
|
||||||
|
|
||||||
|
// 1) Inform Native/Renderer
|
||||||
|
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rot) } catch (_: Throwable) {}
|
||||||
|
|
||||||
|
// 2) Initiate host resize
|
||||||
|
if (isGameRunning) {
|
||||||
|
handler.post {
|
||||||
|
try { mainViewModel?.gameHost?.onOrientationOrSizeChanged(rot) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) For SENSOR_LANDSCAPE possibly pulse, if 90↔270 flip
|
||||||
|
if (pref == QuickSettings.OrientationPreference.SensorLandscape && old != null && rot != null) {
|
||||||
|
val isSideFlip = (old == Surface.ROTATION_90 && rot == Surface.ROTATION_270) ||
|
||||||
|
(old == Surface.ROTATION_270 && rot == Surface.ROTATION_90)
|
||||||
|
if (isSideFlip) doOrientationPulse(rot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deg(r: Int?): Int = when (r) {
|
||||||
|
Surface.ROTATION_0 -> 0
|
||||||
|
Surface.ROTATION_90 -> 90
|
||||||
|
Surface.ROTATION_180 -> 180
|
||||||
|
Surface.ROTATION_270 -> 270
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doOrientationPulse(currentRot: Int) {
|
||||||
|
if (pulsingOrientation) return
|
||||||
|
pulsingOrientation = true
|
||||||
|
|
||||||
|
// Short lock on the target page (instead of portrait intermediate step; prevents flickering)
|
||||||
|
val lock = if (currentRot == Surface.ROTATION_90)
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
else
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
|
||||||
|
try { requestedOrientation = lock } catch (_: Throwable) {}
|
||||||
|
handler.post {
|
||||||
|
if (isGameRunning) {
|
||||||
|
try { KenjinxNative.setSurfaceRotationByAndroidRotation(currentRot) } catch (_: Throwable) {}
|
||||||
|
try { mainViewModel?.gameHost?.onOrientationOrSizeChanged(currentRot) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a short time back to SENSOR_LANDSCAPE
|
||||||
|
handler.postDelayed({
|
||||||
|
try { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } catch (_: Throwable) {}
|
||||||
|
handler.post {
|
||||||
|
if (isGameRunning) {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
KenjinxNative.setSurfaceRotationByAndroidRotation(display?.rotation)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
mainViewModel?.gameHost?.onOrientationOrSizeChanged(display?.rotation)
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pulsingOrientation = false
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var mainViewModel: MainViewModel? = null
|
var mainViewModel: MainViewModel? = null
|
||||||
var AppPath: String = ""
|
var AppPath: String = ""
|
||||||
|
|
@ -79,7 +176,6 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
if (_isInit) return
|
if (_isInit) return
|
||||||
|
|
||||||
val appPath: String = AppPath
|
val appPath: String = AppPath
|
||||||
|
|
||||||
var quickSettings = QuickSettings(this)
|
var quickSettings = QuickSettings(this)
|
||||||
|
|
@ -114,7 +210,6 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
window.attributes.layoutInDisplayCutoutMode =
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
|
@ -127,15 +222,16 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
}
|
}
|
||||||
|
|
||||||
uiHandler = UiHandler()
|
uiHandler = UiHandler()
|
||||||
|
displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||||
|
|
||||||
mainViewModel = MainViewModel(this)
|
mainViewModel = MainViewModel(this)
|
||||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||||
mainViewModel!!.motionSensorManager = motionSensorManager
|
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||||
|
|
||||||
mainViewModel!!.refreshFirmwareVersion()
|
mainViewModel!!.refreshFirmwareVersion()
|
||||||
|
|
||||||
mainViewModel?.apply {
|
mainViewModel?.apply {
|
||||||
|
|
@ -152,6 +248,9 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
storedIntent = intent
|
storedIntent = intent
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
Log.d(TAG_ROT, "onCreate: initial display.rotation=${display?.rotation} → ${deg(display?.rotation)}°")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|
@ -171,9 +270,7 @@ 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)) return true }
|
||||||
if (physicalControllerManager.onKeyEvent(this)) return true
|
|
||||||
}
|
|
||||||
return super.dispatchKeyEvent(event)
|
return super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,9 +282,7 @@ class MainActivity : BaseActivity() {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
@ -195,21 +290,24 @@ class MainActivity : BaseActivity() {
|
||||||
// Reapply alignment if necessary
|
// Reapply alignment if necessary
|
||||||
applyOrientationPreference()
|
applyOrientationPreference()
|
||||||
|
|
||||||
|
// Enable display listener
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
lastKnownRotation = display?.rotation
|
||||||
|
Log.d(TAG_ROT, "onResume: display.rotation=${display?.rotation} → ${deg(display?.rotation)}°")
|
||||||
|
}
|
||||||
|
try { displayManager.registerDisplayListener(displayListener, handler) } catch (_: Throwable) {}
|
||||||
|
|
||||||
handler.postDelayed(delayedHandleIntent, 10)
|
handler.postDelayed(delayedHandleIntent, 10)
|
||||||
isActive = true
|
isActive = true
|
||||||
|
if (isGameRunning && QuickSettings(this).enableMotion) motionSensorManager.register()
|
||||||
if (isGameRunning) {
|
|
||||||
if (QuickSettings(this).enableMotion) 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()
|
||||||
|
try { displayManager.unregisterDisplayListener(displayListener) } catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntent() {
|
private fun handleIntent() {
|
||||||
|
|
@ -224,7 +322,6 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
if (documentFile != null) {
|
if (documentFile != null) {
|
||||||
val gameModel = GameModel(documentFile, this)
|
val gameModel = GameModel(documentFile, this)
|
||||||
|
|
||||||
gameModel.getGameInfo()
|
gameModel.getGameInfo()
|
||||||
mainViewModel?.loadGameModel?.value = gameModel
|
mainViewModel?.loadGameModel?.value = gameModel
|
||||||
mainViewModel?.bootPath?.value = "gameItem_${gameModel.titleName}"
|
mainViewModel?.bootPath?.value = "gameItem_${gameModel.titleName}"
|
||||||
|
|
@ -238,6 +335,13 @@ class MainActivity : BaseActivity() {
|
||||||
private fun applyOrientationPreference() {
|
private fun applyOrientationPreference() {
|
||||||
val pref = QuickSettings(this).orientationPreference
|
val pref = QuickSettings(this).orientationPreference
|
||||||
requestedOrientation = pref.value
|
requestedOrientation = pref.value
|
||||||
|
val rot = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
this.display?.rotation
|
||||||
|
} else {
|
||||||
|
TODO("VERSION.SDK_INT < R")
|
||||||
|
}
|
||||||
|
Log.d(TAG_ROT, "applyOrientationPreference: rot=$rot → ${deg(rot)}°, pref=${pref.name}")
|
||||||
|
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rot) } catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdownAndRestart() {
|
fun shutdownAndRestart() {
|
||||||
|
|
@ -245,7 +349,6 @@ class MainActivity : BaseActivity() {
|
||||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||||
val componentName = intent?.component
|
val componentName = intent?.component
|
||||||
val restartIntent = Intent.makeRestartActivityTask(componentName)
|
val restartIntent = Intent.makeRestartActivityTask(componentName)
|
||||||
|
|
||||||
mainViewModel?.let { it.performanceManager?.setTurboMode(false) }
|
mainViewModel?.let { it.performanceManager?.setTurboMode(false) }
|
||||||
startActivity(restartIntent)
|
startActivity(restartIntent)
|
||||||
Runtime.getRuntime().exit(0)
|
Runtime.getRuntime().exit(0)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.kenjinx.android.views
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -43,7 +44,6 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -259,11 +259,21 @@ class SettingViews {
|
||||||
selectedOrientation = orientationPref.value,
|
selectedOrientation = orientationPref.value,
|
||||||
onOrientationSelected = { sel ->
|
onOrientationSelected = { sel ->
|
||||||
orientationPref.value = sel
|
orientationPref.value = sel
|
||||||
// Save and use immediately
|
|
||||||
val qs = QuickSettings(mainViewModel.activity)
|
val qs = QuickSettings(mainViewModel.activity)
|
||||||
qs.orientationPreference = sel
|
qs.orientationPreference = sel
|
||||||
qs.save()
|
qs.save()
|
||||||
mainViewModel.activity.requestedOrientation = sel.value
|
|
||||||
|
// 1) Set activity alignment
|
||||||
|
val act = mainViewModel.activity
|
||||||
|
act.requestedOrientation = sel.value
|
||||||
|
|
||||||
|
// 2) Submit rotation/size immediately to the rendering
|
||||||
|
val rot = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
act.display?.rotation
|
||||||
|
} else {
|
||||||
|
TODO("VERSION.SDK_INT < R")
|
||||||
|
}
|
||||||
|
mainViewModel.gameHost?.onOrientationOrSizeChanged(rot)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,14 @@ namespace LibKenjinx
|
||||||
private static long _surfacePtr;
|
private static long _surfacePtr;
|
||||||
private static long _window = 0;
|
private static long _window = 0;
|
||||||
|
|
||||||
|
// Remembers the last set renderer size (for the jiggle)
|
||||||
|
private static int _lastRenderWidth = 0;
|
||||||
|
private static int _lastRenderHeight = 0;
|
||||||
|
|
||||||
|
// NEW: Rotation Debounce + Pending Buffer
|
||||||
|
private static int _lastRotationDegrees = -1;
|
||||||
|
private static int _pendingRotationDegrees = -1;
|
||||||
|
|
||||||
public static VulkanLoader? VulkanLoader { get; private set; }
|
public static VulkanLoader? VulkanLoader { get; private set; }
|
||||||
|
|
||||||
[DllImport("libkenjinxjni")]
|
[DllImport("libkenjinxjni")]
|
||||||
|
|
@ -310,6 +318,30 @@ namespace LibKenjinx
|
||||||
|
|
||||||
var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), createInfo, null, out var surface);
|
var result = surfaceExtension.CreateAndroidSurface(new Instance(instance), createInfo, null, out var surface);
|
||||||
|
|
||||||
|
// If a rotation was applied before the surface was created → apply it now
|
||||||
|
if (_window != 0 && _pendingRotationDegrees != -1)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int t = _pendingRotationDegrees switch
|
||||||
|
{
|
||||||
|
0 => 0, // IDENTITY
|
||||||
|
90 => 4, // ROTATE_90
|
||||||
|
180 => 3, // ROTATE_180 (H|V mirror)
|
||||||
|
270 => 7, // ROTATE_270 (ROT_90 | H|V)
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
setCurrentTransform(_window, t);
|
||||||
|
Logger.Trace?.Print(LogClass.Application, $"[JNI] Apply pending SurfaceTransform {_pendingRotationDegrees}° (t={t}, window=0x{_window:x})");
|
||||||
|
_lastRotationDegrees = _pendingRotationDegrees;
|
||||||
|
_pendingRotationDegrees = -1;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Apply pending transform failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (nint)surface.Handle;
|
return (nint)surface.Handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +354,9 @@ namespace LibKenjinx
|
||||||
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetSize")]
|
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetSize")]
|
||||||
public static void JnaSetRendererSizeNative(int width, int height)
|
public static void JnaSetRendererSizeNative(int width, int height)
|
||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
Logger.Trace?.Print(LogClass.Application, $"graphicsRendererSetSize -> {width}x{height}");
|
||||||
|
_lastRenderWidth = width;
|
||||||
|
_lastRenderHeight = height;
|
||||||
Renderer?.Window?.SetSize(width, height);
|
Renderer?.Window?.SetSize(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,6 +594,100 @@ namespace LibKenjinx
|
||||||
|
|
||||||
CloseUser(userId);
|
CloseUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Window Handle Update (Android) ---
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "deviceSetWindowHandle")]
|
||||||
|
public static void JniSetWindowHandle(long handle)
|
||||||
|
{
|
||||||
|
_window = handle;
|
||||||
|
Logger.Trace?.Print(Ryujinx.Common.Logging.LogClass.Application,
|
||||||
|
$"Window handle updated: 0x{handle:X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Surface Rotation Bridge (Android) ---
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "deviceSetSurfaceRotation")]
|
||||||
|
public static void JniDeviceSetSurfaceRotation(int degrees)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Normalize
|
||||||
|
degrees = degrees switch { 0 => 0, 90 => 90, 180 => 180, 270 => 270, _ => 0 };
|
||||||
|
|
||||||
|
if (degrees == _lastRotationDegrees)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, $"[JNI] SurfaceTransform unchanged ({degrees}°), skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORRECT bitmask mapping according to NDK:
|
||||||
|
// 0 -> 0 (IDENTITY)
|
||||||
|
// 90 -> 4 (ROTATE_90)
|
||||||
|
// 180 -> 3 (H|V mirror == 180°)
|
||||||
|
// 270 -> 7 (ROTATE_270 == ROT_90 | H|V)
|
||||||
|
int transform = degrees switch
|
||||||
|
{
|
||||||
|
0 => 0,
|
||||||
|
90 => 4,
|
||||||
|
180 => 3,
|
||||||
|
270 => 7,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_window != 0)
|
||||||
|
{
|
||||||
|
setCurrentTransform(_window, transform);
|
||||||
|
_lastRotationDegrees = degrees;
|
||||||
|
Logger.Trace?.Print(LogClass.Application, $"[JNI] SurfaceTransform -> {degrees}° (t={transform}, window=0x{_window:x})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingRotationDegrees = degrees; // apply later (see createSurfaceFunc)
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"[JNI] deviceSetSurfaceRotation: _window == 0 (pending {degrees}°)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"deviceSetSurfaceRotation failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vulkan/GL: Swapchain/Surface Reconfiguration via Size Jiggle ---
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "deviceRecreateSwapchain")]
|
||||||
|
public static void JniDeviceRecreateSwapchain()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Renderer?.Window == null)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, "[JNI] deviceRecreateSwapchain: Renderer.Window == null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = _lastRenderWidth;
|
||||||
|
int h = _lastRenderHeight;
|
||||||
|
|
||||||
|
if (w > 0 && h > 0)
|
||||||
|
{
|
||||||
|
int jiggleW = w;
|
||||||
|
int jiggleH = h;
|
||||||
|
if (w <= h) jiggleW = Math.Max(1, w - 1); else jiggleH = Math.Max(1, h - 1);
|
||||||
|
|
||||||
|
Logger.Trace?.Print(LogClass.Application, $"[JNI] deviceRecreateSwapchain: jiggle {jiggleW}x{jiggleH} -> {w}x{h}");
|
||||||
|
Renderer.Window.SetSize(jiggleW, jiggleH);
|
||||||
|
Renderer.Window.SetSize(w, h);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "[JNI] deviceRecreateSwapchain: unknown last size -> 1x1 -> 2x2 jiggle");
|
||||||
|
Renderer.Window.SetSize(1, 1);
|
||||||
|
Renderer.Window.SetSize(2, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"deviceRecreateSwapchain failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static partial class Logcat
|
internal static partial class Logcat
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue