diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 8c16a4bf2..09c8711dd 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -105,6 +105,10 @@ bool HideCursorEnabled() { return g_config->hide_cursor_enabled; } +bool StretchEnabled() { + return g_config->stretch_enabled; +} + bool VrEnabled() { return g_config->vr_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 56c026d84..47a6b9ab6 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -64,6 +64,7 @@ struct ConfigOptions { bool fullscreen_enabled = false; bool hdr_enabled = false; bool hide_cursor_enabled = false; + bool stretch_enabled = false; bool splash_enabled = true; bool vr_enabled = false; bool amd_cpu_enabled = false; @@ -125,6 +126,7 @@ int32_t GetGpuIndex(); bool FullscreenEnabled(); bool HdrEnabled(); bool HideCursorEnabled(); +bool StretchEnabled(); bool VrEnabled(); bool AmdCpuEnabled(); bool IsNetworkConnected(); diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index 22b56b261..46eb79c88 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -290,7 +290,8 @@ public: private: vk::SurfaceFormatKHR SelectFormat(); void Destroy(); - void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer); + void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer, + const vk::Rect2D& rect); WindowContext& m_window; vk::SwapchainKHR m_handle = nullptr; @@ -637,7 +638,23 @@ bool Swapchain::PrepareSystemOverlay() { return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount()); } -void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer) { +static vk::Rect2D FitAspect(const vk::Extent3D& source, const vk::Extent2D& target) { + const uint64_t src_w = std::max(source.width, 1u); + const uint64_t src_h = std::max(source.height, 1u); + uint32_t w = target.width; + uint32_t h = target.height; + if (src_w * h > src_h * w) { + h = std::max(1, static_cast(src_h * w / src_w)); + } else { + w = std::max(1, static_cast(src_w * h / src_h)); + } + return {{static_cast((target.width - w) / 2), + static_cast((target.height - h) / 2)}, + {w, h}}; +} + +void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer, + const vk::Rect2D& rect) { auto device = m_window.graphic_ctx.device; if (m_overlay_pipeline == nullptr) { vk::SamplerCreateInfo sampler {}; @@ -756,12 +773,14 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; command.beginRendering(&rendering); - const vk::Viewport viewport { - 0.0f, 0.0f, static_cast(m_extent.width), static_cast(m_extent.height), - 0.0f, 1.0f}; - const vk::Rect2D scissor {{0, 0}, m_extent}; + const vk::Viewport viewport {static_cast(rect.offset.x), + static_cast(rect.offset.y), + static_cast(rect.extent.width), + static_cast(rect.extent.height), + 0.0f, + 1.0f}; command.setViewport(0, 1, &viewport); - command.setScissor(0, 1, &scissor); + command.setScissor(0, 1, &rect); command.draw(3, 1, 0, 0); command.endRendering(); } @@ -799,6 +818,24 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1, &to_transfer); + const auto* aspect_source = source != nullptr ? source : overlay.frame; + const auto game_rect = !Config::StretchEnabled() && aspect_source != nullptr + ? FitAspect(aspect_source->image.extent, m_extent) + : vk::Rect2D {{0, 0}, m_extent}; + if (source == nullptr || game_rect.extent != m_extent) { + const vk::ClearColorValue black {}; + vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, + &black, 1, &to_transfer.subresourceRange); + if (source != nullptr) { + vk::ImageMemoryBarrier after_clear = to_transfer; + after_clear.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + after_clear.oldLayout = vk::ImageLayout::eTransferDstOptimal; + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1, + &after_clear); + } + } if (source != nullptr) { vk::ImageBlit region {}; region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; @@ -812,16 +849,14 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* region.dstSubresource.mipLevel = 0; region.dstSubresource.baseArrayLayer = 0; region.dstSubresource.layerCount = 1; - region.dstOffsets[1].x = static_cast(m_extent.width); - region.dstOffsets[1].y = static_cast(m_extent.height); + region.dstOffsets[0].x = game_rect.offset.x; + region.dstOffsets[0].y = game_rect.offset.y; + region.dstOffsets[1].x = game_rect.offset.x + static_cast(game_rect.extent.width); + region.dstOffsets[1].y = game_rect.offset.y + static_cast(game_rect.extent.height); region.dstOffsets[1].z = 1; vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal, m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1, ®ion, vk::Filter::eLinear); - } else { - const vk::ClearColorValue black {}; - vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, - &black, 1, &to_transfer.subresourceRange); } vk::ImageMemoryBarrier to_present {}; @@ -848,7 +883,7 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* &to_present); if (draw_attachment) { if (draw_overlay) { - DrawOverlay(vk_command, overlay); + DrawOverlay(vk_command, overlay, game_rect); if (draw_system_overlay) { to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index efc2a531a..d10bc44dc 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -115,7 +115,17 @@ - Window resolution + Window resolution as WIDTHxHEIGHT; pick a preset or type a custom size. Non-16:9 sizes such as 21:9 show the game with black bars unless Stretch to window is on. + true + QComboBox::NoInsert + 1280x720 + 1920x1080 + 2560x1440 + 3840x2160 + 2560x1080 + 3440x1440 + 3840x1600 + 5120x2160 @@ -256,6 +266,12 @@ Auto-hide cursor + + + Stretch the game image to fill the window instead of keeping its aspect ratio with black bars + Stretch to window + + Show FPS, frame times, CPU/GPU usage, RAM and memory pressure in the game window. Press F2 (Cmd+P on macOS) to toggle it, and add Shift for the detailed view. diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 74329e16f..3ab86bc10 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -21,10 +21,7 @@ inline QStringList EnumToList() { auto me = QMetaEnum::fromType(); int count = me.keyCount(); for (int i = 0; i < count; i++) { - auto key = QString(me.key(i)); - ret << (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() - ? key.remove('R').toLower() - : key); + ret << QString(me.key(i)); } return ret; } @@ -32,16 +29,12 @@ inline QStringList EnumToList() { template T TextToEnum(const QString& text) { auto me = QMetaEnum::fromType(); - return static_cast(me.keyToValue( - ((text.size() > 1 && text.at(0).isDigit()) ? 'R' + text.toUpper() : text).toUtf8().data())); + return static_cast(me.keyToValue(text.toUtf8().constData())); } template QString EnumToText(T value) { - auto me = QMetaEnum::fromType(); - auto key = QString(me.valueToKey(static_cast(value))); - return (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() ? key.remove('R').toLower() - : key); + return QString(QMetaEnum::fromType().valueToKey(static_cast(value))); } struct ControllerSettings { @@ -80,13 +73,10 @@ public: static constexpr int DEFAULT_CONSOLE_LANGUAGE = 1; static constexpr int MAX_CONSOLE_LANGUAGE = 29; - enum class Resolution { - R1280X720, - R1920X1080, - R2560X1440, - R3840X2160, - }; - Q_ENUM(Resolution) + static constexpr int MIN_SCREEN_SIZE = 320; + static constexpr int MAX_SCREEN_SIZE = 16384; + + static QString NormalizeResolution(const QString& text); enum class ShaderOptimizationType { None, Size, Performance }; Q_ENUM(ShaderOptimizationType) @@ -117,7 +107,7 @@ public: // Controller preferences always come from the global configuration. ControllerSettings controller; - Resolution screen_resolution = Resolution::R1280X720; + QString screen_resolution = QStringLiteral("1280x720"); QString user_name = "Kyty"; int user_id = Config::DEFAULT_USER_ID; QString audio_input_device; @@ -126,6 +116,7 @@ public: bool fullscreen_enabled = false; bool hdr_enabled = false; bool hide_cursor_enabled = false; + bool stretch_enabled = false; bool readback_linear_images = false; bool readback_tiled_images = false; bool sync_raw_image_buffers = false; @@ -168,6 +159,7 @@ public: fullscreen_enabled = other.fullscreen_enabled; hdr_enabled = other.hdr_enabled; hide_cursor_enabled = other.hide_cursor_enabled; + stretch_enabled = other.stretch_enabled; readback_linear_images = other.readback_linear_images; readback_tiled_images = other.readback_tiled_images; sync_raw_image_buffers = other.sync_raw_image_buffers; diff --git a/src/launcher/include/configurationEditDialog.h b/src/launcher/include/configurationEditDialog.h index 2408e9f21..cdee8ca03 100644 --- a/src/launcher/include/configurationEditDialog.h +++ b/src/launcher/include/configurationEditDialog.h @@ -46,11 +46,15 @@ private: QLineEdit* m_addons_dir_edit = nullptr; QToolButton* m_remove_game_dir_button = nullptr; bool m_global_settings = false; + bool m_geometry_restored = false; protected: void Init(const Configuration& info); void InitGameDirectories(); void AddGameDirectoryItem(const QString& dir); + void MakeTabsScrollable(); + void GrowToFitContents(); + void ShowTab(QWidget* page); void moveEvent(QMoveEvent* event) override; void resizeEvent(QResizeEvent* event) override; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index 0b3b7aa85..ad07c12d6 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -1,5 +1,23 @@ #include "configuration.h" +#include + +QString Configuration::NormalizeResolution(const QString& text) { + // Older settings stored the enum key, e.g. "R1920X1080". + static const QRegularExpression re(QStringLiteral("^\\s*R?(\\d+)\\s*[xX]\\s*(\\d+)\\s*$")); + const auto match = re.match(text); + if (!match.hasMatch()) { + return {}; + } + const int width = match.captured(1).toInt(); + const int height = match.captured(2).toInt(); + if (width < MIN_SCREEN_SIZE || height < MIN_SCREEN_SIZE || width > MAX_SCREEN_SIZE || + height > MAX_SCREEN_SIZE) { + return {}; + } + return QStringLiteral("%1x%2").arg(width).arg(height); +} + QVariantMap Configuration::GameSettings() const { QVariantMap values; #define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString()) @@ -12,6 +30,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(fullscreen_enabled); KYTY_CFG_SET(hdr_enabled); KYTY_CFG_SET(hide_cursor_enabled); + KYTY_CFG_SET(stretch_enabled); KYTY_CFG_SET(readback_linear_images); KYTY_CFG_SET(readback_tiled_images); KYTY_CFG_SET(sync_raw_image_buffers); @@ -48,7 +67,8 @@ QVariantMap Configuration::GameSettings() const { template void Configuration::ReadGameSettingsValues(const Settings& s) { #define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value() - KYTY_CFG_GET(screen_resolution); + const auto resolution = NormalizeResolution(s.value("screen_resolution").toString()); + screen_resolution = resolution.isEmpty() ? QStringLiteral("1280x720") : resolution; user_name = s.value("user_name", user_name).toString(); bool user_id_ok = false; auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok); @@ -64,6 +84,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { KYTY_CFG_GET(fullscreen_enabled); KYTY_CFG_GET(hdr_enabled); KYTY_CFG_GET(hide_cursor_enabled); + KYTY_CFG_GET(stretch_enabled); KYTY_CFG_GET(readback_linear_images); KYTY_CFG_GET(readback_tiled_images); KYTY_CFG_GET(sync_raw_image_buffers); @@ -134,7 +155,9 @@ bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) if (!it.value().isString()) { return fail(tr("Game setting %1 must be a string.").arg(it.key())); } - values.insert(it.key(), it.value().toString()); + const auto value = it.value().toString(); + values.insert(it.key(), it.key() == "screen_resolution" ? NormalizeResolution(value) + : value); } ReadGameSettingsValues(values); diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index cb5811b17..c9de0ee79 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -122,10 +124,10 @@ static QString GameDirectoryKey(const QString& dir) { } ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent) - : QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog), - m_info(info) { + : QDialog(parent, Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint), + m_ui(new Ui::ConfigurationEditDialog), m_info(info) { m_ui->setupUi(this); - setMinimumWidth(width()); + MakeTabsScrollable(); InitGameDirectories(); m_ui->controller_group->setVisible(false); m_ui->settings_button->setVisible(false); @@ -170,9 +172,11 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p setSizeGripEnabled(true); - restoreGeometry(g_last_geometry); - Init(info); + m_geometry_restored = restoreGeometry(g_last_geometry); + if (!m_geometry_restored) { + GrowToFitContents(); + } } QByteArray ConfigurationEditDialog::g_last_geometry; @@ -248,7 +252,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { info.audio_input_device); } microphone->setCurrentIndex(microphone->findData(info.audio_input_device)); - ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution); + m_ui->comboBox_screen_resolution->setCurrentText(info.screen_resolution); ListInit(m_ui->comboBox_present_mode, info.present_mode); m_ui->comboBox_gpu->clear(); m_ui->comboBox_gpu->addItem(tr("Auto")); @@ -285,6 +289,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled); m_ui->checkBox_hdr->setChecked(info.hdr_enabled); m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled); + m_ui->checkBox_stretch->setChecked(info.stretch_enabled); m_ui->checkBox_readback->setChecked(info.readback_linear_images); m_ui->checkBox_readback_tiled->setChecked(info.readback_tiled_images); m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers); @@ -413,8 +418,46 @@ void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs, const Q m_game_dirs_group->setVisible(true); m_ui->controller_group->setVisible(true); update_game_directory_buttons(); + if (!m_geometry_restored) { + GrowToFitContents(); + } +} + +void ConfigurationEditDialog::MakeTabsScrollable() { + auto* tabs = m_ui->settings_tabs; + for (int i = 0; i < tabs->count(); i++) { + auto* page = tabs->widget(i); + auto title = tabs->tabText(i); + auto* scroll = new QScrollArea; + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + tabs->removeTab(i); + scroll->setWidget(page); + tabs->insertTab(i, scroll, title); + } + tabs->setCurrentIndex(0); +} + +void ConfigurationEditDialog::GrowToFitContents() { layout()->activate(); - resize(size().expandedTo(minimumSizeHint())); + auto* tabs = m_ui->settings_tabs; + QSize content; + for (int i = 0; i < tabs->count(); i++) { + content = content.expandedTo( + static_cast(tabs->widget(i))->widget()->minimumSizeHint()); + } + const QSize chrome = size() - tabs->currentWidget()->size(); + resize(size().expandedTo(content + chrome).boundedTo(screen()->availableGeometry().size())); +} + +void ConfigurationEditDialog::ShowTab(QWidget* page) { + auto* tabs = m_ui->settings_tabs; + for (int i = 0; i < tabs->count(); i++) { + if (tabs->widget(i)->isAncestorOf(page)) { + tabs->setCurrentIndex(i); + return; + } + } } QString ConfigurationEditDialog::GetAddonsDirectory() const { @@ -474,13 +517,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt(); } info.screen_resolution = - TextToEnum(ui.comboBox_screen_resolution->currentText()); + Configuration::NormalizeResolution(ui.comboBox_screen_resolution->currentText()); info.present_mode = TextToEnum(ui.comboBox_present_mode->currentText()); info.gpu_index = ui.comboBox_gpu->currentIndex() - 1; info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked(); info.hdr_enabled = ui.checkBox_hdr->isChecked(); info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked(); + info.stretch_enabled = ui.checkBox_stretch->isChecked(); info.readback_linear_images = ui.checkBox_readback->isChecked(); info.readback_tiled_images = ui.checkBox_readback_tiled->isChecked(); info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked(); @@ -518,7 +562,7 @@ void ConfigurationEditDialog::save() { for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file, m_ui->lineEdit_cmd_dump_folder}) { if (field->isEnabled() && field->text().isEmpty()) { - m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab); + ShowTab(m_ui->debug_tab); field->setFocus(); break; } @@ -540,6 +584,16 @@ void ConfigurationEditDialog::save() { tr("User ID cannot be 254 (everyone) or 255 (system)")); return; } + if (Configuration::NormalizeResolution(m_ui->comboBox_screen_resolution->currentText()) + .isEmpty()) { + ShowTab(m_ui->settings_tab); + m_ui->comboBox_screen_resolution->setFocus(); + QMessageBox::critical(this, tr("Save failed"), + tr("Screen resolution must be WIDTHxHEIGHT, each between %1 and %2") + .arg(Configuration::MIN_SCREEN_SIZE) + .arg(Configuration::MAX_SCREEN_SIZE)); + return; + } UpdateInfo(m_info, *m_ui, m_global_settings); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index cd6e100a8..21d0af992 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -134,7 +134,8 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal. #endif - main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/); + main_dialog->setWindowFlags(Qt::Dialog | Qt::WindowMinMaxButtonsHint | + Qt::WindowCloseButtonHint); connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter, Qt::QueuedConnection); @@ -240,7 +241,7 @@ static void AddFlag(QStringList& args, bool enabled, const char* name) { static QStringList CreateEmulatorArgs(const Configuration& info) { QStringList args; - auto r = EnumToText(info.screen_resolution).split('x'); + auto r = Configuration::NormalizeResolution(info.screen_resolution).split('x'); if (r.size() != 2) { return {}; @@ -266,6 +267,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { AddFlag(args, info.fullscreen_enabled, "--fullscreen"); AddFlag(args, info.hdr_enabled, "--hdr"); AddFlag(args, info.hide_cursor_enabled, "--hide-cursor"); + AddFlag(args, info.stretch_enabled, "--stretch"); AddFlag(args, info.readback_linear_images, "--readback-linear-images"); AddFlag(args, info.readback_tiled_images, "--readback-tiled-images"); AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers"); diff --git a/src/main.cpp b/src/main.cpp index 13c759301..87cab9934 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -70,6 +70,8 @@ static void PrintUsage() { ::printf(" --hdr Enable HDR10 output when supported.\n"); ::printf( " --hide-cursor Hide the cursor after 2 s idle. Default: off.\n"); + ::printf( + " --stretch Stretch the image to the window. Default: off.\n"); ::printf(" --no-splash Disable the startup splash screen.\n"); ::printf(" --vr Enable the virtual VR headset.\n"); ::printf(" --amd-cpu Apply AMD CPU instruction patches.\n"); @@ -199,6 +201,7 @@ static constexpr Flag FLAGS[] = { {"--fullscreen", &ConfigOptions::fullscreen_enabled}, {"--hdr", &ConfigOptions::hdr_enabled}, {"--hide-cursor", &ConfigOptions::hide_cursor_enabled}, + {"--stretch", &ConfigOptions::stretch_enabled}, {"--no-splash", &ConfigOptions::splash_enabled, false}, {"--vr", &ConfigOptions::vr_enabled}, {"--amd-cpu", &ConfigOptions::amd_cpu_enabled},