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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Fable 5.1 committed 2026-10-08 21:17:49 +02:00
1 parent c7a2cb6f63
commit d0e45b94d9
6 files changed
+52 -10

No files matched your search

+3 -1
View File
@@ -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);
+1 -1
View File
@@ -53,7 +53,7 @@
<meta-data android:name="android.max_aspect" android:value="2.4" />
<activity
android:name=".PpssppActivity"
android:configChanges="locale|keyboard|keyboardHidden|navigation|uiMode"
android:configChanges="locale|keyboard|keyboardHidden|navigation|uiMode|orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/ppsspp_style"
android:launchMode="singleInstance"
android:resizeableActivity="true"
+10 -4
View File
@@ -940,8 +940,14 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) {
INFO_LOG(Log::System, "NativeApp.shutdown() -- end");
}
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint pixel_xres, jint pixel_yres, jint format) {
INFO_LOG(Log::System, "NativeApp.backbufferResize(%d x %d)", pixel_xres, pixel_yres);
extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint pixel_xres, jint pixel_yres, jint format, jint rotation) {
INFO_LOG(Log::System, "NativeApp.backbufferResize(%d x %d, rotation %d)", pixel_xres, pixel_yres, rotation);
// The display's Surface.ROTATION_*. A 180 degree flip changes this but not the size, and the swapchain
// still needs recreating, for the pre-rotation.
static int lastRotation = -1;
const bool rotated = lastRotation != -1 && rotation != lastRotation;
lastRotation = rotation;
int old_w = g_display.pixel_xres;
int old_h = g_display.pixel_yres;
@@ -966,8 +972,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv
INFO_LOG(Log::G3D, "RecalcDPI: g_dpi=%d scaled_dpi_x=%f scaled_dpi_y=%f display_scale_x=%f display_scale_y=%f g_dpi_scale_x=%f g_dpi_scale_y=%f dp_xres=%d dp_yres=%d",
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) {
INFO_LOG(Log::G3D, "Size change detected (previously %d,%d) - calling NativeResized()", old_w, old_h);
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);
NativeResized();
} else {
INFO_LOG(Log::G3D, "NativeApp::backbufferResize: Size didn't change.");
+3 -1
View File
@@ -41,7 +41,9 @@ public class NativeApp {
public static native int getDesiredBackbufferHeight();
public static native void setDisplayParameters(int display_xres, int display_yres, int dpi, float refreshRate);
public static native void backbufferResize(int bufferWidth, int bufferHeight, int format);
// rotation is the Surface.ROTATION_* of the display. A change in it alone (a 180 degree flip) keeps the size
// but the swapchain still has to be recreated, for the pre-rotation.
public static native void backbufferResize(int bufferWidth, int bufferHeight, int format, int rotation);
public static native boolean isLandscape();
public static native boolean isAtTopLevel();
@@ -111,6 +111,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
// switched-away from or rotated etc.
private boolean shuttingDown;
// surfaceChanged now also fires for rotations and resizes, where the thread just keeps going.
private boolean renderLoopRunning = false;
// Allow for multiple connected gamepads but just consider them the same for now.
// Actually this is not entirely true, see the code.
private final ArrayList<InputDeviceState> 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;
}
+23 -1
View File
@@ -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 {