From c222e5f30cdd15173d2356cd1a0e644c3d56b308 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Sun, 21 Jun 2026 13:30:25 -0700 Subject: [PATCH 1/2] Add composited output rotation for displays that can't rotate at scanout Attempting to use --force-orientation on drivers that can't rotate planes at scanout (i915 can't do 90/270, some ARM devices can't rotate at all) leaves gamescope without any output when an unsupported rotation is scanned out. To work around this, bake a rotation pass into our composite scenegraph to allow rotation through composition rather than only at scanout. The scene is laid out in logical space and only the final store coordinate is rotated into a physically-oriented output image, so sampling, blending, input, cursor and EDID are untouched. It engages automatically when the primary plane can't do the needed 90/180/270 rotation, or via --force-composition-rotation. Hardware that rotates at scanout is unaffected. Screenshots and the upscale cache stay logical-sized and unrotated. --- src/Backends/DRMBackend.cpp | 48 +++++++++++++++++++++++-- src/main.cpp | 7 ++++ src/main.hpp | 3 ++ src/rendervulkan.cpp | 40 ++++++++++++++------- src/shaders/blit_push_data.h | 2 ++ src/shaders/composite.h | 21 +++++++++-- src/shaders/cs_composite_blit.comp | 6 ++-- src/shaders/cs_composite_blur.comp | 6 ++-- src/shaders/cs_composite_blur_cond.comp | 6 ++-- src/shaders/cs_composite_rcas.comp | 6 ++-- src/steamcompmgr.cpp | 9 ++++- 11 files changed, 126 insertions(+), 28 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 134e19ac5f..716f9b48e1 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -267,6 +267,7 @@ namespace gamescope static std::optional Instantiate( const char *pszName, CDRMAtomicObject *pObject, const DRMObjectRawProperties& rawProperties ); + uint32_t GetPropertyId() const { return m_uPropertyId; } uint64_t GetPendingValue() const { return m_ulPendingValue; } uint64_t GetCurrentValue() const { return m_ulCurrentValue; } uint64_t GetInitialValue() const { return m_ulInitialValue; } @@ -1934,7 +1935,7 @@ LiftoffStateCacheEntry FrameInfoToLiftoffStateCacheEntry( struct drm_t *drm, con uint64_t crtcW = srcWidth / frameInfo->layers[ i ].scale.x; uint64_t crtcH = srcHeight / frameInfo->layers[ i ].scale.y; - if (g_bRotated) + if (g_bRotated && g_uOutputRotation == 0) { int64_t imageH = frameInfo->layers[ i ].tex->contentHeight() / frameInfo->layers[ i ].scale.y; @@ -2668,7 +2669,7 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_H", entry.layerState[i].srcH ); uint64_t ulOrientation = DRM_MODE_ROTATE_0; - switch ( drm->pConnector->GetCurrentOrientation() ) + switch ( g_uOutputRotation != 0 ? GAMESCOPE_PANEL_ORIENTATION_0 : drm->pConnector->GetCurrentOrientation() ) { default: case GAMESCOPE_PANEL_ORIENTATION_0: @@ -3278,6 +3279,29 @@ static void drm_unset_mode( struct drm_t *drm ) g_nDynamicRefreshHz = 0; g_bRotated = false; + g_uOutputRotation = 0; +} + +// Bitmask of DRM_MODE_ROTATE_* the plane can do at scanout (just ROTATE_0 if it can't rotate). +static uint64_t drm_plane_supported_rotations( struct drm_t *drm, gamescope::CDRMPlane *pPlane ) +{ + if ( !pPlane->GetProperties().rotation ) + return DRM_MODE_ROTATE_0; + + drmModePropertyRes *pProp = drmModeGetProperty( drm->fd, pPlane->GetProperties().rotation->GetPropertyId() ); + if ( !pProp ) + return DRM_MODE_ROTATE_0; + defer( drmModeFreeProperty( pProp ) ); + + if ( !( pProp->flags & DRM_MODE_PROP_BITMASK ) ) + return DRM_MODE_ROTATE_0; + + uint64_t ulSupported = 0; + for ( int i = 0; i < pProp->count_enums; i++ ) + if ( pProp->enums[i].value < 64 ) + ulSupported |= 1ull << pProp->enums[i].value; + + return ulSupported; } bool drm_set_mode( struct drm_t *drm, const drmModeModeInfo *mode ) @@ -3312,6 +3336,25 @@ bool drm_set_mode( struct drm_t *drm, const drmModeModeInfo *mode ) break; } + // Rotate in the compositor when the scanout plane can't do the panel's + // orientation. 90/270 transpose the output (g_bRotated); 180 flips in place. + g_uOutputRotation = 0; + uint32_t uStep = 0; + uint64_t uNeeded = 0; + switch ( drm->pConnector->GetCurrentOrientation() ) + { + case GAMESCOPE_PANEL_ORIENTATION_90: uStep = 1u; uNeeded = DRM_MODE_ROTATE_90; break; + case GAMESCOPE_PANEL_ORIENTATION_180: uStep = 2u; uNeeded = DRM_MODE_ROTATE_180; break; + case GAMESCOPE_PANEL_ORIENTATION_270: uStep = 3u; uNeeded = DRM_MODE_ROTATE_270; break; + default: break; + } + if ( uStep ) + { + const bool bScanoutCanRotate = drm->pPrimaryPlane && ( drm_plane_supported_rotations( drm, drm->pPrimaryPlane ) & uNeeded ); + if ( g_bForceCompositionRotation || !bScanoutCanRotate ) + g_uOutputRotation = uStep; + } + return true; } @@ -3566,6 +3609,7 @@ namespace gamescope } bNeedsFullComposite |= !!(g_uCompositeDebug & CompositeDebugFlag::Heatmap); + bNeedsFullComposite |= g_uOutputRotation != 0; // can't rotate planes at scanout bool bDoComposite = true; if ( !bNeedsFullComposite && !bWantsPartialComposite ) diff --git a/src/main.cpp b/src/main.cpp index 9fc54f0de3..968191b3d7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -136,6 +136,7 @@ const struct option *gamescope_options = (struct option[]){ { "composite-debug", no_argument, nullptr, 0 }, { "disable-xres", no_argument, nullptr, 'x' }, { "fade-out-duration", required_argument, nullptr, 0 }, + { "force-composition-rotation", no_argument, nullptr, 0 }, { "force-orientation", required_argument, nullptr, 0 }, { "force-windows-fullscreen", no_argument, nullptr, 0 }, @@ -201,6 +202,7 @@ const char usage[] = " -e, --steam enable Steam integration\n" " --xwayland-count create N xwayland servers\n" " --prefer-vk-device prefer Vulkan device for compositing (ex: 1002:7300)\n" + " --force-composition-rotation always rotate the output in the compositor instead of at scanout (autodetected otherwise)\n" " --force-orientation rotate the internal display (left, right, normal, upsidedown)\n" " --force-windows-fullscreen force windows inside of gamescope to be the size of the nested display (fullscreen)\n" " --cursor-scale-height if specified, sets a base output height to linearly scale the cursor against.\n" @@ -366,6 +368,9 @@ static gamescope::GamescopeModeGeneration parse_gamescope_mode_generation( const } } +bool g_bForceCompositionRotation = false; +uint32_t g_uOutputRotation = 0; + GamescopePanelOrientation g_DesiredInternalOrientation = GAMESCOPE_PANEL_ORIENTATION_AUTO; static GamescopePanelOrientation force_orientation(const char *str) { @@ -798,6 +803,8 @@ int main(int argc, char **argv) gamescope::cv_touch_click_mode = (gamescope::TouchClickMode) parse_integer( optarg, opt_name ); } else if (strcmp(opt_name, "generate-drm-mode") == 0) { g_eGamescopeModeGeneration = parse_gamescope_mode_generation( optarg ); + } else if (strcmp(opt_name, "force-composition-rotation") == 0) { + g_bForceCompositionRotation = true; } else if (strcmp(opt_name, "force-orientation") == 0) { g_DesiredInternalOrientation = force_orientation( optarg ); } else if (strcmp(opt_name, "sharpness") == 0 || diff --git a/src/main.hpp b/src/main.hpp index 45c5ec5a87..b4fc9027d7 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -22,6 +22,9 @@ extern int g_nOutputRefresh; // mHz extern bool g_bOutputHDREnabled; extern bool g_bForceInternal; +extern bool g_bForceCompositionRotation; +extern uint32_t g_uOutputRotation; + extern bool g_bFullscreen; extern bool g_bGrabbed; diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp index aefc6b21e9..83d1f12ee0 100644 --- a/src/rendervulkan.cpp +++ b/src/rendervulkan.cpp @@ -3300,8 +3300,14 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) uint32_t uDRMFormat = pOutput->uOutputFormat; + // Output images are physical-oriented; the panel scans out unrotated. + uint32_t uOutputWidth = g_nOutputWidth; + uint32_t uOutputHeight = g_nOutputHeight; + if ( g_uOutputRotation & 1u ) + std::swap( uOutputWidth, uOutputHeight ); + pOutput->outputImages[0] = new CVulkanTexture(); - bool bSuccess = pOutput->outputImages[0]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); + bool bSuccess = pOutput->outputImages[0]->BInit( uOutputWidth, uOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3309,7 +3315,7 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) } pOutput->outputImages[1] = new CVulkanTexture(); - bSuccess = pOutput->outputImages[1]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); + bSuccess = pOutput->outputImages[1]->BInit( uOutputWidth, uOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3317,7 +3323,7 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) } pOutput->outputImages[2] = new CVulkanTexture(); - bSuccess = pOutput->outputImages[2]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); + bSuccess = pOutput->outputImages[2]->BInit( uOutputWidth, uOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3332,7 +3338,7 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) uint32_t uPartialDRMFormat = pOutput->uOutputFormatOverlay; pOutput->outputImagesPartialOverlay[0] = new CVulkanTexture(); - bool bSuccess = pOutput->outputImagesPartialOverlay[0]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[0].get() ); + bool bSuccess = pOutput->outputImagesPartialOverlay[0]->BInit( uOutputWidth, uOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[0].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3340,7 +3346,7 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) } pOutput->outputImagesPartialOverlay[1] = new CVulkanTexture(); - bSuccess = pOutput->outputImagesPartialOverlay[1]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[1].get() ); + bSuccess = pOutput->outputImagesPartialOverlay[1]->BInit( uOutputWidth, uOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[1].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3348,7 +3354,7 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) } pOutput->outputImagesPartialOverlay[2] = new CVulkanTexture(); - bSuccess = pOutput->outputImagesPartialOverlay[2]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[2].get() ); + bSuccess = pOutput->outputImagesPartialOverlay[2]->BInit( uOutputWidth, uOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[2].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); @@ -3703,10 +3709,13 @@ struct BlitPushData_t float u_itmSdrNits; // unset float u_itmTargetNits; // unset - explicit BlitPushData_t(const struct FrameInfo_t *frameInfo) + uint32_t u_rotation; + + explicit BlitPushData_t(const struct FrameInfo_t *frameInfo, uint32_t rotation = 0) { u_shaderFilter = 0; u_alphaMode = 0; + u_rotation = rotation; for (int i = 0; i < frameInfo->layerCount; i++) { const FrameInfo_t::Layer_t *layer = &frameInfo->layers[i]; @@ -3751,6 +3760,7 @@ struct BlitPushData_t opacity[0] = 1.0f; u_shaderFilter = (uint32_t)GamescopeUpscaleFilter::LINEAR; u_alphaMode = 0; + u_rotation = 0; ctm[0] = glm::mat3x4 { 1, 0, 0, 0, @@ -3837,10 +3847,13 @@ struct RcasPushData_t float u_itmSdrNits; // unset float u_itmTargetNits; // unset - RcasPushData_t(const struct FrameInfo_t *frameInfo, float sharpness) + uint32_t u_rotation; + + RcasPushData_t(const struct FrameInfo_t *frameInfo, float sharpness, uint32_t rotation = 0) { uvec4_t tmp; FsrRcasCon(&tmp.x, sharpness); + u_rotation = rotation; u_layer0Offset.x = uint32_t(int32_t(frameInfo->layers[0].offset.x)); u_layer0Offset.y = uint32_t(int32_t(frameInfo->layers[0].offset.y)); u_borderMask = frameInfo->borderMask() >> 1u; @@ -4031,6 +4044,9 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco else compositeImage = partial ? g_output.outputImagesPartialOverlay[ g_output.nOutImage ] : g_output.outputImages[ g_output.nOutImage ]; + // Overrides (screenshots, upscale cache) are logical-sized, so never rotated. + const uint32_t uOutputRotation = pOutputOverride ? 0u : g_uOutputRotation; + auto cmdBuffer = pInCommandBuffer ? std::move( pInCommandBuffer ) : g_device.commandBuffer(); for (uint32_t i = 0; i < EOTF_Count; i++) @@ -4065,7 +4081,7 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco cmdBuffer->setSamplerUnnormalized(0, false); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->bindTarget(compositeImage); - cmdBuffer->uploadConstants(frameInfo, g_upscaleFilterSharpness / 10.0f); + cmdBuffer->uploadConstants(frameInfo, g_upscaleFilterSharpness / 10.0f, uOutputRotation); cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); } @@ -4108,7 +4124,7 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco cmdBuffer->bindPipeline( g_device.pipeline(SHADER_TYPE_BLIT, nisFrameInfo.layerCount, nisFrameInfo.ycbcrMask(), 0u, nisFrameInfo.colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), &nisFrameInfo); cmdBuffer->bindTarget(compositeImage); - cmdBuffer->uploadConstants(&nisFrameInfo); + cmdBuffer->uploadConstants(&nisFrameInfo, uOutputRotation); int pixelsPerGroup = 8; @@ -4134,7 +4150,7 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco cmdBuffer->setSamplerUnnormalized(i, true); cmdBuffer->setSamplerNearest(i, false); } - cmdBuffer->uploadConstants(frameInfo); + cmdBuffer->uploadConstants(frameInfo, uOutputRotation); int pixelsPerGroup = 8; @@ -4158,7 +4174,7 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco cmdBuffer->bindPipeline( g_device.pipeline(SHADER_TYPE_BLIT, frameInfo->layerCount, frameInfo->ycbcrMask(), 0u, frameInfo->colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), frameInfo); cmdBuffer->bindTarget(compositeImage); - cmdBuffer->uploadConstants(frameInfo); + cmdBuffer->uploadConstants(frameInfo, uOutputRotation); const int pixelsPerGroup = 8; diff --git a/src/shaders/blit_push_data.h b/src/shaders/blit_push_data.h index caff0f3781..f81096b9cd 100644 --- a/src/shaders/blit_push_data.h +++ b/src/shaders/blit_push_data.h @@ -16,5 +16,7 @@ uniform layers_t { float u_nitsToLinear; // hdr -> sdr float u_itmSdrNits; float u_itmTargetNits; + + uint u_rotation; }; diff --git a/src/shaders/composite.h b/src/shaders/composite.h index 961379b0ab..229296e320 100644 --- a/src/shaders/composite.h +++ b/src/shaders/composite.h @@ -3,6 +3,23 @@ #include "shaderfilter.h" #include "alphamode.h" +// Rotate only the final store coordinate (CCW 90-degree steps: 1=90, 2=180, +// 3=270); the scene stays logical so sampling and blending are unchanged. +uvec2 outputLogicalSize(uint rotation) { + uvec2 size = imageSize(dst); + return (rotation & 1u) != 0u ? uvec2(size.y, size.x) : size; +} + +ivec2 rotateOutputCoord(uvec2 coord, uint rotation) { + uvec2 size = imageSize(dst); + switch (rotation) { + case 1u: return ivec2(coord.y, size.y - 1u - coord.x); + case 2u: return ivec2(size.x - 1u - coord.x, size.y - 1u - coord.y); + case 3u: return ivec2(size.x - 1u - coord.y, coord.x); + default: return ivec2(coord); + } +} + vec4 sampleRegular(sampler2D tex, vec2 coord, uint colorspace) { vec4 color = textureLod(tex, coord, 0); color.rgb = colorspace_plane_degamma_tf(color.rgb, colorspace); @@ -50,7 +67,7 @@ uint pseudo_random(uint seed) { return seed * 1664525u + 1013904223u; } -void compositing_debug(uvec2 coord) { +void compositing_debug(uvec2 coord, uint rotation) { uvec2 pos = coord; pos.x -= (u_frameId & 2) != 0 ? 128 : 0; pos.y -= (u_frameId & 1) != 0 ? 128 : 0; @@ -66,7 +83,7 @@ void compositing_debug(uvec2 coord) { if (time.x + time.y + time.z + time.w < 2.0f) value = vec4(0.0f, 0.0f, 0.0f, 1.0f); } - imageStore(dst, ivec2(coord), value); + imageStore(dst, rotateOutputCoord(coord, rotation), value); } } diff --git a/src/shaders/cs_composite_blit.comp b/src/shaders/cs_composite_blit.comp index b728d46c0e..4d974dcb15 100644 --- a/src/shaders/cs_composite_blit.comp +++ b/src/shaders/cs_composite_blit.comp @@ -21,7 +21,7 @@ vec4 sampleLayer(uint layerIdx, vec2 uv) { void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); - uvec2 outSize = imageSize(dst); + uvec2 outSize = outputLogicalSize(u_rotation); if (coord.x >= outSize.x || coord.y >= outSize.y) return; @@ -42,9 +42,9 @@ void main() { } outputValue.rgb = encodeOutputColor(outputValue.rgb); - imageStore(dst, ivec2(coord), outputValue); + imageStore(dst, rotateOutputCoord(coord, u_rotation), outputValue); // Indicator to quickly tell if we're in the compositing path or not. if (checkDebugFlag(compositedebug_Markers)) - compositing_debug(coord); + compositing_debug(coord, u_rotation); } diff --git a/src/shaders/cs_composite_blur.comp b/src/shaders/cs_composite_blur.comp index 2132b032c4..318b0e9f1e 100644 --- a/src/shaders/cs_composite_blur.comp +++ b/src/shaders/cs_composite_blur.comp @@ -24,7 +24,7 @@ vec4 sampleLayer(uint layerIdx, vec2 uv) { void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); - uvec2 outSize = imageSize(dst); + uvec2 outSize = outputLogicalSize(u_rotation); if (coord.x >= outSize.x || coord.y >= outSize.y) return; @@ -44,8 +44,8 @@ void main() { } outputValue = encodeOutputColor(outputValue); - imageStore(dst, ivec2(coord), vec4(outputValue, 0)); + imageStore(dst, rotateOutputCoord(coord, u_rotation), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) - compositing_debug(coord); + compositing_debug(coord, u_rotation); } diff --git a/src/shaders/cs_composite_blur_cond.comp b/src/shaders/cs_composite_blur_cond.comp index 9a997bb33a..39b040554b 100644 --- a/src/shaders/cs_composite_blur_cond.comp +++ b/src/shaders/cs_composite_blur_cond.comp @@ -24,7 +24,7 @@ vec4 sampleLayer(uint layerIdx, vec2 uv) { void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); - uvec2 outSize = imageSize(dst); + uvec2 outSize = outputLogicalSize(u_rotation); if (coord.x >= outSize.x || coord.y >= outSize.y) return; @@ -61,8 +61,8 @@ void main() { } outputValue = encodeOutputColor(outputValue); - imageStore(dst, ivec2(coord), vec4(outputValue, 0)); + imageStore(dst, rotateOutputCoord(coord, u_rotation), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) - compositing_debug(coord); + compositing_debug(coord, u_rotation); } diff --git a/src/shaders/cs_composite_rcas.comp b/src/shaders/cs_composite_rcas.comp index 20cad55179..3caefb1487 100644 --- a/src/shaders/cs_composite_rcas.comp +++ b/src/shaders/cs_composite_rcas.comp @@ -29,6 +29,8 @@ uniform layers_t { float u_nitsToLinear; float u_itmSdrNits; float u_itmTargetNits; + + uint u_rotation; }; #include "composite.h" @@ -89,10 +91,10 @@ void rcasComposite(uvec2 pos) } outputValue = encodeOutputColor(outputValue); - imageStore(dst, ivec2(pos), vec4(outputValue, 0)); + imageStore(dst, rotateOutputCoord(pos, u_rotation), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) - compositing_debug(pos); + compositing_debug(pos, u_rotation); } void main() diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index cfdfc3bc74..7a242d3e1d 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -892,6 +892,7 @@ global_focus_t *GetCurrentMouseFocus() uint32_t currentOutputWidth, currentOutputHeight; int currentOutputRefresh; +uint32_t currentOutputRotation = 0; bool currentHDROutput = false; bool currentHDRForce = false; @@ -2920,7 +2921,11 @@ paint_all( global_focus_t *pFocus, bool async ) std::optional oScreenshotSeq; if ( drmCaptureFormat == DRM_FORMAT_NV12 ) - oScreenshotSeq = vulkan_composite( &frameInfo, pScreenshotTexture, false, nullptr ); + { + // Logical-sized scratch keeps the capture unrotated and off the live output image. + gamescope::Rc pRGBTexture = vulkan_acquire_screenshot_texture( g_nOutputWidth, g_nOutputHeight, false, DRM_FORMAT_XRGB2101010 ); + oScreenshotSeq = vulkan_screenshot( &frameInfo, pRGBTexture, pScreenshotTexture ); + } else if ( oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_FULL_COMPOSITION || oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) oScreenshotSeq = vulkan_composite( &frameInfo, nullptr, false, pScreenshotTexture ); @@ -8704,6 +8709,7 @@ steamcompmgr_main(int argc, char **argv) if ( currentOutputWidth != g_nOutputWidth || currentOutputHeight != g_nOutputHeight || currentOutputRefresh != g_nOutputRefresh || + currentOutputRotation != g_uOutputRotation || currentHDROutput != g_bOutputHDREnabled || currentHDRForce != g_bForceHDRSupportDebug ) { @@ -8754,6 +8760,7 @@ steamcompmgr_main(int argc, char **argv) currentOutputWidth = g_nOutputWidth; currentOutputHeight = g_nOutputHeight; currentOutputRefresh = g_nOutputRefresh; + currentOutputRotation = g_uOutputRotation; currentHDROutput = g_bOutputHDREnabled; currentHDRForce = g_bForceHDRSupportDebug; From 0a0122c883258134ff32f15c46e6d9fb482cc506 Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Mon, 22 Jun 2026 23:08:15 -0700 Subject: [PATCH 2/2] steamcompmgr: classify the interactive overlay by output width, not a fixed 1200px The overlay-vs-notification heuristic gated the focusable overlay on a hardcoded width > 1200, so a full-screen QAM on a panel whose logical width is <= 1200 (e.g. a 1920x1200 panel in portrait) was misclassified as a notification and denied seat and cursor focus, breaking touch and controller input over a game. Key off the window spanning the output (>= root_width) or asking for input (STEAM_INPUT_FOCUS) instead. On landscape this tightens the gate from > 1200 to >= root_width, but the interactive overlay sets STEAM_INPUT_FOCUS so it still qualifies regardless of width. --- src/steamcompmgr.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index 7a242d3e1d..6b31076917 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -3755,7 +3755,9 @@ void xwayland_ctx_t::DetermineAndApplyFocus( const std::vector< steamcompmgr_win { if (w->isOverlay) { - if (w->GetGeometry().nWidth > 1200 && w->opacity >= maxOpacity) + // The interactive overlay (Steam/QAM) spans the full output width or asks + // for input. Anything narrower is a notification. + if (( w->GetGeometry().nWidth >= ctx->root_width || w->inputFocusMode ) && w->opacity >= maxOpacity) { ctx->focus.overlayWindow = w; maxOpacity = w->opacity; @@ -5907,7 +5909,7 @@ handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev) { if (w->isOverlay) { - if (w->GetGeometry().nWidth > 1200 && w->opacity >= maxOpacity) + if (( w->GetGeometry().nWidth >= ctx->root_width || w->inputFocusMode ) && w->opacity >= maxOpacity) { ctx->focus.overlayWindow = w; maxOpacity = w->opacity;