diff --git a/app/src/main/java/app/gamenative/display/GamescopeCommandQueue.kt b/app/src/main/java/app/gamenative/display/GamescopeCommandQueue.kt index 8472cac079..970ecaf3a2 100644 --- a/app/src/main/java/app/gamenative/display/GamescopeCommandQueue.kt +++ b/app/src/main/java/app/gamenative/display/GamescopeCommandQueue.kt @@ -116,6 +116,15 @@ object GamescopeCommandQueue { /** Set the frame-rate cap (0 = uncapped); re-applied with the rest of the state. */ fun setFpsLimit(fps: Int) = update { fpsLimit = fps } + /** + * Ask gamescope to re-read the connector and re-select its mode. + * + * The mode is read once when the session starts, so a rate the connector advertises later (the + * compositor's grid for a cap, a multiplier or a new panel) only becomes the mode once the + * backend is dirtied. A one-shot action, not state: it carries no value to re-apply. + */ + fun nudgeDisplayMode() = submitAction(GamescopeConVar.BACKEND_SET_DIRTY) + /** Queue a one-shot action; sent once on the next apply (immediately when applicable). */ fun submitAction(action: GamescopeConVar) { val sendNow: Boolean diff --git a/app/src/main/java/app/gamenative/display/GamescopeConVar.kt b/app/src/main/java/app/gamenative/display/GamescopeConVar.kt index a8df7f5205..f81ed9163c 100644 --- a/app/src/main/java/app/gamenative/display/GamescopeConVar.kt +++ b/app/src/main/java/app/gamenative/display/GamescopeConVar.kt @@ -11,4 +11,7 @@ enum class GamescopeConVar(val id: String) { SET_LOOK("set_look"), SCREENSHOT("screenshot"), DEBUG_FORCE_REPAINT("debug_force_repaint"), + + /** Dirty the backend and re-poll it: gamescope re-reads the connector and re-selects its mode. */ + BACKEND_SET_DIRTY("backend_set_dirty"), } diff --git a/app/src/main/java/app/gamenative/display/GamescopePayload.kt b/app/src/main/java/app/gamenative/display/GamescopePayload.kt index 20cdc3babe..957b592de5 100644 --- a/app/src/main/java/app/gamenative/display/GamescopePayload.kt +++ b/app/src/main/java/app/gamenative/display/GamescopePayload.kt @@ -11,7 +11,7 @@ import java.io.File /** Versioned gamescope payload (gamescope, XWayland, scaling clients), fetched on the first gamescope launch. */ object GamescopePayload { - const val VERSION = "20261031.3" + const val VERSION = "20261101.6" const val ARCHIVE = "gamescope-gamenative-$VERSION.tzst" private const val STAMP = ".payload.version" diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index bfcd30d94a..4449c5668c 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -680,6 +680,8 @@ fun XServerScreen( var lsfgMultiplier by rememberSaveable(container.id) { mutableIntStateOf(initialLsfgSettings.multiplier) } var lsfgFlowScale by rememberSaveable(container.id) { mutableStateOf(initialLsfgSettings.flowScale) } var lsfgPerformanceMode by rememberSaveable(container.id) { mutableStateOf(initialLsfgSettings.performanceMode) } + /** The rate last reported to the daemon, so a mode nudge is only asked for when it changes. */ + var advertisedPanelHz by remember(container.id) { mutableIntStateOf(0) } fun persistFpsLimiterState() { container.putExtra(FPS_LIMITER_ENABLED_EXTRA, fpsLimiterEnabled) @@ -735,6 +737,28 @@ fun XServerScreen( } } + /** + * Report the rate the session's emulated display advertises: the display's own, and nothing else. + * + * Neither the cap nor the frame-generation multiplier enters it. The mode is the rate the + * compositor runs at, so it belongs to the display and changes only when the display does; the + * cap lives at acquire, before a frame is drawn, and generation inserts `multiplier - 1` frames + * per source frame whatever the mode — a 40 cap at x4 produces and counts 160 on a 120 Hz mode. + * Advertising the produced rate instead would make the compositor's grid follow the QuickMenu. + */ + fun applyPanelRefresh(panelHz: Int = detectRefreshRateHz(context, ui.xServerView as? View)) { + if (isX11Display || panelHz <= 0) return + GameNativeDrm.nativeSetPanelRefreshHz(panelHz) + // The daemon's connector advertises the new rate from here on, but gamescope reads the mode + // once, when the session starts: dirty its backend so a display change becomes the mode now + // rather than on the next launch. Only on a change — a nudge forces a modeset — and not on + // the first report, which is the rate the session starts on anyway. + if (advertisedPanelHz != 0 && advertisedPanelHz != panelHz) { + GamescopeCommandQueue.nudgeDisplayMode() + } + advertisedPanelHz = panelHz + } + fun applyFpsLimiterToEngines(limit: Int) { if (isX11Display) { // With LSFG active the layer owns ALL pacing (vsync-locked via vsync.txt) and presents at limit * @@ -751,6 +775,8 @@ fun XServerScreen( // With frame generation active the presenter owns pacing, exactly as the layer does on X11: // the cap is disarmed and the output rate becomes limit x multiplier. GamescopeCommandQueue.setFpsLimit(if (isLsfgAvailable && lsfgMultiplier >= 2) 0 else limit) + // The mode is the display's rate: the cap and the multiplier never move it. + applyPanelRefresh() } PowerManager.targetFps = limit @@ -762,6 +788,7 @@ fun XServerScreen( fun effectiveFpsLimit(): Int = if (fpsLimiterEnabled) fpsLimiterTarget else 0 + fun applyLsfgSettings() { LsfgQuickMenuHelper.applySettings( container, @@ -794,8 +821,10 @@ fun XServerScreen( fun applyLsfgMultiplier(mult: Int) { lsfgMultiplier = LsfgQuickMenuHelper.sanitizeMultiplier(mult) - applyLsfgSettings() + // The rate first, then the settings: the session's config carries the rate, and the shim + // opens the generator from what the daemon holds when it is created. applyFpsLimiterToEngines(effectiveFpsLimit()) + applyLsfgSettings() } fun applyLsfgFlowScale(scale: Float) { @@ -813,7 +842,7 @@ fun XServerScreen( ui.detectedMaxRefreshRateHz = refreshHz * FPS_LIMITER_MAX_MULTIPLIER if (!isX11Display) - GameNativeDrm.nativeSetPanelRefreshHz(refreshHz) + applyPanelRefresh(refreshHz) } /** diff --git a/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so b/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so index 9d703a77a6..163bb0b45e 100755 Binary files a/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so and b/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so differ