mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
fix: Steam's UI draws at an even share of the refresh near 60 fps instead of the panel's full rate (lag and heat since 0.6.7) (#1809)
This commit is contained in:
1 parent
b0a8c2065d
commit
f69e2d0171
9 files changed
+92
-12
No files matched your search
@@ -211,6 +211,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private lateinit var touchscreenGestures: TouchscreenGestures
|
||||
private var gestureConfig = com.droiddeck.launcher.input.TouchGestureConfig()
|
||||
private var gestureGame: String? = null
|
||||
private var steamUiFocused = false
|
||||
private var gestureSettingsOpen by mutableStateOf(false)
|
||||
private var steamImportAsk by mutableStateOf<com.droiddeck.launcher.session.SteamImportPrompt.Ask?>(null)
|
||||
private var steamImportAnswered: String? = null
|
||||
@@ -829,6 +830,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
androidx.compose.runtime.LaunchedEffect(picturePosition, SessionState.running, pipUi, cardboard.enabled) { applyGameAlignment() }
|
||||
androidx.compose.runtime.LaunchedEffect(AgentGuest.focusRevision) {
|
||||
val steamUi = SessionState.mode == SessionService.MODE_STEAM && kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
AgentGuest.focus(this@SessionActivity)?.optLong("focusedApp")?.let { it <= 0 || it == 769L } ?: true
|
||||
}
|
||||
if (steamUi != steamUiFocused) {
|
||||
steamUiFocused = steamUi
|
||||
applySteamUiPacing()
|
||||
}
|
||||
}
|
||||
DroidDeckTheme {
|
||||
if (pipUi) {
|
||||
if (SessionState.suspended) androidx.compose.foundation.layout.Box(
|
||||
@@ -2060,7 +2070,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
CompositorHost.startOrAttach(
|
||||
this, holder.surface, runtimeDir.path,
|
||||
driverId?.let { turnip.driverPath(it) }, driverId?.let { turnip.libraryName(it) },
|
||||
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(), outputFpsLimit(),
|
||||
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(), outputFpsLimit(), steamUiLimit(refreshHz()),
|
||||
)
|
||||
onOutputAttached(holder)
|
||||
if (SessionState.running && size != previous) {
|
||||
@@ -2200,12 +2210,23 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
private fun outputFpsLimit(): Int = SessionPrefs.compositorFpsLimit(mirrorRefreshLimit())
|
||||
|
||||
private fun steamUiLimit(hz: Float): Int =
|
||||
if (steamUiFocused && SessionPrefs.steamUiPacing(this)) SessionPrefs.steamUiFpsLimit(hz) else 0
|
||||
|
||||
private fun applySteamUiPacing() {
|
||||
if (!CompositorHost.isStarted) return
|
||||
val hz = refreshHz()
|
||||
val limit = steamUiLimit(hz)
|
||||
CompositorHost.updatePacing(hz, outputFpsLimit(), limit)
|
||||
Log.i(TAG, "pacing: " + if (limit > 0) "Steam UI at $limit fps on a ${Math.round(hz)} Hz screen" else "uncapped")
|
||||
}
|
||||
|
||||
private fun applyOutputPacing() {
|
||||
Log.i(TAG, "perf: " + PerfMode.apply(this, outputFpsLimit(), mirrorRefreshLimit()))
|
||||
if (CompositorHost.isStarted) {
|
||||
val hz = refreshHz()
|
||||
val changed = kotlin.math.abs(SessionState.refreshHz - hz) > 0.1f
|
||||
CompositorHost.updatePacing(hz, outputFpsLimit())
|
||||
CompositorHost.updatePacing(hz, outputFpsLimit(), steamUiLimit(hz))
|
||||
pushCardboard(activeCardboard)
|
||||
if (changed && ::surfaceView.isInitialized) applyFrameGen()
|
||||
}
|
||||
|
||||
@@ -1094,6 +1094,15 @@ object SessionPrefs {
|
||||
fun compositorFpsLimit(mirrorRefreshLimit: Int): Int =
|
||||
if (mirrorRefreshLimit <= 0) FPS_UNPACED else mirrorRefreshLimit
|
||||
|
||||
private const val STEAM_UI_FPS = 60f
|
||||
|
||||
fun steamUiPacing(context: Context): Boolean = prefs(context).getBoolean("steamUiPacing", true)
|
||||
|
||||
fun steamUiFpsLimit(refreshHz: Float): Int {
|
||||
val steps = ((refreshHz + 0.5f) / STEAM_UI_FPS).toInt()
|
||||
return if (steps < 2) 0 else (refreshHz / steps + 0.01f).toInt()
|
||||
}
|
||||
|
||||
val upscalerChoices = listOf(
|
||||
0 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 3 to "Snapdragon GSR",
|
||||
8 to "Snapdragon GSR (quality)", 7 to "NVIDIA NIS", 6 to "Sharpen only",
|
||||
|
||||
@@ -535,10 +535,6 @@ class SessionService : Service() {
|
||||
// gamescope's WSI layer and winex11 reuse their per-present window checks for this long.
|
||||
guest.add("DROIDDECK_WSI_BYPASS_CHECK_MS=100")
|
||||
guest.add("WINE_OFFSCREEN_CHECK_MS=100")
|
||||
// gamescope turns buffers around slower than an uncapped game draws; more images keep it drawing.
|
||||
// The WSI layer goes back to its default count while Steam's Frame Limit holds the game.
|
||||
guest.add("GAMESCOPE_WSI_MIN_IMAGE_COUNT=8")
|
||||
guest.add("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT=1")
|
||||
guest.add("BL_REFRESH=" + Math.round(SessionState.refreshHz))
|
||||
guest.add("BL_LOG=" + sessionLog.path)
|
||||
guest.add("BL_DEBUG_DIR=" + sessionDir.path)
|
||||
|
||||
@@ -51,10 +51,11 @@ object CompositorHost {
|
||||
outputHeight: Int,
|
||||
refreshHz: Float,
|
||||
fpsLimit: Int,
|
||||
uiLimit: Int = 0,
|
||||
): Boolean {
|
||||
ownVsyncThread = SessionPrefs.vsyncThread(context)
|
||||
WaylandCompositor.nativeSetRenderOnCommit(SessionPrefs.renderOnCommit(context))
|
||||
pace(refreshHz, fpsLimit)
|
||||
pace(refreshHz, fpsLimit, uiLimit)
|
||||
if (!SessionState.running) WaylandCompositor.nativeSetOutputSize(outputWidth, outputHeight)
|
||||
if (started) {
|
||||
attached = surface
|
||||
@@ -72,16 +73,16 @@ object CompositorHost {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun pace(refreshHz: Float, fpsLimit: Int) {
|
||||
WaylandCompositor.nativeSetFpsLimit(fpsLimit)
|
||||
private fun pace(refreshHz: Float, fpsLimit: Int, uiLimit: Int = 0) {
|
||||
WaylandCompositor.nativeSetFpsLimit(if (fpsLimit == SessionPrefs.FPS_UNPACED && uiLimit > 0) uiLimit else fpsLimit)
|
||||
WaylandCompositor.nativeSetLayerFrameRate(if (fpsLimit > 0) fpsLimit.toFloat() else refreshHz)
|
||||
WaylandCompositor.nativeSetOutputRefreshRate(refreshHz)
|
||||
SessionState.refreshHz = refreshHz
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updatePacing(refreshHz: Float, fpsLimit: Int) {
|
||||
if (started) pace(refreshHz, fpsLimit)
|
||||
fun updatePacing(refreshHz: Float, fpsLimit: Int, uiLimit: Int = 0) {
|
||||
if (started) pace(refreshHz, fpsLimit, uiLimit)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class SteamUiPacingTest {
|
||||
@Test fun aSixtyOrNinetyHertzScreenIsLeftUncapped() {
|
||||
assertEquals(0, SessionPrefs.steamUiFpsLimit(60f))
|
||||
assertEquals(0, SessionPrefs.steamUiFpsLimit(59.94f))
|
||||
assertEquals(0, SessionPrefs.steamUiFpsLimit(90f))
|
||||
}
|
||||
|
||||
@Test fun fasterScreensGetAnEvenDivisorOfTheirRefresh() {
|
||||
assertEquals(60, SessionPrefs.steamUiFpsLimit(120f))
|
||||
assertEquals(60, SessionPrefs.steamUiFpsLimit(119.99f))
|
||||
assertEquals(60, SessionPrefs.steamUiFpsLimit(120.00001f))
|
||||
assertEquals(72, SessionPrefs.steamUiFpsLimit(144.00002f))
|
||||
assertEquals(82, SessionPrefs.steamUiFpsLimit(165f))
|
||||
assertEquals(61, SessionPrefs.steamUiFpsLimit(185.00002f))
|
||||
assertEquals(60, SessionPrefs.steamUiFpsLimit(240f))
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,14 @@ gamescope's WSI layer presents every swapchain in FIFO at the limit. A
|
||||
`vblank_mode` from the launch options or the environment editor is kept, and
|
||||
**Remove** in the editor leaves Mesa's own default.
|
||||
|
||||
A game launch (`waitforexitandrun`) also gets
|
||||
`GAMESCOPE_WSI_MIN_IMAGE_COUNT=8` and `GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT=1`,
|
||||
so an uncapped game keeps drawing while gamescope turns buffers around. The
|
||||
session itself no longer sets them: the Steam client's UI renders through the
|
||||
same WSI layer, and with eight images and no frame limit it drew flat out
|
||||
(about 240 frames a second at 120 Hz) and heated the device. Values set in the
|
||||
launch options or the environment editor are kept.
|
||||
|
||||
## Defaults and available suggestions
|
||||
|
||||
The initial configuration enables Mesa shader caching with
|
||||
|
||||
@@ -448,6 +448,14 @@ def gl_vsync(env):
|
||||
return True
|
||||
|
||||
|
||||
def game_images(env):
|
||||
if game_id(env) is None:
|
||||
return False
|
||||
env.setdefault("GAMESCOPE_WSI_MIN_IMAGE_COUNT", "8")
|
||||
env.setdefault("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT", "1")
|
||||
return True
|
||||
|
||||
|
||||
def apply_config(env, config, app_id, fixes=None):
|
||||
if config.get("version") != 1:
|
||||
raise ValueError("unsupported environment configuration")
|
||||
@@ -747,6 +755,8 @@ def main():
|
||||
if app_id:
|
||||
gl_default = gl_vsync(env)
|
||||
waiting = len(command) > 1 and command[1] == "waitforexitandrun"
|
||||
if waiting:
|
||||
game_images(env)
|
||||
fixes = {}
|
||||
if waiting:
|
||||
command = skip_red_launcher(command)
|
||||
|
||||
@@ -1086,7 +1086,8 @@ class LauncherTest(SyncTestCase):
|
||||
(str(tool / "proton"), command)])
|
||||
self.assertEqual((calls[0][2]["WINEESYNC"], calls[0][2]["PROTON_NO_ESYNC"], calls[0][2]["BL_SYNC_PACK"]), ("0", "1", pack["id"]))
|
||||
self.assertEqual(calls[1][2], self.env(STEAM_COMPAT_APP_ID="2630", PROTON_NO_NTSYNC="1", BL_SYNC="0", WINEESYNC="1",
|
||||
vblank_mode="0"))
|
||||
vblank_mode="0", GAMESCOPE_WSI_MIN_IMAGE_COUNT="8",
|
||||
GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT="1"))
|
||||
self.assertIn("could not start (", said.getvalue())
|
||||
|
||||
def test_generated_launchers_run_the_dist_proton(self):
|
||||
|
||||
@@ -605,6 +605,18 @@ class GameEnvironmentTest(unittest.TestCase):
|
||||
self.assertEqual(env.get("WINE_CPU_TOPOLOGY"), expected, env)
|
||||
self.assertEqual(env.get("PROTON_CPU_TOPOLOGY"), proton, env)
|
||||
|
||||
def test_only_games_get_the_larger_swapchain(self):
|
||||
game = {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/945360"}
|
||||
self.assertTrue(MODULE["game_images"](game))
|
||||
self.assertEqual(game["GAMESCOPE_WSI_MIN_IMAGE_COUNT"], "8")
|
||||
self.assertEqual(game["GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT"], "1")
|
||||
own = {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/945360", "GAMESCOPE_WSI_MIN_IMAGE_COUNT": "3"}
|
||||
MODULE["game_images"](own)
|
||||
self.assertEqual(own["GAMESCOPE_WSI_MIN_IMAGE_COUNT"], "3")
|
||||
for env in ({}, {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0"}):
|
||||
self.assertFalse(MODULE["game_images"](env))
|
||||
self.assertNotIn("GAMESCOPE_WSI_MIN_IMAGE_COUNT", env)
|
||||
|
||||
def test_opengl_games_do_not_wait_for_the_screen_refresh(self):
|
||||
cases = [
|
||||
({}, "0"),
|
||||
|
||||
Reference in new issue
Block a user