From 9e01fe2c2c4908bbd1fb058d9aeb212f1e9b4deb Mon Sep 17 00:00:00 2001 From: AverageConsumer <35539970+AverageConsumer@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:23:20 +0200 Subject: [PATCH] Fix secondary display lifecycle cleanup --- main.lua | 1 + .../java/org/love2d/android/GameActivity.java | 18 ++++++++--- tests/engine/android_host_extension_test.lua | 32 +++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/main.lua b/main.lua index b820766c..2c352977 100644 --- a/main.lua +++ b/main.lua @@ -327,6 +327,7 @@ local function returnToLauncher() if love.audio and love.audio.stop then pcall(love.audio.stop) end + pcall(function() require("src.render.SecondScreen").setEnabled(false) end) local GameVersion = require("src.core.GameVersion") local currentVersion = GameVersion.get() diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java index c484e900..e59fe574 100644 --- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java +++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java @@ -398,11 +398,15 @@ public class GameActivity extends SDLActivity { @Override protected void onDestroy() { + secondaryHostResumed = false; if (vibrator != null) { Log.d("GameActivity", "Cancelling vibration"); vibrator.cancel(); } unregisterSecondaryDisplayListener(); + teardownSecondaryDisplay(); + secondaryEnabled = false; + synchronized (secondaryFrameLock) { secondaryFrame = null; } unregisterAudioDeviceCallback(); abandonAudioFocus(); onHostDestroy(); @@ -411,6 +415,7 @@ public class GameActivity extends SDLActivity { @Override protected void onPause() { + secondaryHostResumed = false; if (vibrator != null) { Log.d("GameActivity", "Cancelling vibration"); vibrator.cancel(); @@ -426,6 +431,7 @@ public class GameActivity extends SDLActivity { @Override public void onResume() { super.onResume(); + secondaryHostResumed = true; onHostResume(); requestGameAudioFocus(); registerAudioDeviceCallback(); @@ -1933,6 +1939,7 @@ public class GameActivity extends SDLActivity { private static volatile int secondaryActivityTarget = Display.INVALID_DISPLAY; private static volatile long secondaryRetryAfter; private static volatile boolean secondaryEnabled = false; + private static volatile boolean secondaryHostResumed = false; private static volatile int secondaryTarget = SECONDARY_TARGET_AUTO; private static volatile int dualScreenDisplayMode = -1; private static volatile byte[] secondaryFrame; @@ -1963,7 +1970,7 @@ public class GameActivity extends SDLActivity { if (self == null) return; self.runOnUiThread(new Runnable() { @Override public void run() { - if (on) { + if (on && secondaryHostResumed) { self.refreshDualScreenDisplayMode(); self.registerSecondaryDisplayListener(); rebindSecondaryDisplay(); @@ -2035,9 +2042,11 @@ public class GameActivity extends SDLActivity { private static void rebindSecondaryDisplay() { GameActivity self = (GameActivity) mSingleton; - if (self == null || !secondaryEnabled || secondaryOutputIsPreferred(self)) return; + if (self == null || !secondaryHostResumed || !secondaryEnabled + || secondaryOutputIsPreferred(self)) return; self.runOnUiThread(() -> { - if (!secondaryEnabled || secondaryOutputIsPreferred(self)) return; + if (!secondaryHostResumed || !secondaryEnabled + || secondaryOutputIsPreferred(self)) return; teardownSecondaryDisplay(); setupSecondaryDisplay(); }); @@ -2045,7 +2054,8 @@ public class GameActivity extends SDLActivity { private static void setupSecondaryDisplay() { GameActivity self = (GameActivity) mSingleton; - if (self == null || !secondaryEnabled || secondaryPresentation != null + if (self == null || !secondaryHostResumed || !secondaryEnabled + || secondaryPresentation != null || secondaryActivity != null || secondaryActivityPending || android.os.SystemClock.elapsedRealtime() < secondaryRetryAfter) return; try { diff --git a/tests/engine/android_host_extension_test.lua b/tests/engine/android_host_extension_test.lua index aabef3f7..4033cbed 100644 --- a/tests/engine/android_host_extension_test.lua +++ b/tests/engine/android_host_extension_test.lua @@ -58,6 +58,38 @@ check(position("if (secondaryEnabled) registerSecondaryDisplayListener();") < "secondary display monitoring starts before initial discovery") check(source:find("!monitor.hasDisplay(display.getDisplayId())", 1, true), "a disconnected active display is rebound without replacing a live one") +check(source:find("private static volatile boolean secondaryHostResumed = false;", + 1, true), "secondary output tracks the primary activity lifecycle") +check(source:find("if (on && secondaryHostResumed)", 1, true) + and source:find("self == null || !secondaryHostResumed || !secondaryEnabled", + 1, true), + "paused hosts cannot reopen secondary output from a late mod frame") + +local pause = position("protected void onPause()") +local paused = assert(source:find("secondaryHostResumed = false;", pause, true)) +local teardown = assert(source:find("teardownSecondaryDisplay();", pause, true)) +local pauseSuper = assert(source:find("super.onPause();", pause, true)) +check(pause < paused and paused < teardown and teardown < pauseSuper, + "pause blocks secondary setup before dismissing its output") + +local resume = position("public void onResume()") +local resumeSuper = assert(source:find("super.onResume();", resume, true)) +local resumed = assert(source:find("secondaryHostResumed = true;", resume, true)) +local resumeSetup = assert(source:find("setupSecondaryDisplay();", resume, true)) +check(resume < resumeSuper and resumeSuper < resumed and resumed < resumeSetup, + "resume permits secondary setup only after the primary activity resumes") + +local destroy = position("protected void onDestroy()") +local destroyTeardown = assert(source:find("teardownSecondaryDisplay();", destroy, true)) +local destroySuper = assert(source:find("super.onDestroy();", destroy, true)) +check(destroy < destroyTeardown and destroyTeardown < destroySuper, + "destroy always dismisses secondary output before SDL destruction") + +local mainFile = assert(io.open("main.lua", "rb")) +local main = mainFile:read("*a") +mainFile:close() +check(main:find('require("src.render.SecondScreen").setEnabled(false)', 1, true), + "returning from a game disables mod-owned secondary output") check(not source:lower():find("openxr", 1, true), "generic Android activity must not require OpenXR")