From 5d945d7784248be6cd294c3ce830c56ef513bbd8 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:25:31 -0400 Subject: [PATCH] refactor: remove duplicate session frame limiter (#1616) --- .../droiddeck/launcher/FrontEndActivity.kt | 5 +- .../com/droiddeck/launcher/SessionActivity.kt | 3 +- .../launcher/glasses/GlassesRendering.kt | 6 -- .../launcher/session/SessionPrefs.kt | 18 ++---- .../launcher/session/SessionService.kt | 7 +-- .../launcher/session/SessionState.kt | 3 - .../launcher/ui/ModeSettingsDialog.kt | 4 -- app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values-b+es+419/strings.xml | 2 - app/src/main/res/values-bg/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 2 - app/src/main/res/values-da/strings.xml | 2 - app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-el/strings.xml | 2 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-fi/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values-hi/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-in/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-ko/strings.xml | 2 - app/src/main/res/values-ms/strings.xml | 2 - app/src/main/res/values-nb/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 2 - app/src/main/res/values-pl/strings.xml | 2 - app/src/main/res/values-pt-rPT/strings.xml | 2 - app/src/main/res/values-pt/strings.xml | 2 - app/src/main/res/values-ro/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-th/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-uk/strings.xml | 2 - app/src/main/res/values-vi/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values-zh-rHK/strings.xml | 2 - app/src/main/res/values-zh-rTW/strings.xml | 2 - app/src/main/res/values/strings.xml | 6 +- .../glasses/GlassesMirrorPacingTest.kt | 31 ++++++++--- .../launcher/glasses/GlassesRenderingTest.kt | 7 --- .../launcher/session/PerformanceModeTest.kt | 10 +--- .../launcher/session/SettingsResetTest.kt | 4 +- docs/development/game-environment.md | 2 +- .../overlay/usr/local/bin/droiddeck-session | 39 +------------ tools/tests/test_session_pacing.py | 55 +++++++++++++++++++ 47 files changed, 94 insertions(+), 170 deletions(-) create mode 100644 tools/tests/test_session_pacing.py diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index f7d514c49..1f754d0b7 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -392,7 +392,6 @@ class FrontEndActivity : ComponentActivity() { private var theme by mutableStateOf("graphite") private var appScale by mutableStateOf(com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE) private var hdrOn by mutableStateOf(false) - private var fpsLimit by mutableStateOf(0) private var performanceMode by mutableStateOf(false) private var upscaler by mutableStateOf(0) private var upscaleSharpness by mutableStateOf(75) @@ -1703,7 +1702,7 @@ class FrontEndActivity : ComponentActivity() { ModeSettings( mode = mode, resolution = resolution, panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(this), - hdr = hdrOn, hdrReason = hdrReason, fpsLimit = fpsLimit, performanceMode = performanceMode, + hdr = hdrOn, hdrReason = hdrReason, performanceMode = performanceMode, upscaler = upscaler, upscaleSharpness = upscaleSharpness, gpuDrivers = drivers.summary(), touchMode = touchMode, @@ -1769,7 +1768,6 @@ class FrontEndActivity : ComponentActivity() { onResolution = { value -> SessionPrefs.setResolutionChoice(this, mode, value); resolution = value }, onHdr = { on -> SessionPrefs.setHdr(this, mode, on); hdrOn = on }, onGpuDrivers = { openComponents(focusContent = true, tab = com.droiddeck.launcher.ui.GPU_TAB) }, - onFpsLimit = { fps -> SessionPrefs.setFpsLimit(this, mode, fps); fpsLimit = fps }, onPerformanceMode = { on -> SessionPrefs.setPerformanceMode(this, mode, on); performanceMode = on }, onUpscaler = { m -> SessionPrefs.setUpscaler(this, m); upscaler = m }, onUpscaleSharpness = { pct -> SessionPrefs.setUpscaleSharpness(this, pct); upscaleSharpness = pct }, @@ -2021,7 +2019,6 @@ class FrontEndActivity : ComponentActivity() { runSteamAtStartup = SessionPrefs.runSteamAtStartup(this) loadBootMovie() hdrOn = SessionPrefs.hdr(this, mode) - fpsLimit = SessionPrefs.fpsLimit(this, mode) performanceMode = SessionPrefs.performanceMode(this, mode) upscaler = SessionPrefs.upscaler(this) upscaleSharpness = SessionPrefs.upscaleSharpness(this) diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 2d6970c4f..2f9bcf190 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -492,7 +492,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (!SessionState.running) { val prefMode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) - SessionState.fpsLimit = SessionPrefs.fpsLimit(this, prefMode) SessionState.performanceMode = SessionPrefs.performanceMode(this, prefMode) } applyOutputPacing() @@ -1976,7 +1975,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { GlassesRendering.mirrorRefreshLimit(glassesSettings, glasses, glassesScreen != null) private fun outputFpsLimit(): Int = SessionPrefs.compositorFpsLimit( - GlassesRendering.fpsLimit(SessionState.fpsLimit, mirrorRefreshLimit()), SessionState.performanceMode, + mirrorRefreshLimit(), SessionState.performanceMode, ) private fun applyOutputPacing() { diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt index f8d6f4c5f..b07221556 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt @@ -32,12 +32,6 @@ internal object GlassesRendering { return if (hz == 30 && kotlin.math.abs(state.hz - 30f) >= 0.5f) 60 else hz } - fun fpsLimit(requested: Int, mirrorRefreshLimit: Int): Int = when { - mirrorRefreshLimit <= 0 -> requested - requested <= 0 -> mirrorRefreshLimit - else -> minOf(requested, mirrorRefreshLimit) - } - fun usesGlasses(state: GlassesState, outputActive: Boolean, steamVrRunning: Boolean): Boolean = outputActive || (steamVrRunning && state.usable) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 48cad1918..95e867f8a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -903,19 +903,8 @@ object SessionPrefs { prefs(context).edit().putBoolean("hdr.$mode", on).apply() } - fun fpsLimit(context: Context, mode: String): Int = prefs(context).getInt("fpsLimit.$mode", 0).coerceAtLeast(0) - - fun setFpsLimit(context: Context, mode: String, fps: Int) { - prefs(context).edit().putInt("fpsLimit.$mode", fps.coerceAtLeast(0)).apply() - } - - val fpsLimitChoices = listOf(0 to "Off", 30 to "30", 40 to "40", 45 to "45", 60 to "60", 90 to "90", 120 to "120") - - fun fpsLimitChoices(context: Context): List> = - fpsLimitChoices.map { (fps, label) -> fps to if (fps == 0) context.getString(R.string.frame_gen_off) else label } - /** - * Performance mode: without a frame limit, buffers go back to the game as soon as they are replaced + * Performance mode: buffers go back to the game as soon as they are replaced * instead of once per refresh, and gamescope's WSI layer and winex11 reuse their per-present window checks. */ fun performanceMode(context: Context, mode: String): Boolean = prefs(context).getBoolean("performanceMode.$mode", false) @@ -927,8 +916,9 @@ object SessionPrefs { /** Compositor frame limit that releases buffers to the game unpaced. */ const val FPS_UNPACED = -1 - /** The limit handed to the compositor: Performance mode without a limit is unpaced, a chosen limit still paces. */ - fun compositorFpsLimit(limit: Int, performanceMode: Boolean): Int = if (performanceMode && limit <= 0) FPS_UNPACED else limit + /** Mirroring still follows the glasses' refresh rate when Performance mode is on. */ + fun compositorFpsLimit(mirrorRefreshLimit: Int, performanceMode: Boolean): Int = + if (performanceMode && mirrorRefreshLimit <= 0) FPS_UNPACED else mirrorRefreshLimit val upscalerChoices = listOf( 0 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 3 to "Snapdragon GSR", diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index c81c1831c..0b7f32db1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -478,19 +478,16 @@ class SessionService : Service() { guest.add("DXVK_HDR=1") Log.i(TAG, "hdr: gamescope --hdr-enabled, DXVK_HDR=1") } - guest.add("BL_FPS=" + SessionState.fpsLimit) // gamescope's WSI layer and winex11 reuse their per-present window checks for this long; 0 checks on every present. val windowCheckMs = if (SessionState.performanceMode) 100 else 0 guest.add("DROIDDECK_WSI_BYPASS_CHECK_MS=$windowCheckMs") guest.add("WINE_OFFSCREEN_CHECK_MS=$windowCheckMs") - // With a frame limit, gamescope's limiter also holds the games to it instead of only pacing the screen. - guest.add("DROIDDECK_GAME_FRAME_LIMIT=" + (if (SessionState.performanceMode) 1 else 0)) - if (SessionPrefs.compositorFpsLimit(SessionState.fpsLimit, SessionState.performanceMode) == SessionPrefs.FPS_UNPACED) { + if (SessionState.performanceMode) { // gamescope turns buffers around slower than an unpaced game draws; more images keep it drawing. guest.add("GAMESCOPE_WSI_MIN_IMAGE_COUNT=8") guest.add("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT=1") } - Log.i(TAG, "performance mode " + (if (SessionState.performanceMode) "on" else "off") + ", frame limit " + SessionState.fpsLimit) + Log.i(TAG, "performance mode " + (if (SessionState.performanceMode) "on" else "off")) guest.add("BL_REFRESH=" + Math.round(SessionState.refreshHz)) guest.add("BL_LOG=" + sessionLog.path) guest.add("BL_DEBUG_DIR=" + sessionDir.path) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt index 5db2211a1..bb1ea3923 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt @@ -103,9 +103,6 @@ object SessionState { @Volatile var upscaleRatio: Float = 0f - @Volatile - var fpsLimit: Int = 0 - @Volatile var performanceMode = false diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index fd9933e25..a2239bb72 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -60,7 +60,6 @@ class ModeSettings( val hdr: Boolean, val hdrReason: String?, val gpuDrivers: String? = null, - val fpsLimit: Int = 0, val performanceMode: Boolean = false, val upscaler: Int = 0, val upscaleSharpness: Int = 75, @@ -117,7 +116,6 @@ class ModeSettingsActions( val onResolution: (String) -> Unit, val onHdr: (Boolean) -> Unit, val onGpuDrivers: () -> Unit = {}, - val onFpsLimit: (Int) -> Unit = {}, val onPerformanceMode: (Boolean) -> Unit = {}, val onUpscaler: (Int) -> Unit = {}, val onUpscaleSharpness: (Int) -> Unit = {}, @@ -246,8 +244,6 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: SettingsGroup(stringResource(R.string.display_frame_rate)) { ToggleRow(host, "performance", stringResource(R.string.mode_performance), stringResource(R.string.mode_performance_hint), s.performanceMode, onChange = a.onPerformanceMode) - ChoiceRow(host, "fps", stringResource(R.string.mode_fps), stringResource(R.string.common_applies_next_session), - SessionPrefs.fpsLimitChoices(LocalContext.current), s.fpsLimit, note = stringResource(R.string.mode_fps_note), onPick = a.onFpsLimit) } SettingsGroup(stringResource(R.string.mode_hdr)) { ToggleRow( diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0d17340d6..fe7c0e138 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -247,8 +247,6 @@ نسبة أبعاد الشاشة تحددها الدقة المخصّصة. يستخدم الخيار «تلقائي» نسبة 16:9 على الأقل. - حد الإطارات - يحدّ الجلسة بأكملها. تنتقل الشاشة إلى معدل تحديث يكون من مضاعفات الحد. HDR إخراج HDR10 غير متاح: %1$s. diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 35abfeb41..1609361f2 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -247,8 +247,6 @@ Relación de aspecto Definida por la resolución personalizada. Auto usa al menos 16:9. - Límite de fotogramas - Limita toda la sesión. La pantalla cambia a una tasa de refresco que el límite divide exactamente. HDR Salida HDR10 No disponible: %1$s. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index e4eda5acf..a40f3e38d 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -247,8 +247,6 @@ Пропорции на екрана Определени от персонализираната резолюция. Автоматично използва поне 16:9. - Ограничение на кадрите - Ограничава цялата сесия. Екранът преминава към честота на опресняване, която се дели точно на ограничението. HDR HDR10 изход Не е налично: %1$s. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f3c5a4b8f..4d493645a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -247,8 +247,6 @@ Poměr stran obrazovky Určen vlastním rozlišením. Automaticky použije alespoň 16:9. - Omezení snímků - Omezuje celou relaci. Displej přejde na obnovovací frekvenci, kterou lze tímto limitem dělit beze zbytku. HDR Výstup HDR10 Není k dispozici: %1$s. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index f34cad249..f737e6bd7 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -247,8 +247,6 @@ Billedformat Angivet af den brugerdefinerede opløsning. Auto bruger mindst 16:9. - Billedgrænse - Begrænser hele sessionen. Skærmen skifter til en opdateringshastighed, grænsen går lige op med. HDR HDR10-output Ikke tilgængelig: %1$s. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index db80fbc59..8e9b65332 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -247,8 +247,6 @@ Seitenverhältnis Durch die benutzerdefinierte Auflösung festgelegt. Auto verwendet mindestens 16:9. - Bildratenlimit - Begrenzt die gesamte Sitzung. Das Display wechselt auf eine Bildwiederholrate, die ein ganzzahliges Vielfaches des Limits ist. HDR HDR10-Ausgabe Nicht verfügbar: %1$s. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 0fb72f3a3..2da1c1b2a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -247,8 +247,6 @@ Αναλογία οθόνης Καθορίζεται από την προσαρμοσμένη ανάλυση. Το Αυτόματο χρησιμοποιεί τουλάχιστον 16:9. - Όριο καρέ - Περιορίζει ολόκληρη τη συνεδρία. Η οθόνη μεταβαίνει σε ρυθμό ανανέωσης που διαιρείται ακριβώς με το όριο. HDR Έξοδος HDR10 Μη διαθέσιμο: %1$s. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e599874c8..45f4cfc67 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -247,8 +247,6 @@ Relación de aspecto Definida por la resolución personalizada. Auto usa al menos 16:9. - Límite de fotogramas - Limita toda la sesión. La pantalla cambia a una tasa de refresco que el límite divide exactamente. HDR Salida HDR10 No disponible: %1$s. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0fc0768a8..ddf4d4605 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -247,8 +247,6 @@ Näytön kuvasuhde Määräytyy mukautetun resoluution mukaan. Automaattinen käyttää vähintään 16:9:ää. - Kuvanopeusraja - Rajoittaa koko istuntoa. Näyttö vaihtaa virkistystaajuuteen, jonka rajoitus jakaa tasan. HDR HDR10-lähtö Ei käytettävissä: %1$s. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ceaad3f8a..535cbf487 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -247,8 +247,6 @@ Format d\'écran Défini par la résolution personnalisée. Le mode automatique utilise au moins 16:9. - Limite de taux de rafraîchissement - Limite le taux de rafraîchissement de toute la session. Le moniteur basculera vers un taux de rafraîchissement divisible par cette limite. HDR Sortie HDR10 Non disponible : %1$s. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 72455444e..be2c1b803 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -247,8 +247,6 @@ स्क्रीन रेशियो कस्टम रिज़ॉल्यूशन से तय होता है। ऑटो कम से कम 16:9 इस्तेमाल करता है। - फ़्रेम सीमा - पूरे सेशन को सीमित करता है। डिस्प्ले उस रिफ्रेश रेट पर चला जाता है जिसे यह सीमा बराबर बांटती है। HDR HDR10 आउटपुट उपलब्ध नहीं: %1$s। diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index fd309f9ad..6902c2cc3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -247,8 +247,6 @@ Képernyőarány Az egyéni felbontás határozza meg. Az Automatikus legalább 16:9-et használ. - Képkocka-korlát - Korlátozza a teljes munkamenetet. A kijelző olyan frissítési frekvenciára vált, amelyet a korlát egyenlően oszt. HDR HDR10-kimenet Nem elérhető: %1$s. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6f8d35ffc..24ce23f45 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -247,8 +247,6 @@ Rasio layar Ditentukan oleh resolusi khusus. Otomatis menggunakan setidaknya 16:9. - Batas frame rate - Membatasi seluruh sesi. Tampilan beralih ke refresh rate yang habis dibagi oleh batas ini. HDR Output HDR10 Tidak tersedia: %1$s. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f41000707..524eba544 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -247,8 +247,6 @@ Proporzioni schermo Definite dalla risoluzione personalizzata. Auto usa almeno 16:9. - Limite frame - Limita l\'intera sessione. Lo schermo passa a una frequenza di aggiornamento multipla del limite. HDR Uscita HDR10 Non disponibile: %1$s. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5de521b82..4d605fa6b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -247,8 +247,6 @@ 画面比率 カスタム解像度で設定されます。 自動モードでは少なくとも16:9を使用します。 - フレームレート制限 - セッション全体のフレームレートを制限します。モニターはこの上限で割り切れるリフレッシュレートに切り替わります。 HDR HDR10出力 利用不可:%1$s。 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 5c87b95a3..9420ce605 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -247,8 +247,6 @@ 화면 비율 사용자 정의 해상도로 설정됩니다. 자동 모드에서는 최소 16:9를 사용합니다. - 프레임 속도 제한 - 세션 전체의 프레임 속도를 제한합니다. 모니터는 이 상한으로 나누어떨어지는 주사율로 전환됩니다. HDR HDR10 출력 사용 불가: %1$s. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 562683f18..218fc9a81 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -247,8 +247,6 @@ Nisbah skrin Ditetapkan oleh resolusi tersuai. Auto menggunakan sekurang-kurangnya 16:9. - Had bingkai - Mengehadkan keseluruhan sesi. Paparan beralih kepada kadar penyegaran yang boleh dibahagi sama rata dengan had ini. HDR Output HDR10 Tidak tersedia: %1$s. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index a979f2c44..bbb6c8a24 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -247,8 +247,6 @@ Skjermforhold Angis av den egendefinerte oppløsningen. Auto bruker minst 16:9. - Bildefrekvensgrense - Begrenser hele sesjonen. Skjermen bytter til en oppdateringsfrekvens som grensen går opp i. HDR HDR10-utgang Ikke tilgjengelig: %1$s. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ccef5c5df..e8c394cf1 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -247,8 +247,6 @@ Beeldverhouding Ingesteld door de aangepaste resolutie. Automatisch gebruikt minimaal 16:9. - Framelimiet - Begrenst de hele sessie. Het scherm schakelt naar een verversingssnelheid die een veelvoud van de limiet is. HDR HDR10-uitvoer Niet beschikbaar: %1$s. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 145d207a3..8d1736c64 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -247,8 +247,6 @@ Proporcje ekranu Ustalane przez własną rozdzielczość. Auto używa co najmniej 16:9. - Limit klatek - Ogranicza całą sesję. Wyświetlacz przełącza się na częstotliwość odświeżania będącą wielokrotnością limitu. HDR Wyjście HDR10 Niedostępne: %1$s. diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 3a54ebd0d..f2c4bf67c 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -247,8 +247,6 @@ Proporção do ecrã Definida pela resolução personalizada. Automático utiliza pelo menos 16:9. - Limite de fotogramas - Limita toda a sessão. O ecrã muda para uma taxa de atualização que seja múltipla do limite. HDR Saída HDR10 Indisponível: %1$s. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c5b7bd327..3ade72aee 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -247,8 +247,6 @@ Proporção da tela Definida pela resolução personalizada. O Automático usa no mínimo 16:9. - Limite de quadros - Limita a sessão inteira. A tela muda para uma taxa de atualização que seja múltipla do limite. HDR Saída HDR10 Indisponível: %1$s. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d4aa16f9d..2305d87ab 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -247,8 +247,6 @@ Raport de ecran Stabilit de rezoluția personalizată. Automat folosește cel puțin 16:9. - Limită de cadre - Limitează întreaga sesiune. Afișajul trece la o rată de reîmprospătare pe care limita o împarte exact. HDR Ieșire HDR10 Indisponibil: %1$s. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0de18cd36..9a7447b3d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -247,8 +247,6 @@ Соотношение сторон Задается пользовательским разрешением. Автоматический режим использует не менее 16:9. - Ограничение частоты кадров - Ограничивает частоту кадров всей сессии. Экран переключится на частоту обновления, кратную этому пределу. HDR Вывод HDR10 Недоступно: %1$s. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 7e5ccf7d6..f50d90edf 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -247,8 +247,6 @@ Bildförhållande Bestäms av den anpassade upplösningen. Auto använder minst 16:9. - Bildgräns - Begränsar hela sessionen. Skärmen byter till en uppdateringsfrekvens som är en jämn multipel av gränsen. HDR HDR10-utgång Inte tillgängligt: %1$s. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 762cb944d..ef164e408 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -247,8 +247,6 @@ อัตราส่วนหน้าจอ กำหนดโดยความละเอียดกำหนดเอง อัตโนมัติใช้อย่างน้อย 16:9 - ขีดจำกัดเฟรม - จำกัดทั้งเซสชัน หน้าจอจะเปลี่ยนไปใช้อัตรารีเฟรชที่หารขีดจำกัดได้พอดี HDR เอาต์พุต HDR10 ไม่พร้อมใช้งาน: %1$s diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 693747e85..379511296 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -247,8 +247,6 @@ Ekran oranı Özel çözünürlüğe göre ayarlanır. Otomatik en az 16:9 kullanır. - Kare sınırı - Tüm oturumu sınırlar. Ekran, sınırın tam katı olan bir yenileme hızına geçer. HDR HDR10 çıkışı Kullanılamıyor: %1$s. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 44e0418dd..78eef7de8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -247,8 +247,6 @@ Співвідношення сторін Визначається власною роздільною здатністю. «Авто» використовує щонайменше 16:9. - Обмеження кадрів - Обмежує весь сеанс. Екран перемикається на частоту оновлення, кратну цьому обмеженню. HDR Вивід HDR10 Недоступно: %1$s. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7a8194842..475bef045 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -247,8 +247,6 @@ Tỷ lệ màn hình Được đặt theo độ phân giải tùy chỉnh. Tự động dùng tối thiểu 16:9. - Giới hạn khung hình - Giới hạn toàn bộ phiên. Màn hình chuyển sang tốc độ làm mới mà giới hạn chia hết. HDR Xuất HDR10 Không khả dụng: %1$s. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5e16b2160..2b5f4083a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -247,8 +247,6 @@ 屏幕比例 由自定义分辨率决定。 自动模式至少使用 16:9。 - 帧率上限 - 限制整个会话。显示器会切换到能被该上限整除的刷新率。 HDR HDR10 输出 不可用:%1$s。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 6ca43c17f..54058fe19 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -247,8 +247,6 @@ 螢幕比例 由自訂解像度決定。 自動模式至少使用 16:9。 - 幀率限制 - 限制整個會話的幀率。螢幕會切換至能被此上限整除的刷新率。 HDR HDR10 輸出 不可用:%1$s。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 77156849a..88adc508f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -247,8 +247,6 @@ 螢幕比例 由自訂解析度設定。 自動模式至少使用 16:9。 - 影格率限制 - 限制整個工作階段的影格率。顯示器會切換到能夠被該上限整除的重新整理率。 HDR HDR10 輸出 無法使用:%1$s。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6de9cdb35..f546a543a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,7 +94,7 @@ Set to 0 to disable Proton’s gamepad-navigation helper for this game. On by default. Starts an Easy Anti-Cheat game without its anti-cheat when the game supports offline play. Turn off to launch through Easy Anti-Cheat as usual. Online modes that require Easy Anti-Cheat remain unavailable. DXVK overlay for D3D9–11. fps displays frame rate; unset hides it. - DXVK settings separated by semicolons. Recent builds use dxvk.maxFrameRate for frame limiting; prefer the session frame limiter for ordinary play. + DXVK settings separated by semicolons. Recent builds use dxvk.maxFrameRate for frame limiting; prefer Steam\'s Quick Access menu frame limiter for ordinary play. CPU cores for this game, such as 0-4 or 0,2,4, in place of the session\'s game cores. Fewer cores run cooler. D3D12 frame-rate limit. 0 disables the limit. Mesa OpenGL threading. true can help CPU-limited OpenGL games; test per game. @@ -256,10 +256,8 @@ Screen ratio Set by the custom resolution. Auto uses at least 16:9. - Frame limit Performance mode - With no frame limit, games run as fast as they can: frames go back to the game without waiting for the screen. With a frame limit, games are held to it instead of drawing frames the screen never shows. Either way, gamescope and Wine skip repeated window checks. Uses more power and runs warmer without a limit. Applies next session. - Caps the whole session. The display switches to a refresh rate the cap divides evenly. + Lets games run without waiting for the screen and reduces repeated window checks in gamescope and Wine. Can use more power and run warmer. Use Steam\'s Quick Access menu to limit Steam games. Applies next session. HDR HDR10 output Not available: %1$s. diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMirrorPacingTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMirrorPacingTest.kt index b8306b936..4b97723d3 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMirrorPacingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMirrorPacingTest.kt @@ -1,9 +1,11 @@ package com.droiddeck.launcher.glasses +import android.content.Context import android.hardware.display.DisplayManager import android.view.Display import androidx.compose.runtime.MutableState import com.droiddeck.launcher.SessionActivity +import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.session.SessionState import org.junit.After import org.junit.Assert.assertEquals @@ -36,10 +38,14 @@ class GlassesMirrorPacingTest { val panel = Shadow.extract(activity.display) panel.supported = arrayOf(mode(1, 60f), mode(2, 90f), mode(3, 120f)) panel.current = panel.supported.last() - SessionState.fpsLimit = 0 + SessionState.performanceMode = false + activity.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit() } - @After fun cleanup() { SessionState.fpsLimit = 0 } + @After fun cleanup() { + SessionState.performanceMode = false + activity.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit() + } @Test fun connectingChangingRateAndDisconnectingUpdatePhonePacingWithoutSavingPreferences() { apply(connected, settings) @@ -48,18 +54,25 @@ class GlassesMirrorPacingTest { expect(2, 90f, 90) apply(GlassesState(), settings) expect(3, 120f, 0) - assertEquals(0, SessionState.fpsLimit) } - @Test fun lowerSavedLimitSurvivesMirroringAndStalePhoneRefreshReports() { - SessionState.fpsLimit = 30 + @Test fun retiredSavedLimitsDoNotCapThePhoneOrMirroredOutput() { + activity.getSharedPreferences("session", Context.MODE_PRIVATE).edit() + .putInt("fpsLimit.steam", 30).putInt("fpsLimit.desktop", 40).commit() apply(connected, settings) - expect(1, 60f, 30) + expect(1, 60f, 60) apply(connected.copy(hz = 120f), settings) - expect(1, 60f, 30) + expect(1, 60f, 60) apply(GlassesState(), settings) - expect(3, 120f, 30) - assertEquals(30, SessionState.fpsLimit) + expect(3, 120f, 0) + } + + @Test fun performanceModeResumesUnpacedOutputAfterMirrorDisconnects() { + SessionState.performanceMode = true + apply(connected, settings) + expect(1, 60f, 60) + apply(GlassesState(), settings) + expect(3, 120f, SessionPrefs.FPS_UNPACED) } @Test fun pacingFollowsThePanelWhenTheSystemKeepsALowerRateThanRequested() { diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt index 51d9aa7b7..4b5972ede 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt @@ -58,13 +58,6 @@ class GlassesRenderingTest { assertEquals(0, GlassesRendering.mirrorRefreshLimit(settings, ultra, true)) } - @Test fun mirrorCapPreservesLowerLimitsAndRestoresTheRequestedLimitAfterDisconnect() { - for (requested in listOf(0, 30, 60, 90, 120)) { - assertEquals(if (requested == 30) 30 else 60, GlassesRendering.fpsLimit(requested, 60)) - assertEquals(requested, GlassesRendering.fpsLimit(requested, 0)) - } - } - private fun render(settings: GlassesSettings, state: GlassesState = ultra, steamVr: Boolean = false) = GlassesRendering.cardboard(base, settings, state, outputActive = true, steamVrRunning = steamVr) diff --git a/app/src/test/java/com/droiddeck/launcher/session/PerformanceModeTest.kt b/app/src/test/java/com/droiddeck/launcher/session/PerformanceModeTest.kt index f59157c3b..9805a66de 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/PerformanceModeTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/PerformanceModeTest.kt @@ -24,19 +24,13 @@ class PerformanceModeTest { assertFalse(SessionPrefs.performanceMode(app, SessionService.MODE_DESKTOP)) } - @Test fun offKeepsTodaysPacing() { + @Test fun offKeepsDisplayAndMirrorPacing() { assertEquals(0, SessionPrefs.compositorFpsLimit(0, false)) assertEquals(60, SessionPrefs.compositorFpsLimit(60, false)) } - @Test fun onWithoutALimitIsUnpacedAndAChosenLimitStillPaces() { + @Test fun onIsUnpacedUnlessMirroring() { assertEquals(SessionPrefs.FPS_UNPACED, SessionPrefs.compositorFpsLimit(0, true)) assertEquals(60, SessionPrefs.compositorFpsLimit(60, true)) } - - @Test fun anOldUncappedFrameLimitReadsAsOff() { - session.edit().putInt("fpsLimit.steam", -1).commit() - assertEquals(0, SessionPrefs.fpsLimit(app, SessionService.MODE_STEAM)) - assertFalse(SessionPrefs.fpsLimitChoices.any { it.first < 0 }) - } } diff --git a/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt index e73905cbe..97b1102a3 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt @@ -29,7 +29,7 @@ class SettingsResetTest { SessionPrefs.setTheme(app, "ocean") SessionPrefs.setUnfoldedControlsWithController(app, true) SessionPrefs.setNoXalia(app, false) - SessionPrefs.setFpsLimit(app, SessionService.MODE_STEAM, 30) + session.edit().putInt("fpsLimit.steam", 30).commit() SessionPrefs.setHdr(app, SessionService.MODE_DESKTOP, true) SessionPrefs.setResolutionChoice(app, SessionService.MODE_STEAM, "1280x800") SessionPrefs.setGuestHostname(app, "MyDeck") @@ -48,7 +48,7 @@ class SettingsResetTest { assertEquals("graphite", SessionPrefs.theme(app)) assertFalse(SessionPrefs.unfoldedControlsWithController(app)) assertTrue(SessionPrefs.noXalia(app)) - assertEquals(0, SessionPrefs.fpsLimit(app, SessionService.MODE_STEAM)) + assertFalse(session.contains("fpsLimit.steam")) assertFalse(SessionPrefs.hdr(app, SessionService.MODE_DESKTOP)) assertFalse(SessionPrefs.resolutionChosen(app, SessionService.MODE_STEAM)) assertEquals(SessionPrefs.DEFAULT_GUEST_HOSTNAME, SessionPrefs.guestHostname(app)) diff --git a/docs/development/game-environment.md b/docs/development/game-environment.md index 62530ca48..bebd67ce6 100644 --- a/docs/development/game-environment.md +++ b/docs/development/game-environment.md @@ -98,7 +98,7 @@ custom components can support a different set of options. Android Wine wrapper, ALSA-server, Box64 and patched async-DXVK options are identified in the editor as requiring a different/custom component; custom entries remain allowed. The old `DXVK_FRAME_RATE` environment variable was removed from current DXVK, so it is -not offered as a suggestion. Prefer the session frame limiter for ordinary play. +not offered as a suggestion. Prefer Steam's Quick Access menu frame limiter for ordinary play. ## Launch path and references diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 755a9fce8..59d950af0 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -16,7 +16,7 @@ if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then fi exec > >({ dd of="$BL_LOG" oflag=append conv=notrunc iflag=count_bytes count="${BL_LOG_CAP:-268435456}" bs=65536 status=none; cat >/dev/null; }) 2>&1 echo "== droiddeck-session $(date) mode=$mode $*" - echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} fps=${BL_FPS:-0} icd=${VK_ICD_FILENAMES:-none}" + echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} icd=${VK_ICD_FILENAMES:-none}" echo "== gamescope: $(gamescope --version 2>&1 | head -1) at $(command -v gamescope)" echo "== rootfs: $(cat /.version 2>/dev/null || echo unversioned)" fi @@ -70,31 +70,6 @@ steam_update_stuck() { [ "$steam_updates" -ge 3 ] } -# gamescope's --framerate-limit lasts only until its first frame, which recomputes the limit -# from the per-app value set over gamescope_control, and Steam resets that value to 0 while it -# starts. Watch gamescope's limiter state file (first byte 1 = limiter on) and set the value -# again whenever it is off; the limiter then makes the WSI layer hold Vulkan games (DXVK and -# VKD3D included) to it. Shell builtins only, so the once-a-second check starts no process. -hold_games_to_frame_limit() { - local fps=$1 file=$2 parent=$3 state sent=0 nap napfd= - command -v gamescopectl >/dev/null || { echo "== gamescope: no gamescopectl, the frame limit paces the screen only"; return 0; } - nap=$(mktemp -u "${XDG_RUNTIME_DIR:-/tmp}/droiddeck-nap.XXXXXX") && mkfifo "$nap" 2>/dev/null \ - && exec {napfd}<>"$nap"; rm -f "$nap" - while kill -0 "$parent" 2>/dev/null; do - if [ -f "$file" ]; then - state= - IFS= read -r -n1 -d '' state < "$file" - if [ "$state" != $'\x01' ] && gamescopectl debug_set_fps_limit "$fps" >/dev/null 2>&1; then - sent=$((sent + 1)) - if [ "$sent" = 1 ]; then echo "== gamescope: games held to $fps fps" - elif [ "$sent" -le 4 ]; then echo "== gamescope: the frame limit was reset, set it again" - fi - fi - fi - if [ -n "$napfd" ]; then read -r -t 1 -u "$napfd" _; else sleep 1; fi - done -} - run_gamescope() ( local status_file child_status compositor_status status_file=$(mktemp "${XDG_RUNTIME_DIR:-/tmp}/droiddeck-steam-exit.XXXXXX") || exit 73 @@ -261,16 +236,7 @@ if [ -z "${BL_INSIDE:-}" ]; then args+=(-S stretch) export BL_STRETCHED=1 fi - if [ "${BL_FPS:-0}" -gt 0 ]; then - args+=(-r "$BL_FPS") - # -r alone only paces what reaches the screen: a game without vsync keeps drawing - # hundreds of frames that are thrown away. Performance mode also holds the games to it. - if [ "${DROIDDECK_GAME_FRAME_LIMIT:-0}" = 1 ]; then - game_frame_limit=$BL_FPS - else - echo "== gamescope: frame limit $BL_FPS paces the screen only" - fi - elif [ "${BL_REFRESH:-0}" -gt 0 ]; then + if [ "${BL_REFRESH:-0}" -gt 0 ]; then args+=(-r "$BL_REFRESH") fi [ "$mode" = steam ] && [ "${BL_STEAM_UI:-}" != desktop ] && args+=(-e) @@ -294,7 +260,6 @@ if [ -z "${BL_INSIDE:-}" ]; then rm -f "${XDG_RUNTIME_DIR:-/tmp}"/gamescope-limiter-* export GAMESCOPE_LIMITER_FILE="${XDG_RUNTIME_DIR:-/tmp}/droiddeck-gamescope-limiter" rm -f "$GAMESCOPE_LIMITER_FILE" - [ -n "${game_frame_limit:-}" ] && hold_games_to_frame_limit "$game_frame_limit" "$GAMESCOPE_LIMITER_FILE" $$ & if [ "$mode" = steam ]; then if /usr/local/bin/droiddeck-pipewire start; then eval "$(/usr/local/bin/droiddeck-pipewire env)" diff --git a/tools/tests/test_session_pacing.py b/tools/tests/test_session_pacing.py new file mode 100644 index 000000000..e7c0df938 --- /dev/null +++ b/tools/tests/test_session_pacing.py @@ -0,0 +1,55 @@ +"""Steam owns game caps; the nested compositor follows the output refresh rate.""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SESSION = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin/droiddeck-session' + + +class SessionPacingTest(unittest.TestCase): + def launch(self, refresh, **overrides): + source = SESSION.read_text() + start = source.index(' width=${BL_WIDTH:-1280}') + end = source.index('\nfi\n\nbl_displays()', start) + block = source[start:end].replace('/usr/local/bin/droiddeck-pipewire', 'pipewire_stub') + with tempfile.TemporaryDirectory() as directory: + env = dict(os.environ, XDG_RUNTIME_DIR=directory, BL_REFRESH=str(refresh), + BL_MANGOAPP='0', BL_FPS='0', DROIDDECK_GAME_FRAME_LIMIT='0') + env.update(overrides) + script = ''' +set -u +mode=steam +pipewire_stub() { return 1; } +run_gamescope() { printf 'ARG:%s\\n' "$@"; } +''' + block + result = subprocess.run(['bash', '-c', script], env=env, capture_output=True, + text=True, timeout=5) + self.assertEqual(0, result.returncode, result.stderr) + return [line.removeprefix('ARG:') for line in result.stdout.splitlines() + if line.startswith('ARG:')] + + def test_display_refresh_sets_the_nested_compositor_rate(self): + for refresh in (60, 90, 120): + with self.subTest(refresh=refresh): + args = self.launch(refresh) + self.assertEqual(str(refresh), args[args.index('-r') + 1]) + self.assertIn('-e', args) + + def test_retired_cap_variables_do_not_override_the_display_rate(self): + for performance in ('0', '1'): + with self.subTest(performance=performance): + args = self.launch(120, BL_FPS='30', DROIDDECK_GAME_FRAME_LIMIT=performance) + self.assertEqual('120', args[args.index('-r') + 1]) + self.assertNotIn('--framerate-limit', args) + + def test_retired_cap_cannot_set_the_rate_when_the_display_rate_is_unknown(self): + args = self.launch(0, BL_FPS='30', DROIDDECK_GAME_FRAME_LIMIT='1') + self.assertNotIn('-r', args) + self.assertNotIn('--framerate-limit', args) + + +if __name__ == '__main__': + unittest.main()