1084 lines
26 KiB
C++
1084 lines
26 KiB
C++
/* Copyright (c) 2013-2019 Jeffrey Pfau
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
#include "DisplayGL.h"
|
|
|
|
#if defined(BUILD_GL) || defined(BUILD_GLES2) || defined(BUILD_GLES3) || defined(USE_EPOXY)
|
|
|
|
#include <QApplication>
|
|
#include <QMutexLocker>
|
|
#include <QOpenGLExtraFunctions>
|
|
#include <QOpenGLFunctions>
|
|
#include <QOpenGLPaintDevice>
|
|
#include <QResizeEvent>
|
|
#include <QScreen>
|
|
#include <QTimer>
|
|
#include <QWindow>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <cmath>
|
|
|
|
#include <mgba/core/core.h>
|
|
#include <mgba-util/math.h>
|
|
#ifdef BUILD_GL
|
|
#include "platform/opengl/gl.h"
|
|
#endif
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
#include "platform/opengl/gles2.h"
|
|
#ifdef _WIN32
|
|
#include <epoxy/wgl.h>
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#elif defined(Q_OS_MAC)
|
|
#include <OpenGL/OpenGL.h>
|
|
#endif
|
|
#ifdef USE_GLX
|
|
#define GLX_GLXEXT_PROTOTYPES
|
|
typedef struct _XDisplay Display;
|
|
#include <GL/glx.h>
|
|
#include <GL/glxext.h>
|
|
#endif
|
|
#ifdef USE_EGL
|
|
#include <EGL/egl.h>
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#define OVERHEAD_NSEC 1000000
|
|
#else
|
|
#define OVERHEAD_NSEC 300000
|
|
#endif
|
|
|
|
// Legacy define from X11/X.h
|
|
#ifdef Unsorted
|
|
#undef Unsorted
|
|
#endif
|
|
|
|
#include "LogController.h"
|
|
#include "OpenGLBug.h"
|
|
#include "utils.h"
|
|
|
|
using namespace QGBA;
|
|
|
|
QHash<QSurfaceFormat, bool> DisplayGL::s_supports;
|
|
|
|
uint qHash(const QSurfaceFormat& format, uint seed) {
|
|
QByteArray representation;
|
|
QDataStream stream(&representation, QIODevice::WriteOnly);
|
|
stream << format.version() << format.renderableType() << format.profile();
|
|
return qHash(representation, seed);
|
|
}
|
|
|
|
mGLWidget::mGLWidget(QWidget* parent)
|
|
: QOpenGLWidget(parent)
|
|
{
|
|
setUpdateBehavior(QOpenGLWidget::PartialUpdate);
|
|
|
|
connect(&m_refresh, &QTimer::timeout, this, static_cast<void (QWidget::*)()>(&QWidget::update));
|
|
}
|
|
|
|
mGLWidget::~mGLWidget() {
|
|
// This is needed for unique_ptr<QOpenGLPaintDevice> to work
|
|
}
|
|
|
|
void mGLWidget::initializeGL() {
|
|
m_vao = std::make_unique<QOpenGLVertexArrayObject>();
|
|
m_vao->create();
|
|
|
|
m_program = std::make_unique<QOpenGLShaderProgram>();
|
|
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");
|
|
|
|
m_vaoDone = false;
|
|
m_tex = 0;
|
|
|
|
m_paintDev = std::make_unique<QOpenGLPaintDevice>();
|
|
}
|
|
|
|
bool mGLWidget::finalizeVAO() {
|
|
if (!context() || !m_vao) {
|
|
return false;
|
|
}
|
|
QOpenGLExtraFunctions* fn = context()->extraFunctions();
|
|
if (!fn) {
|
|
return false;
|
|
}
|
|
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;
|
|
}
|
|
return m_vaoDone;
|
|
}
|
|
|
|
void mGLWidget::reset() {
|
|
m_vaoDone = false;
|
|
}
|
|
|
|
void mGLWidget::paintGL() {
|
|
if (!m_vaoDone && !finalizeVAO()) {
|
|
return;
|
|
}
|
|
if (!m_tex) {
|
|
m_refresh.start(10);
|
|
return;
|
|
}
|
|
QOpenGLExtraFunctions* fn = context()->extraFunctions();
|
|
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);
|
|
}
|
|
|
|
if (m_showOSD && m_messagePainter) {
|
|
qreal r = window()->devicePixelRatio();
|
|
m_paintDev->setDevicePixelRatio(r);
|
|
m_paintDev->setSize(size() * r);
|
|
QPainter painter(m_paintDev.get());
|
|
m_messagePainter->paint(&painter);
|
|
painter.end();
|
|
}
|
|
}
|
|
|
|
void mGLWidget::setMessagePainter(MessagePainter* messagePainter) {
|
|
m_messagePainter = messagePainter;
|
|
}
|
|
|
|
void mGLWidget::setShowOSD(bool showOSD) {
|
|
m_showOSD = showOSD;
|
|
}
|
|
|
|
DisplayGL::DisplayGL(const QSurfaceFormat& format, QWidget* parent)
|
|
: Display(parent)
|
|
{
|
|
setAttribute(Qt::WA_NativeWindow);
|
|
window()->windowHandle()->setFormat(format);
|
|
windowHandle()->setSurfaceType(QSurface::OpenGLSurface);
|
|
windowHandle()->create();
|
|
|
|
#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);
|
|
m_gl->setMessagePainter(messagePainter());
|
|
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);
|
|
|
|
connect(&m_drawThread, &QThread::started, m_painter.get(), &PainterGL::create);
|
|
connect(m_painter.get(), &PainterGL::started, this, [this] {
|
|
m_hasStarted = true;
|
|
resizePainter();
|
|
emit drawingStarted();
|
|
});
|
|
m_drawThread.start();
|
|
}
|
|
|
|
DisplayGL::~DisplayGL() {
|
|
stopDrawing();
|
|
QMetaObject::invokeMethod(m_painter.get(), "destroy", Qt::BlockingQueuedConnection);
|
|
m_drawThread.exit();
|
|
m_drawThread.wait();
|
|
}
|
|
|
|
bool DisplayGL::supportsShaders() const {
|
|
return m_painter->supportsShaders();
|
|
}
|
|
|
|
VideoShader* DisplayGL::shaders() {
|
|
VideoShader* shaders = nullptr;
|
|
QMetaObject::invokeMethod(m_painter.get(), "shaders", Qt::BlockingQueuedConnection, Q_RETURN_ARG(VideoShader*, shaders));
|
|
return shaders;
|
|
}
|
|
|
|
void DisplayGL::startDrawing(std::shared_ptr<CoreController> controller) {
|
|
if (m_isDrawing) {
|
|
return;
|
|
}
|
|
m_isDrawing = true;
|
|
m_painter->setContext(controller);
|
|
m_painter->setMessagePainter(messagePainter());
|
|
m_context = std::move(controller);
|
|
if (videoProxy()) {
|
|
videoProxy()->moveToThread(&m_drawThread);
|
|
}
|
|
|
|
lockAspectRatio(isAspectRatioLocked());
|
|
lockIntegerScaling(isIntegerScalingLocked());
|
|
interframeBlending(hasInterframeBlending());
|
|
showOSDMessages(isShowOSD());
|
|
showFrameCounter(isShowFrameCounter());
|
|
filter(isFiltered());
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
|
messagePainter()->resize(size(), devicePixelRatioF());
|
|
#else
|
|
messagePainter()->resize(size(), devicePixelRatio());
|
|
#endif
|
|
|
|
CoreController::Interrupter interrupter(m_context);
|
|
QMetaObject::invokeMethod(m_painter.get(), "start");
|
|
if (!m_gl) {
|
|
if (shouldDisableUpdates()) {
|
|
setUpdatesEnabled(false);
|
|
}
|
|
} else {
|
|
show();
|
|
m_gl->reset();
|
|
}
|
|
|
|
QTimer::singleShot(8, this, &DisplayGL::updateContentSize);
|
|
}
|
|
|
|
bool DisplayGL::highestCompatible(QSurfaceFormat& format) {
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3) || defined(USE_EPOXY)
|
|
if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
|
|
format.setVersion(3, 3);
|
|
format.setProfile(QSurfaceFormat::CoreProfile);
|
|
if (DisplayGL::supportsFormat(format)) {
|
|
return true;
|
|
}
|
|
} else {
|
|
#if defined(BUILD_GLES3) || defined(USE_EPOXY)
|
|
format.setVersion(3, 1);
|
|
if (DisplayGL::supportsFormat(format)) {
|
|
return true;
|
|
}
|
|
#endif
|
|
#if defined(BUILD_GLES2) || defined(USE_EPOXY)
|
|
format.setVersion(2, 0);
|
|
if (DisplayGL::supportsFormat(format)) {
|
|
return true;
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
#ifdef BUILD_GL
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3) || defined(USE_EPOXY)
|
|
LOG(QT, WARN) << tr("Failed to create an OpenGL 3 context, trying old-style...");
|
|
#endif
|
|
if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
|
|
format.setVersion(1, 4);
|
|
} else {
|
|
format.setVersion(1, 1);
|
|
}
|
|
format.setOption(QSurfaceFormat::DeprecatedFunctions);
|
|
if (DisplayGL::supportsFormat(format)) {
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool DisplayGL::supportsFormat(const QSurfaceFormat& format) {
|
|
if (!s_supports.contains(format)) {
|
|
QOpenGLContext context;
|
|
context.setFormat(format);
|
|
if (!context.create()) {
|
|
s_supports[format] = false;
|
|
return false;
|
|
}
|
|
auto foundVersion = context.format().version();
|
|
if (foundVersion == format.version()) {
|
|
// Match!
|
|
s_supports[format] = true;
|
|
} else if (format.version() >= qMakePair(3, 2) && foundVersion > format.version()) {
|
|
// At least as good
|
|
s_supports[format] = true;
|
|
} else if (format.majorVersion() == 1 && (foundVersion < qMakePair(3, 0) ||
|
|
context.format().profile() == QSurfaceFormat::CompatibilityProfile ||
|
|
context.format().testOption(QSurfaceFormat::DeprecatedFunctions))) {
|
|
// Supports the old stuff
|
|
QOffscreenSurface surface;
|
|
surface.create();
|
|
if (!context.makeCurrent(&surface)) {
|
|
s_supports[format] = false;
|
|
return false;
|
|
}
|
|
#ifdef Q_OS_WIN
|
|
QLatin1String renderer(reinterpret_cast<const char*>(context.functions()->glGetString(GL_RENDERER)));
|
|
if (renderer == "GDI Generic") {
|
|
// Windows' software OpenGL 1.1 implementation is not sufficient
|
|
s_supports[format] = false;
|
|
return false;
|
|
}
|
|
#endif
|
|
s_supports[format] = context.hasExtension("GL_EXT_blend_color"); // Core as of 1.2
|
|
context.doneCurrent();
|
|
} else if (!context.isOpenGLES() && format.version() >= qMakePair(2, 1) && foundVersion < qMakePair(3, 0) && foundVersion >= qMakePair(2, 1)) {
|
|
// Weird edge case we support if ARB_framebuffer_object is present
|
|
QOffscreenSurface surface;
|
|
surface.create();
|
|
if (!context.makeCurrent(&surface)) {
|
|
s_supports[format] = false;
|
|
return false;
|
|
}
|
|
s_supports[format] = context.hasExtension("GL_ARB_framebuffer_object");
|
|
context.doneCurrent();
|
|
} else {
|
|
// No match
|
|
s_supports[format] = false;
|
|
}
|
|
}
|
|
return s_supports[format];
|
|
}
|
|
|
|
void DisplayGL::stopDrawing() {
|
|
if (m_hasStarted || m_isDrawing) {
|
|
m_isDrawing = false;
|
|
m_hasStarted = false;
|
|
CoreController::Interrupter interrupter(m_context);
|
|
m_painter->stop();
|
|
if (m_gl) {
|
|
hide();
|
|
}
|
|
setUpdatesEnabled(true);
|
|
}
|
|
m_context.reset();
|
|
}
|
|
|
|
void DisplayGL::pauseDrawing() {
|
|
if (m_hasStarted) {
|
|
m_isDrawing = false;
|
|
QMetaObject::invokeMethod(m_painter.get(), "pause", Qt::BlockingQueuedConnection);
|
|
if (!shouldDisableUpdates()) {
|
|
setUpdatesEnabled(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DisplayGL::unpauseDrawing() {
|
|
if (m_hasStarted) {
|
|
m_isDrawing = true;
|
|
QMetaObject::invokeMethod(m_painter.get(), "unpause", Qt::BlockingQueuedConnection);
|
|
if (!m_gl && shouldDisableUpdates()) {
|
|
setUpdatesEnabled(false);
|
|
}
|
|
QMetaObject::invokeMethod(this, "updateContentSize", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
void DisplayGL::forceDraw() {
|
|
if (m_hasStarted) {
|
|
QMetaObject::invokeMethod(m_painter.get(), "forceDraw");
|
|
QMetaObject::invokeMethod(this, "updateContentSize", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
void DisplayGL::lockAspectRatio(bool lock) {
|
|
Display::lockAspectRatio(lock);
|
|
QMetaObject::invokeMethod(m_painter.get(), "lockAspectRatio", Q_ARG(bool, lock));
|
|
}
|
|
|
|
void DisplayGL::lockIntegerScaling(bool lock) {
|
|
Display::lockIntegerScaling(lock);
|
|
QMetaObject::invokeMethod(m_painter.get(), "lockIntegerScaling", Q_ARG(bool, lock));
|
|
}
|
|
|
|
void DisplayGL::interframeBlending(bool enable) {
|
|
Display::interframeBlending(enable);
|
|
QMetaObject::invokeMethod(m_painter.get(), "interframeBlending", Q_ARG(bool, enable));
|
|
}
|
|
|
|
void DisplayGL::showOSDMessages(bool enable) {
|
|
Display::showOSDMessages(enable);
|
|
if (m_gl) {
|
|
m_gl->setShowOSD(enable);
|
|
}
|
|
QMetaObject::invokeMethod(m_painter.get(), "showOSD", Q_ARG(bool, enable));
|
|
}
|
|
|
|
void DisplayGL::showFrameCounter(bool enable) {
|
|
Display::showFrameCounter(enable);
|
|
QMetaObject::invokeMethod(m_painter.get(), "showFrameCounter", Q_ARG(bool, enable));
|
|
}
|
|
|
|
void DisplayGL::filter(bool filter) {
|
|
Display::filter(filter);
|
|
QMetaObject::invokeMethod(m_painter.get(), "filter", Q_ARG(bool, filter));
|
|
}
|
|
|
|
void DisplayGL::swapInterval(int interval) {
|
|
QMetaObject::invokeMethod(m_painter.get(), "swapInterval", Q_ARG(int, interval));
|
|
}
|
|
|
|
void DisplayGL::framePosted() {
|
|
m_painter->enqueue(m_context->drawContext());
|
|
QMetaObject::invokeMethod(m_painter.get(), "draw");
|
|
}
|
|
|
|
void DisplayGL::setShaders(struct VDir* shaders) {
|
|
QMetaObject::invokeMethod(m_painter.get(), "setShaders", Qt::BlockingQueuedConnection, Q_ARG(struct VDir*, shaders));
|
|
}
|
|
|
|
void DisplayGL::clearShaders() {
|
|
QMetaObject::invokeMethod(m_painter.get(), "clearShaders", Qt::BlockingQueuedConnection);
|
|
}
|
|
|
|
void DisplayGL::resizeContext() {
|
|
m_painter->interrupt();
|
|
QMetaObject::invokeMethod(m_painter.get(), "resizeContext");
|
|
}
|
|
|
|
void DisplayGL::setVideoScale(int scale) {
|
|
if (m_context) {
|
|
m_painter->interrupt();
|
|
mCoreConfigSetIntValue(&m_context->thread()->core->config, "videoScale", scale);
|
|
}
|
|
QMetaObject::invokeMethod(m_painter.get(), "resizeContext");
|
|
}
|
|
|
|
void DisplayGL::setBackgroundImage(const QImage& image) {
|
|
QMetaObject::invokeMethod(m_painter.get(), "setBackgroundImage", Q_ARG(const QImage&, image));
|
|
QMetaObject::invokeMethod(this, "updateContentSize", Qt::QueuedConnection);
|
|
}
|
|
|
|
void DisplayGL::resizeEvent(QResizeEvent* event) {
|
|
Display::resizeEvent(event);
|
|
resizePainter();
|
|
}
|
|
|
|
void DisplayGL::resizePainter() {
|
|
if (m_hasStarted) {
|
|
QMetaObject::invokeMethod(m_painter.get(), "resize", Qt::BlockingQueuedConnection, Q_ARG(QSize, size()));
|
|
}
|
|
}
|
|
|
|
bool DisplayGL::shouldDisableUpdates() {
|
|
if (QGuiApplication::platformName() == "windows") {
|
|
return true;
|
|
}
|
|
if (QGuiApplication::platformName() == "xcb") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void DisplayGL::setVideoProxy(std::shared_ptr<VideoProxy> proxy) {
|
|
Display::setVideoProxy(proxy);
|
|
if (proxy) {
|
|
proxy->moveToThread(&m_drawThread);
|
|
}
|
|
m_painter->setVideoProxy(std::move(proxy));
|
|
}
|
|
|
|
void DisplayGL::updateContentSize() {
|
|
QMetaObject::invokeMethod(m_painter.get(), "contentSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QSize, m_cachedContentSize));
|
|
}
|
|
|
|
int DisplayGL::framebufferHandle() {
|
|
return m_painter->glTex();
|
|
}
|
|
|
|
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());
|
|
}
|
|
connect(&m_drawTimer, &QTimer::timeout, this, &PainterGL::draw);
|
|
m_drawTimer.setSingleShot(true);
|
|
}
|
|
|
|
PainterGL::~PainterGL() {
|
|
if (m_gl) {
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
void PainterGL::setThread(QThread* thread) {
|
|
moveToThread(thread);
|
|
m_drawTimer.moveToThread(thread);
|
|
}
|
|
|
|
void PainterGL::makeCurrent() {
|
|
m_gl->makeCurrent(m_surface);
|
|
#if defined(_WIN32) && defined(USE_EPOXY)
|
|
epoxy_handle_external_wglMakeCurrent();
|
|
#endif
|
|
}
|
|
|
|
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();
|
|
|
|
#ifdef BUILD_GL
|
|
mGLContext* glBackend;
|
|
#endif
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
mGLES2Context* gl2Backend;
|
|
#endif
|
|
|
|
if (!m_widget) {
|
|
m_paintDev = std::make_unique<QOpenGLPaintDevice>();
|
|
}
|
|
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (m_supportsShaders) {
|
|
gl2Backend = static_cast<mGLES2Context*>(malloc(sizeof(mGLES2Context)));
|
|
mGLES2ContextCreate(gl2Backend);
|
|
m_backend = &gl2Backend->d;
|
|
}
|
|
#endif
|
|
|
|
#ifdef BUILD_GL
|
|
if (!m_backend) {
|
|
glBackend = static_cast<mGLContext*>(malloc(sizeof(mGLContext)));
|
|
mGLContextCreate(glBackend);
|
|
m_backend = &glBackend->d;
|
|
}
|
|
#endif
|
|
m_backend->swap = [](VideoBackend* v) {
|
|
PainterGL* painter = static_cast<PainterGL*>(v->user);
|
|
if (!painter->m_gl->isValid()) {
|
|
return;
|
|
}
|
|
painter->m_gl->swapBuffers(painter->m_surface);
|
|
painter->makeCurrent();
|
|
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
mGLES2Context* gl2Backend = reinterpret_cast<mGLES2Context*>(painter->m_backend);
|
|
if (painter->m_widget && painter->supportsShaders()) {
|
|
QOpenGLFunctions* fn = painter->m_gl->functions();
|
|
fn->glFinish();
|
|
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);
|
|
}
|
|
#endif
|
|
};
|
|
|
|
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_shader.preprocessShader = static_cast<void*>(&reinterpret_cast<mGLES2Context*>(m_backend)->initialShader);
|
|
}
|
|
#endif
|
|
|
|
m_backend->user = this;
|
|
m_backend->filter = false;
|
|
m_backend->lockAspectRatio = false;
|
|
m_backend->interframeBlending = false;
|
|
m_gl->doneCurrent();
|
|
|
|
emit created();
|
|
}
|
|
|
|
void PainterGL::destroy() {
|
|
if (!m_gl) {
|
|
return;
|
|
}
|
|
makeCurrent();
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (m_shader.passes) {
|
|
mGLES2ShaderFree(&m_shader);
|
|
}
|
|
#endif
|
|
m_backend->deinit(m_backend);
|
|
m_gl->doneCurrent();
|
|
m_paintDev.reset();
|
|
m_gl.reset();
|
|
|
|
free(m_backend);
|
|
m_backend = nullptr;
|
|
}
|
|
|
|
void PainterGL::setContext(std::shared_ptr<CoreController> context) {
|
|
m_context = std::move(context);
|
|
}
|
|
|
|
void PainterGL::resizeContext() {
|
|
if (!m_context) {
|
|
return;
|
|
}
|
|
|
|
if (m_started) {
|
|
mCore* core = m_context->thread()->core;
|
|
core->reloadConfigOption(core, "videoScale", NULL);
|
|
}
|
|
m_interrupter.resume();
|
|
|
|
QSize size = m_context->screenDimensions();
|
|
if (m_dims == size) {
|
|
return;
|
|
}
|
|
dequeueAll(false);
|
|
|
|
mRectangle dims = {0, 0, size.width(), size.height()};
|
|
m_backend->setLayerDimensions(m_backend, VIDEO_LAYER_IMAGE, &dims);
|
|
recenterLayers();
|
|
m_dims = size;
|
|
}
|
|
|
|
void PainterGL::setMessagePainter(MessagePainter* messagePainter) {
|
|
m_messagePainter = messagePainter;
|
|
}
|
|
|
|
void PainterGL::recenterLayers() {
|
|
if (!m_context) {
|
|
return;
|
|
}
|
|
VideoBackendRecenter(m_backend, std::max(1U, m_context->videoScale()));
|
|
}
|
|
|
|
void PainterGL::resize(const QSize& size) {
|
|
qreal r = m_window->devicePixelRatio();
|
|
m_size = size;
|
|
if (m_paintDev) {
|
|
m_paintDev->setSize(m_size * r);
|
|
m_paintDev->setDevicePixelRatio(r);
|
|
}
|
|
if (m_started && !m_active) {
|
|
forceDraw();
|
|
}
|
|
}
|
|
|
|
void PainterGL::lockAspectRatio(bool lock) {
|
|
m_backend->lockAspectRatio = lock;
|
|
resize(m_size);
|
|
}
|
|
|
|
void PainterGL::lockIntegerScaling(bool lock) {
|
|
m_backend->lockIntegerScaling = lock;
|
|
resize(m_size);
|
|
}
|
|
|
|
void PainterGL::interframeBlending(bool enable) {
|
|
m_backend->interframeBlending = enable;
|
|
}
|
|
|
|
void PainterGL::showOSD(bool enable) {
|
|
m_showOSD = enable;
|
|
}
|
|
|
|
void PainterGL::showFrameCounter(bool enable) {
|
|
m_showFrameCounter = enable;
|
|
}
|
|
|
|
void PainterGL::filter(bool filter) {
|
|
m_backend->filter = filter;
|
|
if (m_started && !m_active) {
|
|
forceDraw();
|
|
}
|
|
}
|
|
|
|
void PainterGL::swapInterval(int interval) {
|
|
if (!m_started) {
|
|
return;
|
|
}
|
|
m_swapInterval = interval;
|
|
#ifdef Q_OS_WIN
|
|
wglSwapIntervalEXT(interval);
|
|
#elif defined(Q_OS_MAC)
|
|
CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval);
|
|
#else
|
|
#ifdef USE_GLX
|
|
if (QGuiApplication::platformName() == "xcb") {
|
|
::Display* display = glXGetCurrentDisplay();
|
|
GLXDrawable drawable = glXGetCurrentDrawable();
|
|
glXSwapIntervalEXT(display, drawable, interval);
|
|
}
|
|
#endif
|
|
#ifdef USE_EGL
|
|
if (QGuiApplication::platformName().contains("egl") || QGuiApplication::platformName() == "wayland") {
|
|
EGLDisplay display = eglGetCurrentDisplay();
|
|
eglSwapInterval(display, interval);
|
|
}
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
#ifndef GL_DEBUG_OUTPUT_SYNCHRONOUS
|
|
#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242
|
|
#endif
|
|
|
|
void PainterGL::start() {
|
|
makeCurrent();
|
|
#if defined(BUILD_GLES3) && !defined(Q_OS_MAC)
|
|
if (glContextHasBug(OpenGLBug::GLTHREAD_BLOCKS_SWAP)) {
|
|
// Suggested on Discord as a way to strongly hint that glthread should be disabled
|
|
// See https://gitlab.freedesktop.org/mesa/mesa/-/issues/8035
|
|
QOpenGLFunctions* fn = m_gl->functions();
|
|
fn->glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
|
|
}
|
|
#endif
|
|
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (m_supportsShaders && m_shader.passes) {
|
|
mGLES2ShaderAttach(reinterpret_cast<mGLES2Context*>(m_backend), static_cast<mGLES2Shader*>(m_shader.passes), m_shader.nPasses);
|
|
}
|
|
#endif
|
|
resizeContext();
|
|
|
|
m_buffer = nullptr;
|
|
m_active = true;
|
|
m_started = true;
|
|
swapInterval(1);
|
|
emit started();
|
|
}
|
|
|
|
void PainterGL::draw() {
|
|
if (!m_started || m_queue.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (m_interrupter.held()) {
|
|
// A resize event is pending; that needs to happen first
|
|
if (!m_drawTimer.isActive()) {
|
|
m_drawTimer.start(0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
mCoreSync* sync = &m_context->thread()->impl->sync;
|
|
if (!mCoreSyncWaitFrameStart(sync)) {
|
|
mCoreSyncWaitFrameEnd(sync);
|
|
if (!sync->audioWait && !sync->videoFrameWait) {
|
|
return;
|
|
}
|
|
if (m_delayTimer.elapsed() >= 1000 / m_window->screen()->refreshRate()) {
|
|
return;
|
|
}
|
|
if (!m_drawTimer.isActive()) {
|
|
m_drawTimer.start(1);
|
|
}
|
|
return;
|
|
}
|
|
int wantSwap = sync->audioWait || sync->videoFrameWait;
|
|
if (m_swapInterval != wantSwap) {
|
|
swapInterval(wantSwap);
|
|
}
|
|
dequeue();
|
|
bool forceRedraw = true;
|
|
if (!m_delayTimer.isValid()) {
|
|
m_delayTimer.start();
|
|
} else {
|
|
if (wantSwap) {
|
|
while (m_delayTimer.nsecsElapsed() + OVERHEAD_NSEC < 1000000000 / sync->fpsTarget) {
|
|
QThread::usleep(500);
|
|
}
|
|
forceRedraw = sync->videoFrameWait;
|
|
}
|
|
if (!forceRedraw) {
|
|
forceRedraw = m_delayTimer.nsecsElapsed() + OVERHEAD_NSEC >= 1000000000 / m_window->screen()->refreshRate();
|
|
}
|
|
}
|
|
mCoreSyncWaitFrameEnd(sync);
|
|
|
|
if (forceRedraw) {
|
|
m_delayTimer.restart();
|
|
performDraw();
|
|
m_backend->swap(m_backend);
|
|
}
|
|
}
|
|
|
|
void PainterGL::forceDraw() {
|
|
performDraw();
|
|
if (!m_context->thread()->impl->sync.audioWait && !m_context->thread()->impl->sync.videoFrameWait) {
|
|
if (m_delayTimer.elapsed() < 1000 / m_window->screen()->refreshRate()) {
|
|
return;
|
|
}
|
|
m_delayTimer.restart();
|
|
}
|
|
m_backend->swap(m_backend);
|
|
}
|
|
|
|
void PainterGL::stop() {
|
|
m_started = false;
|
|
QMetaObject::invokeMethod(this, "doStop", Qt::BlockingQueuedConnection);
|
|
}
|
|
|
|
void PainterGL::doStop() {
|
|
m_drawTimer.stop();
|
|
m_active = false;
|
|
m_started = false;
|
|
dequeueAll(false);
|
|
if (m_context) {
|
|
if (m_videoProxy) {
|
|
m_videoProxy->detach(m_context.get());
|
|
}
|
|
m_context->setFramebufferHandle(-1);
|
|
m_context.reset();
|
|
if (m_videoProxy) {
|
|
m_videoProxy->processData();
|
|
}
|
|
}
|
|
m_backend->clear(m_backend);
|
|
m_backend->swap(m_backend);
|
|
if (m_videoProxy) {
|
|
m_videoProxy->reset();
|
|
m_videoProxy->moveToThread(m_window->thread());
|
|
m_videoProxy.reset();
|
|
}
|
|
}
|
|
|
|
void PainterGL::pause() {
|
|
m_drawTimer.stop();
|
|
m_active = false;
|
|
dequeueAll(true);
|
|
}
|
|
|
|
void PainterGL::unpause() {
|
|
m_active = true;
|
|
}
|
|
|
|
void PainterGL::performDraw() {
|
|
float r = m_window->devicePixelRatio();
|
|
m_backend->contextResized(m_backend, m_size.width() * r, m_size.height() * r);
|
|
if (m_buffer) {
|
|
m_backend->setImage(m_backend, VIDEO_LAYER_IMAGE, m_buffer);
|
|
}
|
|
m_backend->drawFrame(m_backend);
|
|
if (m_showOSD && m_messagePainter && m_paintDev && !glContextHasBug(OpenGLBug::IG4ICD_CRASH)) {
|
|
m_painter.begin(m_paintDev.get());
|
|
m_messagePainter->paint(&m_painter);
|
|
m_painter.end();
|
|
}
|
|
}
|
|
|
|
void PainterGL::enqueue(const uint32_t* backing) {
|
|
QMutexLocker locker(&m_mutex);
|
|
uint32_t* buffer = nullptr;
|
|
if (backing) {
|
|
if (m_free.isEmpty()) {
|
|
buffer = m_queue.dequeue();
|
|
} else {
|
|
buffer = m_free.takeLast();
|
|
}
|
|
if (buffer) {
|
|
QSize size = m_context->screenDimensions();
|
|
memcpy(buffer, backing, size.width() * size.height() * BYTES_PER_PIXEL);
|
|
}
|
|
}
|
|
m_queue.enqueue(buffer);
|
|
}
|
|
|
|
void PainterGL::dequeue() {
|
|
QMutexLocker locker(&m_mutex);
|
|
if (!m_queue.isEmpty()) {
|
|
uint32_t* buffer = m_queue.dequeue();
|
|
if (m_buffer) {
|
|
m_free.append(m_buffer);
|
|
}
|
|
m_buffer = buffer;
|
|
}
|
|
}
|
|
|
|
void PainterGL::dequeueAll(bool keep) {
|
|
QMutexLocker locker(&m_mutex);
|
|
uint32_t* buffer = nullptr;
|
|
while (!m_queue.isEmpty()) {
|
|
buffer = m_queue.dequeue();
|
|
if (keep) {
|
|
if (m_buffer && buffer) {
|
|
m_free.append(m_buffer);
|
|
m_buffer = buffer;
|
|
}
|
|
} else if (buffer) {
|
|
m_free.append(buffer);
|
|
}
|
|
}
|
|
if (m_buffer && !keep) {
|
|
m_free.append(m_buffer);
|
|
m_buffer = nullptr;
|
|
}
|
|
}
|
|
|
|
void PainterGL::setVideoProxy(std::shared_ptr<VideoProxy> proxy) {
|
|
m_videoProxy = proxy;
|
|
if (proxy) {
|
|
proxy->setProxiedBackend(m_backend);
|
|
}
|
|
}
|
|
|
|
void PainterGL::interrupt() {
|
|
m_interrupter.interrupt(m_context);
|
|
}
|
|
|
|
void PainterGL::setShaders(struct VDir* dir) {
|
|
if (!supportsShaders()) {
|
|
return;
|
|
}
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (!m_started) {
|
|
makeCurrent();
|
|
}
|
|
|
|
if (m_shader.passes) {
|
|
mGLES2ShaderDetach(reinterpret_cast<mGLES2Context*>(m_backend));
|
|
mGLES2ShaderFree(&m_shader);
|
|
}
|
|
if (mGLES2ShaderLoad(&m_shader, dir)) {
|
|
mGLES2ShaderAttach(reinterpret_cast<mGLES2Context*>(m_backend), static_cast<mGLES2Shader*>(m_shader.passes), m_shader.nPasses);
|
|
}
|
|
|
|
if (!m_started) {
|
|
m_gl->doneCurrent();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void PainterGL::clearShaders() {
|
|
if (!supportsShaders()) {
|
|
return;
|
|
}
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (!m_started) {
|
|
makeCurrent();
|
|
}
|
|
|
|
if (m_shader.passes) {
|
|
mGLES2ShaderDetach(reinterpret_cast<mGLES2Context*>(m_backend));
|
|
mGLES2ShaderFree(&m_shader);
|
|
}
|
|
|
|
if (!m_started) {
|
|
m_gl->doneCurrent();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
VideoShader* PainterGL::shaders() {
|
|
return &m_shader;
|
|
}
|
|
|
|
QSize PainterGL::contentSize() const {
|
|
unsigned width, height;
|
|
VideoBackendGetFrameSize(m_backend, &width, &height);
|
|
return {saturateCast<int>(width),
|
|
saturateCast<int>(height)};
|
|
}
|
|
|
|
int PainterGL::glTex() {
|
|
#if defined(BUILD_GLES2) || defined(BUILD_GLES3)
|
|
if (supportsShaders()) {
|
|
mGLES2Context* gl2Backend = reinterpret_cast<mGLES2Context*>(m_backend);
|
|
return gl2Backend->tex[VIDEO_LAYER_IMAGE];
|
|
}
|
|
#endif
|
|
#ifdef BUILD_GL
|
|
mGLContext* glBackend = reinterpret_cast<mGLContext*>(m_backend);
|
|
return glBackend->tex[0];
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
QOpenGLContext* PainterGL::shareContext() {
|
|
if (m_widget) {
|
|
return m_widget->context();
|
|
} else {
|
|
return m_gl.get();
|
|
}
|
|
}
|
|
|
|
void PainterGL::setBackgroundImage(const QImage& image) {
|
|
if (!m_started) {
|
|
makeCurrent();
|
|
}
|
|
|
|
m_backend->setImageSize(m_backend, VIDEO_LAYER_BACKGROUND, image.width(), image.height());
|
|
recenterLayers();
|
|
|
|
if (!image.isNull()) {
|
|
m_background = image.convertToFormat(QImage::Format_RGB32);
|
|
m_background = m_background.rgbSwapped();
|
|
m_backend->setImage(m_backend, VIDEO_LAYER_BACKGROUND, m_background.constBits());
|
|
} else {
|
|
m_background = QImage();
|
|
}
|
|
|
|
if (!m_started) {
|
|
m_gl->doneCurrent();
|
|
}
|
|
}
|
|
|
|
#endif
|