diff --git a/app/src/main/java/app/gamenative/display/GameNativeDrm.kt b/app/src/main/java/app/gamenative/display/GameNativeDrm.kt index cdffd9b24d..ee8a996c25 100644 --- a/app/src/main/java/app/gamenative/display/GameNativeDrm.kt +++ b/app/src/main/java/app/gamenative/display/GameNativeDrm.kt @@ -33,6 +33,10 @@ object GameNativeDrm { @JvmStatic external fun nativeSetPanelRefreshHz(refreshHz: Int) + /** The scan-out size the daemon emulates its mode at, before [nativeStart]; `0, 0` clears it. */ + @JvmStatic + external fun nativeSetScanoutGeometry(width: Int, height: Int) + /** Start the daemon: create the SurfaceControl layer on [surface], then serve [socketName]. */ @JvmStatic external fun nativeStart(socketName: String, surface: Surface, width: Int, height: Int): Int diff --git a/app/src/main/java/app/gamenative/display/GamescopePayload.kt b/app/src/main/java/app/gamenative/display/GamescopePayload.kt index 8b6f0701fe..2a2845ea5b 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 = "20261030.1" + const val VERSION = "20261030.2" const val ARCHIVE = "gamescope-gamenative-$VERSION.tzst" private const val STAMP = ".payload.version" diff --git a/app/src/main/java/app/gamenative/display/GamescopeState.kt b/app/src/main/java/app/gamenative/display/GamescopeState.kt index f3014d1529..eee3108af7 100644 --- a/app/src/main/java/app/gamenative/display/GamescopeState.kt +++ b/app/src/main/java/app/gamenative/display/GamescopeState.kt @@ -26,15 +26,15 @@ object GamescopeSettings { /** Output resolution the container presents at, as "WxH", [OUTPUT_RESOLUTION_DEVICE] or [OUTPUT_RESOLUTION_OFF]. */ const val EXTRA_OUTPUT_RESOLUTION = "gamescopeOutputResolution" - /** 1080p — the output resolution a session presents at until the container picks another. */ - const val OUTPUT_RESOLUTION_DEFAULT = "1920x1080" - /** `-W`/`-H` follow the container's screen size: the game presents 1:1, nothing is resampled. */ const val OUTPUT_RESOLUTION_OFF = "off" /** `-W`/`-H` follow the panel's real pixel size (the pre-setting behaviour). */ const val OUTPUT_RESOLUTION_DEVICE = "device" + /** The default: no upscale — `-W/-H` follow the container's screen size, so the session presents 1:1. */ + const val OUTPUT_RESOLUTION_DEFAULT = OUTPUT_RESOLUTION_OFF + // Sharpness is stored in the UI's own units: 0..SHARPNESS_STEPS, higher = sharper — the scale the // QuickMenu row shows. const val SHARPNESS_STEPS = 5 @@ -88,6 +88,11 @@ object GamescopeSettings { return " -W ${size[0]} -H ${size[1]}" } + /** The session's scan-out size — [outputResolution] read back as `[w, h]`, or null if unreadable. */ + @JvmStatic + fun scanoutSize(container: Container?): IntArray? = + AppUtils.parseScreenSize(outputResolution(container)) + /** * The session's X socket — gamescope's own Xwayland, whose root window the live scaling-mode * property is set on. diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt index ba0aefe2b4..df4439a0b7 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt @@ -125,8 +125,7 @@ fun GraphicsTabContent(state: ContainerConfigState, default: Boolean = false) { } }, ) - // Upscaling resolution gamescope presents at (-W/-H): Disabled follows the container screen size, Device's - // follows the panel, and the rest are the General tab's resolutions — whose 1920x1080 entry is the default. + // Upscaling resolution: the session's scan-out size (-W/-H and the daemon's emulated mode); Disabled follows the container screen size. if (!isX11Display) { val outputResolutionLabels = stringArrayResource(R.array.screen_size_entries).drop(1).reversed() val outputResolutionValues = listOf( 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 13dff9d07f..8d7791c7b0 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 @@ -311,27 +311,28 @@ 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 + ?.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 gamescope session runs. */ +private var gamescopePanelRefreshListener: Pair? = null + private data class XServerViewReleaseBinding( val xServerView: XServerRendererView, val windowModificationListener: WindowManager.OnWindowModificationListener, @@ -804,7 +805,7 @@ fun XServerScreen( true } // Scaled here, not in the resolver, which must keep returning the display's real rate. - val detectedMax = detectMaxRefreshRateHz(context, ui.xServerView as? View) * FPS_LIMITER_MAX_MULTIPLIER + val detectedMax = detectRefreshRateHz(context, ui.xServerView as? View) * FPS_LIMITER_MAX_MULTIPLIER ui.detectedMaxRefreshRateHz = detectedMax val clampedTarget = fpsLimiterTarget.coerceAtMost(detectedMax).coerceAtLeast(5) if (clampedTarget != fpsLimiterTarget) { @@ -2073,9 +2074,26 @@ fun XServerScreen( EnvVars(container.envVars).get("GNDRM_SCANOUT_LAYOUT"), ) xServerToUse.setInputEventSink(GamescopeInputSink()) + // Ahead of nativeStart: the daemon's mode is the Upscaling Resolution. + GamescopeSettings.scanoutSize(container)?.let { + GameNativeDrm.nativeSetScanoutGeometry(it[0], it[1]) + } ?: GameNativeDrm.nativeSetScanoutGeometry(0, 0) // Ahead of nativeStart, which fixes the emulated mode's rate from this // view's surface callback, so the panel's real rate must be set first. - GameNativeDrm.nativeSetPanelRefreshHz(detectMaxRefreshRateHz(context, xServerViewInstance as? View)) + // The active rate, and again whenever the display changes mode. + GameNativeDrm.nativeSetPanelRefreshHz(detectRefreshRateHz(context, xServerViewInstance as? View)) + val displayManager = ContextCompat.getSystemService(context, DisplayManager::class.java) + if (displayManager != null && gamescopePanelRefreshListener == null) { + val listener = object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) {} + override fun onDisplayRemoved(displayId: Int) {} + override fun onDisplayChanged(displayId: Int) { + GameNativeDrm.nativeSetPanelRefreshHz(detectRefreshRateHz(context, xServerViewInstance as? View)) + } + } + displayManager.registerDisplayListener(listener, Handler(Looper.getMainLooper())) + gamescopePanelRefreshListener = displayManager to listener + } // The presenter opens its frame-generation session on the first // present, from whatever the daemon holds: without this the daemon // has nothing until the first Quick Menu change. @@ -5369,6 +5387,10 @@ private fun exit( } // The gndrm daemon (gamescope display mode) is torn down only at session exit: a display power // cycle retargets the surface in XServerView instead of stopping it. + gamescopePanelRefreshListener?.let { (displayManager, listener) -> + displayManager.unregisterDisplayListener(listener) + gamescopePanelRefreshListener = null + } GameNativeDrm.nativeStop() PluviaApp.shutdownEnvironment() EaLaunchSupport.stop() diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreenState.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreenState.kt index f6e4f14055..36140e976a 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreenState.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreenState.kt @@ -74,7 +74,7 @@ internal class XServerScreenState(container: Container, context: Context) { var hasInternalTouchpad by mutableStateOf(false) var hasUpdatedScreenGamepad by mutableStateOf(false) var isPerformanceHudEnabled by mutableStateOf(PrefManager.showFps) - var detectedMaxRefreshRateHz by mutableIntStateOf(detectMaxRefreshRateHz(context, null)) + var detectedMaxRefreshRateHz by mutableIntStateOf(detectRefreshRateHz(context, null)) var performanceHudConfig by mutableStateOf(loadPerformanceHudConfig()) var performanceHudView by mutableStateOf(null) var performanceHudHost by mutableStateOf(null) diff --git a/app/src/main/java/com/winlator/widget/XServerView.java b/app/src/main/java/com/winlator/widget/XServerView.java index be74e0498a..ae8c0eda14 100644 --- a/app/src/main/java/com/winlator/widget/XServerView.java +++ b/app/src/main/java/com/winlator/widget/XServerView.java @@ -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. int[] defaultSize = AppUtils.parseScreenSize(PluviaApp.getNativeDisplaySize()); if (defaultSize == null) defaultSize = AppUtils.parseScreenSize(PluviaApp.getDefaultScreenSize()); if (defaultSize == null) { diff --git a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java index 293e25cac7..b9a11d1a22 100644 --- a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java +++ b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java @@ -553,9 +553,8 @@ private void applyGamescopeEnv(Context context, EnvVars envVars, File rootDir) { if (container != null && container.getGraphicsDriver() != null) envVars.put("GNDRM_DRIVER_TYPE", container.getGraphicsDriver()); - // Same source as gamescope's -W/-H, so the two cannot drift. The nested pair is seeded from the - // same value the -w/-h arguments are formatted from (see gamescopeNestedSize). - int[] scanoutOutput = AppUtils.parseScreenSize(GamescopeSettings.outputResolution(container)); + // Same source as gamescope's -W/-H and the daemon's mode; the panel is the fallback when the resolution is unreadable. + int[] scanoutOutput = GamescopeSettings.scanoutSize(container); if (scanoutOutput == null) scanoutOutput = displaySize(); if (scanoutOutput == null) @@ -750,11 +749,7 @@ private int[] gamescopeNestedSize() { return AppUtils.parseScreenSize(container != null ? container.getScreenSize() : null); } - /** - * The panel's real pixel size, or null if unreadable. This is what scanout needs: the - * container's screen size is an internal desktop resolution, so using it here would make - * gamescope present at 720p (or whatever preset) and resample onto the panel. - */ + /** The panel's real pixel size, or null if unreadable; only the shim seed's last-resort fallback. */ private static int[] displaySize() { int[] nativeSize = AppUtils.parseScreenSize(PluviaApp.getNativeDisplaySize()); if (nativeSize != null) return nativeSize; diff --git a/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so b/app/src/main/jniLibs/arm64-v8a/libgamenative_drm.so index 79f6cd490e..be53191ab2 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