Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ namespace config {
{} // wa
}, // display_device

0, // manual_rotation

0, // max_bitrate
0 // minimum_fps_target (0 = framerate)
};
Expand Down Expand Up @@ -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});

Expand Down
2 changes: 2 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
};
Expand Down
42 changes: 39 additions & 3 deletions src/platform/linux/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -739,10 +740,19 @@ namespace egl {

sws.serial = std::numeric_limits<std::uint64_t>::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;
Expand Down Expand Up @@ -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<const char *, std::string_view> members[] {
std::make_pair("color_vec_y", util::view(color_p->color_vec_y)),
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src_assets/common/assets/web/configs/tabs/AudioVideo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ const config = ref(props.config)
:config="config"
/>

<!-- Manual Rotation (Linux only) -->
<div class="mb-3" v-if="platform === 'linux'">
<label for="manual_rotation" class="form-label">{{ $t('config.manual_rotation') }}</label>
<select id="manual_rotation" class="form-select" v-model="config.manual_rotation">
<option value="0">{{ $t('config.manual_rotation_0') }}</option>
<option value="90">{{ $t('config.manual_rotation_90') }}</option>
<option value="180">{{ $t('config.manual_rotation_180') }}</option>
<option value="270">{{ $t('config.manual_rotation_270') }}</option>
</select>
<div class="form-text">{{ $t('config.manual_rotation_desc') }}</div>
</div>

<DisplayDeviceOptions
:platform="platform"
:config="config"
Expand Down
6 changes: 6 additions & 0 deletions src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@
"locale_desc": "The locale used for Sunshine's user interface.",
"log_path": "Logfile Path",
"log_path_desc": "The file where the current logs of Sunshine are stored.",
"manual_rotation": "Manual Display Rotation",
"manual_rotation_0": "0° (No rotation)",
"manual_rotation_90": "90° (Clockwise)",
"manual_rotation_180": "180° (Upside down)",
"manual_rotation_270": "270° (Counter-clockwise)",
"manual_rotation_desc": "Manually rotate the captured display output. Useful for portrait panels used in landscape orientation (e.g. Steam Deck) where KMS reports the raw panel resolution instead of the logical orientation.",
"max_bitrate": "Maximum Bitrate",
"max_bitrate_desc": "The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.",
"minimum_fps_target": "Minimum FPS Target",
Expand Down
27 changes: 22 additions & 5 deletions src_assets/linux/assets/shaders/opengl/ConvertUV.vert
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@ precision mediump float;
#endif

uniform float width_i;
uniform int rotation;

out vec3 uuv;

// Note: duplicated in Scene.vert (no shader include mechanism available)
vec2 rotate_uv(vec2 uv, int rot) {
if (rot == 90) {
return vec2(uv.y, 1.0 - uv.x);
} else if (rot == 180) {
return vec2(1.0 - uv.x, 1.0 - uv.y);
} else if (rot == 270) {
return vec2(1.0 - uv.y, uv.x);
}
return uv;
}

//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
Expand All @@ -18,10 +32,13 @@ void main()
float x = idHigh * 4.0 - 1.0;
float y = idLow * 4.0 - 1.0;

float u_right = idHigh * 2.0;
float u_left = u_right - width_i;
float v = idLow * 2.0;
float u_base = idHigh * 2.0;
float v_base = idLow * 2.0;

// Apply rotation to texture coordinates
vec2 uv_right = rotate_uv(vec2(u_base, v_base), rotation);
vec2 uv_left = rotate_uv(vec2(u_base - width_i, v_base), rotation);

uuv = vec3(u_left, u_right, v);
uuv = vec3(uv_left.x, uv_right.x, uv_right.y);
gl_Position = vec4(x, y, 0.0, 1.0);
}
}
18 changes: 16 additions & 2 deletions src_assets/linux/assets/shaders/opengl/Scene.vert
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@
precision mediump float;
#endif

uniform int rotation;

out vec2 tex;

// Note: duplicated in ConvertUV.vert (no shader include mechanism available)
vec2 rotate_uv(vec2 uv, int rot) {
if (rot == 90) {
return vec2(uv.y, 1.0 - uv.x);
} else if (rot == 180) {
return vec2(1.0 - uv.x, 1.0 - uv.y);
} else if (rot == 270) {
return vec2(1.0 - uv.y, uv.x);
}
return uv;
}

void main()
{
float idHigh = float(gl_VertexID >> 1);
Expand All @@ -18,5 +32,5 @@ void main()
float v = idLow * 2.0;

gl_Position = vec4(x, y, 0.0, 1.0);
tex = vec2(u, v);
}
tex = rotate_uv(vec2(u, v), rotation);
}
73 changes: 73 additions & 0 deletions tests/unit/test_config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @file tests/unit/test_config.cpp
* @brief Test src/config.cpp
*/
#include "../tests_common.h"

// standard includes
#include <string>
#include <unordered_map>

// local includes
#include <src/config.h>

// Forward-declare the internal apply_config function for testing
namespace config {
void apply_config(std::unordered_map<std::string, std::string> &&vars);

Check warning on line 16 in tests/unit/test_config.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the transparent equality "std::equal_to<>" and a custom transparent heterogeneous hasher with this associative string container.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ0JeozyNJS_9PPJdMc0&open=AZ0JeozyNJS_9PPJdMc0&pullRequest=4877
}

class ManualRotationTest: public ::testing::TestWithParam<std::pair<std::string, int>> {
protected:
void SetUp() override {
// Reset to default before each test
config::video.manual_rotation = 0;
}
};

TEST_P(ManualRotationTest, ParsesRotationValues) {
auto [input, expected] = GetParam();

Check warning on line 28 in tests/unit/test_config.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid this unnecessary copy by using a "const" reference.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ0JeozyNJS_9PPJdMc1&open=AZ0JeozyNJS_9PPJdMc1&pullRequest=4877

std::unordered_map<std::string, std::string> vars;

Check warning on line 30 in tests/unit/test_config.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the transparent equality "std::equal_to<>" and a custom transparent heterogeneous hasher with this associative string container.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ0JeozyNJS_9PPJdMc2&open=AZ0JeozyNJS_9PPJdMc2&pullRequest=4877
vars["manual_rotation"] = input;
config::apply_config(std::move(vars));

EXPECT_EQ(config::video.manual_rotation, expected);
}

INSTANTIATE_TEST_SUITE_P(
ConfigTests,
ManualRotationTest,
testing::Values(
// Valid rotation values
std::make_pair("0", 0),
std::make_pair("90", 90),
std::make_pair("180", 180),
std::make_pair("270", 270),
// Invalid values should normalize to 0
std::make_pair("45", 0),
std::make_pair("360", 0),
std::make_pair("-90", 0),
std::make_pair("1", 0),
std::make_pair("abc", 0)
),
[](const testing::TestParamInfo<ManualRotationTest::ParamType> &info) {
auto input = info.param.first;
// Replace non-alphanumeric chars for valid test name
std::replace_if(
input.begin(), input.end(),
[](char c) { return !std::isalnum(c); },
'_'
);
return "rotation_" + input;
}
);

TEST(ManualRotationDefaultTest, DefaultIsZero) {
// Reset config and apply empty vars to verify default
config::video.manual_rotation = 999;
std::unordered_map<std::string, std::string> vars;

Check warning on line 68 in tests/unit/test_config.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the transparent equality "std::equal_to<>" and a custom transparent heterogeneous hasher with this associative string container.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ0JeozyNJS_9PPJdMc3&open=AZ0JeozyNJS_9PPJdMc3&pullRequest=4877
vars["manual_rotation"] = "0";
config::apply_config(std::move(vars));

EXPECT_EQ(config::video.manual_rotation, 0);
}