From a475a61aeee64326ab0d7424391d5803366c5182 Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <vital.had@gmail.com>
Date: Wed, 25 Feb 2026 02:30:08 +0800
Subject: [PATCH 2/6] GL fixes

---
 src/qmplay2/CMakeLists.txt             |  27 +++-
 src/qmplay2/opengl/OpenGLCommon.cpp    |  53 +++++--
 src/qmplay2/opengl/OpenGLInstance.cpp  |  48 ++-----
 src/qmplay2/opengl/OpenGLWidget.cpp    |  15 +-
 src/qmplay2/opengl/OpenGLWidget.hpp    |  20 +--
 src/qmplay2/opengl/OpenGLWidgetQt4.cpp |  85 +++++++++++
 src/qmplay2/opengl/OpenGLWidgetQt4.hpp |  47 +++++++
 src/qmplay2/opengl/OpenGLWindow.cpp    |  81 +++++------
 src/qmplay2/opengl/OpenGLWindow.hpp    |  24 ++--
 src/qmplay2/opengl/OpenGLWindowQt4.cpp | 187 +++++++++++++++++++++++++
 src/qmplay2/opengl/OpenGLWindowQt4.hpp |  61 ++++++++
 src/qmplay2/opengl/OpenGLWriter.cpp    |  10 +-
 12 files changed, 527 insertions(+), 131 deletions(-)
 create mode 100644 src/qmplay2/opengl/OpenGLWidgetQt4.cpp
 create mode 100644 src/qmplay2/opengl/OpenGLWidgetQt4.hpp
 create mode 100644 src/qmplay2/opengl/OpenGLWindowQt4.cpp
 create mode 100644 src/qmplay2/opengl/OpenGLWindowQt4.hpp

diff --git a/src/qmplay2/CMakeLists.txt b/src/qmplay2/CMakeLists.txt
index 7eacdcba..1da1822e 100644
--- a/src/qmplay2/CMakeLists.txt
+++ b/src/qmplay2/CMakeLists.txt
@@ -111,8 +111,6 @@ if(USE_OPENGL)
         opengl/OpenGLInstance.hpp
         opengl/OpenGLWriter.hpp
         opengl/OpenGLCommon.hpp
-        opengl/OpenGLWindow.hpp
-        opengl/OpenGLWidget.hpp
         opengl/OpenGLVertices.hpp
         opengl/OpenGLHWInterop.hpp
     )
@@ -120,11 +118,32 @@ if(USE_OPENGL)
         opengl/OpenGLInstance.cpp
         opengl/OpenGLWriter.cpp
         opengl/OpenGLCommon.cpp
-        opengl/OpenGLWindow.cpp
-        opengl/OpenGLWidget.cpp
         opengl/opengl.qrc
     )
 
+    if(BUILD_WITH_QT4)
+        # Qt4 has no QOpenGLWidget/QOpenGLWindow (QWindow doesn't exist pre-Qt5),
+        # so OpenGLWidget/OpenGLWindow are built against QGLWidget instead, from
+        # separate Qt4-only files. This keeps the Qt5/Qt6 files untouched.
+        list(APPEND QMPLAY2_OPENGL_HDR
+            opengl/OpenGLWindowQt4.hpp
+            opengl/OpenGLWidgetQt4.hpp
+        )
+        list(APPEND QMPLAY2_OPENGL_SRC
+            opengl/OpenGLWindowQt4.cpp
+            opengl/OpenGLWidgetQt4.cpp
+        )
+    else()
+        list(APPEND QMPLAY2_OPENGL_HDR
+            opengl/OpenGLWindow.hpp
+            opengl/OpenGLWidget.hpp
+        )
+        list(APPEND QMPLAY2_OPENGL_SRC
+            opengl/OpenGLWindow.cpp
+            opengl/OpenGLWidget.cpp
+        )
+    endif()
+
     # Qt4 has OpenGL support built into QtOpenGL (already linked via QT_USE_FILE)
     # For macOS 10.6.8, we use OpenGL 2.0
     find_package(OpenGL REQUIRED)
diff --git a/src/qmplay2/opengl/OpenGLCommon.cpp b/src/qmplay2/opengl/OpenGLCommon.cpp
index 3ad1d289..c7fe3540 100644
--- a/src/qmplay2/opengl/OpenGLCommon.cpp
+++ b/src/qmplay2/opengl/OpenGLCommon.cpp
@@ -27,6 +27,9 @@
 #include <Functions.hpp>
 
 #include <QtOpenGL/QGLShader>
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+    #include <QtOpenGL/QGLContext>
+#endif
 #include <QResizeEvent>
 #include <QResource>
 #include <QPainter>
@@ -56,6 +59,33 @@
     #define GL_RED GL_RED_EXT
 #endif
 
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+// Qt4: <OpenGL/gl.h> only declares core GL 1.x entry points on macOS, so
+// glMapBufferRange (GL_ARB_map_buffer_range / core since GL 3.0) has no
+// prototype and isn't guaranteed to be a directly linkable symbol. Resolve it
+// through QGLContext::getProcAddress(), the standard Qt4 mechanism for GL
+// extension functions, instead of calling it as a directly-linked symbol.
+// This only runs when m_glInstance->hasMapBufferRange is true (i.e. the
+// extension was actually detected), so GL2.0-only hardware without it simply
+// never reaches this code path (see OpenGLInstance.cpp's capability probe).
+typedef GLvoid *(*QMPlay2MapBufferRangeFn)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
+static QMPlay2MapBufferRangeFn qmplay2_glMapBufferRange = nullptr;
+static bool qmplay2_glMapBufferRangeResolved = false;
+static GLvoid *qmplay2_mapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access)
+{
+    if (!qmplay2_glMapBufferRangeResolved)
+    {
+        qmplay2_glMapBufferRangeResolved = true;
+        if (auto ctx = QGLContext::currentContext())
+            qmplay2_glMapBufferRange = reinterpret_cast<QMPlay2MapBufferRangeFn>(ctx->getProcAddress(QString("glMapBufferRange")));
+    }
+    if (!qmplay2_glMapBufferRange)
+        return nullptr;
+    return qmplay2_glMapBufferRange(target, offset, length, access);
+}
+    #define glMapBufferRange qmplay2_mapBufferRange
+#endif
+
 OpenGLCommon::OpenGLCommon() :
     VideoOutputCommon(false),
     vSync(true),
@@ -77,9 +107,8 @@ OpenGLCommon::OpenGLCommon() :
     texCoordYCbCr[0] = texCoordYCbCr[4] = texCoordYCbCr[5] = texCoordYCbCr[7] = 0.0f;
     texCoordYCbCr[1] = texCoordYCbCr[3] = 1.0f;
 
-#ifndef Q_OS_MAC
+    // Qt4+macOS may have OpenGL 2.0, not 3.0 - check the actual version
     m_gl3 = (m_glInstance->glVer >= 30);
-#endif
 
     m_matrixChangeFn = [this] {
         setMatrix = true;
@@ -270,6 +299,12 @@ void OpenGLCommon::initializeGL()
     if (target == GL_TEXTURE_RECTANGLE_ARB)
         videoFrag.prepend("#define TEXTURE_RECTANGLE\n");
     shaderProgramVideo->addShaderFromSourceCode(QGLShader::Fragment, videoFrag);
+    if (!shaderProgramVideo->link())
+    {
+        QMPlay2Core.logError(tr("Shader link error: ") + shaderProgramVideo->log());
+        isOK = false;
+        return;
+    }
     if (shaderProgramVideo->bind())
     {
         texCoordYCbCrLoc = shaderProgramVideo->attributeLocation("aTexCoord");
@@ -294,6 +329,12 @@ void OpenGLCommon::initializeGL()
     /* OSD shader */
     shaderProgramOSD->addShaderFromSourceCode(QGLShader::Vertex, readShader(":/opengl/OSD.vert"));
     shaderProgramOSD->addShaderFromSourceCode(QGLShader::Fragment, readShader(":/opengl/OSD.frag"));
+    if (!shaderProgramOSD->link())
+    {
+        QMPlay2Core.logError(tr("Shader link error: ") + shaderProgramOSD->log());
+        isOK = false;
+        return;
+    }
     if (shaderProgramOSD->bind())
     {
         texCoordOSDLoc = shaderProgramOSD->attributeLocation("aTexCoord");
@@ -451,12 +492,10 @@ void OpenGLCommon::paintGL()
                 {
                     glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[p + 1]);
                     quint8 *dst = nullptr;
-#if !defined(QT_OPENGL_ES_2) && !defined(QT_FEATURE_opengles2)
-    #if !defined(Q_OS_MAC)
                     if (m_glInstance->hasMapBufferRange)
                         dst = (quint8 *)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, w * h * bytesMultiplier, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
+#if !defined(QT_OPENGL_ES_2) && !defined(QT_FEATURE_opengles2)
                     else
-    #endif
                         dst = (quint8 *)::glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
 #endif
                     if (!dst)
@@ -629,12 +668,10 @@ void OpenGLCommon::paintGL()
                 if (hasNewSize)
                     glBufferData(GL_PIXEL_UNPACK_BUFFER, dataSize, nullptr, GL_DYNAMIC_DRAW);
                 quint8 *dst = nullptr;
-#if !defined(QT_OPENGL_ES_2) && !defined(QT_FEATURE_opengles2)
-    #if !defined(Q_OS_MAC)
                 if (m_glInstance->hasMapBufferRange)
                     dst = (quint8 *)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, dataSize, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
+#if !defined(QT_OPENGL_ES_2) && !defined(QT_FEATURE_opengles2)
                 else
-    #endif
                     dst = (quint8 *)::glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
 #endif
                 if (!dst)
diff --git a/src/qmplay2/opengl/OpenGLInstance.cpp b/src/qmplay2/opengl/OpenGLInstance.cpp
index 96102785..ffb9149c 100644
--- a/src/qmplay2/opengl/OpenGLInstance.cpp
+++ b/src/qmplay2/opengl/OpenGLInstance.cpp
@@ -10,45 +10,30 @@
 
 bool OpenGLInstance::init()
 {
-    // Qt4: Need an active GL context to query
-    // Create a temporary context if needed
-#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
-    QGLWidget *tempWidget = nullptr;
-    const QGLContext *existingContext = QGLContext::currentContext();
-    if (!existingContext)
+    // Qt4: no QOffscreenSurface/QOpenGLContext, so make sure a GL context is
+    // current by creating a temporary QGLWidget if one isn't already active.
+    // This lets us probe real driver/extension capabilities instead of
+    // hardcoding GL 2.0 defaults, so this also works correctly on newer
+    // macOS/drivers that support GL 2.1+ (PBOs, GL_ARB_map_buffer_range, etc.),
+    // not just the conservative macOS 10.6 baseline.
+    std::unique_ptr<QGLWidget> tempWidget;
+    if (!QGLContext::currentContext())
     {
-        // Create temporary offscreen widget to get a context
         QGLFormat format;
         format.setVersion(2, 0);
-        tempWidget = new QGLWidget(format);
-
-        // Ensure context is created - on some platforms we need this
+        tempWidget.reset(new QGLWidget(format));
         if (!tempWidget->isValid())
-        {
-            delete tempWidget;
             return false;
-        }
 
         tempWidget->makeCurrent();
-
-        // Verify context is now current
         if (!QGLContext::currentContext())
-        {
-            delete tempWidget;
             return false;
-        }
     }
-#endif
 
     // Get OpenGL version
     const char *versionStr = (const char *)glGetString(GL_VERSION);
     if (!versionStr)
-    {
-#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
-        delete tempWidget;
-#endif
         return false;
-    }
 
     // Parse version string (e.g., "2.1 ..." or "3.0 ...")
     int majorVersion = 2;
@@ -92,17 +77,10 @@ bool OpenGLInstance::init()
                  ((majorVersion >= 2) || extensionSet.contains("GL_ARB_pixel_buffer_object"));
     }
 
-#ifndef Q_OS_MAC
-    glVer = majorVersion * 10 + minorVersion;
-#else
-    // macOS 10.6.8 has OpenGL 2.0/2.1
-    glVer = 20;
-#endif
-
-#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
-    // Clean up temporary widget
-    delete tempWidget;
-#endif
+    // Use the detected version rather than hardcoding 2.0, so a newer
+    // macOS/driver reporting 2.1+ (or 3.0+) is reflected accurately; falls
+    // back to 2.0 only if somehow the parsed version is below that.
+    glVer = (majorVersion >= 2) ? (majorVersion * 10 + minorVersion) : 20;
 
     return true;
 }
diff --git a/src/qmplay2/opengl/OpenGLWidget.cpp b/src/qmplay2/opengl/OpenGLWidget.cpp
index 84a945d2..f7a7da05 100644
--- a/src/qmplay2/opengl/OpenGLWidget.cpp
+++ b/src/qmplay2/opengl/OpenGLWidget.cpp
@@ -18,11 +18,12 @@
 
 #include "OpenGLWidget.hpp"
 
+#include <QOpenGLContext>
+
 OpenGLWidget::OpenGLWidget()
 {
     m_widget = this;
     connect(&updateTimer, SIGNAL(timeout()), this, SLOT(update()));
-    connect(&m_rotAnimation, SIGNAL(valueChanged(QVariant)), this, SLOT(onRotValueUpdated(QVariant)));
 }
 OpenGLWidget::~OpenGLWidget()
 {
@@ -47,7 +48,7 @@ void OpenGLWidget::setVSync(bool enable)
     Q_UNUSED(enable)
     // Not supported
 }
-void OpenGLWidget::requestGLUpdate(bool requestDelayed)
+void OpenGLWidget::updateGL(bool requestDelayed)
 {
     if (requestDelayed)
         QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
@@ -57,10 +58,7 @@ void OpenGLWidget::requestGLUpdate(bool requestDelayed)
 
 void OpenGLWidget::initializeGL()
 {
-#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
     connect(context(), SIGNAL(aboutToBeDestroyed()), this, SLOT(aboutToBeDestroyed()), Qt::DirectConnection);
-#endif
-    // Qt4: QGLContext doesn't emit aboutToBeDestroyed(), cleanup happens in destructor
     OpenGLCommon::initializeGL();
 }
 void OpenGLWidget::paintGL()
@@ -76,13 +74,8 @@ void OpenGLWidget::aboutToBeDestroyed()
     doneCurrent();
 }
 
-void OpenGLWidget::onRotValueUpdated(const QVariant &value)
-{
-    rotValueUpdated(value);
-}
-
 bool OpenGLWidget::event(QEvent *e)
 {
     dispatchEvent(e, parent());
-    return QGLWidget::event(e);
+    return QOpenGLWidget::event(e);
 }
diff --git a/src/qmplay2/opengl/OpenGLWidget.hpp b/src/qmplay2/opengl/OpenGLWidget.hpp
index 4c41a5a4..43503855 100644
--- a/src/qmplay2/opengl/OpenGLWidget.hpp
+++ b/src/qmplay2/opengl/OpenGLWidget.hpp
@@ -20,26 +20,26 @@
 
 #include "OpenGLCommon.hpp"
 
-#include <QtOpenGL/QGLWidget>
+#include <QSurfaceFormat>
+#include <QOpenGLWidget>
 
-class OpenGLWidget : public QGLWidget, public OpenGLCommon
+class OpenGLWidget final : public QOpenGLWidget, public OpenGLCommon
 {
     Q_OBJECT
 public:
     OpenGLWidget();
     ~OpenGLWidget();
 
-    bool makeContextCurrent();
-    void doneContextCurrent();
+    bool makeContextCurrent() override;
+    void doneContextCurrent() override;
 
-    void setVSync(bool enable);
-    void requestGLUpdate(bool requestDelayed);
+    void setVSync(bool enable) override;
+    void updateGL(bool requestDelayed) override;
 
-    void initializeGL();
-    void paintGL();
+    void initializeGL() override;
+    void paintGL() override;
 private slots:
     void aboutToBeDestroyed();
-    void onRotValueUpdated(const QVariant &value);
 private:
-    bool event(QEvent *e);
+    bool event(QEvent *e) override;
 };
diff --git a/src/qmplay2/opengl/OpenGLWidgetQt4.cpp b/src/qmplay2/opengl/OpenGLWidgetQt4.cpp
new file mode 100644
index 00000000..b81fa6c7
--- /dev/null
+++ b/src/qmplay2/opengl/OpenGLWidgetQt4.cpp
@@ -0,0 +1,85 @@
+/*
+    QMPlay2 is a video and audio player.
+    Copyright (C) 2010-2026  Błażej Szczygieł
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "OpenGLWidgetQt4.hpp"
+
+OpenGLWidget::OpenGLWidget()
+{
+    m_widget = this;
+    connect(&updateTimer, SIGNAL(timeout()), this, SLOT(update()));
+    connect(&m_rotAnimation, SIGNAL(valueChanged(QVariant)), this, SLOT(onRotValueUpdated(QVariant)));
+}
+OpenGLWidget::~OpenGLWidget()
+{
+    makeCurrent();
+}
+
+bool OpenGLWidget::makeContextCurrent()
+{
+    if (!context())
+        return false;
+
+    makeCurrent();
+    return true;
+}
+void OpenGLWidget::doneContextCurrent()
+{
+    doneCurrent();
+}
+
+void OpenGLWidget::setVSync(bool enable)
+{
+    Q_UNUSED(enable)
+    // Not supported
+}
+void OpenGLWidget::requestGLUpdate(bool requestDelayed)
+{
+    if (requestDelayed)
+        QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
+    else
+        update();
+}
+
+void OpenGLWidget::initializeGL()
+{
+    // Qt4: QGLContext doesn't emit aboutToBeDestroyed(), cleanup happens in destructor
+    OpenGLCommon::initializeGL();
+}
+void OpenGLWidget::paintGL()
+{
+    glClear(GL_COLOR_BUFFER_BIT);
+    OpenGLCommon::paintGL();
+}
+
+void OpenGLWidget::aboutToBeDestroyed()
+{
+    makeCurrent();
+    contextAboutToBeDestroyed();
+    doneCurrent();
+}
+
+void OpenGLWidget::onRotValueUpdated(const QVariant &value)
+{
+    rotValueUpdated(value);
+}
+
+bool OpenGLWidget::event(QEvent *e)
+{
+    dispatchEvent(e, parent());
+    return QGLWidget::event(e);
+}
diff --git a/src/qmplay2/opengl/OpenGLWidgetQt4.hpp b/src/qmplay2/opengl/OpenGLWidgetQt4.hpp
new file mode 100644
index 00000000..731f6667
--- /dev/null
+++ b/src/qmplay2/opengl/OpenGLWidgetQt4.hpp
@@ -0,0 +1,47 @@
+/*
+    QMPlay2 is a video and audio player.
+    Copyright (C) 2010-2026  Błażej Szczygieł
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include "OpenGLCommon.hpp"
+
+#include <QtOpenGL/QGLWidget>
+
+// Qt4: No QOpenGLWidget, use QGLWidget instead. Kept as a separate class/file
+// from OpenGLWidget so Qt5/Qt6 builds are unaffected (see OpenGLWidget.hpp).
+class OpenGLWidget : public QGLWidget, public OpenGLCommon
+{
+    Q_OBJECT
+public:
+    OpenGLWidget();
+    ~OpenGLWidget();
+
+    bool makeContextCurrent();
+    void doneContextCurrent();
+
+    void setVSync(bool enable);
+    void requestGLUpdate(bool requestDelayed);
+
+    void initializeGL();
+    void paintGL();
+private slots:
+    void aboutToBeDestroyed();
+    void onRotValueUpdated(const QVariant &value);
+private:
+    bool event(QEvent *e);
+};
diff --git a/src/qmplay2/opengl/OpenGLWindow.cpp b/src/qmplay2/opengl/OpenGLWindow.cpp
index 1c9c4b78..b44399ac 100644
--- a/src/qmplay2/opengl/OpenGLWindow.cpp
+++ b/src/qmplay2/opengl/OpenGLWindow.cpp
@@ -20,37 +20,29 @@
 
 #include <QMPlay2Core.hpp>
 
+#include <QGuiApplication>
+#include <QOpenGLContext>
 #include <QDockWidget>
 
 OpenGLWindow::OpenGLWindow()
-    : m_platformName()
+    : m_platformName(QGuiApplication::platformName())
     , m_passEventsToParent(
-#ifdef Q_OS_MAC
-          true // macOS - pass events to parent
-#elif defined(Q_OS_WIN) || defined(Q_OS_ANDROID)
-          false // Windows/Android - transparent input
-#else
-          false // Linux/X11 - transparent input
+#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
+          !m_platformName.contains(QStringLiteral("wayland")) &&
+          m_platformName != QStringLiteral("windows") &&
 #endif
-      )
+          m_platformName != QStringLiteral("xcb") && m_platformName != QStringLiteral("android"))
 {
     connect(&updateTimer, SIGNAL(timeout()), this, SLOT(doUpdateGL()));
-    connect(&m_rotAnimation, SIGNAL(valueChanged(QVariant)), this, SLOT(onRotValueUpdated(QVariant)));
-
-    // Qt4: QGLWidget is already a widget, no need for container
-    m_widget = this;
-
-#if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID)
-    setAttribute(Qt::WA_NativeWindow);
-#endif
-
-    setAcceptDrops(false);
 
     if (!m_passEventsToParent)
-    {
-        // Make widget transparent to input events
-        setAttribute(Qt::WA_TransparentForMouseEvents);
-    }
+        setFlags(Qt::WindowTransparentForInput);
+
+    m_widget = QWidget::createWindowContainer(this);
+    if (!m_platformName.contains(QStringLiteral("wayland")) && m_platformName != QStringLiteral("android"))
+        m_widget->setAttribute(Qt::WA_NativeWindow);
+    m_widget->installEventFilter(this);
+    m_widget->setAcceptDrops(false);
 
     connect(&QMPlay2Core, SIGNAL(videoDockVisible(bool)), this, SLOT(videoVisible(bool)));
 }
@@ -79,35 +71,37 @@ void OpenGLWindow::doneContextCurrent()
 
 void OpenGLWindow::setVSync(bool enable)
 {
-    // Qt4: Use QGLFormat instead of QSurfaceFormat
-    QGLFormat fmt = format();
-    if (enable != fmt.swapInterval())
+    QSurfaceFormat fmt = format();
+    if (!handle())
     {
-        fmt.setSwapInterval(enable ? 1 : 0);
-        fmt.setDoubleBuffer(true);
+        fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer); //Probably it doesn't work
+        fmt.setSwapInterval(enable);
         setFormat(fmt);
-        // Qt4: May need recreation for format changes
-        // But that can cause issues, so just set for next time
+    }
+    else if (enable != fmt.swapInterval())
+    {
+        fmt.setSwapInterval(enable);
+        destroy();
+        setFormat(fmt);
+        create();
+        setVisible(true);
     }
     vSync = enable;
 }
-void OpenGLWindow::requestGLUpdate(bool requestDelayed)
+void OpenGLWindow::updateGL(bool requestDelayed)
 {
-    // Qt4: Use isVisible() instead of isExposed()
-    if (m_visible && isVisible())
+    if (m_visible && isExposed())
         QMetaObject::invokeMethod(this, "doUpdateGL", Qt::QueuedConnection, Q_ARG(bool, requestDelayed));
 }
 
 void OpenGLWindow::initializeGL()
 {
-    // Qt4: QGLContext doesn't have aboutToBeDestroyed signal
-    // Context destruction is handled in destructor
+    connect(context(), SIGNAL(aboutToBeDestroyed()), this, SLOT(aboutToBeDestroyed()), Qt::DirectConnection);
     OpenGLCommon::initializeGL();
 }
 void OpenGLWindow::paintGL()
 {
-    // Qt4: Use isVisible() instead of isExposed()
-    if (isVisible())
+    if (isExposed())
     {
         glClear(GL_COLOR_BUFFER_BIT);
         OpenGLCommon::paintGL();
@@ -136,15 +130,10 @@ void OpenGLWindow::videoVisible(bool v)
     m_visible = v && (m_widget->visibleRegion() != QRegion() || QMPlay2Core.getVideoDock()->visibleRegion() != QRegion());
 }
 
-void OpenGLWindow::onRotValueUpdated(const QVariant &value)
-{
-    rotValueUpdated(value);
-}
-
 bool OpenGLWindow::eventFilter(QObject *o, QEvent *e)
 {
-    // Qt4: m_widget is now this, so handle parent dispatching
-    dispatchEvent(e, parent());
+    if (o == m_widget)
+        dispatchEvent(e, m_widget->parent());
     return false;
 }
 
@@ -158,9 +147,7 @@ bool OpenGLWindow::event(QEvent *e)
         case QEvent::MouseMove:
         case QEvent::FocusIn:
         case QEvent::FocusOut:
-#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
         case QEvent::FocusAboutToChange:
-#endif
         case QEvent::Enter:
         case QEvent::Leave:
         case QEvent::TabletMove:
@@ -171,10 +158,8 @@ bool OpenGLWindow::event(QEvent *e)
         case QEvent::TouchBegin:
         case QEvent::TouchUpdate:
         case QEvent::TouchEnd:
-#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
         case QEvent::InputMethodQuery:
         case QEvent::TouchCancel:
-#endif
             if (m_passEventsToParent)
                 return QCoreApplication::sendEvent(parent(), e);
         case QEvent::Wheel:
@@ -183,5 +168,5 @@ bool OpenGLWindow::event(QEvent *e)
         default:
             break;
     }
-    return QGLWidget::event(e);
+    return QOpenGLWindow::event(e);
 }
diff --git a/src/qmplay2/opengl/OpenGLWindow.hpp b/src/qmplay2/opengl/OpenGLWindow.hpp
index 574da766..b61ab6e4 100644
--- a/src/qmplay2/opengl/OpenGLWindow.hpp
+++ b/src/qmplay2/opengl/OpenGLWindow.hpp
@@ -20,36 +20,34 @@
 
 #include "OpenGLCommon.hpp"
 
-#include <QtOpenGL/QGLWidget>
+#include <QOpenGLWindow>
 
-// Qt4: No QOpenGLWindow, use QGLWidget
-class OpenGLWindow : public QGLWidget, public OpenGLCommon
+class OpenGLWindow final : private QOpenGLWindow, public OpenGLCommon
 {
     Q_OBJECT
 public:
     OpenGLWindow();
     ~OpenGLWindow();
 
-    void deleteMe();
+    void deleteMe() override;
 
-    bool makeContextCurrent();
-    void doneContextCurrent();
+    bool makeContextCurrent() override;
+    void doneContextCurrent() override;
 
-    void setVSync(bool enable);
-    void requestGLUpdate(bool requestDelayed);
+    void setVSync(bool enable) override;
+    void updateGL(bool requestDelayed) override;
 
-    void initializeGL();
-    void paintGL();
+    void initializeGL() override;
+    void paintGL() override;
 
 private slots:
     void doUpdateGL(bool queued = false);
     void aboutToBeDestroyed();
     void videoVisible(bool v);
-    void onRotValueUpdated(const QVariant &value);
 private:
-    bool eventFilter(QObject *o, QEvent *e);
+    bool eventFilter(QObject *o, QEvent *e) override;
 
-    bool event(QEvent *e);
+    bool event(QEvent *e) override;
 
 private:
     const QString m_platformName;
diff --git a/src/qmplay2/opengl/OpenGLWindowQt4.cpp b/src/qmplay2/opengl/OpenGLWindowQt4.cpp
new file mode 100644
index 00000000..6e3d86d2
--- /dev/null
+++ b/src/qmplay2/opengl/OpenGLWindowQt4.cpp
@@ -0,0 +1,187 @@
+/*
+    QMPlay2 is a video and audio player.
+    Copyright (C) 2010-2026  Błażej Szczygieł
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "OpenGLWindowQt4.hpp"
+
+#include <QMPlay2Core.hpp>
+
+#include <QDockWidget>
+
+OpenGLWindow::OpenGLWindow()
+    : m_platformName()
+    , m_passEventsToParent(
+#ifdef Q_OS_MAC
+          true // macOS - pass events to parent
+#elif defined(Q_OS_WIN) || defined(Q_OS_ANDROID)
+          false // Windows/Android - transparent input
+#else
+          false // Linux/X11 - transparent input
+#endif
+      )
+{
+    connect(&updateTimer, SIGNAL(timeout()), this, SLOT(doUpdateGL()));
+    connect(&m_rotAnimation, SIGNAL(valueChanged(QVariant)), this, SLOT(onRotValueUpdated(QVariant)));
+
+    // Qt4: QGLWidget is already a widget, no need for container
+    m_widget = this;
+
+#if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID)
+    setAttribute(Qt::WA_NativeWindow);
+#endif
+
+    setAcceptDrops(false);
+
+    if (!m_passEventsToParent)
+    {
+        // Make widget transparent to input events
+        setAttribute(Qt::WA_TransparentForMouseEvents);
+    }
+
+    connect(&QMPlay2Core, SIGNAL(videoDockVisible(bool)), this, SLOT(videoVisible(bool)));
+}
+OpenGLWindow::~OpenGLWindow()
+{
+    makeCurrent();
+}
+
+void OpenGLWindow::deleteMe()
+{
+    delete m_widget;
+}
+
+bool OpenGLWindow::makeContextCurrent()
+{
+    if (!context())
+        return false;
+
+    makeCurrent();
+    return true;
+}
+void OpenGLWindow::doneContextCurrent()
+{
+    doneCurrent();
+}
+
+void OpenGLWindow::setVSync(bool enable)
+{
+    // Qt4: Use QGLFormat instead of QSurfaceFormat
+    QGLFormat fmt = format();
+    if (enable != fmt.swapInterval())
+    {
+        fmt.setSwapInterval(enable ? 1 : 0);
+        fmt.setDoubleBuffer(true);
+        setFormat(fmt);
+        // Qt4: May need recreation for format changes
+        // But that can cause issues, so just set for next time
+    }
+    vSync = enable;
+}
+void OpenGLWindow::requestGLUpdate(bool requestDelayed)
+{
+    // Qt4: Use isVisible() instead of isExposed()
+    if (m_visible && isVisible())
+        QMetaObject::invokeMethod(this, "doUpdateGL", Qt::QueuedConnection, Q_ARG(bool, requestDelayed));
+}
+
+void OpenGLWindow::initializeGL()
+{
+    // Qt4: QGLContext doesn't have aboutToBeDestroyed signal
+    // Context destruction is handled in destructor
+    OpenGLCommon::initializeGL();
+}
+void OpenGLWindow::paintGL()
+{
+    // Qt4: Use isVisible() instead of isExposed()
+    if (isVisible())
+    {
+        glClear(GL_COLOR_BUFFER_BIT);
+        OpenGLCommon::paintGL();
+    }
+}
+
+void OpenGLWindow::doUpdateGL(bool queued)
+{
+    if (queued)
+        QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest), Qt::LowEventPriority);
+    else
+    {
+        //sendEvent() doesn't enqueue the event here
+        QEvent updateEvent(QEvent::UpdateRequest);
+        QCoreApplication::sendEvent(this, &updateEvent);
+    }
+}
+void OpenGLWindow::aboutToBeDestroyed()
+{
+    makeCurrent();
+    contextAboutToBeDestroyed();
+    doneCurrent();
+}
+void OpenGLWindow::videoVisible(bool v)
+{
+    m_visible = v && (m_widget->visibleRegion() != QRegion() || QMPlay2Core.getVideoDock()->visibleRegion() != QRegion());
+}
+
+void OpenGLWindow::onRotValueUpdated(const QVariant &value)
+{
+    rotValueUpdated(value);
+}
+
+bool OpenGLWindow::eventFilter(QObject *o, QEvent *e)
+{
+    // Qt4: m_widget is now this, so handle parent dispatching
+    dispatchEvent(e, parent());
+    return false;
+}
+
+bool OpenGLWindow::event(QEvent *e)
+{
+    switch (e->type())
+    {
+        case QEvent::MouseButtonPress:
+        case QEvent::MouseButtonRelease:
+        case QEvent::MouseButtonDblClick:
+        case QEvent::MouseMove:
+        case QEvent::FocusIn:
+        case QEvent::FocusOut:
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+        case QEvent::FocusAboutToChange:
+#endif
+        case QEvent::Enter:
+        case QEvent::Leave:
+        case QEvent::TabletMove:
+        case QEvent::TabletPress:
+        case QEvent::TabletRelease:
+        case QEvent::TabletEnterProximity:
+        case QEvent::TabletLeaveProximity:
+        case QEvent::TouchBegin:
+        case QEvent::TouchUpdate:
+        case QEvent::TouchEnd:
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+        case QEvent::InputMethodQuery:
+        case QEvent::TouchCancel:
+#endif
+            if (m_passEventsToParent)
+                return QCoreApplication::sendEvent(parent(), e);
+        case QEvent::Wheel:
+            if (m_passEventsToParent)
+                return QCoreApplication::sendEvent(const_cast<QWidget *>(QMPlay2Core.getVideoDock()), e);
+        default:
+            break;
+    }
+    return QGLWidget::event(e);
+}
diff --git a/src/qmplay2/opengl/OpenGLWindowQt4.hpp b/src/qmplay2/opengl/OpenGLWindowQt4.hpp
new file mode 100644
index 00000000..bfd45aa4
--- /dev/null
+++ b/src/qmplay2/opengl/OpenGLWindowQt4.hpp
@@ -0,0 +1,61 @@
+/*
+    QMPlay2 is a video and audio player.
+    Copyright (C) 2010-2026  Błażej Szczygieł
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include "OpenGLCommon.hpp"
+
+#include <QtOpenGL/QGLWidget>
+
+// Qt4: No QOpenGLWindow, use QGLWidget. Kept as a separate class/file from
+// OpenGLWindow so Qt5/Qt6 builds are unaffected (see OpenGLWindow.hpp).
+class OpenGLWindow : public QGLWidget, public OpenGLCommon
+{
+    Q_OBJECT
+public:
+    OpenGLWindow();
+    ~OpenGLWindow();
+
+    void deleteMe();
+
+    bool makeContextCurrent();
+    void doneContextCurrent();
+
+    void setVSync(bool enable);
+    void requestGLUpdate(bool requestDelayed);
+
+    void initializeGL();
+    void paintGL();
+
+private slots:
+    void doUpdateGL(bool queued = false);
+    void aboutToBeDestroyed();
+    void videoVisible(bool v);
+    void onRotValueUpdated(const QVariant &value);
+private:
+    bool eventFilter(QObject *o, QEvent *e);
+
+    bool event(QEvent *e);
+
+private:
+    const QString m_platformName;
+
+    const bool m_passEventsToParent;
+
+    bool m_visible = true;
+};
diff --git a/src/qmplay2/opengl/OpenGLWriter.cpp b/src/qmplay2/opengl/OpenGLWriter.cpp
index b9e35e96..01113883 100644
--- a/src/qmplay2/opengl/OpenGLWriter.cpp
+++ b/src/qmplay2/opengl/OpenGLWriter.cpp
@@ -18,8 +18,14 @@
 
 #include "OpenGLWriter.hpp"
 
-#include "OpenGLWindow.hpp"
-#include "OpenGLWidget.hpp"
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+    #include "OpenGLWindow.hpp"
+    #include "OpenGLWidget.hpp"
+#else
+    // Qt4: OpenGLWidget/OpenGLWindow need QGLWidget instead of QOpenGLWidget/QOpenGLWindow
+    #include "OpenGLWindowQt4.hpp"
+    #include "OpenGLWidgetQt4.hpp"
+#endif
 #include "OpenGLHWInterop.hpp"
 
 #include <Frame.hpp>
