sensor landscape fix

This commit is contained in:
BeZide93 2025-09-24 17:39:57 -05:00 committed by KeatonTheBot
parent 09bc10b15d
commit c42663115b
6 changed files with 356 additions and 42 deletions

View file

@ -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">

View file

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

View file

@ -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(

View file

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

View file

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

View file

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