Qt: Add QOpenGLWidget cross-thread codepath for macOS (fixes #1754)

This commit is contained in:
Vicki Pfau 2022-01-11 22:54:46 -08:00
parent c4e481c110
commit b6e2faaba9
7 changed files with 208 additions and 33 deletions

View File

@ -35,6 +35,7 @@ Misc:
- Qt: Save positions of multiplayer windows (closes mgba.io/i/2128)
- Qt: Add optional frame counter to OSD (closes mgba.io/i/1728)
- Qt: Add optional emulation-related information on reset (closes mgba.io/i/1780)
- Qt: Add QOpenGLWidget cross-thread codepath for macOS (fixes mgba.io/i/1754)
- Windows: Attach to console if present
0.9.3: (2021-12-17)

View File

@ -235,6 +235,7 @@ static void mGLES2ContextResized(struct VideoBackend* v, unsigned w, unsigned h)
context->shaders[n].dirty = true;
}
}
context->finalShader.dirty = true;
glBindTexture(GL_TEXTURE_2D, context->finalShader.tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, drawW, drawH, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
glBindFramebuffer(GL_FRAMEBUFFER, context->finalShader.fbo);
@ -375,7 +376,6 @@ void mGLES2ContextDrawFrame(struct VideoBackend* v) {
glGetIntegerv(GL_VIEWPORT, viewport);
context->finalShader.filter = v->filter;
context->finalShader.dirty = true;
_drawShader(context, &context->initialShader);
if (v->interframeBlending) {
context->interframeShader.blend = true;
@ -437,15 +437,19 @@ void mGLES2ContextCreate(struct mGLES2Context* context) {
}
void mGLES2ContextUseFramebuffer(struct mGLES2Context* context) {
glGenFramebuffers(1, &context->finalShader.fbo);
glGenTextures(1, &context->finalShader.tex);
if (!context->finalShader.fbo) {
glGenFramebuffers(1, &context->finalShader.fbo);
}
if (!context->finalShader.tex) {
glGenTextures(1, &context->finalShader.tex);
glBindTexture(GL_TEXTURE_2D, context->finalShader.tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(GL_TEXTURE_2D, context->finalShader.tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
}
glBindFramebuffer(GL_FRAMEBUFFER, context->finalShader.fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, context->finalShader.tex, 0);

View File

@ -4,6 +4,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(PLATFORM_SRC)
set(QT_STATIC OFF)
set(QT_DEFINES)
if(BUILD_SDL)
add_definitions(-DBUILD_SDL)
@ -35,6 +36,11 @@ if(NOT Qt5Widgets_FOUND)
endif()
if(APPLE)
execute_process(COMMAND xcrun --show-sdk-version OUTPUT_VARIABLE MACOSX_SDK)
if(MACOSX_SDK VERSION_GREATER 10.14)
list(APPEND QT_DEFINES USE_SHARE_WIDGET)
endif()
if(Qt5Widgets_VERSION MATCHES "^5.1[0-9]")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mmacosx-version-min=10.8")
else()
@ -183,7 +189,6 @@ if(M_CORE_GB)
list(APPEND PLATFORM_SRC ${GB_SRC})
endif()
set(QT_DEFINES)
if(Qt5Multimedia_FOUND)
list(APPEND AUDIO_SRC
AudioProcessorQt.cpp

View File

@ -38,7 +38,9 @@ Display* Display::create(QWidget* parent) {
format.setVersion(3, 2);
}
format.setProfile(QSurfaceFormat::CoreProfile);
if (!DisplayGL::supportsFormat(format)) {
if (DisplayGL::supportsFormat(format)) {
QSurfaceFormat::setDefaultFormat(format);
} else {
#ifdef BUILD_GL
LOG(QT, WARN) << ("Failed to create an OpenGL Core context, trying old-style...");
format.setVersion(1, 4);

View File

@ -9,14 +9,14 @@
#include <QApplication>
#include <QMutexLocker>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLFunctions>
#include <QOpenGLFunctions_3_2_Core>
#include <QOpenGLPaintDevice>
#include <QResizeEvent>
#include <QScreen>
#include <QTimer>
#include <QWindow>
#include <QVBoxLayout>
#include <cmath>
@ -43,13 +43,96 @@ uint qHash(const QSurfaceFormat& format, uint seed) {
return qHash(representation, seed);
}
void mGLWidget::initializeGL() {
m_vao.create();
m_program.create();
m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, R"(#version 150 core
in vec4 position;
out vec2 texCoord;
void main() {
gl_Position = position;
texCoord = (position.st + 1.0) * 0.5;
})");
m_program.addShaderFromSourceCode(QOpenGLShader::Fragment, R"(#version 150 core
in vec2 texCoord;
out vec4 color;
uniform sampler2D tex;
void main() {
color = vec4(texture(tex, texCoord).rgb, 1.0);
})");
m_program.link();
m_program.setUniformValue("tex", 0);
m_positionLocation = m_program.attributeLocation("position");
connect(&m_refresh, &QTimer::timeout, this, static_cast<void (QWidget::*)()>(&QWidget::update));
}
void mGLWidget::finalizeVAO() {
QOpenGLFunctions_3_2_Core* fn = context()->versionFunctions<QOpenGLFunctions_3_2_Core>();
fn->glGetError(); // Clear the error
m_vao.bind();
fn->glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
fn->glEnableVertexAttribArray(m_positionLocation);
fn->glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, NULL);
m_vao.release();
if (fn->glGetError() == GL_NO_ERROR) {
m_vaoDone = true;
}
}
void mGLWidget::paintGL() {
if (!m_vaoDone) {
finalizeVAO();
}
QOpenGLFunctions_3_2_Core* fn = context()->versionFunctions<QOpenGLFunctions_3_2_Core>();
m_program.bind();
m_vao.bind();
fn->glBindTexture(GL_TEXTURE_2D, m_tex);
fn->glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
fn->glBindTexture(GL_TEXTURE_2D, 0);
m_vao.release();
m_program.release();
// TODO: Better timing
++m_refreshResidue;
if (m_refreshResidue == 3) {
m_refresh.start(16);
m_refreshResidue = 0;
} else {
m_refresh.start(17);
}
}
DisplayGL::DisplayGL(const QSurfaceFormat& format, QWidget* parent)
: Display(parent)
{
setAttribute(Qt::WA_NativeWindow);
window()->windowHandle()->setFormat(format);
windowHandle()->create();
m_painter = std::make_unique<PainterGL>(windowHandle(), format);
#ifdef USE_SHARE_WIDGET
bool useShareWidget = true;
#else
// TODO: Does using this on Wayland help?
bool useShareWidget = false;
#endif
if (useShareWidget) {
m_gl = new mGLWidget;
m_gl->setAttribute(Qt::WA_NativeWindow);
m_gl->setFormat(format);
QBoxLayout* layout = new QVBoxLayout;
layout->addWidget(m_gl);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
} else {
m_gl = nullptr;
}
m_painter = std::make_unique<PainterGL>(windowHandle(), m_gl, format);
m_drawThread.setObjectName("Painter Thread");
m_painter->setThread(&m_drawThread);
@ -106,7 +189,9 @@ void DisplayGL::startDrawing(std::shared_ptr<CoreController> controller) {
CoreController::Interrupter interrupter(controller);
QMetaObject::invokeMethod(m_painter.get(), "start");
setUpdatesEnabled(false);
if (!m_gl) {
setUpdatesEnabled(false);
}
}
bool DisplayGL::supportsFormat(const QSurfaceFormat& format) {
@ -188,7 +273,9 @@ void DisplayGL::unpauseDrawing() {
m_isDrawing = true;
QMetaObject::invokeMethod(m_painter.get(), "unpause", Qt::BlockingQueuedConnection);
#ifndef Q_OS_MAC
setUpdatesEnabled(false);
if (!m_gl) {
setUpdatesEnabled(false);
}
#endif
}
}
@ -278,10 +365,21 @@ int DisplayGL::framebufferHandle() {
return m_painter->glTex();
}
PainterGL::PainterGL(QWindow* surface, const QSurfaceFormat& format)
: m_surface(surface)
PainterGL::PainterGL(QWindow* window, mGLWidget* widget, const QSurfaceFormat& format)
: m_window(window)
, m_format(format)
, m_widget(widget)
{
if (widget) {
m_format = widget->format();
QOffscreenSurface* surface = new QOffscreenSurface;
surface->setScreen(window->screen());
surface->setFormat(m_format);
surface->create();
m_surface = surface;
} else {
m_surface = m_window;
}
m_supportsShaders = m_format.version() >= qMakePair(2, 0);
for (auto& buf : m_buffers) {
m_free.append(&buf.front());
@ -311,6 +409,9 @@ void PainterGL::makeCurrent() {
void PainterGL::create() {
m_gl = std::make_unique<QOpenGLContext>();
m_gl->setFormat(m_format);
if (m_widget) {
m_gl->setShareContext(m_widget->context());
}
m_gl->create();
makeCurrent();
@ -321,7 +422,7 @@ void PainterGL::create() {
mGLES2Context* gl2Backend;
#endif
m_window = std::make_unique<QOpenGLPaintDevice>();
m_paintDev = std::make_unique<QOpenGLPaintDevice>();
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
auto version = m_format.version();
@ -346,11 +447,36 @@ void PainterGL::create() {
}
painter->m_gl->swapBuffers(painter->m_surface);
painter->makeCurrent();
if (painter->m_widget && painter->supportsShaders()) {
QOpenGLFunctions_3_2_Core* fn = painter->m_gl->versionFunctions<QOpenGLFunctions_3_2_Core>();
fn->glFinish();
mGLES2Context* gl2Backend = reinterpret_cast<mGLES2Context*>(painter->m_backend);
painter->m_widget->setTex(painter->m_finalTex[painter->m_finalTexIdx]);
painter->m_finalTexIdx ^= 1;
gl2Backend->finalShader.tex = painter->m_finalTex[painter->m_finalTexIdx];
mGLES2ContextUseFramebuffer(gl2Backend);
}
};
m_backend->init(m_backend, 0);
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
if (m_supportsShaders) {
if (m_widget) {
m_widget->setVBO(gl2Backend->vbo);
gl2Backend->finalShader.tex = 0;
mGLES2ContextUseFramebuffer(gl2Backend);
m_finalTex[0] = gl2Backend->finalShader.tex;
gl2Backend->finalShader.tex = 0;
mGLES2ContextUseFramebuffer(gl2Backend);
m_finalTex[1] = gl2Backend->finalShader.tex;
m_finalTexIdx = 0;
gl2Backend->finalShader.tex = m_finalTex[m_finalTexIdx];
m_widget->setTex(m_finalTex[m_finalTexIdx]);
}
m_shader.preprocessShader = static_cast<void*>(&reinterpret_cast<mGLES2Context*>(m_backend)->initialShader);
}
#endif
@ -373,7 +499,7 @@ void PainterGL::destroy() {
#endif
m_backend->deinit(m_backend);
m_gl->doneCurrent();
m_window.reset();
m_paintDev.reset();
m_gl.reset();
free(m_backend);
@ -408,10 +534,10 @@ void PainterGL::setMessagePainter(MessagePainter* messagePainter) {
}
void PainterGL::resize(const QSize& size) {
qreal r = m_surface->devicePixelRatio();
qreal r = m_window->devicePixelRatio();
m_size = size;
m_window->setSize(m_size * r);
m_window->setDevicePixelRatio(r);
m_paintDev->setSize(m_size * r);
m_paintDev->setDevicePixelRatio(r);
if (m_started && !m_active) {
forceDraw();
}
@ -472,7 +598,7 @@ void PainterGL::draw() {
if (!sync->audioWait && !sync->videoFrameWait) {
return;
}
if (m_delayTimer.elapsed() >= 1000 / m_surface->screen()->refreshRate()) {
if (m_delayTimer.elapsed() >= 1000 / m_window->screen()->refreshRate()) {
return;
}
if (!m_drawTimer.isActive()) {
@ -492,7 +618,7 @@ void PainterGL::draw() {
forceRedraw = sync->videoFrameWait;
}
if (!forceRedraw) {
forceRedraw = m_delayTimer.nsecsElapsed() + 1000000 >= 1000000000 / m_surface->screen()->refreshRate();
forceRedraw = m_delayTimer.nsecsElapsed() + 1000000 >= 1000000000 / m_window->screen()->refreshRate();
}
}
mCoreSyncWaitFrameEnd(sync);
@ -507,7 +633,7 @@ void PainterGL::draw() {
void PainterGL::forceDraw() {
performDraw();
if (!m_context->thread()->impl->sync.audioWait && !m_context->thread()->impl->sync.videoFrameWait) {
if (m_delayTimer.elapsed() < 1000 / m_surface->screen()->refreshRate()) {
if (m_delayTimer.elapsed() < 1000 / m_window->screen()->refreshRate()) {
return;
}
m_delayTimer.restart();
@ -532,7 +658,7 @@ void PainterGL::stop() {
}
if (m_videoProxy) {
m_videoProxy->reset();
m_videoProxy->moveToThread(m_surface->thread());
m_videoProxy->moveToThread(m_window->thread());
m_videoProxy.reset();
}
m_backend->clear(m_backend);
@ -550,14 +676,14 @@ void PainterGL::unpause() {
}
void PainterGL::performDraw() {
float r = m_surface->devicePixelRatio();
float r = m_window->devicePixelRatio();
m_backend->resized(m_backend, m_size.width() * r, m_size.height() * r);
if (m_buffer) {
m_backend->postFrame(m_backend, m_buffer);
}
m_backend->drawFrame(m_backend);
if (m_showOSD && m_messagePainter) {
m_painter.begin(m_window.get());
m_painter.begin(m_paintDev.get());
m_messagePainter->paint(&m_painter);
m_painter.end();
}

View File

@ -21,7 +21,11 @@
#include <QHash>
#include <QList>
#include <QMouseEvent>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLWidget>
#include <QPainter>
#include <QQueue>
#include <QThread>
@ -35,11 +39,37 @@
#include "platform/video-backend.h"
class QOpenGLPaintDevice;
class QOpenGLWidget;
uint qHash(const QSurfaceFormat&, uint seed = 0);
namespace QGBA {
class mGLWidget : public QOpenGLWidget {
Q_OBJECT
public:
void setTex(GLuint tex) { m_tex = tex; }
void setVBO(GLuint vbo) { m_vbo = vbo; }
void finalizeVAO();
protected:
void initializeGL() override;
void paintGL() override;
private:
GLuint m_tex;
GLuint m_vbo;
bool m_vaoDone = false;
QOpenGLVertexArrayObject m_vao;
QOpenGLShaderProgram m_program;
GLuint m_positionLocation;
QTimer m_refresh;
int m_refreshResidue = 0;
};
class PainterGL;
class DisplayGL : public Display {
Q_OBJECT
@ -88,13 +118,14 @@ private:
std::unique_ptr<PainterGL> m_painter;
QThread m_drawThread;
std::shared_ptr<CoreController> m_context;
mGLWidget* m_gl;
};
class PainterGL : public QObject {
Q_OBJECT
public:
PainterGL(QWindow* surface, const QSurfaceFormat& format);
PainterGL(QWindow* surface, mGLWidget* widget, const QSurfaceFormat& format);
~PainterGL();
void setThread(QThread*);
@ -146,10 +177,14 @@ private:
uint32_t* m_buffer = nullptr;
QPainter m_painter;
QMutex m_mutex;
QWindow* m_surface;
QWindow* m_window;
QSurface* m_surface;
QSurfaceFormat m_format;
std::unique_ptr<QOpenGLPaintDevice> m_window;
std::unique_ptr<QOpenGLPaintDevice> m_paintDev;
std::unique_ptr<QOpenGLContext> m_gl;
int m_finalTexIdx = 0;
GLuint m_finalTex[2];
mGLWidget* m_widget;
bool m_active = false;
bool m_started = false;
QTimer m_drawTimer;

View File

@ -754,7 +754,9 @@ void Window::focusInEvent(QFocusEvent*) {
updateMultiplayerActive(true);
}
}
m_display->forceDraw();
if (m_display) {
m_display->forceDraw();
}
}
void Window::focusOutEvent(QFocusEvent*) {