From c42663115b3b40f37624cf12ed84df5f51628529 Mon Sep 17 00:00:00 2001 From: BeZide93 Date: Wed, 24 Sep 2025 17:39:57 -0500 Subject: [PATCH] sensor landscape fix --- .../app/src/main/AndroidManifest.xml | 3 +- .../main/java/org/kenjinx/android/GameHost.kt | 62 ++++++- .../java/org/kenjinx/android/KenjinxNative.kt | 36 ++++- .../java/org/kenjinx/android/MainActivity.kt | 151 +++++++++++++++--- .../org/kenjinx/android/views/SettingViews.kt | 16 +- src/LibKenjinx/Android/JniExportedMethods.cs | 130 ++++++++++++++- 6 files changed, 356 insertions(+), 42 deletions(-) diff --git a/src/KenjinxAndroid/app/src/main/AndroidManifest.xml b/src/KenjinxAndroid/app/src/main/AndroidManifest.xml index bb894bc57..a19981735 100644 --- a/src/KenjinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/KenjinxAndroid/app/src/main/AndroidManifest.xml @@ -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"> diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt index 1a63f88d5..14bb5852f 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameHost.kt @@ -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 diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt index 95739695a..8a7fdc3ac 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/KenjinxNative.kt @@ -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 fun deviceGetDlcContentList(path: String, titleId: Long): Array 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( diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt index 929210a0b..bf9ecfbdf 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt @@ -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) diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt index a2554f78f..56fb71601 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/SettingViews.kt @@ -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) } ) diff --git a/src/LibKenjinx/Android/JniExportedMethods.cs b/src/LibKenjinx/Android/JniExportedMethods.cs index 1bc13695a..9d64bdba2 100644 --- a/src/LibKenjinx/Android/JniExportedMethods.cs +++ b/src/LibKenjinx/Android/JniExportedMethods.cs @@ -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