diff --git a/src/gui/kernel/qopenglcontext.cpp b/src/gui/kernel/qopenglcontext.cpp index d8da482ecc3..02781f4aa0c 100644 --- a/src/gui/kernel/qopenglcontext.cpp +++ b/src/gui/kernel/qopenglcontext.cpp @@ -138,6 +138,22 @@ QOpenGLContext *qt_gl_global_share_context() application is portable between different platforms. However, if you use QOpenGLFunctions::glBindFramebuffer(), this is done automatically for you. + \warning WebAssembly + + We recommend that only one QOpenGLContext is made current with a QSurface, + for the entire lifetime of the QSurface. Should more than once context be used, + it is important to understand that multiple QOpenGLContext instances may be + backed by the same native context underneath with the WebAssembly platform. + Therefore, calling makeCurrent() with the same QSurface on two QOpenGLContext + objects may not switch to a different native context in the second call. As + a result, any OpenGL state changes done after the second makeCurrent() may + alter the state of the first QOpenGLContext as well, as they are all backed + by the same native context. + + \note This means that when targeting WebAssembly with existing OpenGL-based + Qt code, some porting may be required to cater to these limitations. + + \sa QOpenGLFunctions, QOpenGLBuffer, QOpenGLShaderProgram, QOpenGLFramebufferObject */ diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp index 07ae1d1cf53..8a4664ec8cd 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp @@ -152,7 +152,13 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface) { - if (auto *shareContext = m_qGlContext->shareContext()) + static bool sentSharingWarning = false; + if (!sentSharingWarning && isSharing()) { + qWarning() << "The functionality for sharing OpenGL contexts is limited, see documentation"; + sentSharingWarning = true; + } + + if (auto *shareContext = m_qGlContext->shareContext()) return shareContext->makeCurrent(surface->surface()); const auto context = obtainEmscriptenContext(surface); diff --git a/tests/auto/wasm/selenium/CMakeLists.txt b/tests/auto/wasm/selenium/CMakeLists.txt index b544c2a0d65..335b6d23d9b 100644 --- a/tests/auto/wasm/selenium/CMakeLists.txt +++ b/tests/auto/wasm/selenium/CMakeLists.txt @@ -12,9 +12,23 @@ qt_internal_add_test(tst_qwasmwindow_harness LIBRARIES Qt::Core Qt::Gui + Qt::OpenGL Qt::Widgets ) +# Resources: +set(shaders_resource_files + "fshader.glsl" + "vshader.glsl" +) + +qt6_add_resources(tst_qwasmwindow_harness "shaders" + PREFIX + "/" + FILES + ${shaders_resource_files} +) + if(CMAKE_HOST_WIN32) SET(RUNSHCMD run.bat) SET(RUNSHARG "NotUsed") diff --git a/tests/auto/wasm/selenium/fshader.glsl b/tests/auto/wasm/selenium/fshader.glsl new file mode 100644 index 00000000000..252735f91c0 --- /dev/null +++ b/tests/auto/wasm/selenium/fshader.glsl @@ -0,0 +1,21 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Set fragment color from texture + gl_FragColor = texture2D(texture, v_texcoord); +} +//! [0] + diff --git a/tests/auto/wasm/selenium/qwasmwindow.py b/tests/auto/wasm/selenium/qwasmwindow.py index 1932feb4bb9..dbec21f61a7 100644 --- a/tests/auto/wasm/selenium/qwasmwindow.py +++ b/tests/auto/wasm/selenium/qwasmwindow.py @@ -13,6 +13,7 @@ from selenium.webdriver.support.expected_conditions import presence_of_element_l from selenium.webdriver.support.ui import WebDriverWait from webdriver_manager.chrome import ChromeDriverManager +import time import unittest from enum import Enum, auto @@ -436,7 +437,40 @@ class WidgetTestCase(unittest.TestCase): self.assertEqual(w1_w1_w1.color_at(0, 0), Color(r=255, g=255, b=0)) - #TODO FIX IN CI + def test_opengl_painting(self): + screen = Screen(self._driver, ScreenPosition.FIXED, + x=0, y=0, width=800, height=800) + bottom = Window(parent=screen, rect=Rect(x=0, y=0, width=400, height=400), title='root',opengl=1) + bottom.set_background_color(Color(r=255, g=0, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(bottom.window_color_at_0_0(), Color(r=255, g=0, b=0)) + + w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=600, height=600), title='w1', opengl=1) + w1.set_background_color(Color(r=0, g=255, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1.window_color_at_0_0(), Color(r=0, g=255, b=0)) + + w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=400, height=400), title='w1_w1', opengl=1) + w1_w1.set_parent(w1) + w1_w1.set_background_color(Color(r=0, g=0, b=255)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1_w1.window_color_at_0_0(), Color(r=0, g=0, b=255)) + + w1_w1_w1 = Window(parent=screen, rect=Rect(x=100, y=100, width=200, height=200), title='w1_w1_w1', opengl=1) + w1_w1_w1.set_parent(w1_w1) + w1_w1_w1.set_background_color(Color(r=255, g=255, b=0)) + wait_for_animation_frame(self._driver) + time.sleep(1) + + self.assertEqual(w1_w1_w1.window_color_at_0_0(), Color(r=255, g=255, b=0)) + +#TODO FIX IN CI @unittest.skip('Does not work in CI') def test_keyboard_input(self): screen = Screen(self._driver, ScreenPosition.FIXED, @@ -607,8 +641,9 @@ class Widget: class Window: - def __init__(self, parent=None, rect=None, title=None, element=None, visible=True): + def __init__(self, parent=None, rect=None, title=None, element=None, visible=True, opengl=0): self.driver = parent.driver + self.opengl = opengl if element is not None: self.element = element self.title = element.find_element( @@ -621,7 +656,7 @@ class Window: if isinstance(parent, Window): self.driver.execute_script( f''' - instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'window', '{parent.title}', '{title}'); + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'window', '{parent.title}', '{title}', {opengl}); ''' ) self.screen = parent.screen @@ -629,7 +664,7 @@ class Window: assert(isinstance(parent, Screen)) self.driver.execute_script( f''' - instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'screen', '{parent.name}', '{title}'); + instance.createWindow({rect.x}, {rect.y}, {rect.width}, {rect.height}, 'screen', '{parent.name}', '{title}', {opengl}); ''' ) self.screen = parent @@ -766,6 +801,16 @@ class Window: ''' ) + def window_color_at_0_0(self): + color = call_instance_function_arg(self.driver, 'getOpenGLColorAt_0_0', self.title) + + wcol = color[0] + r = wcol['r'] + g = wcol['g'] + b = wcol['b'] + + return Color(r,g,b) + def color_at(self, x, y): raw = self.driver.execute_script( f''' diff --git a/tests/auto/wasm/selenium/shaders.qrc b/tests/auto/wasm/selenium/shaders.qrc new file mode 100644 index 00000000000..bfc4b251116 --- /dev/null +++ b/tests/auto/wasm/selenium/shaders.qrc @@ -0,0 +1,6 @@ + + + vshader.glsl + fshader.glsl + + diff --git a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp index a54173e058a..365fc74a34e 100644 --- a/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp +++ b/tests/auto/wasm/selenium/tst_qwasmwindow_harness.cpp @@ -17,6 +17,10 @@ #include #include +#include +#include +#include + #include #include #include @@ -25,25 +29,58 @@ #include #include +class TestWindowBase +{ +public: + virtual ~TestWindowBase() {} + virtual void setBackgroundColor(int r, int g, int b) = 0; + virtual void setVisible(bool visible) = 0; + virtual void setParent(QWindow *parent) = 0; + virtual bool close() = 0; + virtual QWindow *qWindow() = 0; + virtual void opengl_color_at_0_0(int *r, int *g, int *b) = 0; +}; + class TestWidget : public QDialog { Q_OBJECT }; - -class TestWindow : public QRasterWindow +class TestWindow : public QRasterWindow, public TestWindowBase { Q_OBJECT public: - void setBackgroundColor(int r, int g, int b) + virtual void setBackgroundColor(int r, int g, int b) override final { m_backgroundColor = QColor::fromRgb(r, g, b); update(); } + virtual void setVisible(bool visible) override final + { + QRasterWindow::setVisible(visible); + } + virtual void setParent(QWindow *parent) override final + { + QRasterWindow::setParent(parent); + } + virtual bool close() override final + { + return QRasterWindow::close(); + } + virtual QWindow *qWindow() override final + { + return static_cast(this); + } + virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final + { + *r = 0; + *g = 0; + *b = 0; + } private: - void closeEvent(QCloseEvent *ev) override + void closeEvent(QCloseEvent *ev) override final { Q_UNUSED(ev); delete this; @@ -78,14 +115,238 @@ private: QColor m_backgroundColor = Qt::white; }; +class ContextGuard +{ +public: + ContextGuard(QOpenGLContext *context, QSurface *surface) : m_context(context) + { + m_contextMutex.lock(); + m_context->makeCurrent(surface); + } + + ~ContextGuard() + { + m_context->doneCurrent(); + m_contextMutex.unlock(); + } + +private: + QOpenGLContext *m_context = nullptr; + static std::mutex m_contextMutex; +}; + +std::mutex ContextGuard::m_contextMutex; + +class TestOpenGLWindow : public QWindow, QOpenGLFunctions, public TestWindowBase +{ + Q_OBJECT + +public: + TestOpenGLWindow() + { + setSurfaceType(OpenGLSurface); + create(); + + // + // Create the texture in the share context + // + m_shareContext = std::make_shared(); + m_shareContext->create(); + + { + ContextGuard guard(m_shareContext.get(), this); + initializeOpenGLFunctions(); + + m_shaderProgram = std::make_shared(); + + if (!m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl") + || !m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, + ":/fshader.glsl") + || !m_shaderProgram->link() || !m_shaderProgram->bind()) { + + qDebug() << " Build problem"; + qDebug() << "Log " << m_shaderProgram->log(); + + m_shaderProgram = nullptr; + } else { + m_shaderProgram->setUniformValue("texture", 0); + } + + // + // Texture + // + glGenTextures(1, &m_TextureId); + glBindTexture(GL_TEXTURE_2D, m_TextureId); + + uint8_t pixel[4] = { 255, 255, 255, 128 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + 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_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + const GLfloat triangleData[] = { -1.0, -1.0, 0.0, 0.5, 0.5, 1.0, -1.0, 0.0, + 0.5, 0.5, -1.0, 1.0, 0.0, 0.5, 0.5 }; + const GLushort indices[] = { 0, 1, 2 }; + + glGenBuffers(1, &m_vertexBufferId); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + glBufferData(GL_ARRAY_BUFFER, sizeof(float[5]) * 3, &triangleData, GL_STATIC_DRAW); + + glGenBuffers(1, &m_indexBufferId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 3, indices, GL_STATIC_DRAW); + + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float[5]), 0); + + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float[5]), (void *)(12)); + } + + // + // We will use the texture in this context + // + m_context = std::make_shared(); + m_context->setShareContext(m_shareContext.get()); + m_context->create(); + + { + ContextGuard guard(m_context.get(), this); + initializeOpenGLFunctions(); + + glBindTexture(GL_TEXTURE_2D, m_TextureId); + glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId); + m_shaderProgram->bind(); + + // Tell OpenGL programmable pipeline how to locate vertex position data + const int vertexLocation = m_shaderProgram->attributeLocation("a_position"); + m_shaderProgram->enableAttributeArray(vertexLocation); + m_shaderProgram->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, sizeof(float[5])); + + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + const int texcoordLocation = m_shaderProgram->attributeLocation("a_texcoord"); + m_shaderProgram->enableAttributeArray(texcoordLocation); + m_shaderProgram->setAttributeBuffer(texcoordLocation, GL_FLOAT, sizeof(float[3]), 2, + sizeof(float[5])); + } + + renderLater(); + } + +public: + virtual void setBackgroundColor(int red, int green, int blue) override final + { + { + ContextGuard guard(m_shareContext.get(), this); + + // + // Update texture + // + const uint8_t pixel[4] = { (uint8_t)red, (uint8_t)green, (uint8_t)blue, 128 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + } + + renderLater(); + } + virtual void setVisible(bool visible) override final { QWindow::setVisible(visible); } + virtual void setParent(QWindow *parent) override final { QWindow::setParent(parent); } + virtual bool close() override final { return QWindow::close(); } + virtual QWindow *qWindow() override final { return static_cast(this); } + virtual void opengl_color_at_0_0(int *r, int *g, int *b) override final + { + ContextGuard guard(m_context.get(), this); + + *r = m_rgba[0]; + *g = m_rgba[1]; + *b = m_rgba[2]; + } + +private: + bool event(QEvent *event) override final + { + switch (event->type()) { + case QEvent::UpdateRequest: + renderNow(); + return true; + default: + return QWindow::event(event); + } + } + + void exposeEvent(QExposeEvent *event) override final + { + Q_UNUSED(event); + + if (isExposed()) + renderNow(); + } + + void closeEvent(QCloseEvent *ev) override final + { + Q_UNUSED(ev); + delete this; + } + + void keyPressEvent(QKeyEvent *event) override final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyPress")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call("reportEvent", std::move(data)); + } + + void keyReleaseEvent(QKeyEvent *event) override final + { + auto data = emscripten::val::object(); + data.set("type", emscripten::val("keyRelease")); + data.set("windowId", emscripten::val(winId())); + data.set("windowTitle", emscripten::val(title().toStdString())); + data.set("key", emscripten::val(event->text().toStdString())); + emscripten::val::global("window")["testSupport"].call("reportEvent", std::move(data)); + } + void renderLater() { requestUpdate(); } + void renderNow() + { + qDebug() << " Render now"; + ContextGuard guard(m_context.get(), this); + const auto sz = size(); + glViewport(0, 0, sz.width(), sz.height()); + + glClearColor(1, 1, 1, 1); + glClear(GL_COLOR_BUFFER_BIT); + + // Draw triangle using indices from VBO + glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, nullptr); + + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, m_rgba); + m_context->swapBuffers(this); + } + +private: + std::shared_ptr m_shaderProgram; + GLuint m_vertexBufferId = 0; + GLuint m_indexBufferId = 0; + GLuint m_TextureId = 0; + + std::shared_ptr m_shareContext; + std::shared_ptr m_context; + uint8_t m_rgba[4]; // Color at location(0, 0) +}; + namespace { -TestWindow *findWindowByTitle(const std::string &title) +TestWindowBase *findWindowByTitle(const std::string &title) { auto windows = qGuiApp->allWindows(); auto window_it = std::find_if(windows.begin(), windows.end(), [&title](QWindow *window) { return window->title() == QString::fromLatin1(title); }); - return window_it == windows.end() ? nullptr : static_cast(*window_it); + return window_it == windows.end() ? nullptr : dynamic_cast(*window_it); } class WidgetStorage @@ -280,7 +541,7 @@ void clearWidgets() } void createWindow(int x, int y, int w, int h, const std::string &parentType, const std::string &parentId, - const std::string &title) + const std::string &title, bool opengl) { QScreen *parentScreen = nullptr; QWindow *parentWindow = nullptr; @@ -295,29 +556,38 @@ void createWindow(int x, int y, int w, int h, const std::string &parentType, con } parentScreen = *screen_it; } else if (parentType == "window") { - auto windows = qGuiApp->allWindows(); - auto window_it = std::find_if(windows.begin(), windows.end(), [&parentId](QWindow *window) { - return window->title() == QString::fromLatin1(parentId); - }); - if (window_it == windows.end()) { - qWarning() << "No such window: " << parentId; + auto testWindow = findWindowByTitle(parentId); + + if (!testWindow) { + qWarning() << "No parent window: " << parentId; return; } - parentWindow = *window_it; + parentWindow = testWindow->qWindow(); parentScreen = parentWindow->screen(); } else { qWarning() << "Wrong parent type " << parentType; return; } - auto *window = new TestWindow; - - window->setFlag(Qt::WindowTitleHint); - window->setFlag(Qt::WindowMaximizeButtonHint); - window->setTitle(QString::fromLatin1(title)); - window->setGeometry(x, y, w, h); - window->setScreen(parentScreen); - window->setParent(parentWindow); + if (opengl) { + qDebug() << "Making OpenGL window"; + auto window = new TestOpenGLWindow; + window->setFlag(Qt::WindowTitleHint); + window->setFlag(Qt::WindowMaximizeButtonHint); + window->setTitle(QString::fromLatin1(title)); + window->setGeometry(x, y, w, h); + window->setScreen(parentScreen); + window->setParent(parentWindow); + } else { + qDebug() << "Making Raster window"; + auto window = new TestWindow; + window->setFlag(Qt::WindowTitleHint); + window->setFlag(Qt::WindowMaximizeButtonHint); + window->setTitle(QString::fromLatin1(title)); + window->setGeometry(x, y, w, h); + window->setScreen(parentScreen); + window->setParent(parentWindow); + } } void setWindowBackgroundColor(const std::string &title, int r, int g, int b) @@ -330,7 +600,8 @@ void setWindowBackgroundColor(const std::string &title, int r, int g, int b) window->setBackgroundColor(r, g, b); } -void setWindowVisible(int windowId, bool visible) { +void setWindowVisible(int windowId, bool visible) +{ auto windows = qGuiApp->allWindows(); auto window_it = std::find_if(windows.begin(), windows.end(), [windowId](QWindow *window) { return window->winId() == WId(windowId); @@ -345,27 +616,54 @@ void setWindowVisible(int windowId, bool visible) { void setWindowParent(const std::string &windowTitle, const std::string &parentTitle) { - QWindow *window = findWindowByTitle(windowTitle); + TestWindowBase *window = findWindowByTitle(windowTitle); if (!window) { - qWarning() << "Window could not be found " << parentTitle; + qWarning() << "Window could not be found " << windowTitle; return; } - QWindow *parent = nullptr; + TestWindowBase *parent = nullptr; if (parentTitle != "none") { if ((parent = findWindowByTitle(parentTitle)) == nullptr) { qWarning() << "Parent window could not be found " << parentTitle; return; } } - window->setParent(parent); + window->setParent(parent ? parent->qWindow() : nullptr); } bool closeWindow(const std::string &title) { - QWindow *window = findWindowByTitle(title); + TestWindowBase *window = findWindowByTitle(title); return window ? window->close() : false; } +std::string colorToJs(int r, int g, int b) +{ + return + "[{" + " r: " + std::to_string(r) + "," + " g: " + std::to_string(g) + "," + " b: " + std::to_string(b) + "" + "}]"; +} + +void getOpenGLColorAt_0_0(const std::string &windowTitle) +{ + TestWindowBase *window = findWindowByTitle(windowTitle); + int r = 0; + int g = 0; + int b = 0; + + if (!window) { + qWarning() << "Window could not be found " << windowTitle; + } else { + window->opengl_color_at_0_0(&r, &g, &b); + } + + emscripten::val::global("window").call("getOpenGLColorAt_0_0Callback", + emscripten::val(colorToJs(r, g, b))); +} + EMSCRIPTEN_BINDINGS(qwasmwindow) { emscripten::function("screenInformation", &screenInformation); @@ -377,6 +675,8 @@ EMSCRIPTEN_BINDINGS(qwasmwindow) emscripten::function("closeWindow", &closeWindow); emscripten::function("setWindowBackgroundColor", &setWindowBackgroundColor); + emscripten::function("getOpenGLColorAt_0_0", &getOpenGLColorAt_0_0); + emscripten::function("createWidget", &createWidget); emscripten::function("setWidgetNoFocusShow", &setWidgetNoFocusShow); emscripten::function("showWidget", &showWidget); diff --git a/tests/auto/wasm/selenium/vshader.glsl b/tests/auto/wasm/selenium/vshader.glsl new file mode 100644 index 00000000000..95e2bab607e --- /dev/null +++ b/tests/auto/wasm/selenium/vshader.glsl @@ -0,0 +1,27 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Calculate vertex position in screen space + gl_Position = a_position; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} +//! [0]