From 9c569f8ef7238525b8f0a48f859326bf13f65e5f Mon Sep 17 00:00:00 2001 From: KasparMetsa Date: Thu, 12 Feb 2026 05:20:20 +0200 Subject: [PATCH 1/4] 1. qt 6.10.2 works with emscripten 4.0.7 2. no async error --- CMakeLists.txt | 13 ++- scripts/build-wasm.sh | 31 +++++++ scripts/server.py | 15 ++++ src/CMakeLists.txt | 33 +++++-- src/clock/mumeclock.cpp | 6 +- src/display/MapCanvasData.h | 62 ++++++++++++-- src/display/MapCanvasRoomDrawer.cpp | 54 ++++++++++++ src/display/mapcanvas.cpp | 73 +++++++++++++++- src/display/mapcanvas.h | 45 +++++++++- src/display/mapcanvas_gl.cpp | 128 +++++++++++++++++++++++++++- src/display/mapwindow.cpp | 21 ++++- src/mainwindow/mainwindow.cpp | 16 ++++ src/mainwindow/utils.cpp | 6 ++ src/opengl/LineRendering.cpp | 1 + src/opengl/OpenGL.cpp | 8 ++ src/opengl/legacy/Legacy.cpp | 16 ++++ src/opengl/legacy/Legacy.h | 2 + src/opengl/legacy/VAO.cpp | 10 +++ 18 files changed, 515 insertions(+), 25 deletions(-) create mode 100755 scripts/build-wasm.sh create mode 100755 scripts/server.py diff --git a/CMakeLists.txt b/CMakeLists.txt index f773196c0..3224ccb10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,9 @@ else() add_compile_definitions(MMAPPER_PACKAGE_TYPE="${PACKAGE_TYPE_NORMALIZED}") endif() +# Required for newer GLM versions that mark gtx/ extensions as experimental +add_compile_definitions(GLM_ENABLE_EXPERIMENTAL) + set(MMAPPER_QT_COMPONENTS Core Widgets Network OpenGL OpenGLWidgets Test) if(WITH_WEBSOCKET) list(APPEND MMAPPER_QT_COMPONENTS WebSockets) @@ -78,6 +81,11 @@ if(Qt6OpenGL_FOUND) if(EMSCRIPTEN) set(QT_HAS_GLES TRUE) set(QT_HAS_OPENGL FALSE) + elseif(APPLE) + # Force OpenGL on macOS - the try_compile check can fail with some Qt versions + # but macOS always has OpenGL 3.3 support via the compatibility profile + set(QT_HAS_GLES FALSE) + set(QT_HAS_OPENGL TRUE) else() file(WRITE ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/CheckQtGLES30.cpp "#include @@ -423,7 +431,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wno-extra-semi-stmt) # enabled only for src/ directory # require explicit template parameters when deduction guides are missing - add_compile_options(-Werror=ctad-maybe-unsupported) + # NOTE: Disabled for Qt 6.10+ because moc-generated code uses CTAD without deduction guides + if(Qt6_VERSION VERSION_LESS "6.10.0") + add_compile_options(-Werror=ctad-maybe-unsupported) + endif() # always errors add_compile_options(-Werror=cast-qual) # always a mistake unless you added the qualifier yourself. diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh new file mode 100755 index 000000000..eb817a3a3 --- /dev/null +++ b/scripts/build-wasm.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Source Emscripten environment +# IMPORTANT: Change this path to match your emsdk installation location +source "$HOME/dev/emsdk/emsdk_env.sh" + +# Paths - automatically detect project root (parent of scripts directory) +MMAPPER_SRC="$(cd "$(dirname "$0")/.." && pwd)" +QT_WASM="$HOME/Qt/6.10.2/wasm_multithread" +QT_HOST="$HOME/Qt/6.10.2/macos" + +# Configure with qt-cmake +"$QT_WASM/bin/qt-cmake" \ + -S "$MMAPPER_SRC" \ + -B "$MMAPPER_SRC/build-wasm" \ + -DQT_HOST_PATH="$QT_HOST" \ + -DWITH_OPENSSL=OFF \ + -DWITH_TESTS=OFF \ + -DWITH_WEBSOCKET=ON \ + -DWITH_UPDATER=OFF \ + -DCMAKE_BUILD_TYPE=Release + +# Build (limited to 4 cores to avoid system slowdown) +cmake --build "$MMAPPER_SRC/build-wasm" --parallel 4 + +echo "" +echo "Build complete!" +echo "To run:" +echo " cd $MMAPPER_SRC/build-wasm/src && python3 $MMAPPER_SRC/scripts/server.py" +echo "Then open: http://localhost:9742/mmapper.html" \ No newline at end of file diff --git a/scripts/server.py b/scripts/server.py new file mode 100755 index 000000000..7e70bf090 --- /dev/null +++ b/scripts/server.py @@ -0,0 +1,15 @@ +import http.server +import socketserver + +PORT = 9742 + +class MyHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + http.server.SimpleHTTPRequestHandler.end_headers(self) + +with socketserver.TCPServer(("", PORT), MyHandler) as httpd: + print(f"Serving MMapper WASM at http://localhost:{PORT}/mmapper.html") + print("Press Ctrl+C to stop") + httpd.serve_forever() \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 03f8daaa6..4be417952 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -686,8 +686,9 @@ if(EMSCRIPTEN) -sFULL_ES3=1 -sMAX_WEBGL_VERSION=2 -sMIN_WEBGL_VERSION=2 - -sASSERTIONS + -sASSERTIONS=0 -sASYNCIFY + -sGL_POOL_TEMP_BUFFERS=0 -Os ) target_link_libraries(mmapper PUBLIC z) @@ -1133,14 +1134,28 @@ if(APPLE) # Bundle the libraries with the binary find_program(MACDEPLOYQT_APP macdeployqt) message(" - macdeployqt path: ${MACDEPLOYQT_APP}") - add_custom_command( - TARGET mmapper - POST_BUILD - COMMAND ${MACDEPLOYQT_APP} ${CMAKE_CURRENT_BINARY_DIR}/mmapper.app -libpath ${QTKEYCHAIN_LIBRARY_DIR} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Deploying the Qt Framework onto the bundle" - VERBATIM - ) + # Qt 6.8+ has a bug where macdeployqt -libpath fails with "Missing library search path" + # The library is still found via @rpath, so we can safely omit -libpath for Qt 6.8+ + if(Qt6Core_VERSION VERSION_GREATER_EQUAL "6.8.0") + message(" - Qt 6.8+ detected: skipping -libpath option (macdeployqt bug workaround)") + add_custom_command( + TARGET mmapper + POST_BUILD + COMMAND ${MACDEPLOYQT_APP} ${CMAKE_CURRENT_BINARY_DIR}/mmapper.app + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Deploying the Qt Framework onto the bundle" + VERBATIM + ) + else() + add_custom_command( + TARGET mmapper + POST_BUILD + COMMAND ${MACDEPLOYQT_APP} ${CMAKE_CURRENT_BINARY_DIR}/mmapper.app -libpath ${QTKEYCHAIN_LIBRARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Deploying the Qt Framework onto the bundle" + VERBATIM + ) + endif() # Codesign the bundle without a personal certificate find_program(CODESIGN_APP codesign) diff --git a/src/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index c27a02a5e..c3deed9bb 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -78,7 +78,11 @@ class NODISCARD QME final public: NODISCARD static int keyToValue(const QString &key) { return g_qme.keyToValue(key.toUtf8()); } - NODISCARD static QString valueToKey(const int value) { return g_qme.valueToKey(value); } + NODISCARD static QString valueToKey(const int value) + { + // Qt 6.10+ changed valueToKey() signature from int to quint64 + return g_qme.valueToKey(static_cast(value)); + } NODISCARD static int keyCount() { return g_qme.keyCount(); } }; diff --git a/src/display/MapCanvasData.h b/src/display/MapCanvasData.h index 886c625bf..1601defdb 100644 --- a/src/display/MapCanvasData.h +++ b/src/display/MapCanvasData.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -97,10 +98,53 @@ struct NODISCARD ScaleFactor final } }; +// Abstraction to get size from either QWidget or QWindow +struct NODISCARD SizeSource final +{ +private: + QWidget *m_widget = nullptr; + QWindow *m_window = nullptr; + +public: + explicit SizeSource(QWidget &widget) + : m_widget{&widget} + {} + explicit SizeSource(QWindow &window) + : m_window{&window} + {} + + NODISCARD int width() const + { + if (m_widget) + return m_widget->width(); + if (m_window) + return m_window->width(); + return 0; + } + + NODISCARD int height() const + { + if (m_widget) + return m_widget->height(); + if (m_window) + return m_window->height(); + return 0; + } + + NODISCARD QRect rect() const + { + if (m_widget) + return m_widget->rect(); + if (m_window) + return QRect(0, 0, m_window->width(), m_window->height()); + return QRect(); + } +}; + struct NODISCARD MapCanvasViewport { private: - QWidget &m_sizeWidget; + SizeSource m_sizeSource; public: glm::mat4 m_viewProj{1.f}; @@ -109,16 +153,22 @@ struct NODISCARD MapCanvasViewport int m_currentLayer = 0; public: - explicit MapCanvasViewport(QWidget &sizeWidget) - : m_sizeWidget{sizeWidget} + explicit MapCanvasViewport(QWidget &sizeSource) + : m_sizeSource{sizeSource} + {} + +#ifdef __EMSCRIPTEN__ + explicit MapCanvasViewport(QWindow &sizeSource) + : m_sizeSource{sizeSource} {} +#endif public: - NODISCARD auto width() const { return m_sizeWidget.width(); } - NODISCARD auto height() const { return m_sizeWidget.height(); } + NODISCARD auto width() const { return m_sizeSource.width(); } + NODISCARD auto height() const { return m_sizeSource.height(); } NODISCARD Viewport getViewport() const { - const auto &r = m_sizeWidget.rect(); + const auto &r = m_sizeSource.rect(); return Viewport{glm::ivec2{r.x(), r.y()}, glm::ivec2{r.width(), r.height()}}; } NODISCARD float getTotalScaleFactor() const { return m_scaleFactor.getTotal(); } diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index f7364ed80..8704af3a6 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -4,6 +4,10 @@ #include "MapCanvasRoomDrawer.h" +#ifdef __EMSCRIPTEN__ +#include "mapcanvas.h" +#endif + #include "../configuration/NamedConfig.h" #include "../configuration/configuration.h" #include "../global/Array.h" @@ -1119,6 +1123,55 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur { const auto visitRoomOptions = getVisitRoomOptions(); +#ifdef __EMSCRIPTEN__ + // WASM: Use synchronous (deferred) execution instead of async. + // Async execution causes crashes because the WebGL context can become + // invalid while the async task is running, and there's no way to + // safely cancel the task mid-execution. + return std::async(std::launch::deferred, + [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { + // Check context before starting + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[SYNC] generateMapDataFinisher - context lost, aborting"; + return SharedMapBatchFinisher{}; + } + + ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, + visitRoomOptions.colorSettings}; + DECL_TIMER(t, "[SYNC] generateAllLayerMeshes"); + + ProgressCounter dummyPc; + map.checkConsistency(dummyPc); + + const auto layerToRooms = std::invoke([map]() -> LayerToRooms { + DECL_TIMER(t2, "[SYNC] generateBatches.layerToRooms"); + LayerToRooms ltr; + map.getRooms().for_each([&map, <r](const RoomId id) { + const auto &r = map.getRoomHandle(id); + const auto z = r.getPosition().z; + auto &layer = ltr[z]; + layer.emplace_back(r); + }); + return ltr; + }); + + // Check again before the expensive operation + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[SYNC] generateMapDataFinisher - context lost before mesh gen, aborting"; + return SharedMapBatchFinisher{}; + } + + auto result = std::make_shared(); + auto &data = deref(result); + generateAllLayerMeshes(data, + deref(font), + layerToRooms, + textures, + visitRoomOptions); + return SharedMapBatchFinisher{result}; + }); +#else + // Desktop: Use async execution for better performance return std::async(std::launch::async, [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, @@ -1149,6 +1202,7 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur visitRoomOptions); return SharedMapBatchFinisher{result}; }); +#endif } void finish(const IMapBatchesFinisher &finisher, diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index 42abf0437..af37ed716 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -53,6 +53,35 @@ NODISCARD static NonOwningPointer &primaryMapCanvas() return primary; } +#ifdef __EMSCRIPTEN__ +// WASM: QOpenGLWindow-based constructor +MapCanvas::MapCanvas(MapData &mapData, + PrespammedPath &prespammedPath, + Mmapper2Group &groupManager) + : QOpenGLWindow{QOpenGLWindow::NoPartialUpdate} + , MapCanvasViewport{static_cast(*this)} + , MapCanvasInputState{prespammedPath} + , m_mapScreen{static_cast(*this)} + , m_opengl{} + , m_glFont{m_opengl} + , m_data{mapData} + , m_groupManager{groupManager} +{ + NonOwningPointer &pmc = primaryMapCanvas(); + if (pmc == nullptr) { + pmc = this; + } + + // Set up surface format for WebGL 2.0 + QSurfaceFormat format; + format.setRenderableType(QSurfaceFormat::OpenGLES); + format.setVersion(3, 0); // WebGL 2.0 = OpenGL ES 3.0 + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + setFormat(format); +} +#else +// Desktop: QOpenGLWidget-based constructor MapCanvas::MapCanvas(MapData &mapData, PrespammedPath &prespammedPath, Mmapper2Group &groupManager, @@ -75,6 +104,7 @@ MapCanvas::MapCanvas(MapData &mapData, grabGesture(Qt::PinchGesture); setContextMenuPolicy(Qt::CustomContextMenu); } +#endif MapCanvas::~MapCanvas() { @@ -121,6 +151,7 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode) // scrollbars or use the new zoom-recenter feature). slot_clearAllSelections(); +#ifndef __EMSCRIPTEN__ switch (mode) { case CanvasMouseModeEnum::MOVE: setCursor(Qt::OpenHandCursor); @@ -142,6 +173,7 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode) setCursor(Qt::ArrowCursor); break; } +#endif m_canvasMouseMode = mode; m_selectedArea = false; @@ -296,6 +328,8 @@ void MapCanvas::slot_onForcedPositionChange() bool MapCanvas::event(QEvent *const event) { +#ifndef __EMSCRIPTEN__ + // Gesture handling is only available on desktop auto tryHandlePinchZoom = [this, event]() -> bool { if (event->type() != QEvent::Gesture) { return false; @@ -332,8 +366,13 @@ bool MapCanvas::event(QEvent *const event) if (tryHandlePinchZoom()) { return true; } +#endif +#ifdef __EMSCRIPTEN__ + return QOpenGLWindow::event(event); +#else return QOpenGLWidget::event(event); +#endif } void MapCanvas::slot_createRoom() @@ -416,8 +455,12 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) MAYBE_UNUSED const bool hasAlt = (event->modifiers() & Qt::ALT) != 0u; if (hasLeftButton && hasAlt) { +#ifndef __EMSCRIPTEN__ m_altDragState.emplace(AltDragState{event->pos(), cursor()}); setCursor(Qt::ClosedHandCursor); +#else + m_altDragState.emplace(AltDragState{event->pos(), QCursor()}); +#endif event->accept(); return; } @@ -470,7 +513,9 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) break; case CanvasMouseModeEnum::MOVE: if (hasLeftButton && hasSel1()) { +#ifndef __EMSCRIPTEN__ setCursor(Qt::ClosedHandCursor); +#endif startMoving(m_sel1.value()); } break; @@ -596,7 +641,9 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) if (m_altDragState.has_value()) { // The user released the Alt key mid-drag. if (!((event->modifiers() & Qt::ALT) != 0u)) { +#ifndef __EMSCRIPTEN__ setCursor(m_altDragState->originalCursor); +#endif m_altDragState.reset(); // Don't accept the event; let the underlying widgets handle it. return; @@ -678,8 +725,9 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) if (hasLeftButton && hasSel1() && hasSel2()) { if (hasInfomarkSelectionMove()) { m_infoMarkSelectionMove->pos = getSel2().pos - getSel1().pos; +#ifndef __EMSCRIPTEN__ setCursor(Qt::ClosedHandCursor); - +#endif } else { m_selectedArea = true; } @@ -721,7 +769,9 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) m_roomSelectionMove->pos = diff; m_roomSelectionMove->wrongPlace = wrongPlace; +#ifndef __EMSCRIPTEN__ setCursor(wrongPlace ? Qt::ForbiddenCursor : Qt::ClosedHandCursor); +#endif } else { m_selectedArea = true; } @@ -770,7 +820,9 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) { if (m_altDragState.has_value()) { +#ifndef __EMSCRIPTEN__ setCursor(m_altDragState->originalCursor); +#endif m_altDragState.reset(); event->accept(); return; @@ -785,7 +837,9 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) switch (m_canvasMouseMode) { case CanvasMouseModeEnum::SELECT_INFOMARKS: +#ifndef __EMSCRIPTEN__ setCursor(Qt::ArrowCursor); +#endif if (m_mouseLeftPressed) { m_mouseLeftPressed = false; if (hasInfomarkSelectionMove()) { @@ -837,7 +891,9 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) case CanvasMouseModeEnum::MOVE: stopMoving(); +#ifndef __EMSCRIPTEN__ setCursor(Qt::OpenHandCursor); +#endif if (m_mouseLeftPressed) { m_mouseLeftPressed = false; } @@ -849,11 +905,16 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) mmqt::StripAnsiEnum::Yes, mmqt::PreviewStyleEnum::ForDisplay); +#ifdef __EMSCRIPTEN__ + // QToolTip requires QWidget; on WASM we skip tooltips for now + Q_UNUSED(message); +#else QToolTip::showText(mapToGlobal(event->position().toPoint()), message, this, rect(), 5000); +#endif } } break; @@ -862,8 +923,9 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) break; case CanvasMouseModeEnum::SELECT_ROOMS: +#ifndef __EMSCRIPTEN__ setCursor(Qt::ArrowCursor); - +#endif // This seems very unusual. if (m_ctrlPressed && m_altPressed) { break; @@ -998,6 +1060,8 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) m_ctrlPressed = false; } +#ifndef __EMSCRIPTEN__ +// QWidget-only methods (QOpenGLWindow doesn't have these) QSize MapCanvas::minimumSizeHint() const { return {sizeHint().width() / 4, sizeHint().height() / 4}; @@ -1007,6 +1071,7 @@ QSize MapCanvas::sizeHint() const { return {1280, 720}; } +#endif void MapCanvas::slot_setScroll(const glm::vec2 &worldPos) { @@ -1111,7 +1176,11 @@ void MapCanvas::screenChanged() return; } +#ifdef __EMSCRIPTEN__ + const auto newDpi = static_cast(devicePixelRatio()); // QOpenGLWindow's DPR +#else const auto newDpi = static_cast(QPaintDevice::devicePixelRatioF()); +#endif const auto oldDpi = gl.getDevicePixelRatio(); if (!utils::equals(newDpi, oldDpi)) { diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index cee1e14a0..8320db4e4 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -30,7 +30,11 @@ #include #include #include +#ifdef __EMSCRIPTEN__ +#include +#else #include +#endif #include class CharacterBatch; @@ -47,9 +51,18 @@ class QWheelEvent; class QWidget; class RoomSelFakeGL; +#ifdef __EMSCRIPTEN__ +// WASM: Use QOpenGLWindow to bypass Qt's RHI compositing issues with WebGL +// QOpenGLWindow is embedded via QWidget::createWindowContainer() +class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWindow, + private MapCanvasViewport, + private MapCanvasInputState +#else +// Desktop: Use QOpenGLWidget for normal widget integration class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, private MapCanvasViewport, private MapCanvasInputState +#endif { Q_OBJECT @@ -57,6 +70,16 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, static constexpr const int BASESIZE = 528; // REVISIT: Why this size? 16*33 isn't special. static constexpr const int SCROLL_SCALE = 64; +#ifdef __EMSCRIPTEN__ + // WASM: Check if WebGL context was lost + static bool isWasmContextLost(); + // WASM: Helper to access the container widget + QWidget *getContainerWidget() const { return m_containerWidget; } + void setContainerWidget(QWidget *widget) { m_containerWidget = widget; } +private: + QWidget *m_containerWidget = nullptr; +#endif + private: struct NODISCARD FrameRateController final { @@ -153,10 +176,18 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, std::optional m_altDragState; public: +#ifdef __EMSCRIPTEN__ + // WASM: QOpenGLWindow-based constructor (no parent widget) + explicit MapCanvas(MapData &mapData, + PrespammedPath &prespammedPath, + Mmapper2Group &groupManager); +#else + // Desktop: QOpenGLWidget-based constructor explicit MapCanvas(MapData &mapData, PrespammedPath &prespammedPath, Mmapper2Group &groupManager, QWidget *parent); +#endif ~MapCanvas() final; public: @@ -168,8 +199,11 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, void cleanupOpenGL(); public: +#ifndef __EMSCRIPTEN__ + // QWidget-only methods (QOpenGLWindow doesn't have these) NODISCARD QSize minimumSizeHint() const override; NODISCARD QSize sizeHint() const override; +#endif using MapCanvasViewport::getTotalScaleFactor; void setZoom(float zoom) @@ -180,9 +214,15 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, NODISCARD float getRawZoom() const { return m_scaleFactor.getRaw(); } public: +#ifdef __EMSCRIPTEN__ + NODISCARD auto width() const { return QOpenGLWindow::width(); } + NODISCARD auto height() const { return QOpenGLWindow::height(); } + NODISCARD auto rect() const { return QRect(0, 0, width(), height()); } +#else NODISCARD auto width() const { return QOpenGLWidget::width(); } NODISCARD auto height() const { return QOpenGLWidget::height(); } NODISCARD auto rect() const { return QOpenGLWidget::rect(); } +#endif private: void onMovement(); @@ -194,9 +234,6 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, protected: void initializeGL() override; void paintGL() override; - - void drawGroupCharacters(CharacterBatch &characterBatch); - void resizeGL(int width, int height) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; @@ -204,6 +241,8 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, void wheelEvent(QWheelEvent *event) override; bool event(QEvent *e) override; + void drawGroupCharacters(CharacterBatch &characterBatch); + private: void setAnimating(bool value); void renderLoop(); diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index fa619578f..ecf458034 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -28,6 +28,7 @@ #include "mapcanvas.h" #include +#include #include #include #include @@ -48,7 +49,11 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#else #include +#endif #include #include #include @@ -93,6 +98,25 @@ void setShowPerfStats(const bool show) } // namespace MapCanvasConfig +#ifdef __EMSCRIPTEN__ +// WASM: MakeCurrentRaii for QOpenGLWindow +class NODISCARD MakeCurrentRaii final +{ +private: + QOpenGLWindow &m_glWindow; + +public: + explicit MakeCurrentRaii(QOpenGLWindow &window) + : m_glWindow{window} + { + m_glWindow.makeCurrent(); + } + ~MakeCurrentRaii() { m_glWindow.doneCurrent(); } + + DELETE_CTORS_AND_ASSIGN_OPS(MakeCurrentRaii); +}; +#else +// Desktop: MakeCurrentRaii for QOpenGLWidget class NODISCARD MakeCurrentRaii final { private: @@ -108,6 +132,7 @@ class NODISCARD MakeCurrentRaii final DELETE_CTORS_AND_ASSIGN_OPS(MakeCurrentRaii); }; +#endif void MapCanvas::cleanupOpenGL() { @@ -168,11 +193,12 @@ void MapCanvas::reportGLVersion() return std::move(oss).str(); }); + const bool contextValid = context()->isValid(); logMsg("Current OpenGL Context:", QString("%1 (%2)") .arg(version.c_str()) // FIXME: This is a bit late to report an invalid context. - .arg(context()->isValid() ? "valid" : "invalid") + .arg(contextValid ? "valid" : "invalid") .toUtf8()); if constexpr (!NO_OPENGL) { logMsg("Highest OpenGL:", mmqt::toQByteArrayUtf8(OpenGLConfig::getGLVersionString())); @@ -181,7 +207,11 @@ void MapCanvas::reportGLVersion() logMsg("Highest GLES:", mmqt::toQByteArrayUtf8(OpenGLConfig::getESVersionString())); } +#ifdef __EMSCRIPTEN__ + logMsg("Display:", QString("%1 DPI").arg(devicePixelRatio()).toUtf8()); +#else logMsg("Display:", QString("%1 DPI").arg(QPaintDevice::devicePixelRatioF()).toUtf8()); +#endif } bool MapCanvas::isBlacklistedDriver() @@ -202,8 +232,33 @@ bool MapCanvas::isBlacklistedDriver() return false; } +// WASM: Track initialization and context state globally +#ifdef __EMSCRIPTEN__ +static bool g_wasmInitialized = false; +static std::atomic g_wasmContextLost{false}; +static std::atomic g_wasmInitAttempts{0}; +#endif + +#ifdef __EMSCRIPTEN__ +bool MapCanvas::isWasmContextLost() +{ + return g_wasmContextLost.load(); +} +#endif + void MapCanvas::initializeGL() { +#ifdef __EMSCRIPTEN__ + // WASM: Track reinitialization attempts. + // With QOpenGLWindow approach, reinit should not happen as frequently. + if (g_wasmInitialized) { + ++g_wasmInitAttempts; + g_wasmContextLost.store(true); + return; + } + g_wasmInitialized = true; +#endif + OpenGL &gl = getOpenGL(); try { gl.initializeOpenGLFunctions(); @@ -213,9 +268,13 @@ void MapCanvas::initializeGL() throw std::runtime_error("unsupported driver"); } } catch (const std::exception &) { +#ifdef __EMSCRIPTEN__ + close(); // QOpenGLWindow uses close() instead of hide() +#else hide(); +#endif doneCurrent(); - QMessageBox::critical(this, + QMessageBox::critical(nullptr, "Unable to initialize OpenGL", "Upgrade your video card drivers"); if constexpr (CURRENT_PLATFORM == PlatformEnum::Windows) { @@ -233,7 +292,11 @@ void MapCanvas::initializeGL() // because the logger purposely calls std::abort() when it receives an error. initLogger(); +#ifdef __EMSCRIPTEN__ + gl.initializeRenderer(static_cast(devicePixelRatio())); +#else gl.initializeRenderer(static_cast(QPaintDevice::devicePixelRatioF())); +#endif updateMultisampling(); // REVISIT: should the font texture have the lowest ID? @@ -251,6 +314,16 @@ void MapCanvas::initializeGL() programs.early_init(); } +#ifdef __EMSCRIPTEN__ + // Clear any GL errors that may have accumulated during initialization. + // WebGL can generate errors for operations that succeed on desktop OpenGL. + { + auto &sharedFuncs = gl.getSharedFunctions(Badge{}); + Legacy::Functions &funcs = deref(sharedFuncs); + funcs.clearErrors(); + } +#endif + setConfig().canvas.showUnsavedChanges.registerChangeCallback(m_lifetime, [this]() { if (getConfig().canvas.showUnsavedChanges.get() && m_diff.highlight.has_value() && m_diff.highlight->highlights.empty()) { @@ -475,6 +548,15 @@ void MapCanvas::setViewportAndMvp(int width, int height) void MapCanvas::resizeGL(int width, int height) { +#ifdef __EMSCRIPTEN__ + // WASM: Check if WebGL context is valid + auto *ctx = context(); + if (ctx == nullptr || !ctx->isValid()) { + g_wasmContextLost.store(true); + return; + } +#endif + if (m_textures.room_highlight == nullptr) { // resizeGL called but initializeGL was not called yet return; @@ -532,6 +614,14 @@ void MapCanvas::updateBatches() void MapCanvas::updateMapBatches() { +#ifdef __EMSCRIPTEN__ + // WASM: Don't start new async batch generation if context is unstable. + // This prevents crashes in the async task. + if (g_wasmContextLost.load()) { + return; + } +#endif + RemeshCookie &remeshCookie = m_batches.remeshCookie; if (remeshCookie.isPending()) { return; @@ -638,6 +728,26 @@ void MapCanvas::actuallyPaintGL() auto &gl = getOpenGL(); gl.bindNamedColorsBuffer(); +#ifdef __EMSCRIPTEN__ + // WASM with QOpenGLWindow: Render directly to default framebuffer (no FBO) + // This avoids potential blit issues with WebGL + if (auto *ctx = QOpenGLContext::currentContext()) { + ctx->functions()->glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + gl.clear(Color{getConfig().canvas.backgroundColor}); + + if (m_data.isEmpty()) { + getGLFont().renderTextCentered("No map loaded"); + return; + } + + paintMap(); + paintBatchedInfomarks(); + paintSelections(); + paintCharacters(); + paintDifferences(); +#else + // Desktop with QOpenGLWidget: Use FBO for compositing gl.bindFbo(); gl.clear(Color{getConfig().canvas.backgroundColor}); @@ -654,6 +764,7 @@ void MapCanvas::actuallyPaintGL() gl.releaseFbo(); gl.blitFboToDefault(); +#endif } NODISCARD bool MapCanvas::Diff::isUpToDate(const Map &saved, const Map ¤t) const @@ -817,6 +928,15 @@ void MapCanvas::paintSelections() void MapCanvas::paintGL() { +#ifdef __EMSCRIPTEN__ + // WASM: Check if WebGL context is valid + auto *ctx = context(); + if (ctx == nullptr || !ctx->isValid()) { + g_wasmContextLost.store(true); + return; + } +#endif + static thread_local double longestBatchMs = 0.0; const bool showPerfStats = MapCanvasConfig::getShowPerfStats(); @@ -859,7 +979,11 @@ void MapCanvas::paintGL() const auto &afterBatches = optAfterBatches.value(); const auto afterPaint = Clock::now(); const bool calledFinish = std::invoke([this]() -> bool { +#ifdef __EMSCRIPTEN__ + if (auto *const ctxt = QOpenGLWindow::context()) { +#else if (auto *const ctxt = QOpenGLWidget::context()) { +#endif if (auto *const func = ctxt->functions()) { func->glFinish(); return true; diff --git a/src/display/mapwindow.cpp b/src/display/mapwindow.cpp index f5e12c263..5869e0b78 100644 --- a/src/display/mapwindow.cpp +++ b/src/display/mapwindow.cpp @@ -43,11 +43,30 @@ MapWindow::MapWindow(MapData &mapData, PrespammedPath &pp, Mmapper2Group &gm, QW m_gridLayout->addWidget(m_horizontalScrollBar.get(), 1, 0, 1, 1); - m_canvas = std::make_unique(mapData, pp, gm, this); +#ifdef __EMSCRIPTEN__ + // WASM: Use QOpenGLWindow embedded via createWindowContainer + // This bypasses Qt's RHI compositing issues with WebGL + m_canvas = std::make_unique(mapData, pp, gm); MapCanvas *const canvas = m_canvas.get(); + // Create a container widget to embed the QOpenGLWindow + QWidget *container = QWidget::createWindowContainer(canvas, this); + container->setMinimumSize(200, 200); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + container->setFocusPolicy(Qt::StrongFocus); + canvas->setContainerWidget(container); + + m_gridLayout->addWidget(container, 0, 0, 1, 1); + m_gridLayout->setRowStretch(0, 1); + m_gridLayout->setColumnStretch(0, 1); + setMinimumSize(200, 200); +#else + // Desktop: Use QOpenGLWidget directly in layout + m_canvas = std::make_unique(mapData, pp, gm, this); + MapCanvas *const canvas = m_canvas.get(); m_gridLayout->addWidget(canvas, 0, 0, 1, 1); setMinimumSize(canvas->minimumSizeHint()); +#endif // Splash setup auto createSplashPixmap = [](const QSize &targetLogicalSize, qreal dpr) -> QPixmap { diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 00392fb0c..e27566983 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -465,7 +465,10 @@ void MainWindow::wireConnections() &MapCanvas::sig_newInfomarkSelection, this, &MainWindow::slot_newInfomarkSelection); +#ifndef __EMSCRIPTEN__ + // QOpenGLWindow doesn't have customContextMenuRequested signal connect(canvas, &QWidget::customContextMenuRequested, this, &MainWindow::slot_showContextMenu); +#endif // Group connect(m_groupManager, &Mmapper2Group::sig_log, this, &MainWindow::slot_log); @@ -1087,6 +1090,10 @@ void MainWindow::hideCanvas(const bool hide) // REVISIT: It seems that updates don't work if the canvas is hidden, // so we may want to save mapChanged() and other similar requests // and send them after we show the canvas. +#ifdef __EMSCRIPTEN__ + // For WASM, MapCanvas is QObject-based and has no show/hide methods + Q_UNUSED(hide); +#else if (MapCanvas *const canvas = getCanvas()) { if (hide) { canvas->hide(); @@ -1094,6 +1101,7 @@ void MainWindow::hideCanvas(const bool hide) canvas->show(); } } +#endif } void MainWindow::setupMenuBar() @@ -1274,7 +1282,12 @@ void MainWindow::slot_showContextMenu(const QPoint &pos) mouseMenu->addAction(mouseMode.modeCreateConnectionAct); mouseMenu->addAction(mouseMode.modeCreateOnewayConnectionAct); +#ifdef __EMSCRIPTEN__ + // On WASM, MapCanvas is a QObject without mapToGlobal; use MapWindow instead + contextMenu->popup(m_mapWindow->mapToGlobal(pos)); +#else contextMenu->popup(getCanvas()->mapToGlobal(pos)); +#endif } void MainWindow::slot_alwaysOnTop() @@ -1313,7 +1326,10 @@ void MainWindow::slot_setShowMenuBar() m_dockDialogGroup->setMouseTracking(!showMenuBar); m_dockDialogLog->setMouseTracking(!showMenuBar); m_dockDialogRoom->setMouseTracking(!showMenuBar); +#ifndef __EMSCRIPTEN__ + // setMouseTracking is QWidget-specific getCanvas()->setMouseTracking(!showMenuBar); +#endif if (showMenuBar) { menuBar()->show(); diff --git a/src/mainwindow/utils.cpp b/src/mainwindow/utils.cpp index 41b911b42..5a67eef6f 100644 --- a/src/mainwindow/utils.cpp +++ b/src/mainwindow/utils.cpp @@ -9,12 +9,18 @@ CanvasDisabler::CanvasDisabler(MapWindow &in_window) : window{in_window} { +#ifndef __EMSCRIPTEN__ + // setEnabled is QWidget-specific window.getCanvas()->setEnabled(false); +#endif } CanvasDisabler::~CanvasDisabler() { +#ifndef __EMSCRIPTEN__ + // setEnabled is QWidget-specific window.getCanvas()->setEnabled(true); +#endif window.hideSplashImage(); } diff --git a/src/opengl/LineRendering.cpp b/src/opengl/LineRendering.cpp index 74b1018e4..610990f7d 100644 --- a/src/opengl/LineRendering.cpp +++ b/src/opengl/LineRendering.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace mmgl { diff --git a/src/opengl/OpenGL.cpp b/src/opengl/OpenGL.cpp index 711b5c101..45247402f 100644 --- a/src/opengl/OpenGL.cpp +++ b/src/opengl/OpenGL.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -262,7 +263,14 @@ void OpenGL::initializeRenderer(const float devicePixelRatio) // REVISIT: Move this somewhere else? GLint maxSamples = 0; +#ifdef __EMSCRIPTEN__ + // WebGL doesn't support GL_TEXTURE_2D_MULTISAMPLE which is needed for our + // multisampling FBO approach. Force disable multisampling on WASM. + maxSamples = 0; + qDebug() << "WASM: Multisampling disabled"; +#else getFunctions().glGetIntegerv(GL_MAX_SAMPLES, &maxSamples); +#endif OpenGLConfig::setMaxSamples(maxSamples); m_rendererInitialized = true; diff --git a/src/opengl/legacy/Legacy.cpp b/src/opengl/legacy/Legacy.cpp index af4371b99..8393483db 100644 --- a/src/opengl/legacy/Legacy.cpp +++ b/src/opengl/legacy/Legacy.cpp @@ -369,12 +369,28 @@ void Functions::checkError() } if (fail) { +#ifdef __EMSCRIPTEN__ + // On WASM/WebGL, don't abort on GL errors - just log them. + // WebGL can generate errors in cases that work fine, and aborting + // makes debugging impossible. + qWarning() << "OpenGL error detected (WASM mode - continuing execution)"; +#else std::abort(); +#endif } #undef CASE } +int Functions::clearErrors() +{ + int count = 0; + while (Base::glGetError() != GL_NO_ERROR) { + ++count; + } + return count; +} + void Functions::configureFbo(int samples) { getFBO().configure(getPhysicalViewport(), samples); diff --git a/src/opengl/legacy/Legacy.h b/src/opengl/legacy/Legacy.h index cfddfcd28..97c85d3f4 100644 --- a/src/opengl/legacy/Legacy.h +++ b/src/opengl/legacy/Legacy.h @@ -447,6 +447,8 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, public: void checkError(); + // Clears any pending GL errors without aborting. Returns the count of errors cleared. + int clearErrors(); public: void configureFbo(int samples); diff --git a/src/opengl/legacy/VAO.cpp b/src/opengl/legacy/VAO.cpp index 1919bcec5..4e6d79f3c 100644 --- a/src/opengl/legacy/VAO.cpp +++ b/src/opengl/legacy/VAO.cpp @@ -19,7 +19,17 @@ void VAO::emplace(const SharedFunctions &sharedFunctions) throw std::runtime_error("Legacy::Functions is no longer valid"); } +#ifdef __EMSCRIPTEN__ + // Clear any pending GL errors before VAO creation + shared->clearErrors(); +#endif + shared->glGenVertexArrays(1, &m_vao); + +#ifdef __EMSCRIPTEN__ + qDebug() << "VAO::emplace - created VAO id:" << m_vao; +#endif + shared->checkError(); if (LOG_VAO_ALLOCATIONS) { From 20106909cf5d693ebba2afcc64d0e1be6cda4f4a Mon Sep 17 00:00:00 2001 From: KasparMetsa Date: Thu, 12 Feb 2026 09:45:31 +0200 Subject: [PATCH 2/4] Refactor & Fixes --- scripts/build-wasm.sh | 31 ------------ scripts/server.py | 15 ------ src/clock/mumeclock.cpp | 4 ++ src/display/MapCanvasRoomDrawer.cpp | 76 ++++++++++++++--------------- src/display/mapcanvas.cpp | 4 +- src/display/mapcanvas.h | 10 +++- src/display/mapcanvas_gl.cpp | 34 +++++++------ 7 files changed, 68 insertions(+), 106 deletions(-) delete mode 100755 scripts/build-wasm.sh delete mode 100755 scripts/server.py diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh deleted file mode 100755 index eb817a3a3..000000000 --- a/scripts/build-wasm.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -set -e - -# Source Emscripten environment -# IMPORTANT: Change this path to match your emsdk installation location -source "$HOME/dev/emsdk/emsdk_env.sh" - -# Paths - automatically detect project root (parent of scripts directory) -MMAPPER_SRC="$(cd "$(dirname "$0")/.." && pwd)" -QT_WASM="$HOME/Qt/6.10.2/wasm_multithread" -QT_HOST="$HOME/Qt/6.10.2/macos" - -# Configure with qt-cmake -"$QT_WASM/bin/qt-cmake" \ - -S "$MMAPPER_SRC" \ - -B "$MMAPPER_SRC/build-wasm" \ - -DQT_HOST_PATH="$QT_HOST" \ - -DWITH_OPENSSL=OFF \ - -DWITH_TESTS=OFF \ - -DWITH_WEBSOCKET=ON \ - -DWITH_UPDATER=OFF \ - -DCMAKE_BUILD_TYPE=Release - -# Build (limited to 4 cores to avoid system slowdown) -cmake --build "$MMAPPER_SRC/build-wasm" --parallel 4 - -echo "" -echo "Build complete!" -echo "To run:" -echo " cd $MMAPPER_SRC/build-wasm/src && python3 $MMAPPER_SRC/scripts/server.py" -echo "Then open: http://localhost:9742/mmapper.html" \ No newline at end of file diff --git a/scripts/server.py b/scripts/server.py deleted file mode 100755 index 7e70bf090..000000000 --- a/scripts/server.py +++ /dev/null @@ -1,15 +0,0 @@ -import http.server -import socketserver - -PORT = 9742 - -class MyHandler(http.server.SimpleHTTPRequestHandler): - def end_headers(self): - self.send_header("Cross-Origin-Opener-Policy", "same-origin") - self.send_header("Cross-Origin-Embedder-Policy", "require-corp") - http.server.SimpleHTTPRequestHandler.end_headers(self) - -with socketserver.TCPServer(("", PORT), MyHandler) as httpd: - print(f"Serving MMapper WASM at http://localhost:{PORT}/mmapper.html") - print("Press Ctrl+C to stop") - httpd.serve_forever() \ No newline at end of file diff --git a/src/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index c3deed9bb..167a71781 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -80,8 +80,12 @@ class NODISCARD QME final NODISCARD static int keyToValue(const QString &key) { return g_qme.keyToValue(key.toUtf8()); } NODISCARD static QString valueToKey(const int value) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) // Qt 6.10+ changed valueToKey() signature from int to quint64 return g_qme.valueToKey(static_cast(value)); +#else + return g_qme.valueToKey(value); +#endif } NODISCARD static int keyCount() { return g_qme.keyCount(); } }; diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index 8704af3a6..e86ff95de 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -1128,48 +1128,44 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur // Async execution causes crashes because the WebGL context can become // invalid while the async task is running, and there's no way to // safely cancel the task mid-execution. - return std::async(std::launch::deferred, - [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { - // Check context before starting - if (MapCanvas::isWasmContextLost()) { - qWarning() << "[SYNC] generateMapDataFinisher - context lost, aborting"; - return SharedMapBatchFinisher{}; - } - - ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, - visitRoomOptions.colorSettings}; - DECL_TIMER(t, "[SYNC] generateAllLayerMeshes"); - - ProgressCounter dummyPc; - map.checkConsistency(dummyPc); - - const auto layerToRooms = std::invoke([map]() -> LayerToRooms { - DECL_TIMER(t2, "[SYNC] generateBatches.layerToRooms"); - LayerToRooms ltr; - map.getRooms().for_each([&map, <r](const RoomId id) { - const auto &r = map.getRoomHandle(id); - const auto z = r.getPosition().z; - auto &layer = ltr[z]; - layer.emplace_back(r); - }); - return ltr; - }); + return std::async( + std::launch::deferred, [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { + // Check context before starting + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[SYNC] generateMapDataFinisher - context lost, aborting"; + return SharedMapBatchFinisher{}; + } - // Check again before the expensive operation - if (MapCanvas::isWasmContextLost()) { - qWarning() << "[SYNC] generateMapDataFinisher - context lost before mesh gen, aborting"; - return SharedMapBatchFinisher{}; - } + ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, + visitRoomOptions.colorSettings}; + DECL_TIMER(t, "[SYNC] generateAllLayerMeshes"); + + ProgressCounter dummyPc; + map.checkConsistency(dummyPc); + + const auto layerToRooms = std::invoke([map]() -> LayerToRooms { + DECL_TIMER(t2, "[SYNC] generateBatches.layerToRooms"); + LayerToRooms ltr; + map.getRooms().for_each([&map, <r](const RoomId id) { + const auto &r = map.getRoomHandle(id); + const auto z = r.getPosition().z; + auto &layer = ltr[z]; + layer.emplace_back(r); + }); + return ltr; + }); + + // Check again before the expensive operation + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[SYNC] generateMapDataFinisher - context lost before mesh gen, aborting"; + return SharedMapBatchFinisher{}; + } - auto result = std::make_shared(); - auto &data = deref(result); - generateAllLayerMeshes(data, - deref(font), - layerToRooms, - textures, - visitRoomOptions); - return SharedMapBatchFinisher{result}; - }); + auto result = std::make_shared(); + auto &data = deref(result); + generateAllLayerMeshes(data, deref(font), layerToRooms, textures, visitRoomOptions); + return SharedMapBatchFinisher{result}; + }); #else // Desktop: Use async execution for better performance return std::async(std::launch::async, diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index af37ed716..29342b05c 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -55,9 +55,7 @@ NODISCARD static NonOwningPointer &primaryMapCanvas() #ifdef __EMSCRIPTEN__ // WASM: QOpenGLWindow-based constructor -MapCanvas::MapCanvas(MapData &mapData, - PrespammedPath &prespammedPath, - Mmapper2Group &groupManager) +MapCanvas::MapCanvas(MapData &mapData, PrespammedPath &prespammedPath, Mmapper2Group &groupManager) : QOpenGLWindow{QOpenGLWindow::NoPartialUpdate} , MapCanvasViewport{static_cast(*this)} , MapCanvasInputState{prespammedPath} diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 8320db4e4..23b63ec78 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -17,6 +17,7 @@ #include "Textures.h" #include +#include #include #include #include @@ -71,12 +72,19 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, static constexpr const int SCROLL_SCALE = 64; #ifdef __EMSCRIPTEN__ - // WASM: Check if WebGL context was lost + // WASM: WebGL context state management + // These are static because WebGL has one context per page/canvas static bool isWasmContextLost(); + static void resetWasmContextState(); + // WASM: Helper to access the container widget QWidget *getContainerWidget() const { return m_containerWidget; } void setContainerWidget(QWidget *widget) { m_containerWidget = widget; } + private: + // WASM: Static context tracking (one WebGL context per page) + static bool s_wasmInitialized; + static std::atomic s_wasmContextLost; QWidget *m_containerWidget = nullptr; #endif diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index ecf458034..763e943d9 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -28,8 +28,8 @@ #include "mapcanvas.h" #include -#include #include +#include #include #include #include @@ -232,17 +232,20 @@ bool MapCanvas::isBlacklistedDriver() return false; } -// WASM: Track initialization and context state globally +// WASM: Static member definitions for context state tracking #ifdef __EMSCRIPTEN__ -static bool g_wasmInitialized = false; -static std::atomic g_wasmContextLost{false}; -static std::atomic g_wasmInitAttempts{0}; -#endif +bool MapCanvas::s_wasmInitialized = false; +std::atomic MapCanvas::s_wasmContextLost{false}; -#ifdef __EMSCRIPTEN__ bool MapCanvas::isWasmContextLost() { - return g_wasmContextLost.load(); + return s_wasmContextLost.load(); +} + +void MapCanvas::resetWasmContextState() +{ + s_wasmInitialized = false; + s_wasmContextLost.store(false); } #endif @@ -251,12 +254,11 @@ void MapCanvas::initializeGL() #ifdef __EMSCRIPTEN__ // WASM: Track reinitialization attempts. // With QOpenGLWindow approach, reinit should not happen as frequently. - if (g_wasmInitialized) { - ++g_wasmInitAttempts; - g_wasmContextLost.store(true); + if (s_wasmInitialized) { + s_wasmContextLost.store(true); return; } - g_wasmInitialized = true; + s_wasmInitialized = true; #endif OpenGL &gl = getOpenGL(); @@ -269,7 +271,7 @@ void MapCanvas::initializeGL() } } catch (const std::exception &) { #ifdef __EMSCRIPTEN__ - close(); // QOpenGLWindow uses close() instead of hide() + close(); // QOpenGLWindow uses close() instead of hide() #else hide(); #endif @@ -552,7 +554,7 @@ void MapCanvas::resizeGL(int width, int height) // WASM: Check if WebGL context is valid auto *ctx = context(); if (ctx == nullptr || !ctx->isValid()) { - g_wasmContextLost.store(true); + s_wasmContextLost.store(true); return; } #endif @@ -617,7 +619,7 @@ void MapCanvas::updateMapBatches() #ifdef __EMSCRIPTEN__ // WASM: Don't start new async batch generation if context is unstable. // This prevents crashes in the async task. - if (g_wasmContextLost.load()) { + if (s_wasmContextLost.load()) { return; } #endif @@ -932,7 +934,7 @@ void MapCanvas::paintGL() // WASM: Check if WebGL context is valid auto *ctx = context(); if (ctx == nullptr || !ctx->isValid()) { - g_wasmContextLost.store(true); + s_wasmContextLost.store(true); return; } #endif From f9badce8ee9b7548a9ca84028e5ce73f4bdf6260 Mon Sep 17 00:00:00 2001 From: KasparMetsa Date: Thu, 12 Feb 2026 18:54:18 +0200 Subject: [PATCH 3/4] Refactor & Fixes --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 1 - src/display/MapCanvasRoomDrawer.cpp | 77 ++++++++++------------------- src/display/mapcanvas.cpp | 43 +--------------- src/display/mapcanvas.h | 20 ++++++-- src/display/mapcanvas_gl.cpp | 9 +++- src/display/mapwindow.cpp | 28 +++++++++++ src/display/mapwindow.h | 6 ++- 8 files changed, 84 insertions(+), 102 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3224ccb10..410a3eaa3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ if(Qt6OpenGL_FOUND) set(QT_HAS_OPENGL FALSE) elseif(APPLE) # Force OpenGL on macOS - the try_compile check can fail with some Qt versions - # but macOS always has OpenGL 3.3 support via the compatibility profile + # but macOS always has OpenGL 3.3 support via the core profile set(QT_HAS_GLES FALSE) set(QT_HAS_OPENGL TRUE) else() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4be417952..c1993894f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -688,7 +688,6 @@ if(EMSCRIPTEN) -sMIN_WEBGL_VERSION=2 -sASSERTIONS=0 -sASYNCIFY - -sGL_POOL_TEMP_BUFFERS=0 -Os ) target_link_libraries(mmapper PUBLIC z) diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index e86ff95de..484a7e352 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -4,13 +4,10 @@ #include "MapCanvasRoomDrawer.h" -#ifdef __EMSCRIPTEN__ -#include "mapcanvas.h" -#endif - #include "../configuration/NamedConfig.h" #include "../configuration/configuration.h" #include "../global/Array.h" +#include "../global/ConfigConsts-Computed.h" #include "../global/ConfigConsts.h" #include "../global/EnumIndexedArray.h" #include "../global/Flags.h" @@ -1123,62 +1120,30 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur { const auto visitRoomOptions = getVisitRoomOptions(); -#ifdef __EMSCRIPTEN__ - // WASM: Use synchronous (deferred) execution instead of async. - // Async execution causes crashes because the WebGL context can become - // invalid while the async task is running, and there's no way to - // safely cancel the task mid-execution. - return std::async( - std::launch::deferred, [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { - // Check context before starting - if (MapCanvas::isWasmContextLost()) { - qWarning() << "[SYNC] generateMapDataFinisher - context lost, aborting"; - return SharedMapBatchFinisher{}; - } - - ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, - visitRoomOptions.colorSettings}; - DECL_TIMER(t, "[SYNC] generateAllLayerMeshes"); - - ProgressCounter dummyPc; - map.checkConsistency(dummyPc); - - const auto layerToRooms = std::invoke([map]() -> LayerToRooms { - DECL_TIMER(t2, "[SYNC] generateBatches.layerToRooms"); - LayerToRooms ltr; - map.getRooms().for_each([&map, <r](const RoomId id) { - const auto &r = map.getRoomHandle(id); - const auto z = r.getPosition().z; - auto &layer = ltr[z]; - layer.emplace_back(r); - }); - return ltr; - }); - - // Check again before the expensive operation - if (MapCanvas::isWasmContextLost()) { - qWarning() << "[SYNC] generateMapDataFinisher - context lost before mesh gen, aborting"; - return SharedMapBatchFinisher{}; - } + // WASM: Use deferred (synchronous) execution to avoid context invalidation during async. + // Desktop: Use async execution for better performance. + constexpr auto launchPolicy = (CURRENT_PLATFORM == PlatformEnum::Wasm) ? std::launch::deferred + : std::launch::async; - auto result = std::make_shared(); - auto &data = deref(result); - generateAllLayerMeshes(data, deref(font), layerToRooms, textures, visitRoomOptions); - return SharedMapBatchFinisher{result}; - }); -#else - // Desktop: Use async execution for better performance - return std::async(std::launch::async, + return std::async(launchPolicy, [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { + // WASM: Check if WebGL context was lost before starting + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[generateMapDataFinisher] context lost, aborting"; + return SharedMapBatchFinisher{}; + } + } + ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, visitRoomOptions.colorSettings}; - DECL_TIMER(t, "[ASYNC] generateAllLayerMeshes"); + DECL_TIMER(t, "generateAllLayerMeshes"); ProgressCounter dummyPc; map.checkConsistency(dummyPc); const auto layerToRooms = std::invoke([map]() -> LayerToRooms { - DECL_TIMER(t2, "[ASYNC] generateBatches.layerToRooms"); + DECL_TIMER(t2, "generateBatches.layerToRooms"); LayerToRooms ltr; map.getRooms().for_each([&map, <r](const RoomId id) { const auto &r = map.getRoomHandle(id); @@ -1189,6 +1154,15 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur return ltr; }); + // WASM: Check again before the expensive mesh generation + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + if (MapCanvas::isWasmContextLost()) { + qWarning() << "[generateMapDataFinisher] context lost before " + "mesh gen, aborting"; + return SharedMapBatchFinisher{}; + } + } + auto result = std::make_shared(); auto &data = deref(result); generateAllLayerMeshes(data, @@ -1198,7 +1172,6 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur visitRoomOptions); return SharedMapBatchFinisher{result}; }); -#endif } void finish(const IMapBatchesFinisher &finisher, diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index 29342b05c..8dfeb42e8 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -32,7 +32,6 @@ #include #include -#include #include #include #include @@ -99,7 +98,6 @@ MapCanvas::MapCanvas(MapData &mapData, } setCursor(Qt::OpenHandCursor); - grabGesture(Qt::PinchGesture); setContextMenuPolicy(Qt::CustomContextMenu); } #endif @@ -326,46 +324,7 @@ void MapCanvas::slot_onForcedPositionChange() bool MapCanvas::event(QEvent *const event) { -#ifndef __EMSCRIPTEN__ - // Gesture handling is only available on desktop - auto tryHandlePinchZoom = [this, event]() -> bool { - if (event->type() != QEvent::Gesture) { - return false; - } - - const auto *const gestureEvent = dynamic_cast(event); - if (gestureEvent == nullptr) { - return false; - } - - // Zoom in / out - QGesture *const gesture = gestureEvent->gesture(Qt::PinchGesture); - const auto *const pinch = dynamic_cast(gesture); - if (pinch == nullptr) { - return false; - } - - const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); - if (changeFlags & QPinchGesture::ScaleFactorChanged) { - const auto pinchFactor = static_cast(pinch->totalScaleFactor()); - m_scaleFactor.setPinch(pinchFactor); - if ((false)) { - zoomChanged(); // Don't call this here, because it's not true yet. - } - } - if (pinch->state() == Qt::GestureFinished) { - m_scaleFactor.endPinch(); - zoomChanged(); // might not have actually changed - } - update(); - return true; - }; - - if (tryHandlePinchZoom()) { - return true; - } -#endif - + // Note: Must use #ifdef here because QOpenGLWidget is not declared on WASM #ifdef __EMSCRIPTEN__ return QOpenGLWindow::event(event); #else diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 23b63ec78..4953efc3b 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -71,10 +71,11 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, static constexpr const int BASESIZE = 528; // REVISIT: Why this size? 16*33 isn't special. static constexpr const int SCROLL_SCALE = 64; -#ifdef __EMSCRIPTEN__ - // WASM: WebGL context state management - // These are static because WebGL has one context per page/canvas + // Context state management (meaningful on WASM, always returns false on desktop) static bool isWasmContextLost(); + +#ifdef __EMSCRIPTEN__ + // WASM-only: Additional context management static void resetWasmContextState(); // WASM: Helper to access the container widget @@ -221,6 +222,19 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, } NODISCARD float getRawZoom() const { return m_scaleFactor.getRaw(); } + // Pinch gesture support (called from MapWindow) + void setPinchZoom(float pinchFactor) + { + m_scaleFactor.setPinch(pinchFactor); + update(); + } + void endPinchZoom() + { + m_scaleFactor.endPinch(); + zoomChanged(); + update(); + } + public: #ifdef __EMSCRIPTEN__ NODISCARD auto width() const { return QOpenGLWindow::width(); } diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index 763e943d9..2372723da 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -232,8 +232,9 @@ bool MapCanvas::isBlacklistedDriver() return false; } -// WASM: Static member definitions for context state tracking +// Context state tracking #ifdef __EMSCRIPTEN__ +// WASM: Static member definitions bool MapCanvas::s_wasmInitialized = false; std::atomic MapCanvas::s_wasmContextLost{false}; @@ -247,6 +248,12 @@ void MapCanvas::resetWasmContextState() s_wasmInitialized = false; s_wasmContextLost.store(false); } +#else +// Desktop: Context is never "lost" in the WASM sense +bool MapCanvas::isWasmContextLost() +{ + return false; +} #endif void MapCanvas::initializeGL() diff --git a/src/display/mapwindow.cpp b/src/display/mapwindow.cpp index 5869e0b78..d150c1dd0 100644 --- a/src/display/mapwindow.cpp +++ b/src/display/mapwindow.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -144,6 +145,9 @@ MapWindow::MapWindow(MapData &mapData, PrespammedPath &pp, Mmapper2Group &gm, QW connect(canvas, &MapCanvas::sig_mapMove, this, &MapWindow::slot_mapMove); connect(canvas, &MapCanvas::sig_zoomChanged, this, &MapWindow::slot_zoomChanged); } + + // Pinch-to-zoom gesture (handled here since MapWindow is always a QWidget) + grabGesture(Qt::PinchGesture); } void MapWindow::hideSplashImage() @@ -172,6 +176,30 @@ void MapWindow::keyReleaseEvent(QKeyEvent *const event) QWidget::keyReleaseEvent(event); } +bool MapWindow::event(QEvent *const event) +{ + // Handle pinch-to-zoom gesture + if (event->type() == QEvent::Gesture) { + const auto *const gestureEvent = dynamic_cast(event); + if (gestureEvent != nullptr) { + QGesture *const gesture = gestureEvent->gesture(Qt::PinchGesture); + const auto *const pinch = dynamic_cast(gesture); + if (pinch != nullptr) { + const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); + if (changeFlags & QPinchGesture::ScaleFactorChanged) { + const auto pinchFactor = static_cast(pinch->totalScaleFactor()); + m_canvas->setPinchZoom(pinchFactor); + } + if (pinch->state() == Qt::GestureFinished) { + m_canvas->endPinchZoom(); + } + return true; + } + } + } + return QWidget::event(event); +} + MapWindow::~MapWindow() = default; void MapWindow::slot_mapMove(const int dx, const int input_dy) diff --git a/src/display/mapwindow.h b/src/display/mapwindow.h index 1f9f76600..e824d9aad 100644 --- a/src/display/mapwindow.h +++ b/src/display/mapwindow.h @@ -66,10 +66,12 @@ class NODISCARD_QOBJECT MapWindow final : public QWidget public: void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - NODISCARD MapCanvas *getCanvas() const; +protected: + void resizeEvent(QResizeEvent *event) override; + bool event(QEvent *event) override; + public: void updateScrollBars(); void setZoom(float zoom); From 136fd4ef3b4f41061bf3e19eba8a2f38b72194d4 Mon Sep 17 00:00:00 2001 From: KasparMetsa Date: Fri, 13 Feb 2026 02:24:07 +0200 Subject: [PATCH 4/4] Code review fixes --- src/display/mapcanvas.cpp | 92 ++++++++++++++++++------------------ src/display/mapcanvas.h | 8 ++++ src/display/mapcanvas_gl.cpp | 12 ++++- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/display/mapcanvas.cpp b/src/display/mapcanvas.cpp index 8dfeb42e8..e86927f60 100644 --- a/src/display/mapcanvas.cpp +++ b/src/display/mapcanvas.cpp @@ -97,7 +97,7 @@ MapCanvas::MapCanvas(MapData &mapData, pmc = this; } - setCursor(Qt::OpenHandCursor); + setCanvasCursor(Qt::OpenHandCursor); setContextMenuPolicy(Qt::CustomContextMenu); } #endif @@ -117,6 +117,37 @@ MapCanvas *MapCanvas::getPrimary() return primaryMapCanvas(); } +QWidget *MapCanvas::getParentWidget() const +{ +#ifdef __EMSCRIPTEN__ + return m_containerWidget; +#else + return qobject_cast(parent()); +#endif +} + +void MapCanvas::setCanvasCursor(const QCursor &cursor) +{ + if (QWidget *w = getParentWidget()) { + w->setCursor(cursor); + } +} + +QCursor MapCanvas::getCanvasCursor() const +{ + if (QWidget *w = getParentWidget()) { + return w->cursor(); + } + return QCursor(); +} + +void MapCanvas::showCanvasTooltip(const QPoint &localPos, const QString &text) +{ + if (QWidget *w = getParentWidget()) { + QToolTip::showText(w->mapToGlobal(localPos), text, w, w->rect(), 5000); + } +} + void MapCanvas::slot_layerUp() { ++m_currentLayer; @@ -147,10 +178,9 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode) // scrollbars or use the new zoom-recenter feature). slot_clearAllSelections(); -#ifndef __EMSCRIPTEN__ switch (mode) { case CanvasMouseModeEnum::MOVE: - setCursor(Qt::OpenHandCursor); + setCanvasCursor(Qt::OpenHandCursor); break; default: @@ -158,7 +188,7 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode) case CanvasMouseModeEnum::RAYPICK_ROOMS: case CanvasMouseModeEnum::SELECT_CONNECTIONS: case CanvasMouseModeEnum::CREATE_INFOMARKS: - setCursor(Qt::CrossCursor); + setCanvasCursor(Qt::CrossCursor); break; case CanvasMouseModeEnum::SELECT_ROOMS: @@ -166,10 +196,9 @@ void MapCanvas::slot_setCanvasMouseMode(const CanvasMouseModeEnum mode) case CanvasMouseModeEnum::CREATE_CONNECTIONS: case CanvasMouseModeEnum::CREATE_ONEWAY_CONNECTIONS: case CanvasMouseModeEnum::SELECT_INFOMARKS: - setCursor(Qt::ArrowCursor); + setCanvasCursor(Qt::ArrowCursor); break; } -#endif m_canvasMouseMode = mode; m_selectedArea = false; @@ -412,12 +441,8 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) MAYBE_UNUSED const bool hasAlt = (event->modifiers() & Qt::ALT) != 0u; if (hasLeftButton && hasAlt) { -#ifndef __EMSCRIPTEN__ - m_altDragState.emplace(AltDragState{event->pos(), cursor()}); - setCursor(Qt::ClosedHandCursor); -#else - m_altDragState.emplace(AltDragState{event->pos(), QCursor()}); -#endif + m_altDragState.emplace(AltDragState{event->pos(), getCanvasCursor()}); + setCanvasCursor(Qt::ClosedHandCursor); event->accept(); return; } @@ -470,9 +495,7 @@ void MapCanvas::mousePressEvent(QMouseEvent *const event) break; case CanvasMouseModeEnum::MOVE: if (hasLeftButton && hasSel1()) { -#ifndef __EMSCRIPTEN__ - setCursor(Qt::ClosedHandCursor); -#endif + setCanvasCursor(Qt::ClosedHandCursor); startMoving(m_sel1.value()); } break; @@ -598,9 +621,7 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) if (m_altDragState.has_value()) { // The user released the Alt key mid-drag. if (!((event->modifiers() & Qt::ALT) != 0u)) { -#ifndef __EMSCRIPTEN__ - setCursor(m_altDragState->originalCursor); -#endif + setCanvasCursor(m_altDragState->originalCursor); m_altDragState.reset(); // Don't accept the event; let the underlying widgets handle it. return; @@ -682,9 +703,7 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) if (hasLeftButton && hasSel1() && hasSel2()) { if (hasInfomarkSelectionMove()) { m_infoMarkSelectionMove->pos = getSel2().pos - getSel1().pos; -#ifndef __EMSCRIPTEN__ - setCursor(Qt::ClosedHandCursor); -#endif + setCanvasCursor(Qt::ClosedHandCursor); } else { m_selectedArea = true; } @@ -726,9 +745,7 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) m_roomSelectionMove->pos = diff; m_roomSelectionMove->wrongPlace = wrongPlace; -#ifndef __EMSCRIPTEN__ - setCursor(wrongPlace ? Qt::ForbiddenCursor : Qt::ClosedHandCursor); -#endif + setCanvasCursor(wrongPlace ? Qt::ForbiddenCursor : Qt::ClosedHandCursor); } else { m_selectedArea = true; } @@ -777,9 +794,7 @@ void MapCanvas::mouseMoveEvent(QMouseEvent *const event) void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) { if (m_altDragState.has_value()) { -#ifndef __EMSCRIPTEN__ - setCursor(m_altDragState->originalCursor); -#endif + setCanvasCursor(m_altDragState->originalCursor); m_altDragState.reset(); event->accept(); return; @@ -794,9 +809,7 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) switch (m_canvasMouseMode) { case CanvasMouseModeEnum::SELECT_INFOMARKS: -#ifndef __EMSCRIPTEN__ - setCursor(Qt::ArrowCursor); -#endif + setCanvasCursor(Qt::ArrowCursor); if (m_mouseLeftPressed) { m_mouseLeftPressed = false; if (hasInfomarkSelectionMove()) { @@ -848,9 +861,7 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) case CanvasMouseModeEnum::MOVE: stopMoving(); -#ifndef __EMSCRIPTEN__ - setCursor(Qt::OpenHandCursor); -#endif + setCanvasCursor(Qt::OpenHandCursor); if (m_mouseLeftPressed) { m_mouseLeftPressed = false; } @@ -862,16 +873,7 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) mmqt::StripAnsiEnum::Yes, mmqt::PreviewStyleEnum::ForDisplay); -#ifdef __EMSCRIPTEN__ - // QToolTip requires QWidget; on WASM we skip tooltips for now - Q_UNUSED(message); -#else - QToolTip::showText(mapToGlobal(event->position().toPoint()), - message, - this, - rect(), - 5000); -#endif + showCanvasTooltip(event->position().toPoint(), message); } } break; @@ -880,9 +882,7 @@ void MapCanvas::mouseReleaseEvent(QMouseEvent *const event) break; case CanvasMouseModeEnum::SELECT_ROOMS: -#ifndef __EMSCRIPTEN__ - setCursor(Qt::ArrowCursor); -#endif + setCanvasCursor(Qt::ArrowCursor); // This seems very unusual. if (m_ctrlPressed && m_altPressed) { break; diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 4953efc3b..f9881367e 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -235,6 +235,14 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWidget, update(); } + // Cursor and tooltip helpers (forward to parent widget for cross-platform support) + void setCanvasCursor(const QCursor &cursor); + NODISCARD QCursor getCanvasCursor() const; + void showCanvasTooltip(const QPoint &localPos, const QString &text); + +private: + NODISCARD QWidget *getParentWidget() const; + public: #ifdef __EMSCRIPTEN__ NODISCARD auto width() const { return QOpenGLWindow::width(); } diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index 2372723da..ba5ab8e65 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -262,6 +262,8 @@ void MapCanvas::initializeGL() // WASM: Track reinitialization attempts. // With QOpenGLWindow approach, reinit should not happen as frequently. if (s_wasmInitialized) { + qWarning() << "[MapCanvas] initializeGL called again - WebGL context likely lost. " + << "Call resetWasmContextState() before retrying initialization."; s_wasmContextLost.store(true); return; } @@ -279,13 +281,19 @@ void MapCanvas::initializeGL() } catch (const std::exception &) { #ifdef __EMSCRIPTEN__ close(); // QOpenGLWindow uses close() instead of hide() + doneCurrent(); + // WASM: MapCanvas is QOpenGLWindow (not QWidget), use nullptr for dialog parent + QMessageBox::critical(nullptr, + "Unable to initialize OpenGL", + "Upgrade your video card drivers"); #else hide(); -#endif doneCurrent(); - QMessageBox::critical(nullptr, + // Desktop: MapCanvas is QOpenGLWidget, use this for proper modality + QMessageBox::critical(this, "Unable to initialize OpenGL", "Upgrade your video card drivers"); +#endif if constexpr (CURRENT_PLATFORM == PlatformEnum::Windows) { // Link to Microsoft OpenGL Compatibility Pack QDesktopServices::openUrl(