Repository navigation
fix(gamescope): the Upscaling Resolution is the session's scan-out size #2283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a8c57d2
e998e01
ac59978
6b8348e
f5cdd49
544a9c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -62,8 +62,6 @@ import androidx.compose.runtime.setValue | |||||||||||||||||||||||||
| import app.gamenative.MainActivity | ||||||||||||||||||||||||||
| import androidx.compose.ui.ExperimentalComposeUiApi | ||||||||||||||||||||||||||
| import androidx.compose.ui.Modifier | ||||||||||||||||||||||||||
| import androidx.compose.ui.input.key.key | ||||||||||||||||||||||||||
| import androidx.compose.ui.input.key.type | ||||||||||||||||||||||||||
| import androidx.compose.foundation.gestures.detectDragGestures | ||||||||||||||||||||||||||
| import androidx.compose.ui.Alignment | ||||||||||||||||||||||||||
| import androidx.compose.ui.input.pointer.PointerIcon | ||||||||||||||||||||||||||
|
|
@@ -139,7 +137,6 @@ import app.gamenative.ui.component.dialog.SteamControllerBindingEditorDialog | |||||||||||||||||||||||||
| import app.gamenative.ui.component.parseBooleanExtra | ||||||||||||||||||||||||||
| import app.gamenative.ui.component.parsePositiveFpsLimit | ||||||||||||||||||||||||||
| import app.gamenative.ui.data.PerformanceHudConfig | ||||||||||||||||||||||||||
| import app.gamenative.ui.data.PerformanceHudSize | ||||||||||||||||||||||||||
| import app.gamenative.ui.data.XServerState | ||||||||||||||||||||||||||
| import app.gamenative.ui.widget.PerformanceHudView | ||||||||||||||||||||||||||
| import app.gamenative.utils.AssetUtils | ||||||||||||||||||||||||||
|
|
@@ -254,7 +251,6 @@ import com.winlator.xserver.extensions.PresentExtension | |||||||||||||||||||||||||
| import kotlinx.coroutines.CompletableDeferred | ||||||||||||||||||||||||||
| import kotlinx.coroutines.CoroutineScope | ||||||||||||||||||||||||||
| import kotlinx.coroutines.Dispatchers | ||||||||||||||||||||||||||
| import kotlinx.coroutines.Job | ||||||||||||||||||||||||||
| import kotlinx.coroutines.delay | ||||||||||||||||||||||||||
| import kotlinx.coroutines.launch | ||||||||||||||||||||||||||
| import kotlinx.coroutines.runBlocking | ||||||||||||||||||||||||||
|
|
@@ -311,27 +307,40 @@ private fun initialFpsLimiterTarget(container: Container): Int = | |||||||||||||||||||||||||
| parsePositiveFpsLimit(container.getExtra(FPS_LIMITER_TARGET_EXTRA)) | ||||||||||||||||||||||||||
| ?: DEFAULT_FPS_LIMITER_TARGET_HZ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| internal fun detectMaxRefreshRateHz(context: Context, attachedView: View?): Int { | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * The panel's refresh rate in Hz: the mode the display is in now, not the fastest it | ||||||||||||||||||||||||||
| * supports. The gndrm daemon measures the panel's latch grid against this period and the | ||||||||||||||||||||||||||
| * FPS limiter's ceiling is derived from it, so a panel a user holds at 60 Hz counts as 60 | ||||||||||||||||||||||||||
| * even when it can do 120; a mode change mid-session is reported again by the display | ||||||||||||||||||||||||||
| * listener below. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| internal fun detectRefreshRateHz(context: Context, attachedView: View?): Int { | ||||||||||||||||||||||||||
| val display = attachedView?.display | ||||||||||||||||||||||||||
| ?: context.display | ||||||||||||||||||||||||||
| ?: ContextCompat.getSystemService(context, DisplayManager::class.java)?.getDisplay(Display.DEFAULT_DISPLAY) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| val refreshRate = when { | ||||||||||||||||||||||||||
| display == null -> DEFAULT_FPS_LIMITER_MAX_HZ.toFloat() | ||||||||||||||||||||||||||
| Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { | ||||||||||||||||||||||||||
| val supportedMax = display.supportedModes.maxOfOrNull { it.refreshRate } ?: display.refreshRate | ||||||||||||||||||||||||||
| if (supportedMax.isFinite() && supportedMax > 0f) supportedMax else display.refreshRate | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| else -> display.refreshRate | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return refreshRate | ||||||||||||||||||||||||||
| .takeIf { it.isFinite() && it > 0f } | ||||||||||||||||||||||||||
| return display?.refreshRate | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Read the physical panel rate. On Android 12 and later,
|
||||||||||||||||||||||||||
| ?.takeIf { it.isFinite() && it > 0f } | ||||||||||||||||||||||||||
| ?.roundToInt() | ||||||||||||||||||||||||||
| ?.coerceAtLeast(5) | ||||||||||||||||||||||||||
| ?: DEFAULT_FPS_LIMITER_MAX_HZ | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** Re-reports the panel's active refresh rate to the daemon while a session runs. */ | ||||||||||||||||||||||||||
| private var panelRefreshListener: Pair<DisplayManager, DisplayManager.DisplayListener>? = null | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Drops the panel-refresh listener. Both session-end paths call this — `exit()` for a finished | ||||||||||||||||||||||||||
| * session and the view's `onRelease` for one disposed without it — so it has to tolerate a | ||||||||||||||||||||||||||
| * second call and a listener another path has already dropped. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| private fun releasePanelRefreshListener() { | ||||||||||||||||||||||||||
| panelRefreshListener?.let { (displayManager, listener) -> | ||||||||||||||||||||||||||
| displayManager.unregisterDisplayListener(listener) | ||||||||||||||||||||||||||
| panelRefreshListener = null | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private data class XServerViewReleaseBinding( | ||||||||||||||||||||||||||
| val xServerView: XServerRendererView, | ||||||||||||||||||||||||||
| val windowModificationListener: WindowManager.OnWindowModificationListener, | ||||||||||||||||||||||||||
|
|
@@ -793,6 +802,33 @@ fun XServerScreen( | |||||||||||||||||||||||||
| applyLsfgSettings() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| fun refreshFpsLimit(view: View?, isX11Display: Boolean) { | ||||||||||||||||||||||||||
| val refreshHz = detectRefreshRateHz(context, view) | ||||||||||||||||||||||||||
| ui.detectedMaxRefreshRateHz = refreshHz * FPS_LIMITER_MAX_MULTIPLIER | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!isX11Display) | ||||||||||||||||||||||||||
| GameNativeDrm.nativeSetPanelRefreshHz(refreshHz) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Registers the display listener that re-reports the panel's rate when the display changes mode. | ||||||||||||||||||||||||||
| * Called from both the view factory and its update block: a session reusing a cached view skips | ||||||||||||||||||||||||||
| * the factory, and would otherwise run without one. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| fun ensurePanelRefreshListener() { | ||||||||||||||||||||||||||
| if (panelRefreshListener != null) return | ||||||||||||||||||||||||||
| val displayManager = ContextCompat.getSystemService(context, DisplayManager::class.java) ?: return | ||||||||||||||||||||||||||
| val listener = object : DisplayManager.DisplayListener { | ||||||||||||||||||||||||||
| override fun onDisplayAdded(displayId: Int) {} | ||||||||||||||||||||||||||
| override fun onDisplayRemoved(displayId: Int) {} | ||||||||||||||||||||||||||
|
Comment on lines
+822
to
+823
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||
| override fun onDisplayAdded(displayId: Int) {} | |
| override fun onDisplayRemoved(displayId: Int) {} | |
| override fun onDisplayAdded(displayId: Int) { | |
| (ui.xServerView as? View)?.post { | |
| refreshFpsLimit(ui.xServerView as? View, isX11Display = isX11Display) | |
| } | |
| } | |
| override fun onDisplayRemoved(displayId: Int) { | |
| (ui.xServerView as? View)?.post { | |
| refreshFpsLimit(ui.xServerView as? View, isX11Display = isX11Display) | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Register the panel listener from the AndroidView update path as well as factory; otherwise a cached XServer can skip this factory call and lose refresh-rate updates after the previous listener was released.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, line 2125:
<comment>Register the panel listener from the `AndroidView` `update` path as well as `factory`; otherwise a cached XServer can skip this factory call and lose refresh-rate updates after the previous listener was released.</comment>
<file context>
@@ -2078,29 +2113,17 @@ fun XServerScreen(
+ // Always refresh FPS Limit, Both X11 and Gamescope should refresh the fps limit when display is changed
+ refreshFpsLimit(xServerViewInstance as? View, isX11Display = sessionDisplay == SessionDisplay.X11)
+ ensurePanelRefreshListener()
+
val xServerView = xServerViewInstance.apply {
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: onRelease can unregister a newer session's process-global listener because it does not verify ownership. Associate the registration with the released view/session or unregister only when the released view still owns the current listener.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, line 3034:
<comment>`onRelease` can unregister a newer session's process-global listener because it does not verify ownership. Associate the registration with the released view/session or unregister only when the released view still owns the current listener.</comment>
<file context>
@@ -3006,6 +3029,10 @@ fun XServerScreen(
+ // A view released without exit() (the activity-destroyed fallback, a restart) still ends
+ // the session, so the panel listener goes with it; exit() calls this too, harmlessly.
+ releasePanelRefreshListener()
+
val releaseBinding = view.tag as? XServerViewReleaseBinding
</file context>
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -73,9 +73,7 @@ public XServerRenderer getRenderer() { | |||||||
| @Override | ||||||||
| public void surfaceCreated(@NonNull SurfaceHolder holder) { | ||||||||
| if (isGamescopeMode()) { | ||||||||
| // Seed from the panel's real size (getWidth/Height may predate layout); surfaceChanged | ||||||||
| // replaces it with the real holder size. The container preset is only a last resort: | ||||||||
| // it's an internal desktop resolution, and seeding it starts the daemon at 720p. | ||||||||
| // The window's size: the daemon's mode only when no scan-out geometry was named. | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This comment calls Prompt for AI agents
Suggested change
|
||||||||
| int[] defaultSize = AppUtils.parseScreenSize(PluviaApp.getNativeDisplaySize()); | ||||||||
| if (defaultSize == null) defaultSize = AppUtils.parseScreenSize(PluviaApp.getDefaultScreenSize()); | ||||||||
| if (defaultSize == null) { | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
git diff --unified=80 ce378f01894eb7cac2eff38b6132306f5489ff8b a8c57d23c75a7c68f63e690386b50fc9aa7dcd85 -- app/src/main/java/app/gamenative/display/GamescopeState.kt rg -n -C 8 'gamescopeOutputResolution|OUTPUT_RESOLUTION_(DEFAULT|OFF)|outputResolution\\(' app/src/main/javaRepository: utkarshdalal/GameNative
Length of output: 6776
Script executed:
Repository: utkarshdalal/GameNative
Length of output: 41868
Script executed:
Repository: utkarshdalal/GameNative
Length of output: 41728
Script executed:
Repository: utkarshdalal/GameNative
Length of output: 23315
Script executed:
Repository: utkarshdalal/GameNative
Length of output: 4995
Preserve legacy defaults without overwriting explicit choices.
A container created under the old default can persist
1920x1080without an explicit user selection. The currentoutputResolution()treats that value the same as an explicit1920x1080choice, so changingOUTPUT_RESOLUTION_DEFAULTdoes not affect those containers. Do not migrate every stored1920x1080tooff, because that would overwrite explicit selections. If legacy unselected containers must adoptoff, add provenance that can identify implicit values; the current string cannot support that migration safely.