wasm: don't recreate WebGL context on surface change

The native WebGL context is tied to a single canvas,
and can only be used with that canvas. (Qt creates
one canvas per QPlatformWindow).

This means that we need new native contexts in cases
where a QWindow is repeatedly created and destroyed.
It can be tempting to just create a new WebGL context
"behind the scenes" on the makeCurrent() call in this
case, but this does not work since GL resources created
by user code with the original WebGL context in place
are now invalid.

Inform user code that this has happened by signaling
a context loss. This is done by returning false from
makeCurrent(), and then making sure isValid() returns
false as well.

The context becomes invalid whenever the owning platform
window is destroyed; add a call from ~QWasmWindow() which
handles that bookkeeping.

Task-number: QTBUG-120138
Change-Id: I929b9bb51153007c16630b1a991399f01ebffa62
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
(cherry picked from commit c2ec20b226f0613db74a1e9fadffcf423ff1a180)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Morten Sørvig 2025-06-03 09:27:58 +02:00 committed by Qt Cherry-pick Bot
parent 355619c2a3
commit f0d89ec153
3 changed files with 86 additions and 79 deletions

View File

@ -20,8 +20,10 @@ EMSCRIPTEN_BINDINGS(qwasmopenglcontext)
QT_BEGIN_NAMESPACE
QHash<QPlatformSurface *, EMSCRIPTEN_WEBGL_CONTEXT_HANDLE> QWasmOpenGLContext::s_contexts;
QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context)
: m_actualFormat(context->format()), m_qGlContext(context)
: m_actualFormat(context->format())
{
m_actualFormat.setRenderableType(QSurfaceFormat::OpenGLES);
@ -35,10 +37,7 @@ QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context)
QWasmOpenGLContext::~QWasmOpenGLContext()
{
// Destroy GL context. Work around bug in emscripten_webgl_destroy_context
// which removes all event handlers on the canvas by temporarily replacing the function
// that does the removal with a function that does nothing.
destroyWebGLContext(m_ownedWebGLContext.handle);
destroyWebGLContext(m_contextOwningSurface);
}
bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format)
@ -51,56 +50,23 @@ bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format)
(format.majorVersion() == 3 && format.minorVersion() == 0));
}
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE
QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface)
void QWasmOpenGLContext::destroyWebGLContext(QPlatformSurface *surface)
{
if (m_ownedWebGLContext.surface == surface)
return m_ownedWebGLContext.handle;
if (surface->surface()->surfaceClass() == QSurface::Offscreen) {
// Reuse the existing context for offscreen drawing, even if it happens to be a canvas
// context. This is because it is impossible to re-home an existing context to the
// new surface and works as an emulation measure.
if (m_ownedWebGLContext.handle)
return m_ownedWebGLContext.handle;
// The non-shared offscreen context is heavily limited on WASM, but we provide it
// anyway for potential pixel readbacks.
m_ownedWebGLContext =
QOpenGLContextData{ .surface = surface,
.handle = createEmscriptenContext(
static_cast<QWasmOffscreenSurface *>(surface)->id(),
m_actualFormat) };
} else {
destroyWebGLContext(m_ownedWebGLContext.handle);
// Create a full on-screen context for the window canvas.
m_ownedWebGLContext = QOpenGLContextData{
.surface = surface,
.handle = createEmscriptenContext(static_cast<QWasmWindow *>(surface)->canvasSelector(),
m_actualFormat)
};
}
EmscriptenWebGLContextAttributes actualAttributes;
EMSCRIPTEN_RESULT attributesResult = emscripten_webgl_get_context_attributes(m_ownedWebGLContext.handle, &actualAttributes);
if (attributesResult == EMSCRIPTEN_RESULT_SUCCESS) {
if (actualAttributes.majorVersion == 1) {
m_actualFormat.setMajorVersion(2);
} else if (actualAttributes.majorVersion == 2) {
m_actualFormat.setMajorVersion(3);
}
m_actualFormat.setMinorVersion(0);
}
return m_ownedWebGLContext.handle;
if (surface == nullptr)
return;
int context = s_contexts.take(surface);
if (context)
destroyWebGLContext(context);
}
void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle)
{
if (!contextHandle)
return;
// Destroy GL context. Work around bug in emscripten_webgl_destroy_context
// which removes all event handlers on the canvas by temporarily replacing the function
// that does the removal with a function that does nothing.
emscripten::val jsEvents = emscripten::val::module_property("JSEvents");
emscripten::val savedRemoveAllHandlersOnTargetFunction = jsEvents["removeAllHandlersOnTarget"];
jsEvents.set("removeAllHandlersOnTarget", emscripten::val::module_property("qtDoNothing"));
@ -108,8 +74,7 @@ void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE con
jsEvents.set("removeAllHandlersOnTarget", savedRemoveAllHandlersOnTargetFunction);
}
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE
QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector,
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector,
QSurfaceFormat format)
{
EmscriptenWebGLContextAttributes attributes;
@ -137,6 +102,24 @@ QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector,
attributes.majorVersion = 1;
contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes);
}
if (contextResult <= 0) {
qWarning() << "WebGL context creation failed";
return contextResult;
}
// Sync up actual format
EmscriptenWebGLContextAttributes actualAttributes;
EMSCRIPTEN_RESULT attributesResult = emscripten_webgl_get_context_attributes(contextResult, &actualAttributes);
if (attributesResult == EMSCRIPTEN_RESULT_SUCCESS) {
if (actualAttributes.majorVersion == 1) {
m_actualFormat.setMajorVersion(2);
} else if (actualAttributes.majorVersion == 2) {
m_actualFormat.setMajorVersion(3);
}
m_actualFormat.setMinorVersion(0);
}
return contextResult;
}
@ -152,20 +135,31 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c
bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface)
{
static bool sentSharingWarning = false;
if (!sentSharingWarning && isSharing()) {
qWarning() << "The functionality for sharing OpenGL contexts is limited, see documentation";
sentSharingWarning = true;
}
// Record this makeCurrent() attempt, since isValid() must repeat the answer
// from this function in order to signal context loss to calling code.
m_madeCurrentSurface = surface;
if (auto *shareContext = m_qGlContext->shareContext())
return shareContext->makeCurrent(surface->surface());
const auto context = obtainEmscriptenContext(surface);
if (!context)
// The native webgl context is tied to a single surface, and can't
// be made current for a different surface.
if (m_contextOwningSurface && m_contextOwningSurface != surface)
return false;
m_usedWebGLContextHandle = context;
// Return existing context or crate a new one.
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context;
if (m_contextOwningSurface && s_contexts.contains(m_contextOwningSurface)) {
context = s_contexts.value(surface);
} else {
m_contextOwningSurface = surface;
bool isOffscreen = surface->surface()->surfaceClass() == QSurface::Offscreen;
auto canvasId = isOffscreen ? static_cast<QWasmOffscreenSurface *>(surface)->id() :
static_cast<QWasmWindow *>(surface)->canvasSelector();
context = createEmscriptenContext(canvasId, m_actualFormat);
s_contexts.insert(surface, context);
}
if (context == 0)
return false;
return emscripten_webgl_make_context_current(context) == EMSCRIPTEN_RESULT_SUCCESS;
}
@ -178,12 +172,15 @@ void QWasmOpenGLContext::swapBuffers(QPlatformSurface *surface)
void QWasmOpenGLContext::doneCurrent()
{
// No doneCurrent on WebGl
m_madeCurrentSurface = nullptr;
}
bool QWasmOpenGLContext::isSharing() const
{
return m_qGlContext->shareContext();
// Return false to signal that context sharing is not supported.
// This will in turn make QOpenGLContext::shareContext() return
// a null context to the application.
return false;
}
bool QWasmOpenGLContext::isValid() const
@ -191,9 +188,21 @@ bool QWasmOpenGLContext::isValid() const
if (!isOpenGLVersionSupported(m_actualFormat))
return false;
// Note: we get isValid() calls before we see the surface and can
// create a native context, so no context is also a valid state.
return !m_usedWebGLContextHandle || !emscripten_is_webgl_context_lost(m_usedWebGLContextHandle);
// We get isValid() calls before we see the surface and are able to
// create a native context, which means that "no context" is a valid state.
if (!m_madeCurrentSurface && !m_contextOwningSurface)
return true;
// Can't use this context for a different surface, since the native
// webgl context is tied to a single canvas.
if (m_madeCurrentSurface != m_contextOwningSurface)
return false;
// If the owning surfce/canvas has been deleted then this context is invalid
if (!s_contexts.contains(m_contextOwningSurface))
return false;
return !emscripten_is_webgl_context_lost(s_contexts.value(m_contextOwningSurface));
}
QFunctionPointer QWasmOpenGLContext::getProcAddress(const char *procName)

View File

@ -4,6 +4,8 @@
#ifndef QWASMOPENGLCONTEXT_H
#define QWASMOPENGLCONTEXT_H
#include <QtCore/qhash.h>
#include <qpa/qplatformopenglcontext.h>
#include <emscripten.h>
@ -29,24 +31,17 @@ public:
bool isValid() const override;
QFunctionPointer getProcAddress(const char *procName) override;
static void destroyWebGLContext(QPlatformSurface *surface);
private:
struct QOpenGLContextData
{
QPlatformSurface *surface = nullptr;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle = 0;
};
static bool isOpenGLVersionSupported(QSurfaceFormat format);
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE obtainEmscriptenContext(QPlatformSurface *surface);
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE
createEmscriptenContext(const std::string &canvasSelector, QSurfaceFormat format);
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE createEmscriptenContext(const std::string &canvasSelector, QSurfaceFormat format);
static void destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle);
QSurfaceFormat m_actualFormat;
QOpenGLContext *m_qGlContext;
QOpenGLContextData m_ownedWebGLContext;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_usedWebGLContextHandle = 0;
QPlatformSurface *m_madeCurrentSurface = nullptr;
QPlatformSurface *m_contextOwningSurface = nullptr;
static QHash<QPlatformSurface *, EMSCRIPTEN_WEBGL_CONTEXT_HANDLE> s_contexts;
};
QT_END_NAMESPACE

View File

@ -26,6 +26,7 @@
#if QT_CONFIG(draganddrop)
#include "qwasmdrag.h"
#endif
#include "qwasmopenglcontext.h"
#include <iostream>
#include <sstream>
@ -210,6 +211,8 @@ void QWasmWindow::registerEventHandlers()
QWasmWindow::~QWasmWindow()
{
QWasmOpenGLContext::destroyWebGLContext(this);
#if QT_CONFIG(accessibility)
QWasmAccessibility::onRemoveWindow(window());
#endif