From d0e45b94d940bf15083e0b9fc820cf703ebe8a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 22 Sep 2026 10:16:11 -0600 Subject: [PATCH 1/4] Android: Handle rotation and window resizing without recreating the activity Every rotation, fold or window resize destroyed the activity and with it all of graphics, then built it all up again. Declare those config changes handled, so they arrive as a surfaceChanged and go through the same resize path as a desktop window resize. That path needed three things it didn't have on Android: - The GPU_DISPLAY_RESIZED message, left out for Android since 2020 when DeviceRestore covered every size change. - A fixed (hardware scaled) surface size is now recomputed on each in-place surface change, since surfaceCreated no longer runs on a rotation. - A 180 degree flip keeps the size but changes the surface transform, so the display rotation is passed along and a change in it is a resize too. Co-Authored-By: Claude Fable 5.1 --- UI/NativeApp.cpp | 4 +++- android/AndroidManifest.xml | 2 +- android/jni/app-android.cpp | 14 +++++++---- android/src/org/ppsspp/ppsspp/NativeApp.java | 4 +++- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 14 +++++++++-- .../src/org/ppsspp/ppsspp/SizeManager.java | 24 ++++++++++++++++++- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index ab2d522120..0bc223ee09 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1345,7 +1345,9 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_screenManager->resized(); // TODO: Move this to the GraphicsContext objects for each backend. -#if !PPSSPP_PLATFORM(WINDOWS) && !defined(ANDROID) + // (Android used to be left out here, since every size change there was a full graphics restart, + // and DeviceRestore covers it. Rotation and window resizing are in-place now.) +#if !PPSSPP_PLATFORM(WINDOWS) PSP_CoreParameter().pixelWidth = g_display.pixel_xres; PSP_CoreParameter().pixelHeight = g_display.pixel_yres; System_PostUIMessage(UIMessage::GPU_DISPLAY_RESIZED); diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index cb83fc8004..f715a4c4da 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -53,7 +53,7 @@ inputPlayers = new ArrayList<>(); @@ -898,10 +901,15 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList return; } + if (renderLoopRunning) { + Log.i(TAG, "startRenderLoopThread: already running, surface changed in place"); + return; + } + Log.w(TAG, "startRenderLoopThread: Starting thread"); applyFrameRate(mSurface, 60.0f); - runRenderLoop(mSurface); + renderLoopRunning = runRenderLoop(mSurface); } private synchronized void joinRenderLoopThread() { @@ -915,6 +923,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList // This will wait until the thread has exited. Log.i(TAG, "requestExitRenderLoop"); requestExitRenderLoop(); + renderLoopRunning = false; } void setupSystemUiCallback() { @@ -1085,7 +1094,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); - Log.i(TAG, "onConfigurationChanged"); + Log.i(TAG, "onConfigurationChanged: orientation=" + newConfig.orientation + " density=" + newConfig.densityDpi + + " screen=" + newConfig.screenWidthDp + "x" + newConfig.screenHeightDp + "dp"); if (m_hasNoNativeBinary) { return; } diff --git a/android/src/org/ppsspp/ppsspp/SizeManager.java b/android/src/org/ppsspp/ppsspp/SizeManager.java index 7e730cb594..585a2b9d0b 100644 --- a/android/src/org/ppsspp/ppsspp/SizeManager.java +++ b/android/src/org/ppsspp/ppsspp/SizeManager.java @@ -110,9 +110,31 @@ public class SizeManager implements SurfaceHolder.Callback { Log.i(TAG, "Surface changed. Resolution: " + width + "x" + height + " Format: " + format); // The window size might have changed (immersive mode, native fullscreen on some devices) - NativeApp.backbufferResize(width, height, format); updateDisplayMeasurements(); + // A rotation or a window resize no longer recreates the activity, so a fixed-size (hardware scaled) + // surface has to be re-fixed here for the new display size and aspect. The fixed size is what the + // surface will report, so only do it when it doesn't already match, or this would loop. + if (!holder.isCreating()) { + Point newDesired = new Point(); + getDesiredBackbufferSize(newDesired); + boolean fixed = desiredSize.x > 0 && desiredSize.y > 0; + boolean wantFixed = newDesired.x > 0 && newDesired.y > 0; + if (wantFixed && (newDesired.x != width || newDesired.y != height)) { + Log.i(TAG, "Display changed, re-fixing surface size to " + newDesired.x + " x " + newDesired.y); + desiredSize.set(newDesired.x, newDesired.y); + holder.setFixedSize(newDesired.x, newDesired.y); + return; // Another surfaceChanged is coming, with the new size. + } else if (!wantFixed && fixed) { + Log.i(TAG, "Display changed, surface no longer needs a fixed size"); + desiredSize.set(0, 0); + holder.setSizeFromLayout(); + return; + } + } + + NativeApp.backbufferResize(width, height, format, activity.getWindowManager().getDefaultDisplay().getRotation()); + if (!paused) { activity.notifySurface(holder.getSurface()); } else { From be6ff0bc9b11e3bc8773e225309dd162b371a953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 22 Sep 2026 10:20:40 -0600 Subject: [PATCH 2/4] Android: Trim SizeManager down to the surface callback, rename to NativeSurfaceManager Now that rotation doesn't recreate the activity, the class is the SurfaceHolder.Callback plus the hardware scaling logic, and the rest had outlived its purpose: - The deferred-surface-while-paused workaround. Also what started the render thread twice on resume. - The "bad orientation" workaround, which called recreate(). - The deprecated system UI visibility listener, which only chose between getMetrics and getRealMetrics, and the view size overrode that whenever it mattered anyway. - The coalescing of display measurement updates, and updateDpi, which updateDisplayMeasurements does itself. Before the first layout the display size now comes from getRealMetrics. It only feeds the first-run config defaults. Co-Authored-By: Claude Fable 5.1 --- .../ppsspp/ppsspp/NativeSurfaceManager.java | 122 ++++++++++ .../src/org/ppsspp/ppsspp/PpssppActivity.java | 45 +--- .../src/org/ppsspp/ppsspp/SizeManager.java | 214 ------------------ 3 files changed, 131 insertions(+), 250 deletions(-) create mode 100644 android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java delete mode 100644 android/src/org/ppsspp/ppsspp/SizeManager.java diff --git a/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java b/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java new file mode 100644 index 0000000000..1a48843a17 --- /dev/null +++ b/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java @@ -0,0 +1,122 @@ +package org.ppsspp.ppsspp; + +import android.graphics.Point; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +// Keeps the native side informed of the display and surface size, and applies the hardware +// scaling (fixed surface size) setting. +public class NativeSurfaceManager implements SurfaceHolder.Callback { + private static final String TAG = "PPSSPPSurfaceManager"; + + final PpssppActivity activity; + SurfaceView surfaceView = null; + + private float densityDpi; + + // The fixed surface size we last asked for, or 0,0 for "size from layout". + private final Point desiredSize = new Point(); + + public NativeSurfaceManager(final PpssppActivity a) { + activity = a; + } + + public void setSurfaceView(SurfaceView view) { + surfaceView = view; + if (surfaceView == null) + return; + + surfaceView.getHolder().addCallback(this); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + int pixelWidth = holder.getSurfaceFrame().width(); + int pixelHeight = holder.getSurfaceFrame().height(); + Display display = activity.getWindowManager().getDefaultDisplay(); + + Log.d(TAG, "Surface created. pixelWidth=" + pixelWidth + ", pixelHeight=" + pixelHeight + " holder: " + holder + " " + display.getRefreshRate() + "Hz"); + NativeApp.setDisplayParameters(pixelWidth, pixelHeight, (int)densityDpi, display.getRefreshRate()); + getDesiredBackbufferSize(desiredSize); + + // Note that desiredSize might be 0,0 here - but that's fine when calling setFixedSize! It means auto. + if (desiredSize.x == 0) { + Log.d(TAG, "Setting auto surface size (not fixed)"); + } else { + Log.d(TAG, "Setting fixed surface size " + desiredSize.x + " x " + desiredSize.y); + } + holder.setFixedSize(desiredSize.x, desiredSize.y); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.v(TAG, "surfaceChanged: isCreating:" + holder.isCreating() + " holder: " + holder); + if (holder.isCreating() && desiredSize.x > 0 && desiredSize.y > 0) { + // We have called setFixedSize which will trigger another surfaceChanged after the initial + // one. This one is the original one, and we don't care about it. + Log.w(TAG, "holder.isCreating = true, ignoring. width=" + width + " height=" + height + " desWidth=" + desiredSize.x + " desHeight=" + desiredSize.y); + return; + } + + Log.i(TAG, "Surface changed. Resolution: " + width + "x" + height + " Format: " + format); + // The window size might have changed (rotation, immersive mode, multi-window) + updateDisplayMeasurements(); + + // A rotation or a window resize doesn't recreate the activity, so a fixed-size (hardware scaled) + // surface has to be re-fixed here for the new display size and aspect. The fixed size is what the + // surface will report, so only do it when it doesn't already match, or this would loop. + if (!holder.isCreating()) { + Point newDesired = new Point(); + getDesiredBackbufferSize(newDesired); + boolean fixed = desiredSize.x > 0 && desiredSize.y > 0; + boolean wantFixed = newDesired.x > 0 && newDesired.y > 0; + if (wantFixed && (newDesired.x != width || newDesired.y != height)) { + Log.i(TAG, "Display changed, re-fixing surface size to " + newDesired.x + " x " + newDesired.y); + desiredSize.set(newDesired.x, newDesired.y); + holder.setFixedSize(newDesired.x, newDesired.y); + return; // Another surfaceChanged is coming, with the new size. + } else if (!wantFixed && fixed) { + Log.i(TAG, "Display changed, surface no longer needs a fixed size"); + desiredSize.set(0, 0); + holder.setSizeFromLayout(); + return; + } + } + + NativeApp.backbufferResize(width, height, format, activity.getWindowManager().getDefaultDisplay().getRotation()); + activity.notifySurface(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + activity.notifySurface(null); + + // Autosize the next created surface. + holder.setSizeFromLayout(); + } + + public void updateDisplayMeasurements() { + Display display = activity.getWindowManager().getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + + // Early in startup, we don't have a view to query, and the display size is the best we have. + // It's only used for config default heuristics then. Later on, we have the exact size. + if (surfaceView != null && surfaceView.getWidth() > 0) { + metrics.widthPixels = surfaceView.getWidth(); + metrics.heightPixels = surfaceView.getHeight(); + } + densityDpi = metrics.densityDpi; + + NativeApp.setDisplayParameters(metrics.widthPixels, metrics.heightPixels, (int)densityDpi, display.getRefreshRate()); + } + + private void getDesiredBackbufferSize(Point sz) { + NativeApp.computeDesiredBackbufferDimensions(); + sz.x = NativeApp.getDesiredBackbufferWidth(); + sz.y = NativeApp.getDesiredBackbufferHeight(); + } +} diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 887a4c8e4f..fcb25ea7f6 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -42,7 +42,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceView; -import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; @@ -100,8 +99,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList private boolean sustainedPerfSupported; - private View navigationCallbackView = null; - // audioFocusChangeListener to listen to changes in audio state private AudioFocusChangeListener audioFocusChangeListener; private AudioManager audioManager; @@ -119,7 +116,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList private final ArrayList inputPlayers = new ArrayList<>(); private PowerSaveModeReceiver mPowerSaveModeReceiver = null; - private SizeManager sizeManager = null; + private NativeSurfaceManager surfaceManager = null; private static LocationHelper mLocationHelper; private static InfraredHelper mInfraredHelper; private static CameraHelper mCameraHelper; @@ -558,7 +555,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList return; } - // WARNING: when adding new modes here, check SizeManager's workaround in surfaceCreated.j + // WARNING: when adding new modes here, check NativeSurfaceManager.surfaceCreated. int nativeRotation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; switch (rot) { @@ -635,7 +632,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList } } - sizeManager.checkDisplayMeasurements(); + surfaceManager.updateDisplayMeasurements(); } // Starts the native render loop thread, which owns the graphics context and its surface. @@ -683,7 +680,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList mSensorManager = (SensorManager)getSystemService(Activity.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - sizeManager = new SizeManager(this); + surfaceManager = new NativeSurfaceManager(this); TextRenderer.init(this); shuttingDown = false; registerCallbacks(); @@ -691,7 +688,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList // This calls NativeApp.setDisplayParameters. Make sure that's done early in order // to be able to set defaults when loading config for the first time. Like figuring out // whether to start at 1x or 2x. - sizeManager.updateDisplayMeasurements(); + surfaceManager.updateDisplayMeasurements(); // On the first run, the shortcut parameter is passed to NativeApp.init in here. final boolean firstRun = !initialized; @@ -716,7 +713,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList updateSystemUiVisibility(); mSurfaceView = new NativeSurfaceView(this); - sizeManager.setSurfaceView(mSurfaceView); + surfaceManager.setSurfaceView(mSurfaceView); setInsetsListener(mSurfaceView); setContentView(mSurfaceView); @@ -926,16 +923,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList renderLoopRunning = false; } - void setupSystemUiCallback() { - final View decorView = getWindow().peekDecorView(); - if (decorView == null || decorView == navigationCallbackView) { - return; - } - - sizeManager.setupSystemUiCallback(decorView); - navigationCallbackView = decorView; - } - @Override protected void onDestroy() { super.onDestroy(); @@ -954,7 +941,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList audioFocusChangeListener = null; audioManager = null; - sizeManager.setSurfaceView(null); + surfaceManager.setSurfaceView(null); if (mPowerSaveModeReceiver != null) { mPowerSaveModeReceiver.destroy(this); mPowerSaveModeReceiver = null; @@ -980,8 +967,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList Log.i(TAG, "in onDestroy, but not shutting down."); } } - navigationCallbackView = null; - // Really ugly workaround for VR issues when PPSSPP restarts if (isVRDevice()) { System.exit(0); @@ -1020,7 +1005,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList mSensorManager.unregisterListener(this); loseAudioFocus(this.audioManager, this.audioFocusChangeListener); - sizeManager.onPause(); Log.i(TAG, "Calling NativeApp.pause..."); NativeApp.pause(); if (mCameraHelper != null) { @@ -1042,7 +1026,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList Log.i(TAG, "onResume begin"); updateSustainedPerformanceMode(); - sizeManager.onResume(); updateSystemUiVisibility(); // OK, config should be initialized, we can query for screen rotation. @@ -1081,16 +1064,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList NativeApp.accelerometer(event.values[0], event.values[1], event.values[2]); } - @Override - public void onAttachedToWindow() { - Log.i(TAG, "onAttachedToWindow"); - super.onAttachedToWindow(); - if (m_hasNoNativeBinary) { - return; - } - setupSystemUiCallback(); - } - @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -1099,8 +1072,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList if (m_hasNoNativeBinary) { return; } + // The surface size change that comes with it is what actually updates the native side. updateSystemUiVisibility(); - sizeManager.updateDpi((float)newConfig.densityDpi); } @Override @@ -1111,7 +1084,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList if (m_hasNoNativeBinary) { return; } - sizeManager.checkDisplayMeasurements(); + surfaceManager.updateDisplayMeasurements(); } // keep this static so we can call this even if we don't diff --git a/android/src/org/ppsspp/ppsspp/SizeManager.java b/android/src/org/ppsspp/ppsspp/SizeManager.java deleted file mode 100644 index 585a2b9d0b..0000000000 --- a/android/src/org/ppsspp/ppsspp/SizeManager.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.ppsspp.ppsspp; - -import android.content.pm.ActivityInfo; -import android.graphics.Point; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Display; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; - -public class SizeManager implements SurfaceHolder.Callback { - private static final String TAG = "PPSSPPSizeManager"; - - final PpssppActivity activity; - SurfaceView surfaceView = null; - - private float densityDpi; - private float refreshRate; - - private boolean navigationHidden = false; - private boolean displayUpdatePending = false; - - private final Point desiredSize = new Point(); - private int badOrientationCount = 0; - - // Used to fix a race condition on some Android versions. - private Surface earlySurface = null; - - private boolean paused = false; - - public SizeManager(final PpssppActivity a) { - activity = a; - } - - public void onResume() { - if (earlySurface != null) { - Log.i(TAG, "Applying deferred surface"); - activity.notifySurface(earlySurface); - earlySurface = null; - } - paused = false; - } - - public void onPause() { - paused = true; - // Make sure. - earlySurface = null; - } - - public void setSurfaceView(SurfaceView view) { - surfaceView = view; - if (surfaceView == null) - return; - - surfaceView.getHolder().addCallback(this); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - int pixelWidth = holder.getSurfaceFrame().width(); - int pixelHeight = holder.getSurfaceFrame().height(); - - // Workaround for terrible bug when locking and unlocking the screen in landscape mode on Nexus 5X. - // TODO: Look into removing this. - int requestedOr = activity.getRequestedOrientation(); - boolean requestedPortrait = requestedOr == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOr == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; - boolean detectedPortrait = pixelHeight > pixelWidth; - if (badOrientationCount < 3 && requestedPortrait != detectedPortrait && requestedOr != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED && requestedOr != ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE && requestedOr != ActivityInfo.SCREEN_ORIENTATION_SENSOR) { - Log.e(TAG, "Bad orientation detected (w=" + pixelWidth + " h=" + pixelHeight + "! Recreating activity."); - badOrientationCount++; - activity.recreate(); - return; - } else if (requestedPortrait == detectedPortrait) { - Log.i(TAG, "Correct orientation detected, resetting orientation counter."); - badOrientationCount = 0; - } else { - Log.i(TAG, "Bad orientation detected but ignored"); - } - - Display display = activity.getWindowManager().getDefaultDisplay(); - - refreshRate = display.getRefreshRate(); - - Log.d(TAG, "Surface created. pixelWidth=" + pixelWidth + ", pixelHeight=" + pixelHeight + " holder: " + holder + " or: " + requestedOr + " " + refreshRate + "Hz"); - NativeApp.setDisplayParameters(pixelWidth, pixelHeight, (int)densityDpi, refreshRate); - getDesiredBackbufferSize(desiredSize); - - // Note that desiredSize might be 0,0 here - but that's fine when calling setFixedSize! It means auto. - if (desiredSize.x == 0) { - Log.d(TAG, "Setting auto surface size (not fixed)"); - } else { - Log.d(TAG, "Setting fixed surface size " + desiredSize.x + " x " + desiredSize.y); - } - holder.setFixedSize(desiredSize.x, desiredSize.y); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Log.v(TAG, "surfaceChanged: isCreating:" + holder.isCreating() + " holder: " + holder); - if (holder.isCreating() && desiredSize.x > 0 && desiredSize.y > 0) { - // We have called setFixedSize which will trigger another surfaceChanged after the initial - // one. This one is the original one, and we don't care about it. - Log.w(TAG, "holder.isCreating = true, ignoring. width=" + width + " height=" + height + " desWidth=" + desiredSize.x + " desHeight=" + desiredSize.y); - - // TODO: Should we still set earlySurface here, to be sure? - return; - } - - Log.i(TAG, "Surface changed. Resolution: " + width + "x" + height + " Format: " + format); - // The window size might have changed (immersive mode, native fullscreen on some devices) - updateDisplayMeasurements(); - - // A rotation or a window resize no longer recreates the activity, so a fixed-size (hardware scaled) - // surface has to be re-fixed here for the new display size and aspect. The fixed size is what the - // surface will report, so only do it when it doesn't already match, or this would loop. - if (!holder.isCreating()) { - Point newDesired = new Point(); - getDesiredBackbufferSize(newDesired); - boolean fixed = desiredSize.x > 0 && desiredSize.y > 0; - boolean wantFixed = newDesired.x > 0 && newDesired.y > 0; - if (wantFixed && (newDesired.x != width || newDesired.y != height)) { - Log.i(TAG, "Display changed, re-fixing surface size to " + newDesired.x + " x " + newDesired.y); - desiredSize.set(newDesired.x, newDesired.y); - holder.setFixedSize(newDesired.x, newDesired.y); - return; // Another surfaceChanged is coming, with the new size. - } else if (!wantFixed && fixed) { - Log.i(TAG, "Display changed, surface no longer needs a fixed size"); - desiredSize.set(0, 0); - holder.setSizeFromLayout(); - return; - } - } - - NativeApp.backbufferResize(width, height, format, activity.getWindowManager().getDefaultDisplay().getRotation()); - - if (!paused) { - activity.notifySurface(holder.getSurface()); - } else { - earlySurface = holder.getSurface(); - Log.i(TAG, "Skipping notifySurface while paused - deferring to resume"); - } - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - // Don't hand a dead surface over on resume. - earlySurface = null; - activity.notifySurface(null); - - // Autosize the next created surface. - holder.setSizeFromLayout(); - } - - public void checkDisplayMeasurements() { - if (displayUpdatePending) { - return; - } - displayUpdatePending = true; - - activity.runOnUiThread(() -> { - Log.d(TAG, "checkDisplayMeasurements: checking now"); - updateDisplayMeasurements(); - }); - } - - public void updateDisplayMeasurements() { - displayUpdatePending = false; - - Display display = activity.getWindowManager().getDefaultDisplay(); - // Early in startup, we don't have a view to query. Do our best to get some kind of size - // that can be used by config default heuristics, and so on. - DisplayMetrics metrics = new DisplayMetrics(); - if (navigationHidden) { - display.getRealMetrics(metrics); - } else { - display.getMetrics(metrics); - } - - // Later on, we have the exact pixel size so let's just use it. - if (surfaceView != null) { - metrics.widthPixels = surfaceView.getWidth(); - metrics.heightPixels = surfaceView.getHeight(); - } - densityDpi = metrics.densityDpi; - refreshRate = display.getRefreshRate(); - - NativeApp.setDisplayParameters(metrics.widthPixels, metrics.heightPixels, (int)densityDpi, refreshRate); - } - - public void setupSystemUiCallback(final View view) { - view.setOnSystemUiVisibilityChangeListener(visibility -> { - // Called when the system UI's visibility changes, regardless of - // whether it's because of our or system actions. - // We will try to force it to follow our preference but will not stupidly - // act as if it's visible if it's not. - navigationHidden = ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0); - // TODO: Check here if it's the state we want. - Log.i(TAG, "SystemUiVisibilityChange! visibility=" + visibility + " navigationHidden: " + navigationHidden + " decorView: " + view.getWidth() + "x" + view.getHeight()); - checkDisplayMeasurements(); - }); - } - - public void updateDpi(float dpi) { - densityDpi = dpi; - } - - private void getDesiredBackbufferSize(Point sz) { - NativeApp.computeDesiredBackbufferDimensions(); - sz.x = NativeApp.getDesiredBackbufferWidth(); - sz.y = NativeApp.getDesiredBackbufferHeight(); - } -} From 114512774d6ab3e3ead2cc0e0c03d35d72fb8ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 22 Sep 2026 10:23:29 -0600 Subject: [PATCH 3/4] Android: Keep the native smallestScreenWidthDp current across config changes It was passed once at init, and decides whether we can restrict the orientation on Android 17+ large screens. Now that a fold or unfold no longer recreates the activity, forward the new value from onConfigurationChanged. Untested on a large screen device. Co-Authored-By: Claude Fable 5.1 --- android/jni/app-android.cpp | 3 +++ android/src/org/ppsspp/ppsspp/PpssppActivity.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index f5a8d3af28..777b3a0ecc 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -1327,6 +1327,9 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNI System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm); } else if (msg == "sustained_perf_supported") { sustainedPerfSupported = true; + } else if (msg == "smallestScreenWidthDp") { + // Changes on fold/unfold. Decides SYSPROP_CAN_RESTRICT_ORIENTATION. + smallestScreenWidthDp = (int)parseLong(prm); } else if (msg == "safe_insets") { // INFO_LOG(Log::System, "Got insets: %s", prm.c_str()); // We don't bother with supporting exact rectangular regions. Safe insets are good enough. diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index fcb25ea7f6..d1c1ce07a0 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1072,7 +1072,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList if (m_hasNoNativeBinary) { return; } - // The surface size change that comes with it is what actually updates the native side. + // The surface size change that comes with it is what actually updates the native side, except + // for this, which the native side was told once at init. Changes on fold/unfold. + NativeApp.sendMessageFromJava("smallestScreenWidthDp", String.valueOf(newConfig.smallestScreenWidthDp)); updateSystemUiVisibility(); } From a2869a47cd7eeeac365ffd19cdf1df3d5029eada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 21:51:35 +0200 Subject: [PATCH 4/4] Android: Re-report the surface when the display density changes Now that density is in configChanges, a density change no longer recreates the activity - but it also doesn't resize the surface, so no surfaceChanged follows it. The DPI is only recalculated in backbufferResize, so the UI kept the old scale until the next real resize or a trip through the background. Re-report the surface we already have when the density has moved. The native side needs no change: Recalculate() already returns true when the dp size changes, which a density change does on its own. Also make the log line say which size changed, since the pixel size now stays put in this case. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC --- android/jni/app-android.cpp | 6 +++- .../ppsspp/ppsspp/NativeSurfaceManager.java | 31 ++++++++++++++++++- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 3 ++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 777b3a0ecc..5b6f4c50cd 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -951,6 +951,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv int old_w = g_display.pixel_xres; int old_h = g_display.pixel_yres; + int old_dp_w = g_display.dp_xres; + int old_dp_h = g_display.dp_yres; // pixel_*res is the backbuffer resolution. backbuffer_format = format; @@ -973,7 +975,9 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv display_dpi, dpi_x, dpi_y, display_scale_x, display_scale_y, g_display.dpi_scale_x, g_display.dpi_scale_y, g_display.dp_xres, g_display.dp_yres); if (new_size || rotated) { - INFO_LOG(Log::G3D, "%s detected (previously %d,%d) - calling NativeResized()", new_size ? "Size change" : "Rotation change", old_w, old_h); + // Note that a density change moves the dp size while the pixel size stays put. + INFO_LOG(Log::G3D, "%s detected (previously %dx%d px, %dx%d dp) - calling NativeResized()", + new_size ? "Size change" : "Rotation change", old_w, old_h, old_dp_w, old_dp_h); NativeResized(); } else { INFO_LOG(Log::G3D, "NativeApp::backbufferResize: Size didn't change."); diff --git a/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java b/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java index 1a48843a17..5227de6e7a 100644 --- a/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java +++ b/android/src/org/ppsspp/ppsspp/NativeSurfaceManager.java @@ -20,6 +20,13 @@ public class NativeSurfaceManager implements SurfaceHolder.Callback { // The fixed surface size we last asked for, or 0,0 for "size from layout". private final Point desiredSize = new Point(); + // What we last told the native side about the surface, and the density we told it with. + // A density change has to re-report it, see densityChanged(). + private int reportedWidth; + private int reportedHeight; + private int reportedFormat; + private float reportedDensityDpi; + public NativeSurfaceManager(final PpssppActivity a) { activity = a; } @@ -86,18 +93,40 @@ public class NativeSurfaceManager implements SurfaceHolder.Callback { } } - NativeApp.backbufferResize(width, height, format, activity.getWindowManager().getDefaultDisplay().getRotation()); + reportSurfaceSize(width, height, format); activity.notifySurface(holder.getSurface()); } @Override public void surfaceDestroyed(SurfaceHolder holder) { activity.notifySurface(null); + reportedWidth = 0; + reportedHeight = 0; // Autosize the next created surface. holder.setSizeFromLayout(); } + private void reportSurfaceSize(int width, int height, int format) { + reportedWidth = width; + reportedHeight = height; + reportedFormat = format; + reportedDensityDpi = densityDpi; + NativeApp.backbufferResize(width, height, format, activity.getWindowManager().getDefaultDisplay().getRotation()); + } + + // A density change rescales the UI but leaves the surface size alone, so no surfaceChanged + // follows it to carry the new density to the native side - the DPI is only recalculated in + // backbufferResize. Re-report the surface we already have, or the UI keeps the old scale + // until the next real resize. Call after updateDisplayMeasurements has picked up the change. + public void densityChanged() { + if (reportedWidth <= 0 || densityDpi == reportedDensityDpi) { + return; + } + Log.i(TAG, "Density is now " + densityDpi + ", re-reporting surface " + reportedWidth + "x" + reportedHeight); + reportSurfaceSize(reportedWidth, reportedHeight, reportedFormat); + } + public void updateDisplayMeasurements() { Display display = activity.getWindowManager().getDefaultDisplay(); DisplayMetrics metrics = new DisplayMetrics(); diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index d1c1ce07a0..a8b159faf3 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1075,7 +1075,10 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList // The surface size change that comes with it is what actually updates the native side, except // for this, which the native side was told once at init. Changes on fold/unfold. NativeApp.sendMessageFromJava("smallestScreenWidthDp", String.valueOf(newConfig.smallestScreenWidthDp)); + // This re-measures the display, so the density check below sees the new value. updateSystemUiVisibility(); + // A density change doesn't resize the surface, so it needs separate handling. + surfaceManager.densityChanged(); } @Override