diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 94a7dadd9..1d9c4187e 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -49,6 +49,13 @@ add_library(ui STATIC qt_softwarerenderer.hpp qt_hardwarerenderer.cpp qt_hardwarerenderer.hpp + qt_openglrenderer.cpp + qt_openglrenderer.hpp + qt_opengloptions.cpp + qt_opengloptions.hpp + qt_opengloptionsdialog.cpp + qt_opengloptionsdialog.hpp + qt_opengloptionsdialog.ui qt_settings.cpp qt_settings.hpp diff --git a/src/qt/qt_mainwindow.cpp b/src/qt/qt_mainwindow.cpp index 2f1b6ca6a..e9e7548cf 100644 --- a/src/qt/qt_mainwindow.cpp +++ b/src/qt/qt_mainwindow.cpp @@ -202,7 +202,7 @@ MainWindow::MainWindow(QWidget *parent) : config_save(); if (QApplication::activeWindow() == this) { - ui->stackedWidget->current->setFocus(); + ui->stackedWidget->setFocusRenderer(); } }); @@ -236,30 +236,47 @@ MainWindow::MainWindow(QWidget *parent) : fprintf(stderr, "OpenGL renderers are unsupported on EGLFS.\n"); vid_api = 0; } + QActionGroup* actGroup = nullptr; - switch (vid_api) { - case 0: - ui->stackedWidget->switchRenderer(RendererStack::Renderer::Software); - ui->actionSoftware_Renderer->setChecked(true); - break; - case 1: - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL); - ui->actionHardware_Renderer_OpenGL->setChecked(true); - break; - case 2: - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGLES); - ui->actionHardware_Renderer_OpenGL_ES->setChecked(true); - break; - case 3: - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL3); - ui->actionOpenGL_3_0_Core->setChecked(true); - break; - } + actGroup = new QActionGroup(this); actGroup->addAction(ui->actionSoftware_Renderer); actGroup->addAction(ui->actionHardware_Renderer_OpenGL); actGroup->addAction(ui->actionHardware_Renderer_OpenGL_ES); actGroup->addAction(ui->actionOpenGL_3_0_Core); + actGroup->setExclusive(true); + + connect(actGroup, &QActionGroup::triggered, [this](QAction* action) { + vid_api = action->property("vid_api").toInt(); + switch (vid_api) + { + case 0: + ui->stackedWidget->switchRenderer(RendererStack::Renderer::Software); + break; + case 1: + ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL); + break; + case 2: + ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGLES); + break; + case 3: + ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL3); + break; + } + }); + + connect(ui->stackedWidget, &RendererStack::rendererChanged, [this]() { + ui->actionRenderer_options->setVisible(ui->stackedWidget->hasOptions()); + }); + + /* Trigger initial renderer switch */ + for (auto action : actGroup->actions()) + if (action->property("vid_api").toInt() == vid_api) { + action->setChecked(true); + emit actGroup->triggered(action); + break; + } + switch (scale) { case 0: ui->action0_5x->setChecked(true); @@ -424,7 +441,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { QCheckBox *chkbox = new QCheckBox(tr("Don't show this message again")); questionbox.setCheckBox(chkbox); chkbox->setChecked(!confirm_exit); - bool confirm_exit_temp = false; + QObject::connect(chkbox, &QCheckBox::stateChanged, [](int state) { confirm_exit = (state == Qt::CheckState::Unchecked); }); @@ -497,7 +514,7 @@ void MainWindow::on_actionHard_Reset_triggered() { QCheckBox *chkbox = new QCheckBox(tr("Don't show this message again")); questionbox.setCheckBox(chkbox); chkbox->setChecked(!confirm_reset); - bool confirm_exit_temp = false; + QObject::connect(chkbox, &QCheckBox::stateChanged, [](int state) { confirm_reset = (state == Qt::CheckState::Unchecked); }); @@ -1099,21 +1116,21 @@ void MainWindow::on_actionFullscreen_triggered() { QCheckBox *chkbox = new QCheckBox(tr("Don't show this message again")); questionbox.setCheckBox(chkbox); chkbox->setChecked(!video_fullscreen_first); - bool confirm_exit_temp = false; + QObject::connect(chkbox, &QCheckBox::stateChanged, [](int state) { video_fullscreen_first = (state == Qt::CheckState::Unchecked); }); questionbox.exec(); config_save(); } + video_fullscreen = 1; setFixedSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); ui->menubar->hide(); ui->statusbar->hide(); ui->toolBar->hide(); showFullScreen(); - video_fullscreen = 1; } - ui->stackedWidget->rendererWindow->onResize(width(), height()); + ui->stackedWidget->onResize(width(), height()); } void MainWindow::getTitle_(wchar_t *title) @@ -1216,30 +1233,6 @@ QSize MainWindow::getRenderWidgetSize() return ui->stackedWidget->size(); } -void MainWindow::on_actionSoftware_Renderer_triggered() { - ui->stackedWidget->switchRenderer(RendererStack::Renderer::Software); - ui->actionHardware_Renderer_OpenGL->setChecked(false); - ui->actionHardware_Renderer_OpenGL_ES->setChecked(false); - ui->actionOpenGL_3_0_Core->setChecked(false); - vid_api = 0; -} - -void MainWindow::on_actionHardware_Renderer_OpenGL_triggered() { - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL); - ui->actionSoftware_Renderer->setChecked(false); - ui->actionHardware_Renderer_OpenGL_ES->setChecked(false); - ui->actionOpenGL_3_0_Core->setChecked(false); - vid_api = 1; -} - -void MainWindow::on_actionHardware_Renderer_OpenGL_ES_triggered() { - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGLES); - ui->actionSoftware_Renderer->setChecked(false); - ui->actionHardware_Renderer_OpenGL->setChecked(false); - ui->actionOpenGL_3_0_Core->setChecked(false); - vid_api = 2; -} - void MainWindow::focusInEvent(QFocusEvent* event) { this->grabKeyboard(); @@ -1335,8 +1328,7 @@ static void update_fullscreen_scale_checkboxes(Ui::MainWindow* ui, QAction* sele if (video_fullscreen > 0) { auto widget = ui->stackedWidget->currentWidget(); - auto rc = ui->stackedWidget->rendererWindow; - rc->onResize(widget->width(), widget->height()); + ui->stackedWidget->onResize(widget->width(), widget->height()); } device_force_redraw(); @@ -1563,16 +1555,6 @@ void MainWindow::setSendKeyboardInput(bool enabled) send_keyboard_input = enabled; } -void MainWindow::on_actionOpenGL_3_0_Core_triggered() -{ - ui->stackedWidget->switchRenderer(RendererStack::Renderer::OpenGL3); - ui->actionSoftware_Renderer->setChecked(false); - ui->actionHardware_Renderer_OpenGL->setChecked(false); - ui->actionHardware_Renderer_OpenGL_ES->setChecked(false); - ui->actionOpenGL_3_0_Core->setChecked(true); - vid_api = 3; -} - void MainWindow::on_actionPreferences_triggered() { ProgSettings progsettings(this); @@ -1618,3 +1600,11 @@ void MainWindow::changeEvent(QEvent* event) #endif QWidget::changeEvent(event); } + +void MainWindow::on_actionRenderer_options_triggered() +{ + auto dlg = ui->stackedWidget->getOptions(this); + + if (dlg) + dlg->exec(); +} diff --git a/src/qt/qt_mainwindow.hpp b/src/qt/qt_mainwindow.hpp index e6e7d3053..5b155079c 100644 --- a/src/qt/qt_mainwindow.hpp +++ b/src/qt/qt_mainwindow.hpp @@ -61,9 +61,6 @@ private slots: void on_actionHard_Reset_triggered(); void on_actionRight_CTRL_is_left_ALT_triggered(); void on_actionKeyboard_requires_capture_triggered(); - void on_actionHardware_Renderer_OpenGL_ES_triggered(); - void on_actionHardware_Renderer_OpenGL_triggered(); - void on_actionSoftware_Renderer_triggered(); void on_actionResizable_window_triggered(bool checked); void on_actionInverted_VGA_monitor_triggered(); void on_action0_5x_triggered(); @@ -96,19 +93,15 @@ private slots: void on_actionHide_status_bar_triggered(); void on_actionHide_tool_bar_triggered(); void on_actionUpdate_status_bar_icons_triggered(); + void on_actionTake_screenshot_triggered(); + void on_actionSound_gain_triggered(); + void on_actionPreferences_triggered(); + void on_actionEnable_Discord_integration_triggered(bool checked); + void on_actionRenderer_options_triggered(); void refreshMediaMenu(); void showMessage_(const QString& header, const QString& message); void getTitle_(wchar_t* title); - void on_actionTake_screenshot_triggered(); - - void on_actionSound_gain_triggered(); - - void on_actionOpenGL_3_0_Core_triggered(); - - void on_actionPreferences_triggered(); - - void on_actionEnable_Discord_integration_triggered(bool checked); protected: void keyPressEvent(QKeyEvent* event) override; diff --git a/src/qt/qt_mainwindow.ui b/src/qt/qt_mainwindow.ui index 98bb6ae2d..69197d015 100644 --- a/src/qt/qt_mainwindow.ui +++ b/src/qt/qt_mainwindow.ui @@ -154,6 +154,8 @@ + + @@ -341,6 +343,9 @@ &Qt (Software) + + 0 + @@ -349,6 +354,9 @@ Qt (&OpenGL) + + 1 + @@ -357,6 +365,9 @@ Qt (OpenGL &ES) + + 2 + @@ -626,6 +637,9 @@ Open&GL (3.0 Core) + + 3 + @@ -700,6 +714,11 @@ false + + + Renderer options... + + diff --git a/src/qt/qt_opengloptions.cpp b/src/qt/qt_opengloptions.cpp new file mode 100644 index 000000000..1f2460794 --- /dev/null +++ b/src/qt/qt_opengloptions.cpp @@ -0,0 +1,207 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * OpenGL renderer options for Qt + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#include +#include +#include + +#include + +#include "qt_opengloptions.hpp" + +extern "C" { +#include <86box/86box.h> +} + +/* Default vertex shader. */ +static const GLchar *vertex_shader = "#version 130\n\ +in vec2 VertexCoord;\n\ +in vec2 TexCoord;\n\ +out vec2 tex;\n\ +void main(){\n\ + gl_Position = vec4(VertexCoord, 0.0, 1.0);\n\ + tex = TexCoord;\n\ +}\n"; + +/* Default fragment shader. */ +static const GLchar *fragment_shader = "#version 130\n\ +in vec2 tex;\n\ +uniform sampler2D texsampler;\n\ +out vec4 color;\n\ +void main() {\n\ + color = texture(texsampler, tex);\n\ +}\n"; + +OpenGLOptions::OpenGLOptions(QObject *parent, bool loadConfig) + : QObject(parent) +{ + if (!loadConfig) + return; + + /* Initialize with config. */ + m_vsync = video_vsync != 0; + m_framerate = video_framerate; + + m_renderBehavior = video_framerate == -1 + ? RenderBehaviorType::SyncWithVideo + : RenderBehaviorType::TargetFramerate; + + m_filter = video_filter_method == 0 + ? FilterType::Nearest + : FilterType::Linear; + + QString shaderPath(video_shader); + + if (shaderPath.isEmpty()) + addDefaultShader(); + else + addShader(shaderPath); + + m_modified = ~ModifiedFlags {}; +} + +void +OpenGLOptions::save() +{ + video_vsync = m_vsync ? 1 : 0; + video_framerate = m_renderBehavior == RenderBehaviorType::SyncWithVideo ? -1 : m_framerate; + video_filter_method = m_filter == FilterType::Nearest ? 0 : 1; + + /* TODO: multiple shaders */ + auto path = m_shaders.first().path.toLocal8Bit(); + + if (!path.isEmpty()) + memcpy(video_shader, path.constData(), path.size()); + else + video_shader[0] = '\0'; +} + +bool +OpenGLOptions::isModified() +{ + /* Filter method is controlled externally */ + auto newfilter = video_filter_method == 0 + ? FilterType::Nearest + : FilterType::Linear; + + if (m_filter != newfilter) { + m_filter = newfilter; + m_modified.setFlag(ModifiedFlag::FilterModified); + } + + return m_modified != ModifiedFlags {}; +} + +OpenGLOptions::ModifiedFlags +OpenGLOptions::modified() +{ + ModifiedFlags temp {}; + std::swap(temp, m_modified); + return temp; +} + +void +OpenGLOptions::setRenderBehavior(RenderBehaviorType value) +{ + m_renderBehavior = value; + m_modified.setFlag(ModifiedFlag::RenderBehaviorModified); +} + +void +OpenGLOptions::setFrameRate(int value) +{ + m_framerate = value; + m_modified.setFlag(ModifiedFlag::FrameRateModified); +} + +void +OpenGLOptions::setVSync(bool value) +{ + m_vsync = value; + m_modified.setFlag(ModifiedFlag::VsyncModified); +} + +void +OpenGLOptions::setFilter(FilterType value) +{ + m_filter = value; + m_modified.setFlag(ModifiedFlag::FilterModified); +} + +void +OpenGLOptions::addShader(const QString &path) +{ + QFile shader_file(path); + + if (!shader_file.open(QIODevice::ReadOnly | QIODevice::Text)) { + throw std::runtime_error( + QString(tr("Error opening \"%1\": %2")) + .arg(path) + .arg(shader_file.errorString()) + .toStdString()); + } + + auto shader_text = QString(shader_file.readAll()); + + shader_file.close(); + + QRegularExpression version("^\\s*(#version\\s+\\w+)", QRegularExpression::MultilineOption); + + auto match = version.match(shader_text); + + QString version_line("#version 130"); + + if (match.hasMatch()) { + /* Extract existing version and remove it. */ + version_line = match.captured(1); + shader_text.remove(version); + } + + auto shader = new QOpenGLShaderProgram(this); + + auto throw_shader_error = [path, shader](const QString &what) { + throw std::runtime_error( + QString(what % ":\n\n %2") + .arg(path) + .arg(shader->log()) + .toStdString()); + }; + + if (!shader->addShaderFromSourceCode(QOpenGLShader::Vertex, version_line % "\n#define VERTEX\n" % shader_text)) + throw_shader_error(tr("Error compiling vertex shader in file \"%1\"")); + + if (!shader->addShaderFromSourceCode(QOpenGLShader::Fragment, version_line % "\n#define FRAGMENT\n" % shader_text)) + throw_shader_error(tr("Error compiling fragment shader in file \"%1\"")); + + if (!shader->link()) + throw_shader_error(tr("Error linking shader program in file \"%1\"")); + + m_shaders << OpenGLShaderPass(shader, path); + + m_modified.setFlag(ModifiedFlag::ShadersModified); +} + +void +OpenGLOptions::addDefaultShader() +{ + auto shader = new QOpenGLShaderProgram(this); + shader->addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader); + shader->addShaderFromSourceCode(QOpenGLShader::Fragment, fragment_shader); + shader->link(); + m_shaders << OpenGLShaderPass(shader, QString()); + + m_modified.setFlag(ModifiedFlag::ShadersModified); +} diff --git a/src/qt/qt_opengloptions.hpp b/src/qt/qt_opengloptions.hpp new file mode 100644 index 000000000..2cbb1f2b2 --- /dev/null +++ b/src/qt/qt_opengloptions.hpp @@ -0,0 +1,103 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for OpenGL renderer options + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#ifndef QT_OPENGLOPTIONS_HPP +#define QT_OPENGLOPTIONS_HPP + +#include +#include +#include +#include + +struct OpenGLShaderPass { + OpenGLShaderPass(QOpenGLShaderProgram *shader, const QString &path) + : shader(shader) + , path(path) + , vertex_coord(shader->attributeLocation("VertexCoord")) + , tex_coord(shader->attributeLocation("TexCoord")) + , color(shader->attributeLocation("Color")) + , mvp_matrix(shader->uniformLocation("MVPMatrix")) + , input_size(shader->uniformLocation("InputSize")) + , output_size(shader->uniformLocation("OutputSize")) + , texture_size(shader->uniformLocation("TextureSize")) + , frame_count(shader->uniformLocation("FrameCount")) + { + } + + QOpenGLShaderProgram *shader; + const QString path; + const GLint vertex_coord; + const GLint tex_coord; + const GLint color; + const GLint mvp_matrix; + const GLint input_size; + const GLint output_size; + const GLint texture_size; + const GLint frame_count; +}; + +class OpenGLOptions : public QObject { + Q_OBJECT + +public: + enum ModifiedFlag { + RenderBehaviorModified = 0x01, + FrameRateModified = 0x02, + VsyncModified = 0x04, + FilterModified = 0x08, + ShadersModified = 0x10 + }; + Q_DECLARE_FLAGS(ModifiedFlags, ModifiedFlag) + + enum RenderBehaviorType { SyncWithVideo, + TargetFramerate }; + + enum FilterType { Nearest, + Linear }; + + OpenGLOptions(QObject *parent = nullptr, bool loadConfig = false); + + void save(); + bool isModified(); + ModifiedFlags modified(); + + RenderBehaviorType renderBehavior() const { return m_renderBehavior; } + int framerate() const { return m_framerate; } + bool vSync() const { return m_vsync; } + FilterType filter() const { return m_filter; } + + QList shaders() const { return m_shaders; }; + + void setRenderBehavior(RenderBehaviorType value); + void setFrameRate(int value); + void setVSync(bool value); + void setFilter(FilterType value); + + void addShader(const QString &path); + void addDefaultShader(); + +private: + RenderBehaviorType m_renderBehavior = SyncWithVideo; + int m_framerate = -1; + bool m_vsync = false; + FilterType m_filter = Nearest; + ModifiedFlags m_modified = {}; + QList m_shaders; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(OpenGLOptions::ModifiedFlags) + +#endif diff --git a/src/qt/qt_opengloptionsdialog.cpp b/src/qt/qt_opengloptionsdialog.cpp new file mode 100644 index 000000000..4503e0b90 --- /dev/null +++ b/src/qt/qt_opengloptionsdialog.cpp @@ -0,0 +1,114 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * OpenGL renderer options dialog for Qt + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#include +#include +#include + +#include + +#include "qt_opengloptionsdialog.hpp" +#include "qt_util.hpp" +#include "ui_qt_opengloptionsdialog.h" + +OpenGLOptionsDialog::OpenGLOptionsDialog(QWidget *parent, const OpenGLOptions &options) + : QDialog(parent) + , ui(new Ui::OpenGLOptionsDialog) +{ + ui->setupUi(this); + + if (options.renderBehavior() == OpenGLOptions::SyncWithVideo) + ui->syncWithVideo->setChecked(true); + else { + ui->syncToFramerate->setChecked(true); + ui->targetFps->setValue(options.framerate()); + } + + ui->vsync->setChecked(options.vSync()); + + if (!options.shaders().isEmpty()) { + auto path = options.shaders().first().path; + if (!path.isEmpty()) + ui->shader->setPlainText(path); + } +} + +OpenGLOptionsDialog::~OpenGLOptionsDialog() +{ + delete ui; +} + +void +OpenGLOptionsDialog::accept() +{ + auto options = new OpenGLOptions(); + + options->setRenderBehavior( + ui->syncWithVideo->isChecked() + ? OpenGLOptions::SyncWithVideo + : OpenGLOptions::TargetFramerate); + + options->setFrameRate(ui->targetFps->value()); + + options->setVSync(ui->vsync->isChecked()); + + auto shader = ui->shader->toPlainText(); + + try { + + if (!shader.isEmpty()) + options->addShader(shader); + else + options->addDefaultShader(); + + } catch (std::runtime_error &e) { + delete options; + + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Shader error")); + msgBox.setText(tr("Could not load shaders.")); + msgBox.setInformativeText(tr("More information in details.")); + msgBox.setDetailedText(e.what()); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Close); + msgBox.setDefaultButton(QMessageBox::Close); + msgBox.setStyleSheet("QTextEdit { min-width: 45em; }"); + msgBox.exec(); + + return; + } + + options->save(); + + emit optionsChanged(options); + + QDialog::accept(); +} + +void +OpenGLOptionsDialog::on_addShader_clicked() +{ + auto shader = QFileDialog::getOpenFileName( + this, + QString(), + QString(), + tr("OpenGL Shaders") % util::DlgFilter({ "glsl" }, true)); + + if (shader.isNull()) + return; + + ui->shader->setPlainText(shader); +} diff --git a/src/qt/qt_opengloptionsdialog.hpp b/src/qt/qt_opengloptionsdialog.hpp new file mode 100644 index 000000000..833c58471 --- /dev/null +++ b/src/qt/qt_opengloptionsdialog.hpp @@ -0,0 +1,48 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for OpenGL renderer options dialog + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#ifndef QT_OPENGLOPTIONSDIALOG_H +#define QT_OPENGLOPTIONSDIALOG_H + +#include + +#include "qt_opengloptions.hpp" + +namespace Ui { +class OpenGLOptionsDialog; +} + +class OpenGLOptionsDialog : public QDialog { + Q_OBJECT + +public: + explicit OpenGLOptionsDialog(QWidget *parent, const OpenGLOptions &options); + ~OpenGLOptionsDialog(); + +signals: + void optionsChanged(OpenGLOptions *options); + +public slots: + void accept() override; + +private: + Ui::OpenGLOptionsDialog *ui; + +private slots: + void on_addShader_clicked(); +}; + +#endif // QT_OPENGLOPTIONSDIALOG_H diff --git a/src/qt/qt_opengloptionsdialog.ui b/src/qt/qt_opengloptionsdialog.ui new file mode 100644 index 000000000..a6f86b6c2 --- /dev/null +++ b/src/qt/qt_opengloptionsdialog.ui @@ -0,0 +1,280 @@ + + + OpenGLOptionsDialog + + + + 0 + 0 + 400 + 320 + + + + OpenGL 3.0 renderer options + + + + + + Render behavior + + + + + + Use target framerate: + + + + + + + false + + + fps + + + 15 + + + 240 + + + 60 + + + + + + + VSync + + + + + + + <html><head/><body><p>Render each frame immediately, in sync with the emulated display.</p><p><span style=" font-style:italic;">This is the recommended option if the shaders in use don't utilize frametime for animated effects.</span></p></body></html> + + + Synchronize with video + + + true + + + + + + + false + + + 15 + + + 240 + + + 60 + + + Qt::Horizontal + + + false + + + QSlider::NoTicks + + + + + + + + + + Shaders + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + true + + + No shader selected + + + + + + + Browse... + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + syncWithVideo + syncToFramerate + fpsSlider + targetFps + vsync + shader + addShader + removeShader + + + + + buttonBox + accepted() + OpenGLOptionsDialog + accept() + + + 257 + 310 + + + 157 + 274 + + + + + buttonBox + rejected() + OpenGLOptionsDialog + reject() + + + 325 + 310 + + + 286 + 274 + + + + + syncToFramerate + toggled(bool) + targetFps + setEnabled(bool) + + + 140 + 71 + + + 380 + 98 + + + + + syncToFramerate + toggled(bool) + fpsSlider + setEnabled(bool) + + + 158 + 66 + + + 168 + 87 + + + + + fpsSlider + valueChanged(int) + targetFps + setValue(int) + + + 252 + 90 + + + 308 + 89 + + + + + targetFps + valueChanged(int) + fpsSlider + setValue(int) + + + 364 + 93 + + + 134 + 93 + + + + + removeShader + clicked() + shader + clear() + + + 333 + 201 + + + 235 + 208 + + + + + diff --git a/src/qt/qt_openglrenderer.cpp b/src/qt/qt_openglrenderer.cpp new file mode 100644 index 000000000..206cf0a69 --- /dev/null +++ b/src/qt/qt_openglrenderer.cpp @@ -0,0 +1,406 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * OpenGL renderer for Qt + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#include +#include +#include +#include + +#include + +#include "qt_opengloptionsdialog.hpp" +#include "qt_openglrenderer.hpp" + +OpenGLRenderer::OpenGLRenderer(QWidget *parent) + : QWindow(parent->windowHandle()) + , renderTimer(new QTimer(this)) +{ + renderTimer->setTimerType(Qt::PreciseTimer); + /* TODO: need's more accuracy, maybe target 1ms earlier and spin yield */ + connect(renderTimer, &QTimer::timeout, this, &OpenGLRenderer::render); + + buf_usage = std::vector(BUFFERCOUNT); + for (auto &flag : buf_usage) + flag.clear(); + + setSurfaceType(QWindow::OpenGLSurface); + + QSurfaceFormat format; + + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + format.setMajorVersion(3); + format.setMinorVersion(0); + + setFormat(format); + + context = new QOpenGLContext(this); + + context->setFormat(format); + + context->create(); + + parentWidget = parent; + + source.setRect(0, 0, INIT_WIDTH, INIT_HEIGHT); +} + +OpenGLRenderer::~OpenGLRenderer() +{ + finalize(); +} + +void +OpenGLRenderer::exposeEvent(QExposeEvent *event) +{ + Q_UNUSED(event); + + if (!isInitialized) + initialize(); +} + +void +OpenGLRenderer::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event); + + onResize(event->size().width(), event->size().height()); + + if (notReady()) + return; + + context->makeCurrent(this); + + glViewport( + destination.x(), + destination.y(), + destination.width(), + destination.height()); +} + +bool +OpenGLRenderer::event(QEvent *event) +{ + Q_UNUSED(event); + + bool res = false; + if (!eventDelegate(event, res)) + return QWindow::event(event); + return res; +} + +void +OpenGLRenderer::initialize() +{ + if (!context->makeCurrent(this) || !initializeOpenGLFunctions()) { + /* TODO: This could be done much better */ + QMessageBox::critical((QWidget *) qApp->findChild(), tr("Error initializing OpenGL"), tr("OpenGL functions could not be initialized. Falling back to software rendering.")); + context->doneCurrent(); + isFinalized = true; + isInitialized = true; + emit errorInitializing(); + return; + } + + setupExtensions(); + + setupBuffers(); + + /* Vertex, texture 2d coordinates and color (white) making a quad as triangle strip */ + const GLfloat surface[] = { + -1.f, 1.f, 0.f, 0.f, 1.f, 1.f, 1.f, 1.f, + 1.f, 1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, + -1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f, 1.f, + 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f + }; + + glGenVertexArrays(1, &vertexArrayID); + + glBindVertexArray(vertexArrayID); + + glGenBuffers(1, &vertexBufferID); + glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID); + glBufferData(GL_ARRAY_BUFFER, sizeof(surface), surface, GL_STATIC_DRAW); + + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_2D, textureID); + + const GLfloat border_color[] = { 0.f, 0.f, 0.f, 1.f }; + + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, INIT_WIDTH, INIT_HEIGHT, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + + options = new OpenGLOptions(this, true); + + applyOptions(); + + glClearColor(0.f, 0.f, 0.f, 1.f); + + glViewport( + destination.x(), + destination.y(), + destination.width(), + destination.height()); + + isInitialized = true; + + emit initialized(); +} + +void +OpenGLRenderer::finalize() +{ + if (isFinalized) + return; + + context->makeCurrent(this); + + if (hasBufferStorage) + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + + glDeleteBuffers(1, &unpackBufferID); + glDeleteTextures(1, &textureID); + glDeleteBuffers(1, &vertexBufferID); + glDeleteVertexArrays(1, &vertexArrayID); + + if (!hasBufferStorage && unpackBuffer) + free(unpackBuffer); + + context->doneCurrent(); + + isFinalized = true; +} + +QDialog * +OpenGLRenderer::getOptions(QWidget *parent) +{ + auto dialog = new OpenGLOptionsDialog(parent, *options); + + connect(dialog, &OpenGLOptionsDialog::optionsChanged, this, &OpenGLRenderer::updateOptions); + + return dialog; +} + +void +OpenGLRenderer::setupExtensions() +{ + if (context->hasExtension("GL_ARB_buffer_storage")) { + hasBufferStorage = true; + + glBufferStorage = (PFNGLBUFFERSTORAGEPROC) context->getProcAddress("glBufferStorage"); + } + + if (context->hasExtension("GL_ARB_debug_output")) { + hasDebugOutput = true; + + glDebugMessageControlARB = (PFNGLDEBUGMESSAGECONTROLARBPROC) context->getProcAddress("glDebugMessageControlARB"); + glDebugMessageInsertARB = (PFNGLDEBUGMESSAGEINSERTARBPROC) context->getProcAddress("glDebugMessageInsertARB"); + glDebugMessageCallbackARB = (PFNGLDEBUGMESSAGECALLBACKARBPROC) context->getProcAddress("glDebugMessageCallbackARB"); + glGetDebugMessageLogARB = (PFNGLGETDEBUGMESSAGELOGARBPROC) context->getProcAddress("glGetDebugMessageLogARB"); + } + + if (context->hasExtension("GL_ARB_sync")) { + hasSync = true; + + glFenceSync = (PFNGLFENCESYNCPROC) context->getProcAddress("glFenceSync"); + glIsSync = (PFNGLISSYNCPROC) context->getProcAddress("glIsSync"); + glDeleteSync = (PFNGLDELETESYNCPROC) context->getProcAddress("glDeleteSync"); + glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) context->getProcAddress("glClientWaitSync"); + glWaitSync = (PFNGLWAITSYNCPROC) context->getProcAddress("glWaitSync"); + glGetInteger64v = (PFNGLGETINTEGER64VPROC) context->getProcAddress("glGetInteger64v"); + glGetSynciv = (PFNGLGETSYNCIVPROC) context->getProcAddress("glGetSynciv"); + } +} + +void +OpenGLRenderer::setupBuffers() +{ + glGenBuffers(1, &unpackBufferID); + + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, unpackBufferID); + + if (hasBufferStorage) { + /* Create persistent buffer for pixel transfer. */ + glBufferStorage(GL_PIXEL_UNPACK_BUFFER, BUFFERBYTES * BUFFERCOUNT, NULL, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + + unpackBuffer = glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, BUFFERBYTES * BUFFERCOUNT, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + } else { + /* Fallback; create our own buffer. */ + unpackBuffer = malloc(BUFFERBYTES * BUFFERCOUNT); + + glBufferData(GL_PIXEL_UNPACK_BUFFER, BUFFERBYTES * BUFFERCOUNT, NULL, GL_STREAM_DRAW); + } +} + +void +OpenGLRenderer::applyOptions() +{ + /* TODO: make something else; this was a bad idea. */ + + auto modified = options->modified(); + + if (modified.testFlag(OpenGLOptions::FrameRateModified) && options->framerate() > 0) { + int interval = (int) ceilf(1000.f / (float) options->framerate()); + renderTimer->setInterval(std::chrono::milliseconds(interval)); + } + + if (modified.testFlag(OpenGLOptions::RenderBehaviorModified)) { + if (options->renderBehavior() == OpenGLOptions::TargetFramerate) + renderTimer->start(); + else + renderTimer->stop(); + } + + if (modified.testFlag(OpenGLOptions::VsyncModified)) { + auto format = this->format(); + format.setSwapInterval(options->vSync() ? 1 : 0); + setFormat(format); + context->setFormat(format); + } + + if (modified.testFlag(OpenGLOptions::FilterModified)) { + GLint filter = options->filter() == OpenGLOptions::Linear ? GL_LINEAR : GL_NEAREST; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); + } +} + +void +OpenGLRenderer::applyShader(const OpenGLShaderPass &shader) +{ + if (!shader.shader->bind()) + return; + + if (shader.vertex_coord != -1) { + glEnableVertexAttribArray(shader.vertex_coord); + glVertexAttribPointer(shader.vertex_coord, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), 0); + } + + if (shader.tex_coord != -1) { + glEnableVertexAttribArray(shader.tex_coord); + glVertexAttribPointer(shader.tex_coord, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void *) (2 * sizeof(GLfloat))); + } + + if (shader.color != -1) { + glEnableVertexAttribArray(shader.color); + glVertexAttribPointer(shader.color, 4, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void *) (4 * sizeof(GLfloat))); + } + + if (shader.mvp_matrix != -1) { + static const GLfloat mvp[] = { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f + }; + glUniformMatrix4fv(shader.mvp_matrix, 1, GL_FALSE, mvp); + } + + if (shader.output_size != -1) + glUniform2f(shader.output_size, destination.width(), destination.height()); + + if (shader.input_size != -1) + glUniform2f(shader.input_size, source.width(), source.height()); + + if (shader.texture_size != -1) + glUniform2f(shader.texture_size, source.width(), source.height()); + + if (shader.frame_count != -1) + glUniform1i(shader.frame_count, frameCounter); +} + +void +OpenGLRenderer::render() +{ + context->makeCurrent(this); + + if (options->isModified()) + applyOptions(); + + /* TODO: multiple shader passes */ + applyShader(options->shaders().first()); + + glClear(GL_COLOR_BUFFER_BIT); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + context->swapBuffers(this); + + frameCounter = (frameCounter + 1) & 1023; +} + +void +OpenGLRenderer::updateOptions(OpenGLOptions *newOptions) +{ + context->makeCurrent(this); + + glUseProgram(0); + + delete options; + + options = newOptions; + + options->setParent(this); + + applyOptions(); +} + +std::vector> +OpenGLRenderer::getBuffers() +{ + std::vector> buffers; + + if (notReady() || !unpackBuffer) + return buffers; + + /* Split the buffer area */ + for (int i = 0; i < BUFFERCOUNT; i++) { + buffers.push_back(std::make_tuple((uint8_t *) unpackBuffer + BUFFERBYTES * i, &buf_usage[i])); + } + + return buffers; +} + +void +OpenGLRenderer::onBlit(int buf_idx, int x, int y, int w, int h) +{ + if (notReady()) + return; + + context->makeCurrent(this); + + if (source.width() != w || source.height() != h) { + source.setRect(0, 0, w, h); + + /* Resize the texture */ + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, source.width(), source.height(), 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, unpackBufferID); + } + + glPixelStorei(GL_UNPACK_SKIP_PIXELS, BUFFERPIXELS * buf_idx + y * ROW_LENGTH + x); + glPixelStorei(GL_UNPACK_ROW_LENGTH, ROW_LENGTH); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + + /* TODO: check if fence sync is implementable here and still has any benefit. */ + glFinish(); + + buf_usage[buf_idx].clear(); + + if (options->renderBehavior() == OpenGLOptions::SyncWithVideo) + render(); +} \ No newline at end of file diff --git a/src/qt/qt_openglrenderer.hpp b/src/qt/qt_openglrenderer.hpp new file mode 100644 index 000000000..1d8bc036d --- /dev/null +++ b/src/qt/qt_openglrenderer.hpp @@ -0,0 +1,117 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header file for OpenGL renderer + * + * Authors: + * Teemu Korhonen + * + * Copyright 2022 Teemu Korhonen + */ + +#ifndef QT_OPENGLRENDERER_HPP +#define QT_OPENGLRENDERER_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "qt_opengloptions.hpp" +#include "qt_renderercommon.hpp" + +class OpenGLRenderer : public QWindow, protected QOpenGLFunctions_3_0, public RendererCommon { + Q_OBJECT + +public: + QOpenGLContext *context; + + OpenGLRenderer(QWidget *parent = nullptr); + ~OpenGLRenderer(); + + std::vector> getBuffers() override; + + void finalize() override final; + bool hasOptions() const override { return true; } + QDialog *getOptions(QWidget *parent) override; + +signals: + void initialized(); + void errorInitializing(); + +public slots: + void onBlit(int buf_idx, int x, int y, int w, int h); + +protected: + void exposeEvent(QExposeEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + bool event(QEvent *event) override; + +private: + static constexpr int INIT_WIDTH = 640; + static constexpr int INIT_HEIGHT = 400; + static constexpr int ROW_LENGTH = 2048; + static constexpr int BUFFERPIXELS = 4194304; + static constexpr int BUFFERBYTES = 16777216; /* Pixel is 4 bytes. */ + static constexpr int BUFFERCOUNT = 3; /* How many buffers to use for pixel transfer (2-3 is commonly recommended). */ + + OpenGLOptions *options; + QTimer *renderTimer; + + bool isInitialized = false; + bool isFinalized = false; + + GLuint unpackBufferID = 0; + GLuint vertexArrayID = 0; + GLuint vertexBufferID = 0; + GLuint textureID = 0; + int frameCounter = 0; + + void *unpackBuffer = nullptr; + + void initialize(); + void setupExtensions(); + void setupBuffers(); + void applyOptions(); + void applyShader(const OpenGLShaderPass &shader); + bool notReady() const { return !isInitialized || isFinalized; } + + /* GL_ARB_buffer_storage */ + bool hasBufferStorage = false; + PFNGLBUFFERSTORAGEPROC glBufferStorage = nullptr; + + /* GL_ARB_debug_output */ + bool hasDebugOutput = false; + PFNGLDEBUGMESSAGECONTROLARBPROC glDebugMessageControlARB = nullptr; + PFNGLDEBUGMESSAGEINSERTARBPROC glDebugMessageInsertARB = nullptr; + PFNGLDEBUGMESSAGECALLBACKARBPROC glDebugMessageCallbackARB = nullptr; + PFNGLGETDEBUGMESSAGELOGARBPROC glGetDebugMessageLogARB = nullptr; + + /* GL_ARB_sync */ + bool hasSync = false; + PFNGLFENCESYNCPROC glFenceSync = nullptr; + PFNGLISSYNCPROC glIsSync = nullptr; + PFNGLDELETESYNCPROC glDeleteSync = nullptr; + PFNGLCLIENTWAITSYNCPROC glClientWaitSync = nullptr; + PFNGLWAITSYNCPROC glWaitSync = nullptr; + PFNGLGETINTEGER64VPROC glGetInteger64v = nullptr; + PFNGLGETSYNCIVPROC glGetSynciv = nullptr; + +private slots: + void render(); + void updateOptions(OpenGLOptions *newOptions); +}; + +#endif diff --git a/src/qt/qt_renderercommon.hpp b/src/qt/qt_renderercommon.hpp index f55f3ee03..1f94781e4 100644 --- a/src/qt/qt_renderercommon.hpp +++ b/src/qt/qt_renderercommon.hpp @@ -1,28 +1,37 @@ #pragma once -#include -#include +#include #include +#include +#include +#include -#include -#include #include #include +#include +#include class QWidget; -class RendererCommon -{ +class RendererCommon { public: RendererCommon(); - void onResize(int width, int height); - virtual std::vector> getBuffers() = 0; -protected: - bool eventDelegate(QEvent* event, bool& result); + void onResize(int width, int height); + virtual void finalize() { } - QRect source, destination; - QWidget* parentWidget{nullptr}; + virtual std::vector> getBuffers() = 0; + + /* Does renderer implement options dialog */ + virtual bool hasOptions() const { return false; } + /* Returns options dialog for renderer */ + virtual QDialog *getOptions(QWidget *parent) { return nullptr; } + +protected: + bool eventDelegate(QEvent *event, bool &result); + + QRect source, destination; + QWidget *parentWidget { nullptr }; std::vector buf_usage; }; diff --git a/src/qt/qt_rendererstack.cpp b/src/qt/qt_rendererstack.cpp index 4788fa4df..be783864d 100644 --- a/src/qt/qt_rendererstack.cpp +++ b/src/qt/qt_rendererstack.cpp @@ -21,8 +21,9 @@ #include "qt_rendererstack.hpp" #include "ui_qt_rendererstack.h" -#include "qt_softwarerenderer.hpp" #include "qt_hardwarerenderer.hpp" +#include "qt_openglrenderer.hpp" +#include "qt_softwarerenderer.hpp" #include "qt_mainwindow.hpp" #include "qt_util.hpp" @@ -32,34 +33,33 @@ #include #ifdef __APPLE__ -#include +# include #endif -extern "C" -{ +extern "C" { #include <86box/mouse.h> #include <86box/plat.h> #include <86box/video.h> } -extern MainWindow* main_window; -RendererStack::RendererStack(QWidget *parent) : - QStackedWidget(parent), - ui(new Ui::RendererStack) +extern MainWindow *main_window; +RendererStack::RendererStack(QWidget *parent) + : QStackedWidget(parent) + , ui(new Ui::RendererStack) { ui->setupUi(this); #ifdef __unix__ -#ifdef WAYLAND +# ifdef WAYLAND if (QApplication::platformName().contains("wayland")) { wl_init(); } -#endif -#ifdef EVDEV_INPUT +# endif +# ifdef EVDEV_INPUT if (QApplication::platformName() == "eglfs") { evdev_init(); } -#endif +# endif if (QApplication::platformName() == "xcb") { extern void xinput2_init(); xinput2_init(); @@ -76,8 +76,7 @@ extern "C" void macos_poll_mouse(); void qt_mouse_capture(int on) { - if (!on) - { + if (!on) { mouse_capture = 0; QApplication::setOverrideCursor(Qt::ArrowCursor); #ifdef __APPLE__ @@ -93,162 +92,193 @@ qt_mouse_capture(int on) return; } -void RendererStack::mousePoll() +void +RendererStack::mousePoll() { #ifdef __APPLE__ return macos_poll_mouse(); #else /* !defined __APPLE__ */ - mouse_x = mousedata.deltax; - mouse_y = mousedata.deltay; - mouse_z = mousedata.deltaz; + mouse_x = mousedata.deltax; + mouse_y = mousedata.deltay; + mouse_z = mousedata.deltaz; mousedata.deltax = mousedata.deltay = mousedata.deltaz = 0; - mouse_buttons = mousedata.mousebuttons; + mouse_buttons = mousedata.mousebuttons; -#ifdef __unix__ -#ifdef WAYLAND +# ifdef __unix__ +# ifdef WAYLAND if (QApplication::platformName().contains("wayland")) wl_mouse_poll(); -#endif +# endif -#ifdef EVDEV_INPUT - if (QApplication::platformName() == "eglfs") evdev_mouse_poll(); +# ifdef EVDEV_INPUT + if (QApplication::platformName() == "eglfs") + evdev_mouse_poll(); else -#endif - if (QApplication::platformName() == "xcb") - { +# endif + if (QApplication::platformName() == "xcb") { extern void xinput2_poll(); xinput2_poll(); } -#endif /* defined __unix__ */ -#endif /* !defined __APPLE__ */ - +# endif /* defined __unix__ */ +#endif /* !defined __APPLE__ */ } int ignoreNextMouseEvent = 1; -void RendererStack::mouseReleaseEvent(QMouseEvent *event) +void +RendererStack::mouseReleaseEvent(QMouseEvent *event) { - if (this->geometry().contains(event->pos()) && event->button() == Qt::LeftButton && !mouse_capture && (isMouseDown & 1)) - { + if (this->geometry().contains(event->pos()) && event->button() == Qt::LeftButton && !mouse_capture && (isMouseDown & 1)) { plat_mouse_capture(1); this->setCursor(Qt::BlankCursor); - if (!ignoreNextMouseEvent) ignoreNextMouseEvent++; // Avoid jumping cursor when moved. + if (!ignoreNextMouseEvent) + ignoreNextMouseEvent++; // Avoid jumping cursor when moved. isMouseDown &= ~1; return; } - if (mouse_capture && event->button() == Qt::MiddleButton && mouse_get_buttons() < 3) - { + if (mouse_capture && event->button() == Qt::MiddleButton && mouse_get_buttons() < 3) { plat_mouse_capture(0); this->setCursor(Qt::ArrowCursor); isMouseDown &= ~1; return; } - if (mouse_capture) - { + if (mouse_capture) { mousedata.mousebuttons &= ~event->button(); } isMouseDown &= ~1; } -void RendererStack::mousePressEvent(QMouseEvent *event) +void +RendererStack::mousePressEvent(QMouseEvent *event) { isMouseDown |= 1; - if (mouse_capture) - { + if (mouse_capture) { mousedata.mousebuttons |= event->button(); } event->accept(); } -void RendererStack::wheelEvent(QWheelEvent *event) +void +RendererStack::wheelEvent(QWheelEvent *event) { - if (mouse_capture) - { + if (mouse_capture) { mousedata.deltaz += event->pixelDelta().y(); } } -void RendererStack::mouseMoveEvent(QMouseEvent *event) +void +RendererStack::mouseMoveEvent(QMouseEvent *event) { - if (QApplication::platformName().contains("wayland")) - { + if (QApplication::platformName().contains("wayland")) { event->accept(); return; } - if (!mouse_capture) { event->ignore(); return; } + if (!mouse_capture) { + event->ignore(); + return; + } #ifdef __APPLE__ event->accept(); return; #else static QPoint oldPos = QCursor::pos(); - if (ignoreNextMouseEvent) { oldPos = event->pos(); ignoreNextMouseEvent--; event->accept(); return; } + if (ignoreNextMouseEvent) { + oldPos = event->pos(); + ignoreNextMouseEvent--; + event->accept(); + return; + } mousedata.deltax += event->pos().x() - oldPos.x(); mousedata.deltay += event->pos().y() - oldPos.y(); - if (QApplication::platformName() == "eglfs") - { - leaveEvent((QEvent*)event); + if (QApplication::platformName() == "eglfs") { + leaveEvent((QEvent *) event); ignoreNextMouseEvent--; } QCursor::setPos(mapToGlobal(QPoint(width() / 2, height() / 2))); ignoreNextMouseEvent = 2; - oldPos = event->pos(); + oldPos = event->pos(); #endif } -void RendererStack::leaveEvent(QEvent* event) +void +RendererStack::leaveEvent(QEvent *event) { - if (QApplication::platformName().contains("wayland")) - { + if (QApplication::platformName().contains("wayland")) { event->accept(); return; } - if (!mouse_capture) return; + if (!mouse_capture) + return; ignoreNextMouseEvent = 2; event->accept(); } -void RendererStack::switchRenderer(Renderer renderer) { +void +RendererStack::switchRenderer(Renderer renderer) +{ startblit(); if (current) { + rendererWindow->finalize(); removeWidget(current.get()); - } + disconnect(this, &RendererStack::blitToRenderer, nullptr, nullptr); + /* Create new renderer only after previous is destroyed! */ + connect(current.get(), &QObject::destroyed, [this, renderer](QObject *) { createRenderer(renderer); }); + + current.release()->deleteLater(); + } else { + createRenderer(renderer); + } +} + +void +RendererStack::createRenderer(Renderer renderer) +{ switch (renderer) { - case Renderer::Software: - { - auto sw = new SoftwareRenderer(this); - rendererWindow = sw; - connect(this, &RendererStack::blitToRenderer, sw, &SoftwareRenderer::onBlit, Qt::QueuedConnection); - current.reset(this->createWindowContainer(sw, this)); + case Renderer::Software: + { + auto sw = new SoftwareRenderer(this); + rendererWindow = sw; + connect(this, &RendererStack::blitToRenderer, sw, &SoftwareRenderer::onBlit, Qt::QueuedConnection); + current.reset(this->createWindowContainer(sw, this)); + } + break; + case Renderer::OpenGL: + { + this->createWinId(); + auto hw = new HardwareRenderer(this); + rendererWindow = hw; + connect(this, &RendererStack::blitToRenderer, hw, &HardwareRenderer::onBlit, Qt::QueuedConnection); + current.reset(this->createWindowContainer(hw, this)); + break; + } + case Renderer::OpenGLES: + { + this->createWinId(); + auto hw = new HardwareRenderer(this, HardwareRenderer::RenderType::OpenGLES); + rendererWindow = hw; + connect(this, &RendererStack::blitToRenderer, hw, &HardwareRenderer::onBlit, Qt::QueuedConnection); + current.reset(this->createWindowContainer(hw, this)); + break; + } + case Renderer::OpenGL3: + { + this->createWinId(); + auto hw = new OpenGLRenderer(this); + rendererWindow = hw; + connect(this, &RendererStack::blitToRenderer, hw, &OpenGLRenderer::onBlit, Qt::QueuedConnection); + connect(hw, &OpenGLRenderer::initialized, [=]() { + /* Buffers are awailable only after initialization. */ + imagebufs = rendererWindow->getBuffers(); + endblit(); + emit rendererChanged(); + }); + connect(hw, &OpenGLRenderer::errorInitializing, [=]() { + /* Renderer could initialize, fallback to software. */ + endblit(); + QTimer::singleShot(0, this, [this]() { switchRenderer(Renderer::Software); }); + }); + current.reset(this->createWindowContainer(hw, this)); + break; + } } - break; - case Renderer::OpenGL: - { - this->createWinId(); - auto hw = new HardwareRenderer(this); - rendererWindow = hw; - connect(this, &RendererStack::blitToRenderer, hw, &HardwareRenderer::onBlit, Qt::QueuedConnection); - current.reset(this->createWindowContainer(hw, this)); - break; - } - case Renderer::OpenGLES: - { - this->createWinId(); - auto hw = new HardwareRenderer(this, HardwareRenderer::RenderType::OpenGLES); - rendererWindow = hw; - connect(this, &RendererStack::blitToRenderer, hw, &HardwareRenderer::onBlit, Qt::QueuedConnection); - current.reset(this->createWindowContainer(hw, this)); - break; - } - case Renderer::OpenGL3: - { - this->createWinId(); - auto hw = new HardwareRenderer(this, HardwareRenderer::RenderType::OpenGL3); - rendererWindow = hw; - connect(this, &RendererStack::blitToRenderer, hw, &HardwareRenderer::onBlit, Qt::QueuedConnection); - current.reset(this->createWindowContainer(hw, this)); - break; - } - } - - imagebufs = std::move(rendererWindow->getBuffers()); current->setFocusPolicy(Qt::NoFocus); current->setFocusProxy(this); @@ -256,31 +286,35 @@ void RendererStack::switchRenderer(Renderer renderer) { this->setStyleSheet("background-color: black"); - endblit(); + currentBuf = 0; + + if (renderer != Renderer::OpenGL3) { + imagebufs = rendererWindow->getBuffers(); + endblit(); + emit rendererChanged(); + } } // called from blitter thread -void RendererStack::blit(int x, int y, int w, int h) +void +RendererStack::blit(int x, int y, int w, int h) { - if ((w <= 0) || (h <= 0) || (w > 2048) || (h > 2048) || (buffer32 == NULL) || std::get(imagebufs[currentBuf])->test_and_set()) - { + if ((w <= 0) || (h <= 0) || (w > 2048) || (h > 2048) || (buffer32 == NULL) || std::get(imagebufs[currentBuf])->test_and_set()) { video_blit_complete(); return; } sx = x; sy = y; sw = this->w = w; - sh = this->h = h; - uint8_t* imagebits = std::get(imagebufs[currentBuf]); - for (int y1 = y; y1 < (y + h); y1++) - { + sh = this->h = h; + uint8_t *imagebits = std::get(imagebufs[currentBuf]); + for (int y1 = y; y1 < (y + h); y1++) { auto scanline = imagebits + (y1 * (2048) * 4) + (x * 4); video_copy(scanline, &(buffer32->line[y1][x]), w * 4); } - if (screenshots) - { - video_screenshot((uint32_t *)imagebits, x, y, 2048); + if (screenshots) { + video_screenshot((uint32_t *) imagebits, x, y, 2048); } video_blit_complete(); emit blitToRenderer(currentBuf, sx, sy, sw, sh); diff --git a/src/qt/qt_rendererstack.hpp b/src/qt/qt_rendererstack.hpp index 9bff340f0..9133a4927 100644 --- a/src/qt/qt_rendererstack.hpp +++ b/src/qt/qt_rendererstack.hpp @@ -1,37 +1,41 @@ #ifndef QT_RENDERERCONTAINER_HPP #define QT_RENDERERCONTAINER_HPP -#include -#include +#include #include -#include -#include +#include +#include +#include + #include +#include #include +#include + +#include "qt_renderercommon.hpp" namespace Ui { class RendererStack; } class RendererCommon; -class RendererStack : public QStackedWidget -{ +class RendererStack : public QStackedWidget { Q_OBJECT public: explicit RendererStack(QWidget *parent = nullptr); ~RendererStack(); - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void leaveEvent(QEvent *event) override; - void keyPressEvent(QKeyEvent* event) override + void keyPressEvent(QKeyEvent *event) override { event->ignore(); } - void keyReleaseEvent(QKeyEvent* event) override + void keyReleaseEvent(QKeyEvent *event) override { event->ignore(); } @@ -44,15 +48,33 @@ public: }; void switchRenderer(Renderer renderer); - RendererCommon* rendererWindow{nullptr}; + /* Does current renderer implement options dialog */ + bool hasOptions() const { return rendererWindow ? rendererWindow->hasOptions() : false; } + /* Returns options dialog for current renderer */ + QDialog *getOptions(QWidget *parent) { return rendererWindow ? rendererWindow->getOptions(parent) : nullptr; } + + void setFocusRenderer() + { + if (current) + current->setFocus(); + } + void onResize(int width, int height) + { + if (rendererWindow) + rendererWindow->onResize(width, height); + } + signals: void blitToRenderer(int buf_idx, int x, int y, int w, int h); + void rendererChanged(); public slots: void blit(int x, int y, int w, int h); void mousePoll(); private: + void createRenderer(Renderer renderer); + Ui::RendererStack *ui; struct mouseinputdata { @@ -63,13 +85,13 @@ private: int x, y, w, h, sx, sy, sw, sh; - int currentBuf = 0; + int currentBuf = 0; int isMouseDown = 0; - std::vector> imagebufs; + std::vector> imagebufs; + + RendererCommon *rendererWindow { nullptr }; std::unique_ptr current; - - friend class MainWindow; }; #endif // QT_RENDERERCONTAINER_HPP diff --git a/src/qt/qt_softwarerenderer.cpp b/src/qt/qt_softwarerenderer.cpp index 3deeb1a9f..82d33a7cb 100644 --- a/src/qt/qt_softwarerenderer.cpp +++ b/src/qt/qt_softwarerenderer.cpp @@ -53,8 +53,8 @@ void SoftwareRenderer::onBlit(int buf_idx, int x, int y, int w, int h) { cur_image = buf_idx; buf_usage[(buf_idx + 1) % 2].clear(); - - source.setRect(x, y, w, h), + + source.setRect(x, y, w, h); update(); }