mirror of
https://git.ryujinx.app/kenji-nx/ryujinx.git
synced 2025-12-14 16:37:07 +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:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:screenOrientation="unspecified"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:theme="@style/Theme.KenjinxAndroid">
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||
// Stabilizer-State
|
||||
private var stabilizerActive = false
|
||||
|
||||
// last known Android rotation (0,1,2,3)
|
||||
private var lastRotation: Int? = null
|
||||
|
||||
var currentSurface: Long = -1
|
||||
private set
|
||||
|
||||
|
|
@ -66,8 +69,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||
val sizeChanged = (_width != width || _height != height)
|
||||
|
||||
if (sizeChanged) {
|
||||
// Requery Surface / Window handle and report to C#
|
||||
currentSurface = _nativeWindow.requeryWindowHandle()
|
||||
_nativeWindow.swapInterval = 0
|
||||
try { KenjinxNative.deviceSetWindowHandle(currentWindowHandle) } catch (_: Throwable) {}
|
||||
}
|
||||
|
||||
_width = width
|
||||
|
|
@ -77,7 +82,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||
start(holder)
|
||||
|
||||
// Do not set size immediately → Stabilizer takes over
|
||||
startStabilizedResize(expectedRotation = null)
|
||||
startStabilizedResize(expectedRotation = lastRotation)
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
|
|
@ -107,11 +112,27 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||
val id = mainViewModel.physicalControllerManager?.connect()
|
||||
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) {
|
||||
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
|
||||
// ✅ Correct: Report current Android rotation directly to the native site
|
||||
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") {
|
||||
runGame()
|
||||
|
|
@ -182,17 +203,42 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
|
|||
}
|
||||
|
||||
/**
|
||||
* Wird von der Activity bei Rotations-/Layoutwechsel aufgerufen.
|
||||
* Reicht die aktuelle Rotation durch, damit wir ggf. Breite/Höhe tauschen können.
|
||||
* Called on the activity when the rotation/layout changes.
|
||||
* Detects 90°↔270° and immediately forces a NativeWindow query.
|
||||
*/
|
||||
fun onOrientationOrSizeChanged(rotation: Int? = null) {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Wait a moment until the surface has its final dimensions after rotation,
|
||||
* checks plausibility (portrait/landscape) and only then sets the size.
|
||||
*/
|
||||
private fun startStabilizedResize(expectedRotation: Int?) {
|
||||
if (_isClosed) return
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.sun.jna.Library
|
|||
import com.sun.jna.Native
|
||||
import org.kenjinx.android.viewmodels.GameInfo
|
||||
import java.util.Collections
|
||||
import android.view.Surface
|
||||
|
||||
interface KenjinxNativeJna : Library {
|
||||
fun deviceInitialize(
|
||||
|
|
@ -87,6 +88,12 @@ interface KenjinxNativeJna : Library {
|
|||
fun userGetAllUsers(): Array<String>
|
||||
fun deviceGetDlcContentList(path: String, titleId: Long): Array<String>
|
||||
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(
|
||||
|
|
@ -112,14 +119,34 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
|
|||
|
||||
@JvmStatic
|
||||
fun updateProgress(infoPtr: Long, progress: Float) {
|
||||
// Get string from native pointer and push into progress overlay
|
||||
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.
|
||||
// No-Op: Placeholder – Hook if needed.
|
||||
}
|
||||
|
||||
@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.
|
||||
* Signature exactly matches the C# call in AndroidUIHandler.cs / Interop.UpdateUiHandler(...).
|
||||
* Variant B (strings directly). Used by newer JNI/interop paths.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun uiHandlerUpdate(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import android.content.Intent
|
|||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -20,6 +22,7 @@ import androidx.core.view.WindowCompat
|
|||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.sun.jna.JNIEnv
|
||||
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.GameModel
|
||||
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() {
|
||||
private var physicalControllerManager: PhysicalControllerManager =
|
||||
|
|
@ -34,17 +43,105 @@ class MainActivity : BaseActivity() {
|
|||
private lateinit var motionSensorManager: MotionSensorManager
|
||||
private var _isInit: Boolean = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val delayedHandleIntent = object : Runnable {
|
||||
override fun run() {
|
||||
handleIntent()
|
||||
}
|
||||
}
|
||||
private val delayedHandleIntent = object : Runnable { override fun run() { handleIntent() } }
|
||||
var storedIntent: Intent = Intent()
|
||||
var isGameRunning = false
|
||||
var isActive = false
|
||||
var storageHelper: SimpleStorageHelper? = null
|
||||
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 {
|
||||
var mainViewModel: MainViewModel? = null
|
||||
var AppPath: String = ""
|
||||
|
|
@ -79,7 +176,6 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
private fun initialize() {
|
||||
if (_isInit) return
|
||||
|
||||
val appPath: String = AppPath
|
||||
|
||||
var quickSettings = QuickSettings(this)
|
||||
|
|
@ -114,7 +210,6 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
AppPath = this.getExternalFilesDir(null)!!.absolutePath
|
||||
|
||||
initialize()
|
||||
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
|
|
@ -127,15 +222,16 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||
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()
|
||||
displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
|
||||
mainViewModel = MainViewModel(this)
|
||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||
|
||||
mainViewModel!!.refreshFirmwareVersion()
|
||||
|
||||
mainViewModel?.apply {
|
||||
|
|
@ -152,6 +248,9 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -171,9 +270,7 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
event.apply {
|
||||
if (physicalControllerManager.onKeyEvent(this)) return true
|
||||
}
|
||||
event.apply { if (physicalControllerManager.onKeyEvent(this)) return true }
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
|
|
@ -185,9 +282,7 @@ class MainActivity : BaseActivity() {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
isActive = false
|
||||
if (isGameRunning) {
|
||||
mainViewModel?.performanceManager?.setTurboMode(false)
|
||||
}
|
||||
if (isGameRunning) mainViewModel?.performanceManager?.setTurboMode(false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -195,21 +290,24 @@ class MainActivity : BaseActivity() {
|
|||
// Reapply alignment if necessary
|
||||
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)
|
||||
isActive = true
|
||||
|
||||
if (isGameRunning) {
|
||||
if (QuickSettings(this).enableMotion) motionSensorManager.register()
|
||||
}
|
||||
if (isGameRunning && QuickSettings(this).enableMotion) motionSensorManager.register()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
isActive = false
|
||||
if (isGameRunning) {
|
||||
mainViewModel?.performanceManager?.setTurboMode(false)
|
||||
}
|
||||
if (isGameRunning) mainViewModel?.performanceManager?.setTurboMode(false)
|
||||
motionSensorManager.unregister()
|
||||
try { displayManager.unregisterDisplayListener(displayListener) } catch (_: Throwable) {}
|
||||
}
|
||||
|
||||
private fun handleIntent() {
|
||||
|
|
@ -224,7 +322,6 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
if (documentFile != null) {
|
||||
val gameModel = GameModel(documentFile, this)
|
||||
|
||||
gameModel.getGameInfo()
|
||||
mainViewModel?.loadGameModel?.value = gameModel
|
||||
mainViewModel?.bootPath?.value = "gameItem_${gameModel.titleName}"
|
||||
|
|
@ -238,6 +335,13 @@ class MainActivity : BaseActivity() {
|
|||
private fun applyOrientationPreference() {
|
||||
val pref = QuickSettings(this).orientationPreference
|
||||
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() {
|
||||
|
|
@ -245,7 +349,6 @@ class MainActivity : BaseActivity() {
|
|||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
val componentName = intent?.component
|
||||
val restartIntent = Intent.makeRestartActivityTask(componentName)
|
||||
|
||||
mainViewModel?.let { it.performanceManager?.setTurboMode(false) }
|
||||
startActivity(restartIntent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.kenjinx.android.views
|
|||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -43,7 +44,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -259,11 +259,21 @@ class SettingViews {
|
|||
selectedOrientation = orientationPref.value,
|
||||
onOrientationSelected = { sel ->
|
||||
orientationPref.value = sel
|
||||
// Save and use immediately
|
||||
val qs = QuickSettings(mainViewModel.activity)
|
||||
qs.orientationPreference = sel
|
||||
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 _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; }
|
||||
|
||||
[DllImport("libkenjinxjni")]
|
||||
|
|
@ -310,6 +318,30 @@ namespace LibKenjinx
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +354,9 @@ namespace LibKenjinx
|
|||
[UnmanagedCallersOnly(EntryPoint = "graphicsRendererSetSize")]
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -560,6 +594,100 @@ namespace LibKenjinx
|
|||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue