diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index c4ffd6ee3..256e3b742 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(common fifo_queue.h file_system.cpp file_system.h + gsvector.cpp gsvector.h gsvector_formatter.h gsvector_neon.h diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 5f99db87d..a8ebe6090 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -56,6 +56,7 @@ <ClCompile Include="error.cpp" /> <ClCompile Include="fastjmp.cpp" /> <ClCompile Include="file_system.cpp" /> + <ClCompile Include="gsvector.cpp" /> <ClCompile Include="layered_settings_interface.cpp" /> <ClCompile Include="log.cpp" /> <ClCompile Include="memmap.cpp" /> diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 474d06552..83084db04 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -78,6 +78,7 @@ </ClCompile> <ClCompile Include="dynamic_library.cpp" /> <ClCompile Include="binary_span_reader_writer.cpp" /> + <ClCompile Include="gsvector.cpp" /> </ItemGroup> <ItemGroup> <Natvis Include="bitfield.natvis" /> diff --git a/src/common/gsvector.cpp b/src/common/gsvector.cpp new file mode 100644 index 000000000..e1660b510 --- /dev/null +++ b/src/common/gsvector.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "gsvector.h" + +#include <cmath> + +GSMatrix2x2::GSMatrix2x2(float e00, float e01, float e10, float e11) +{ + E[0][0] = e00; + E[0][1] = e01; + E[1][0] = e10; + E[1][1] = e11; +} + +GSMatrix2x2 GSMatrix2x2::operator*(const GSMatrix2x2& m) const +{ + GSMatrix2x2 ret; + ret.E[0][0] = E[0][0] * m.E[0][0] + E[0][1] * m.E[1][0]; + ret.E[0][1] = E[0][0] * m.E[0][1] + E[0][1] * m.E[1][1]; + ret.E[1][0] = E[1][0] * m.E[0][0] + E[1][1] * m.E[1][0]; + ret.E[1][1] = E[1][0] * m.E[0][1] + E[1][1] * m.E[1][1]; + return ret; +} + +GSVector2 GSMatrix2x2::operator*(const GSVector2& v) const +{ + return GSVector2(row(0).dot(v), row(1).dot(v)); +} + +GSMatrix2x2 GSMatrix2x2::Identity() +{ + GSMatrix2x2 ret; + ret.E[0][0] = 1.0f; + ret.E[0][1] = 0.0f; + ret.E[1][0] = 0.0f; + ret.E[1][1] = 1.0f; + return ret; +} + +GSMatrix2x2 GSMatrix2x2::Rotation(float angle_in_radians) +{ + const float sin_angle = std::sin(angle_in_radians); + const float cos_angle = std::cos(angle_in_radians); + + GSMatrix2x2 ret; + ret.E[0][0] = cos_angle; + ret.E[0][1] = -sin_angle; + ret.E[1][0] = sin_angle; + ret.E[1][1] = cos_angle; + return ret; +} + +GSVector2 GSMatrix2x2::row(size_t i) const +{ + return GSVector2::load(&E[i][0]); +} + +GSVector2 GSMatrix2x2::col(size_t i) const +{ + return GSVector2(E[0][i], E[1][i]); +} + +void GSMatrix2x2::store(void* m) +{ + std::memcpy(m, E, sizeof(E)); +} diff --git a/src/common/gsvector.h b/src/common/gsvector.h index f19e92e04..fdef42676 100644 --- a/src/common/gsvector.h +++ b/src/common/gsvector.h @@ -12,3 +12,24 @@ #else #include "common/gsvector_nosimd.h" #endif + +class GSMatrix2x2 +{ +public: + GSMatrix2x2() = default; + GSMatrix2x2(float e00, float e01, float e10, float e11); + + GSMatrix2x2 operator*(const GSMatrix2x2& m) const; + + GSVector2 operator*(const GSVector2& v) const; + + static GSMatrix2x2 Identity(); + static GSMatrix2x2 Rotation(float angle_in_radians); + + GSVector2 row(size_t i) const; + GSVector2 col(size_t i) const; + + void store(void* m); + + float E[2][2]; +}; diff --git a/src/common/gsvector_neon.h b/src/common/gsvector_neon.h index b37fbe751..7ffe8c552 100644 --- a/src/common/gsvector_neon.h +++ b/src/common/gsvector_neon.h @@ -800,6 +800,8 @@ public: return vget_lane_s32(vreinterpret_s32_f32(v2s), i); } + ALWAYS_INLINE float dot(const GSVector2& v) const { return vaddv_f32(vmul_f32(v2s, v.v2s)); } + ALWAYS_INLINE static GSVector2 zero() { return GSVector2(vdup_n_f32(0.0f)); } ALWAYS_INLINE static GSVector2 xffffffff() { return GSVector2(vreinterpret_f32_u32(vdup_n_u32(0xFFFFFFFFu))); } diff --git a/src/common/gsvector_nosimd.h b/src/common/gsvector_nosimd.h index 7e0b8765a..02d200b51 100644 --- a/src/common/gsvector_nosimd.h +++ b/src/common/gsvector_nosimd.h @@ -666,6 +666,8 @@ public: return I32[i]; } + ALWAYS_INLINE float dot(const GSVector2& v) const { return (x * v.x + y * v.y); } + ALWAYS_INLINE static constexpr GSVector2 zero() { return GSVector2::cxpr(0.0f, 0.0f); } ALWAYS_INLINE static constexpr GSVector2 xffffffff() diff --git a/src/common/gsvector_sse.h b/src/common/gsvector_sse.h index 99a48e705..a876b2acf 100644 --- a/src/common/gsvector_sse.h +++ b/src/common/gsvector_sse.h @@ -643,6 +643,8 @@ public: return _mm_extract_ps(m, i); } + ALWAYS_INLINE float dot(const GSVector2& v) const { return _mm_cvtss_f32(_mm_dp_ps(m, v.m, 0x31)); } + ALWAYS_INLINE static GSVector2 zero() { return GSVector2(_mm_setzero_ps()); } ALWAYS_INLINE static GSVector2 xffffffff() { return zero() == zero(); } diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index b18f08aef..3cfc2abe0 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -4393,6 +4393,10 @@ void FullscreenUI::DrawDisplaySettingsPage() &Settings::GetDisplayAlignmentName, &Settings::GetDisplayAlignmentDisplayName, DisplayAlignment::Count); + DrawEnumSetting(bsi, FSUI_CSTR("Screen Rotation"), FSUI_CSTR("Determines the rotation of the simulated TV screen."), + "Display", "Rotation", Settings::DEFAULT_DISPLAY_ROTATION, &Settings::ParseDisplayRotation, + &Settings::GetDisplayRotationName, &Settings::GetDisplayRotationDisplayName, DisplayRotation::Count); + if (is_hardware) { DrawEnumSetting(bsi, FSUI_CSTR("Line Detection"), @@ -7382,6 +7386,7 @@ TRANSLATE_NOOP("FullscreenUI", "Determines the amount of audio buffered before b TRANSLATE_NOOP("FullscreenUI", "Determines the emulated hardware type."); TRANSLATE_NOOP("FullscreenUI", "Determines the format that screenshots will be saved/compressed with."); TRANSLATE_NOOP("FullscreenUI", "Determines the position on the screen when black borders must be added."); +TRANSLATE_NOOP("FullscreenUI", "Determines the rotation of the simulated TV screen."); TRANSLATE_NOOP("FullscreenUI", "Determines the size of screenshots created by DuckStation."); TRANSLATE_NOOP("FullscreenUI", "Determines whether a prompt will be displayed to confirm shutting down the emulator/game when the hotkey is pressed."); TRANSLATE_NOOP("FullscreenUI", "Determines which algorithm is used to convert interlaced frames to progressive for display on your system."); @@ -7690,6 +7695,7 @@ TRANSLATE_NOOP("FullscreenUI", "Scaling"); TRANSLATE_NOOP("FullscreenUI", "Scan For New Games"); TRANSLATE_NOOP("FullscreenUI", "Scanning Subdirectories"); TRANSLATE_NOOP("FullscreenUI", "Screen Position"); +TRANSLATE_NOOP("FullscreenUI", "Screen Rotation"); TRANSLATE_NOOP("FullscreenUI", "Screenshot Format"); TRANSLATE_NOOP("FullscreenUI", "Screenshot Quality"); TRANSLATE_NOOP("FullscreenUI", "Screenshot Size"); diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp index 6b680e8ed..f06736946 100644 --- a/src/core/gpu.cpp +++ b/src/core/gpu.cpp @@ -32,6 +32,7 @@ #include "fmt/format.h" #include <cmath> +#include <numbers> #include <thread> Log_SetChannel(GPU); @@ -138,7 +139,8 @@ void GPU::UpdateSettings(const Settings& old_settings) if (!CompileDisplayPipelines(g_settings.display_scaling != old_settings.display_scaling, g_settings.display_deinterlacing_mode != old_settings.display_deinterlacing_mode, - g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing)) + g_settings.display_24bit_chroma_smoothing != + old_settings.display_24bit_chroma_smoothing)) { Panic("Failed to compile display pipeline on settings change."); } @@ -1094,7 +1096,8 @@ void GPU::UpdateCommandTickEvent() void GPU::ConvertScreenCoordinatesToDisplayCoordinates(float window_x, float window_y, float* display_x, float* display_y) const { - const GSVector4i draw_rc = CalculateDrawRect(g_gpu_device->GetWindowWidth(), g_gpu_device->GetWindowHeight(), true); + const GSVector4i draw_rc = + CalculateDrawRect(g_gpu_device->GetWindowWidth(), g_gpu_device->GetWindowHeight(), true, true); // convert coordinates to active display region, then to full display region const float scaled_display_x = (window_x - static_cast<float>(draw_rc.left)) / static_cast<float>(draw_rc.width()); @@ -1104,6 +1107,8 @@ void GPU::ConvertScreenCoordinatesToDisplayCoordinates(float window_x, float win *display_x = scaled_display_x * static_cast<float>(m_crtc_state.display_width); *display_y = scaled_display_y * static_cast<float>(m_crtc_state.display_height); + // TODO: apply rotation matrix + DEV_LOG("win {:.0f},{:.0f} -> local {:.0f},{:.0f}, disp {:.2f},{:.2f} (size {},{} frac {},{})", window_x, window_y, window_x - draw_rc.left, window_y - draw_rc.top, *display_x, *display_y, m_crtc_state.display_width, m_crtc_state.display_height, *display_x / static_cast<float>(m_crtc_state.display_width), @@ -1936,7 +1941,8 @@ bool GPU::PresentDisplay() { FlushRender(); - const GSVector4i draw_rect = CalculateDrawRect(g_gpu_device->GetWindowWidth(), g_gpu_device->GetWindowHeight()); + const GSVector4i draw_rect = CalculateDrawRect(g_gpu_device->GetWindowWidth(), g_gpu_device->GetWindowHeight(), + !g_settings.debugging.show_vram, true); return RenderDisplay(nullptr, draw_rect, !g_settings.debugging.show_vram); } @@ -2007,6 +2013,7 @@ bool GPU::RenderDisplay(GPUTexture* target, const GSVector4i draw_rect, bool pos float src_size[4]; float clamp_rect[4]; float params[4]; + float rotation_matrix[2][2]; } uniforms; std::memset(uniforms.params, 0, sizeof(uniforms.params)); @@ -2060,6 +2067,23 @@ bool GPU::RenderDisplay(GPUTexture* target, const GSVector4i draw_rect, bool pos uniforms.src_size[1] = static_cast<float>(display_texture->GetHeight()); uniforms.src_size[2] = rcp_width; uniforms.src_size[3] = rcp_height; + + if (g_settings.display_rotation != DisplayRotation::Normal) + { + static constexpr const std::array<float, static_cast<size_t>(DisplayRotation::Count) - 1> rotation_radians = {{ + static_cast<float>(std::numbers::pi * 1.5f), // Rotate90 + static_cast<float>(std::numbers::pi), // Rotate180 + static_cast<float>(std::numbers::pi / 2.0), // Rotate270 + }}; + + GSMatrix2x2::Rotation(rotation_radians[static_cast<size_t>(g_settings.display_rotation) - 1]) + .store(uniforms.rotation_matrix); + } + else + { + GSMatrix2x2::Identity().store(uniforms.rotation_matrix); + } + g_gpu_device->PushUniformBuffer(&uniforms, sizeof(uniforms)); g_gpu_device->SetViewportAndScissor(real_draw_rect); @@ -2315,7 +2339,8 @@ bool GPU::ApplyChromaSmoothing() return true; } -GSVector4i GPU::CalculateDrawRect(s32 window_width, s32 window_height, bool apply_aspect_ratio /* = true */) const +GSVector4i GPU::CalculateDrawRect(s32 window_width, s32 window_height, bool apply_rotation, + bool apply_aspect_ratio) const { const bool integer_scale = (g_settings.display_scaling == DisplayScalingMode::NearestInteger || g_settings.display_scaling == DisplayScalingMode::BlinearInteger); @@ -2347,6 +2372,15 @@ GSVector4i GPU::CalculateDrawRect(s32 window_width, s32 window_height, bool appl active_height /= x_scale; } + // swap width/height when rotated, the flipping of padding is taken care of in the shader with the rotation matrix + if (g_settings.display_rotation == DisplayRotation::Rotate90 || + g_settings.display_rotation == DisplayRotation::Rotate270) + { + std::swap(display_width, display_height); + std::swap(active_width, active_height); + std::swap(active_top, active_left); + } + // now fit it within the window float scale; float left_padding, top_padding; @@ -2640,7 +2674,7 @@ bool GPU::RenderScreenshotToFile(std::string filename, DisplayScreenshotMode mod { u32 width = g_gpu_device->GetWindowWidth(); u32 height = g_gpu_device->GetWindowHeight(); - GSVector4i draw_rect = CalculateDrawRect(width, height, true); + GSVector4i draw_rect = CalculateDrawRect(width, height, true, !g_settings.debugging.show_vram); const bool internal_resolution = (mode != DisplayScreenshotMode::ScreenResolution || g_settings.debugging.show_vram); if (internal_resolution && m_display_texture_view_width != 0 && m_display_texture_view_height != 0) diff --git a/src/core/gpu.h b/src/core/gpu.h index 42ec451c1..d1398d3cd 100644 --- a/src/core/gpu.h +++ b/src/core/gpu.h @@ -207,7 +207,7 @@ public: virtual void FlushRender() = 0; /// Helper function for computing the draw rectangle in a larger window. - GSVector4i CalculateDrawRect(s32 window_width, s32 window_height, bool apply_aspect_ratio = true) const; + GSVector4i CalculateDrawRect(s32 window_width, s32 window_height, bool apply_rotation, bool apply_aspect_ratio) const; /// Helper function to save current display texture to PNG. bool WriteDisplayTextureToFile(std::string filename, bool compress_on_thread = false); diff --git a/src/core/gpu_shadergen.cpp b/src/core/gpu_shadergen.cpp index 40400c63c..1be4e495a 100644 --- a/src/core/gpu_shadergen.cpp +++ b/src/core/gpu_shadergen.cpp @@ -12,7 +12,11 @@ GPUShaderGen::~GPUShaderGen() = default; void GPUShaderGen::WriteDisplayUniformBuffer(std::stringstream& ss) { - DeclareUniformBuffer(ss, {"float4 u_src_rect", "float4 u_src_size", "float4 u_clamp_rect", "float4 u_params"}, true); + // Rotation matrix split into rows to avoid padding in HLSL. + DeclareUniformBuffer(ss, + {"float4 u_src_rect", "float4 u_src_size", "float4 u_clamp_rect", "float4 u_params", + "float2 u_rotation_matrix0", "float2 u_rotation_matrix1"}, + true); ss << R"( float2 ClampUV(float2 uv) { @@ -31,6 +35,10 @@ std::string GPUShaderGen::GenerateDisplayVertexShader() float2 pos = float2(float((v_id << 1) & 2u), float(v_id & 2u)); v_tex0 = u_src_rect.xy + pos * u_src_rect.zw; v_pos = float4(pos * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f); + + // Avoid HLSL/GLSL constructor differences by explicitly multiplying the matrix. + v_pos.xy = float2(dot(u_rotation_matrix0, v_pos.xy), dot(u_rotation_matrix1, v_pos.xy)); + #if API_VULKAN v_pos.y = -v_pos.y; #endif diff --git a/src/core/hotkeys.cpp b/src/core/hotkeys.cpp index 910a76937..68d3c7d56 100644 --- a/src/core/hotkeys.cpp +++ b/src/core/hotkeys.cpp @@ -409,7 +409,7 @@ DEFINE_HOTKEY("TogglePostProcessing", TRANSLATE_NOOP("Hotkeys", "Graphics"), PostProcessing::DisplayChain.Toggle(); }) - DEFINE_HOTKEY("ToggleInternalPostProcessing", TRANSLATE_NOOP("Hotkeys", "Graphics"), +DEFINE_HOTKEY("ToggleInternalPostProcessing", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP("Hotkeys", "Toggle Internal Post-Processing"), [](s32 pressed) { if (!pressed && System::IsValid()) PostProcessing::InternalChain.Toggle(); @@ -494,6 +494,27 @@ DEFINE_HOTKEY("ToggleOSD", TRANSLATE_NOOP("Hotkeys", "Graphics"), TRANSLATE_NOOP HotkeyToggleOSD(); }) +DEFINE_HOTKEY("RotateClockwise", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Rotate Display Clockwise"), [](s32 pressed) { + if (!pressed) + { + g_settings.display_rotation = static_cast<DisplayRotation>( + (static_cast<u8>(g_settings.display_rotation) + 1) % static_cast<u8>(DisplayRotation::Count)); + } + }) + +DEFINE_HOTKEY("RotateCounterclockwise", TRANSLATE_NOOP("Hotkeys", "Graphics"), + TRANSLATE_NOOP("Hotkeys", "Rotate Display Counterclockwise"), [](s32 pressed) { + if (!pressed) + { + g_settings.display_rotation = + (g_settings.display_rotation > static_cast<DisplayRotation>(0)) ? + static_cast<DisplayRotation>((static_cast<u8>(g_settings.display_rotation) - 1) % + static_cast<u8>(DisplayRotation::Count)) : + static_cast<DisplayRotation>(static_cast<u8>(DisplayRotation::Count) - 1); + } + }) + DEFINE_HOTKEY("AudioMute", TRANSLATE_NOOP("Hotkeys", "Audio"), TRANSLATE_NOOP("Hotkeys", "Toggle Mute"), [](s32 pressed) { if (!pressed && System::IsValid()) diff --git a/src/core/settings.cpp b/src/core/settings.cpp index e74018644..85ca2c411 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -259,6 +259,10 @@ void Settings::Load(SettingsInterface& si) ParseDisplayAlignment( si.GetStringValue("Display", "Alignment", GetDisplayAlignmentName(DEFAULT_DISPLAY_ALIGNMENT)).c_str()) .value_or(DEFAULT_DISPLAY_ALIGNMENT); + display_rotation = + ParseDisplayRotation( + si.GetStringValue("Display", "Rotation", GetDisplayRotationName(DEFAULT_DISPLAY_ROTATION)).c_str()) + .value_or(DEFAULT_DISPLAY_ROTATION); display_scaling = ParseDisplayScaling(si.GetStringValue("Display", "Scaling", GetDisplayScalingName(DEFAULT_DISPLAY_SCALING)).c_str()) .value_or(DEFAULT_DISPLAY_SCALING); @@ -541,6 +545,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const si.SetBoolValue("Display", "Force4_3For24Bit", display_force_4_3_for_24bit); si.SetStringValue("Display", "AspectRatio", GetDisplayAspectRatioName(display_aspect_ratio)); si.SetStringValue("Display", "Alignment", GetDisplayAlignmentName(display_alignment)); + si.SetStringValue("Display", "Rotation", GetDisplayRotationName(display_rotation)); si.SetStringValue("Display", "Scaling", GetDisplayScalingName(display_scaling)); si.SetBoolValue("Display", "OptimalFramePacing", display_optimal_frame_pacing); si.SetBoolValue("Display", "PreFrameSleep", display_pre_frame_sleep); @@ -1456,6 +1461,38 @@ const char* Settings::GetDisplayAlignmentDisplayName(DisplayAlignment alignment) return Host::TranslateToCString("DisplayAlignment", s_display_alignment_display_names[static_cast<int>(alignment)]); } +static constexpr const std::array s_display_rotation_names = {"Normal", "Rotate90", "Rotate180", "Rotate270"}; +static constexpr const std::array s_display_rotation_display_names = { + TRANSLATE_NOOP("Settings", "No Rotation"), + TRANSLATE_NOOP("Settings", "Rotate 90° (Clockwise)"), + TRANSLATE_NOOP("Settings", "Rotate 180° (Vertical Flip)"), + TRANSLATE_NOOP("Settings", "Rotate 270° (Clockwise)"), +}; + +std::optional<DisplayRotation> Settings::ParseDisplayRotation(const char* str) +{ + int index = 0; + for (const char* name : s_display_rotation_names) + { + if (StringUtil::Strcasecmp(name, str) == 0) + return static_cast<DisplayRotation>(index); + + index++; + } + + return std::nullopt; +} + +const char* Settings::GetDisplayRotationName(DisplayRotation rotation) +{ + return s_display_rotation_names[static_cast<int>(rotation)]; +} + +const char* Settings::GetDisplayRotationDisplayName(DisplayRotation rotation) +{ + return Host::TranslateToCString("Settings", s_display_rotation_display_names[static_cast<size_t>(rotation)]); +} + static constexpr const std::array s_display_scaling_names = { "Nearest", "NearestInteger", "BilinearSmooth", "BilinearSharp", "BilinearInteger", }; diff --git a/src/core/settings.h b/src/core/settings.h index 821535949..c939549c8 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -143,6 +143,7 @@ struct Settings DisplayCropMode display_crop_mode = DEFAULT_DISPLAY_CROP_MODE; DisplayAspectRatio display_aspect_ratio = DEFAULT_DISPLAY_ASPECT_RATIO; DisplayAlignment display_alignment = DEFAULT_DISPLAY_ALIGNMENT; + DisplayRotation display_rotation = DEFAULT_DISPLAY_ROTATION; DisplayScalingMode display_scaling = DEFAULT_DISPLAY_SCALING; DisplayExclusiveFullscreenControl display_exclusive_fullscreen_control = DEFAULT_DISPLAY_EXCLUSIVE_FULLSCREEN_CONTROL; DisplayScreenshotMode display_screenshot_mode = DEFAULT_DISPLAY_SCREENSHOT_MODE; @@ -424,6 +425,10 @@ struct Settings static const char* GetDisplayAlignmentName(DisplayAlignment alignment); static const char* GetDisplayAlignmentDisplayName(DisplayAlignment alignment); + static std::optional<DisplayRotation> ParseDisplayRotation(const char* str); + static const char* GetDisplayRotationName(DisplayRotation alignment); + static const char* GetDisplayRotationDisplayName(DisplayRotation alignment); + static std::optional<DisplayScalingMode> ParseDisplayScaling(const char* str); static const char* GetDisplayScalingName(DisplayScalingMode mode); static const char* GetDisplayScalingDisplayName(DisplayScalingMode mode); @@ -480,6 +485,7 @@ struct Settings static constexpr DisplayCropMode DEFAULT_DISPLAY_CROP_MODE = DisplayCropMode::Overscan; static constexpr DisplayAspectRatio DEFAULT_DISPLAY_ASPECT_RATIO = DisplayAspectRatio::Auto; static constexpr DisplayAlignment DEFAULT_DISPLAY_ALIGNMENT = DisplayAlignment::Center; + static constexpr DisplayRotation DEFAULT_DISPLAY_ROTATION = DisplayRotation::Normal; static constexpr DisplayScalingMode DEFAULT_DISPLAY_SCALING = DisplayScalingMode::BilinearSmooth; static constexpr DisplayExclusiveFullscreenControl DEFAULT_DISPLAY_EXCLUSIVE_FULLSCREEN_CONTROL = DisplayExclusiveFullscreenControl::Automatic; diff --git a/src/core/system.cpp b/src/core/system.cpp index 33a0b5281..604d343bc 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -4065,7 +4065,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings) g_settings.display_24bit_chroma_smoothing != old_settings.display_24bit_chroma_smoothing || g_settings.display_crop_mode != old_settings.display_crop_mode || g_settings.display_aspect_ratio != old_settings.display_aspect_ratio || - g_settings.display_alignment != old_settings.display_alignment || g_settings.display_scaling != old_settings.display_scaling || g_settings.display_show_gpu_usage != old_settings.display_show_gpu_usage || g_settings.gpu_pgxp_enable != old_settings.gpu_pgxp_enable || @@ -5304,11 +5303,14 @@ void System::RequestDisplaySize(float scale /*= 0.0f*/) (static_cast<float>(g_gpu->GetCRTCDisplayWidth()) / static_cast<float>(g_gpu->GetCRTCDisplayHeight())) / g_gpu->ComputeDisplayAspectRatio(); - const u32 requested_width = + u32 requested_width = std::max<u32>(static_cast<u32>(std::ceil(static_cast<float>(g_gpu->GetCRTCDisplayWidth()) * scale)), 1); - const u32 requested_height = + u32 requested_height = std::max<u32>(static_cast<u32>(std::ceil(static_cast<float>(g_gpu->GetCRTCDisplayHeight()) * y_scale * scale)), 1); + if (g_settings.display_rotation == DisplayRotation::Rotate90 || g_settings.display_rotation == DisplayRotation::Rotate180) + std::swap(requested_width, requested_height); + Host::RequestResizeHostDisplay(static_cast<s32>(requested_width), static_cast<s32>(requested_height)); } diff --git a/src/core/types.h b/src/core/types.h index 108db954f..cba718e3a 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -152,6 +152,15 @@ enum class DisplayAlignment : u8 Count }; +enum class DisplayRotation : u8 +{ + Normal, + Rotate90, + Rotate180, + Rotate270, + Count +}; + enum class DisplayScalingMode : u8 { Nearest, diff --git a/src/duckstation-qt/graphicssettingswidget.cpp b/src/duckstation-qt/graphicssettingswidget.cpp index 155cd3f54..63523a0d3 100644 --- a/src/duckstation-qt/graphicssettingswidget.cpp +++ b/src/duckstation-qt/graphicssettingswidget.cpp @@ -116,6 +116,9 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget* SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.displayAlignment, "Display", "Alignment", &Settings::ParseDisplayAlignment, &Settings::GetDisplayAlignmentName, Settings::DEFAULT_DISPLAY_ALIGNMENT); + SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.displayRotation, "Display", "Rotation", + &Settings::ParseDisplayRotation, &Settings::GetDisplayRotationName, + Settings::DEFAULT_DISPLAY_ROTATION); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gpuThread, "GPU", "UseThread", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.threadedPresentation, "GPU", "ThreadedPresentation", Settings::DEFAULT_THREADED_PRESENTATION); @@ -596,6 +599,12 @@ void GraphicsSettingsWidget::setupAdditionalUi() QString::fromUtf8(Settings::GetDisplayAlignmentDisplayName(static_cast<DisplayAlignment>(i)))); } + for (u32 i = 0; i < static_cast<u32>(DisplayRotation::Count); i++) + { + m_ui.displayRotation->addItem( + QString::fromUtf8(Settings::GetDisplayRotationDisplayName(static_cast<DisplayRotation>(i)))); + } + for (u32 i = 0; i < static_cast<u32>(GPULineDetectMode::Count); i++) { m_ui.gpuLineDetectMode->addItem( diff --git a/src/duckstation-qt/graphicssettingswidget.ui b/src/duckstation-qt/graphicssettingswidget.ui index 06ca84fd5..d06e288d7 100644 --- a/src/duckstation-qt/graphicssettingswidget.ui +++ b/src/duckstation-qt/graphicssettingswidget.ui @@ -320,7 +320,7 @@ </widget> </item> <item row="0" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_8" stretch="1,0"> + <layout class="QHBoxLayout" name="horizontalLayout_8"> <item> <widget class="QComboBox" name="fullscreenMode"/> </item> @@ -336,9 +336,6 @@ </property> </widget> </item> - <item row="1" column="1"> - <widget class="QComboBox" name="displayAlignment"/> - </item> <item row="2" column="0" colspan="2"> <layout class="QGridLayout" name="advancedDisplayOptionsLayout"> <item row="0" column="0"> @@ -371,6 +368,16 @@ </item> </layout> </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QComboBox" name="displayAlignment"/> + </item> + <item> + <widget class="QComboBox" name="displayRotation"/> + </item> + </layout> + </item> </layout> </widget> </item> diff --git a/src/duckstation-qt/qttranslations.cpp b/src/duckstation-qt/qttranslations.cpp index 676ead8c8..262c36403 100644 --- a/src/duckstation-qt/qttranslations.cpp +++ b/src/duckstation-qt/qttranslations.cpp @@ -220,6 +220,7 @@ const char* QtHost::GetDefaultLanguage() static constexpr const ImWchar s_base_latin_range[] = { 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x00B0, 0x00B0, // Degree sign 0x2022, 0x2022, // General punctuation }; static constexpr const ImWchar s_central_european_ranges[] = {