From 1cddf254beade64c64c63c47bea40a39ac5b3923 Mon Sep 17 00:00:00 2001 From: SamG Date: Thu, 19 Mar 2026 18:44:44 -0600 Subject: [PATCH 1/2] feat(linux): add manual display rotation setting Adds a manual_rotation config option (0, 90, 180, 270 degrees) for portrait panels used in landscape orientation (e.g. Steam Deck) where KMS reports raw panel resolution instead of logical orientation. --- src/config.cpp | 14 +++++++ src/config.h | 2 + src/platform/linux/graphics.cpp | 42 +++++++++++++++++-- .../assets/web/configs/tabs/AudioVideo.vue | 12 ++++++ .../assets/web/public/assets/locale/en.json | 6 +++ .../assets/shaders/opengl/ConvertUV.vert | 26 +++++++++--- .../linux/assets/shaders/opengl/Scene.vert | 17 +++++++- 7 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 47475a04b4d..9f5f0a6ede7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -504,6 +504,8 @@ namespace config { {} // wa }, // display_device + 0, // manual_rotation + 0, // max_bitrate 0 // minimum_fps_target (0 = framerate) }; @@ -1163,6 +1165,18 @@ namespace config { video.dd.wa.hdr_toggle_delay = std::chrono::milliseconds {value}; } + { + int rotation = 0; + int_f(vars, "manual_rotation", rotation); + // Normalize to valid rotation values + if (rotation == 90 || rotation == 180 || rotation == 270) { + video.manual_rotation = rotation; + } + else { + video.manual_rotation = 0; + } + } + int_f(vars, "max_bitrate", video.max_bitrate); double_between_f(vars, "minimum_fps_target", video.minimum_fps_target, {0.0, 1000.0}); diff --git a/src/config.h b/src/config.h index f683647f571..de9e9b8eae6 100644 --- a/src/config.h +++ b/src/config.h @@ -140,6 +140,8 @@ namespace config { workarounds_t wa; } dd; + int manual_rotation; ///< Manual display rotation in degrees (0, 90, 180, 270). Useful for portrait panels used in landscape orientation. + int max_bitrate; // Maximum bitrate, sets ceiling in kbps for bitrate requested from client double minimum_fps_target; ///< Lowest framerate that will be used when streaming. Range 0-1000, 0 = half of client's requested framerate. }; diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 38eb00717f6..b0d6b692393 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -7,6 +7,7 @@ // local includes #include "graphics.h" +#include "src/config.h" #include "src/file_handler.h" #include "src/logging.h" #include "src/video.h" @@ -739,10 +740,19 @@ namespace egl { sws.serial = std::numeric_limits::max(); + // When rotation is 90 or 270 degrees, swap input dimensions for aspect ratio calculation + // because the shader will rotate the texture, effectively swapping width and height + int effective_in_width = in_width; + int effective_in_height = in_height; + if (config::video.manual_rotation == 90 || config::video.manual_rotation == 270) { + effective_in_width = in_height; + effective_in_height = in_width; + } + // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); - auto out_width_f = in_width * scalar; - auto out_height_f = in_height * scalar; + auto scalar = std::fminf(out_width / (float) effective_in_width, out_height / (float) effective_in_height); + auto out_width_f = effective_in_width * scalar; + auto out_height_f = effective_in_height * scalar; // result is always positive auto offsetX_f = (out_width - out_width_f) / 2; @@ -831,6 +841,15 @@ namespace egl { gl::ctx.UseProgram(sws.program[1].handle()); gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); + // Set rotation uniform on UV shader (program[1]) + { + int rotation = config::video.manual_rotation; + auto loc_rotation = gl::ctx.GetUniformLocation(sws.program[1].handle(), "rotation"); + if (loc_rotation >= 0) { + gl::ctx.Uniform1i(loc_rotation, rotation); + } + } + auto color_p = video::color_vectors_from_colorspace({video::colorspace_e::rec601, false, 8}, true); std::pair members[] { std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), @@ -855,6 +874,23 @@ namespace egl { sws.program[0].bind(sws.color_matrix); sws.program[1].bind(sws.color_matrix); + // Set rotation uniform on Y shader (program[0]) and Scene/Cursor shader (program[2]) + { + int rotation = config::video.manual_rotation; + + gl::ctx.UseProgram(sws.program[0].handle()); + auto loc_rot_y = gl::ctx.GetUniformLocation(sws.program[0].handle(), "rotation"); + if (loc_rot_y >= 0) { + gl::ctx.Uniform1i(loc_rot_y, rotation); + } + + gl::ctx.UseProgram(sws.program[2].handle()); + auto loc_rot_scene = gl::ctx.GetUniformLocation(sws.program[2].handle(), "rotation"); + if (loc_rot_scene >= 0) { + gl::ctx.Uniform1i(loc_rot_scene, rotation); + } + } + gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); gl_drain_errors; diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index cc6a88110b3..8ecc9e32929 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -85,6 +85,18 @@ const config = ref(props.config) :config="config" /> + +
+ + +
{{ $t('config.manual_rotation_desc') }}
+
+ > 1); @@ -18,5 +31,5 @@ void main() float v = idLow * 2.0; gl_Position = vec4(x, y, 0.0, 1.0); - tex = vec2(u, v); -} \ No newline at end of file + tex = rotate_uv(vec2(u, v), rotation); +} From 2fabb1eaab8922dc1e6c98af769b1a8e81052aa8 Mon Sep 17 00:00:00 2001 From: SamG Date: Thu, 19 Mar 2026 22:05:51 -0600 Subject: [PATCH 2/2] Adhere to contribution guidelines: - Locale JSON keys sorted alphabetically - Web UShader duplication documented - Created tests/unit/test_config.cpp with parameterized tests covering: - All valid rotation values (0, 90, 180, 270) - Invalid values that should normalize to 0 (45, 360, -90, 1, non-numeric "abc") - Default value verification --- .../assets/web/configs/tabs/AudioVideo.vue | 4 +- .../assets/web/public/assets/locale/en.json | 12 +-- .../assets/shaders/opengl/ConvertUV.vert | 1 + .../linux/assets/shaders/opengl/Scene.vert | 1 + tests/unit/test_config.cpp | 73 +++++++++++++++++++ 5 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_config.cpp diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 8ecc9e32929..975339f856b 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -85,8 +85,8 @@ const config = ref(props.config) :config="config" /> - -
+ +