comphelper/Library_comphelper.mk | 1 comphelper/source/misc/emscriptenthreading.cxx | 60 ++++++++++++ desktop/util/Executable_soffice_bin-emscripten-exports | 1 include/comphelper/emscriptenthreading.hxx | 40 ++++++++ include/vcl/syswin.hxx | 24 ++++ include/vcl/task.hxx | 2 solenv/gbuild/platform/EMSCRIPTEN_INTEL_GCC.mk | 10 +- vcl/inc/qt5/QtInstance.hxx | 14 ++ vcl/qt5/QtFrame.cxx | 19 +++ vcl/qt5/QtGraphicsBase.cxx | 8 + vcl/qt5/QtInstance.cxx | 82 ++++++++++++++++- vcl/qt5/QtMenu.cxx | 22 +++- vcl/qt5/QtTimer.cxx | 7 + vcl/source/app/scheduler.cxx | 29 ++++++ vcl/source/window/dialog.cxx | 6 - vcl/source/window/syswin.cxx | 21 +++- 16 files changed, 323 insertions(+), 23 deletions(-)
New commits: commit 364508802571c25ac993e9327c67e2d5c575f668 Author: Stephan Bergmann <[email protected]> AuthorDate: Tue Feb 11 14:11:25 2025 +0100 Commit: Stephan Bergmann <[email protected]> CommitDate: Wed Feb 12 13:14:26 2025 +0100 Experimental Emscripten Qt6 --enable-emscripten-proxy-to-pthread ersatz With Qt5, the (enabled by default) --enable-emscripten-proxy-to-pthread (i.e., leveraging Emscripten's -sPROXY_TO_PTHREAD) works reasonably well with the handful of hacks we added to our Qt5 5.15.2+wasm branch. However, for Qt6 any necessary hacking there would need to be excessive and looks unrealistic (see the mailing list thread starting at <https://lists.qt-project.org/pipermail/development/2024-December/045960.html> "[Development] Wasm: Support for Emscripten PROXY_TO_PTHREAD?" and the presentation at <https://fosdem.org/2025/schedule/event/fosdem-2025-5169-lowa-in-need-of-a-vcl-plug/>). But when leveraging the upcoming browser support for JSPI (through Emscripten's -sJSPI, Qt6's -feature-wasm-jspi, and our --enable-emscripten-jspi), it looks feasible to leave the Qt6 code alone and instead of proxying the complete application main thread (including the Qt6 event handling) off the browser main thread to an additional thread (automatically set up through Emscripten -sPROXY_TO_PTHREAD), to just proxy some of the LO event handling off the browser main thread to an additional thread (set up through a new comphelper::emscriptenthreading::setUp). That way, problematic LO code that is triggered from event handling (like "Tools - Extensions..." wanting to spawn additional threads, which requires the browser main thread to be unblocked) can be offloaded to the additional thread and get the browser main thread unblocked. Thus there is a new "Emscripten Qt6 JSPI/non-PROXY_TO_PTHREAD" mode now (via --enable-qt6 --enable-emscripten-jspi --disable-emscripten-proxy-to-pthread). It is still experimental and known to occasionally hang and crash, but has been seen to generally work at least with recent emsdk 4.0.3 and a recent Qt dev branch (towards Qt 6.10, built with -feature-wasm-jspi) and running on recent Chrome 132 with "Experimental WebAssembly JavaScript Promise Integration (JSPI)" enabled under <chrome://flags>. There are two places where code is proxied off the browser main thread to the additional thread: One is QtInstance::ProcessEvent, the other is invoking certain kinds of tasks (just "vcl::Dialog maLayoutIdle", for now) in Scheduler::CallbackTaskScheduling (unconditionally proxying off all kinds of tasks there appeared to cause more issues than it would solve). For the latter, the concept of "transferability" has been added to class Task; the new Emscripten mode is the only case making use of that concept. Also for the latter, it is important that the browser main thread does not get blocked at the > SolarMutexGuard aGuard; in QtTimer::timeoutActivated when such a timer event happens while the additional thread has the SolarMutex locked and tries to do something that requires the browser main thread to not be blocked (like spawning additional threads upon "Tools - Extensions..."). Therefore, as a bad hack, and only for the new Emscripen mode, that SolarMutexGuard has been moved from QtTimer::timeoutActivated down to the call of pTask->Invoke() in Scheduler::CallbackTaskScheduling (see the two TODO comments added to the code). This appears to work relatively well, but is still a bad hack that should rather be done properly. And then there's some code across vcl/qt5 that may now end up running proxied off the browser main thread, but which actually requires to be run on the browser main thread (because, e.g., it calls into Qt6 code that accesses JS entities that are only available there). Those places use a newly introduced QtInstance::EmscriptenLightweightRunInMainThread that takes some code via a lambda: For the new Emscripten mode, the code is explicitly proxied back to the browser main thread, while in all other cases it is run directly in place. Some further notes: * -sPTHREAD_POOL_SIZE needed to be bumped by one, to accommodate for the additional new thread. * The code requires two additional JSPI "entry points", _emscripten_check_mailbox (to complement the suspension in QtInstance::ProcessEvent) and the invocation of the Qt6-internal qstdweb::EventListener::handleEvent (to complement a Qt6-internal suspension point hit when e.g. doing "Tools - Extension Manager..." and clicking "Add"). * The new Emscripten mode requires -fexperimental-library (for std::jthread and std::stop_token) when building at least with emsdk 4.0.3, but it shouldn't hurt to have that switch enabled unconditionally for Emscripten builds (which are self-contained, so can't be hit by any compatibility issues that switch might cause). Change-Id: Id3b13e2cc3c304b072f388c6d736e7e663b36c58 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/181424 Tested-by: Jenkins Reviewed-by: Stephan Bergmann <[email protected]> Reviewed-by: Michael Weghorn <[email protected]> diff --git a/comphelper/Library_comphelper.mk b/comphelper/Library_comphelper.mk index 85a43d6a779b..f4257653d5a1 100644 --- a/comphelper/Library_comphelper.mk +++ b/comphelper/Library_comphelper.mk @@ -107,6 +107,7 @@ $(eval $(call gb_Library_add_exception_objects,comphelper,\ comphelper/source/misc/docpasswordhelper \ comphelper/source/misc/docpasswordrequest \ comphelper/source/misc/documentinfo \ + comphelper/source/misc/emscriptenthreading \ comphelper/source/misc/errcode \ comphelper/source/misc/evtlistenerhlp \ comphelper/source/misc/evtmethodhelper \ diff --git a/comphelper/source/misc/emscriptenthreading.cxx b/comphelper/source/misc/emscriptenthreading.cxx new file mode 100644 index 000000000000..483e3c7445ad --- /dev/null +++ b/comphelper/source/misc/emscriptenthreading.cxx @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> + +#include <comphelper/emscriptenthreading.hxx> +#include <config_emscripten.h> +#include <config_vclplug.h> + +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + +#include <cassert> +#include <mutex> +#include <stop_token> +#include <thread> + +namespace +{ +std::mutex mutex; +comphelper::emscriptenthreading::Data* data = nullptr; +} + +void comphelper::emscriptenthreading::setUp() +{ + std::scoped_lock g(mutex); + assert(data == nullptr); + data = new Data; + data->eventHandlerThread = std::jthread([](std::stop_token token) { + while (!token.stop_requested()) + { + data->proxyingQueue.execute(); + } + }); +} + +void comphelper::emscriptenthreading::tearDown() +{ + std::scoped_lock g(mutex); + assert(data != nullptr); + data->eventHandlerThread.request_stop(); + data->eventHandlerThread.join(); + data = nullptr; +} + +comphelper::emscriptenthreading::Data& comphelper::emscriptenthreading::getData() +{ + std::scoped_lock g(mutex); + assert(data != nullptr); + return *data; +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/desktop/util/Executable_soffice_bin-emscripten-exports b/desktop/util/Executable_soffice_bin-emscripten-exports index 2206ccccb2d5..7fde7f859828 100644 --- a/desktop/util/Executable_soffice_bin-emscripten-exports +++ b/desktop/util/Executable_soffice_bin-emscripten-exports @@ -12,3 +12,4 @@ _lok_preinit _lok_preinit_2 _malloc _free +__ZN10emscripten8internal13MethodInvokerINS0_3rvp11default_tagEMN7qstdweb13EventListenerEFvNS_3valEEvPS5_JS6_EE6invokeERKS8_S9_PNS_7_EM_VALE diff --git a/include/comphelper/emscriptenthreading.hxx b/include/comphelper/emscriptenthreading.hxx new file mode 100644 index 000000000000..6f1b9306caa5 --- /dev/null +++ b/include/comphelper/emscriptenthreading.hxx @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <sal/config.h> + +#include <config_emscripten.h> +#include <config_vclplug.h> + +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + +#include <thread> + +#include <emscripten/proxying.h> + +namespace comphelper::emscriptenthreading +{ +struct Data +{ + emscripten::ProxyingQueue proxyingQueue; + std::jthread eventHandlerThread; +}; + +void setUp(); + +void tearDown(); + +Data& getData(); +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/include/vcl/syswin.hxx b/include/vcl/syswin.hxx index c1a5a8879310..34cbde005260 100644 --- a/include/vcl/syswin.hxx +++ b/include/vcl/syswin.hxx @@ -96,6 +96,25 @@ class VCL_DLLPUBLIC SystemWindow class ImplData; private: + class LayoutIdle: public Idle { + public: + LayoutIdle(char const * pDebugName, SystemWindow & parent, bool transferable): + Idle(pDebugName), parent_(parent), + transferState_( + transferable ? TransferState::Transferable : TransferState::NotTransferable) + {} + + bool DecideTransferredExecution() override; + + bool wasTransferred() const { return transferState_ == TransferState::Transferred; } + + private: + enum class TransferState { NotTransferable, Transferable, Transferred }; + + SystemWindow & parent_; + TransferState transferState_; + }; + std::unique_ptr<ImplData> mpImplData; VclPtr<MenuBar> mpMenuBar; OUString maNotebookBarUIFile; @@ -110,7 +129,7 @@ private: bool mbInSetNoteBookBar : 1 = false; bool mbPaintComplete : 1 = false; bool mbIsDeferredInit : 1 = false; - Idle maLayoutIdle; + LayoutIdle maLayoutIdle; protected: VclPtr<vcl::Window> mpDialogParent; public: @@ -136,7 +155,8 @@ private: protected: // Single argument ctors shall be explicit. - SAL_DLLPRIVATE explicit SystemWindow(WindowType nType, const char* pIdleDebugName); + SAL_DLLPRIVATE explicit SystemWindow( + WindowType nType, const char* pIdleDebugName, bool transferableIdle = false); SAL_DLLPRIVATE void loadUI(vcl::Window* pParent, const OUString& rID, const OUString& rUIXMLDescription, const css::uno::Reference<css::frame::XFrame> &rFrame = css::uno::Reference<css::frame::XFrame>()); SAL_DLLPRIVATE void SetWindowState(const vcl::WindowData& rData); diff --git a/include/vcl/task.hxx b/include/vcl/task.hxx index 41c73bf39644..f6e1d7b49459 100644 --- a/include/vcl/task.hxx +++ b/include/vcl/task.hxx @@ -82,6 +82,8 @@ public: const char *GetDebugName() const { return mpDebugName; } + virtual bool DecideTransferredExecution(); + // Call handler virtual void Invoke() = 0; diff --git a/solenv/gbuild/platform/EMSCRIPTEN_INTEL_GCC.mk b/solenv/gbuild/platform/EMSCRIPTEN_INTEL_GCC.mk index 4165281d1c09..495cc87c9d46 100644 --- a/solenv/gbuild/platform/EMSCRIPTEN_INTEL_GCC.mk +++ b/solenv/gbuild/platform/EMSCRIPTEN_INTEL_GCC.mk @@ -18,7 +18,7 @@ gb_EMSCRIPTEN_LDFLAGS := $(gb_EMSCRIPTEN_CPPFLAGS) gb_EMSCRIPTEN_LDFLAGS += -s TOTAL_MEMORY=1GB ifeq ($(ENABLE_EMSCRIPTEN_PROXY_TO_PTHREAD),) -gb_EMSCRIPTEN_LDFLAGS += -sPTHREAD_POOL_SIZE=6 +gb_EMSCRIPTEN_LDFLAGS += -sPTHREAD_POOL_SIZE=7 endif # Double the main thread stack size, but keep the default value for other threads: @@ -31,7 +31,9 @@ gb_EMSCRIPTEN_LDFLAGS += --bind -s FORCE_FILESYSTEM=1 -s WASM_BIGINT=1 -s ERROR_ gb_EMSCRIPTEN_QTDEFS := -DQT_NO_LINKED_LIST -DQT_NO_JAVA_STYLE_ITERATORS -DQT_NO_EXCEPTIONS -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB ifeq ($(ENABLE_EMSCRIPTEN_JSPI),TRUE) -gb_EMSCRIPTEN_LDFLAGS += -sJSPI +gb_EMSCRIPTEN_LDFLAGS += \ + -sJSPI \ + -sJSPI_EXPORTS=_emscripten_check_mailbox,_ZN10emscripten8internal13MethodInvokerINS0_3rvp11default_tagEMN7qstdweb13EventListenerEFvNS_3valEEvPS5_JS6_EE6invokeERKS8_S9_PNS_7_EM_VALE endif ifeq ($(ENABLE_EMSCRIPTEN_PROXY_POSIX_SOCKETS),TRUE) @@ -56,6 +58,10 @@ endif gb_LinkTarget_LDFLAGS += $(gb_EMSCRIPTEN_LDFLAGS) $(gb_EMSCRIPTEN_CPPFLAGS) \ $(gb_EMSCRIPTEN_EXCEPT) -sEXPORT_EXCEPTION_HANDLING_HELPERS +# Depending on emsdk version being used, enable e.g. std::jthread and std::stop_token used by some +# build configurations: +gb_LinkTarget_CXXFLAGS += -fexperimental-library + ifeq ($(ENABLE_OPTIMIZED),TRUE) ifneq ($(ENABLE_SYMBOLS_FOR),) gb_LinkTarget__emscripten_warnings_ldflags := -Wno-limited-postlink-optimizations diff --git a/vcl/inc/qt5/QtInstance.hxx b/vcl/inc/qt5/QtInstance.hxx index 90c32bf06bd5..50c6fd64bbbf 100644 --- a/vcl/inc/qt5/QtInstance.hxx +++ b/vcl/inc/qt5/QtInstance.hxx @@ -19,6 +19,8 @@ #pragma once +#include <config_emscripten.h> +#include <config_vclplug.h> #include <vclpluginapi.h> #include <unx/geninst.h> #include <salusereventlist.hxx> @@ -43,6 +45,13 @@ class QApplication; class SalYieldMutex; class SalFrame; +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD +namespace comphelper::emscriptenthreading +{ +struct Data; +} +#endif + struct StdFreeCStr { void operator()(char* arg) const noexcept { std::free(arg); } @@ -71,6 +80,10 @@ class VCLPLUG_QT_PUBLIC QtInstance : public QObject, QtFrame* m_pActivePopup; +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + comphelper::emscriptenthreading::Data* m_emscriptenThreadingData; +#endif + DECL_DLLPRIVATE_LINK(updateStyleHdl, Timer*, void); void AfterAppInit() override; @@ -114,6 +127,7 @@ public: static std::unique_ptr<QApplication> CreateQApplication(int& nArgc, char** pArgv); void RunInMainThread(std::function<void()> func); + void EmscriptenLightweightRunInMainThread(std::function<void()> func); virtual SalFrame* CreateFrame(SalFrame* pParent, SalFrameStyleFlags nStyle) override; virtual SalFrame* CreateChildFrame(SystemParentData* pParent, diff --git a/vcl/qt5/QtFrame.cxx b/vcl/qt5/QtFrame.cxx index 791901dcbc29..4d020ec93395 100644 --- a/vcl/qt5/QtFrame.cxx +++ b/vcl/qt5/QtFrame.cxx @@ -195,8 +195,10 @@ QtFrame::~QtFrame() void QtFrame::Damage(sal_Int32 nExtentsX, sal_Int32 nExtentsY, sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) const { - m_pQWidget->update(scaledQRect(QRect(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight), - 1 / devicePixelRatioF())); + GetQtInstance().EmscriptenLightweightRunInMainThread([ + this, r = scaledQRect(QRect(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight), + 1 / devicePixelRatioF()) + ] { m_pQWidget->update(r); }); } SalGraphics* QtFrame::AcquireGraphics() @@ -258,7 +260,13 @@ QWidget* QtFrame::asChild() const return m_pQWidget; } -qreal QtFrame::devicePixelRatioF() const { return asChild()->devicePixelRatioF(); } +qreal QtFrame::devicePixelRatioF() const +{ + qreal ret; + GetQtInstance().EmscriptenLightweightRunInMainThread( + [ child = asChild(), &ret ] { ret = child->devicePixelRatioF(); }); + return ret; +} bool QtFrame::isWindow() const { return asChild()->isWindow(); } @@ -430,7 +438,10 @@ void QtFrame::SetMinClientSize(tools::Long nWidth, tools::Long nHeight) if (!isChild()) { const qreal fRatio = devicePixelRatioF(); - asChild()->setMinimumSize(round(nWidth / fRatio), round(nHeight / fRatio)); + GetQtInstance().EmscriptenLightweightRunInMainThread( + [ child = asChild(), w = round(nWidth / fRatio), h = round(nHeight / fRatio) ] { + child->setMinimumSize(w, h); + }); } } diff --git a/vcl/qt5/QtGraphicsBase.cxx b/vcl/qt5/QtGraphicsBase.cxx index 12f939d1b931..bfed88a8954b 100644 --- a/vcl/qt5/QtGraphicsBase.cxx +++ b/vcl/qt5/QtGraphicsBase.cxx @@ -8,6 +8,7 @@ */ #include <QtGraphicsBase.hxx> +#include <QtInstance.hxx> #include <QtGui/QScreen> @@ -26,8 +27,11 @@ void QtGraphicsBase::ImplGetResolution(QtFrame* pFrame, sal_Int32& rDPIX, sal_In return; QScreen* pScreen = pFrame->GetQWidget()->screen(); - rDPIX = pScreen->logicalDotsPerInchX() * pScreen->devicePixelRatio() + 0.5; - rDPIY = pScreen->logicalDotsPerInchY() * pScreen->devicePixelRatio() + 0.5; + qreal devicePixelRatio; + GetQtInstance().EmscriptenLightweightRunInMainThread( + [pScreen, &devicePixelRatio] { devicePixelRatio = pScreen->devicePixelRatio(); }); + rDPIX = pScreen->logicalDotsPerInchX() * devicePixelRatio + 0.5; + rDPIY = pScreen->logicalDotsPerInchY() * devicePixelRatio + 0.5; } /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/qt5/QtInstance.cxx b/vcl/qt5/QtInstance.cxx index 2a206aaf6a2f..a1482687b874 100644 --- a/vcl/qt5/QtInstance.cxx +++ b/vcl/qt5/QtInstance.cxx @@ -52,6 +52,7 @@ #include <vclpluginapi.h> #include <tools/debug.hxx> +#include <comphelper/emscriptenthreading.hxx> #include <comphelper/flagguard.hxx> #include <config_emscripten.h> #include <config_vclplug.h> @@ -61,6 +62,7 @@ #include <vcl/sysdata.hxx> #include <sal/log.hxx> #include <o3tl/unreachable.hxx> +#include <o3tl/temporary.hxx> #include <osl/process.h> #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && ENABLE_GSTREAMER_1_0 && QT5_HAVE_GOBJECT #include <unx/gstsink.hxx> @@ -78,6 +80,13 @@ Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) #endif #endif +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD +#include <emscripten/promise.h> +#include <emscripten/proxying.h> +#include <emscripten/threading.h> +#include <pthread.h> +#endif + namespace { /// TODO: not much Qt specific here? could be generalised, esp. for OSX... @@ -226,6 +235,13 @@ void QtInstance::RunInMainThread(std::function<void()> func) func(); return; } +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + if (pthread_self() == m_emscriptenThreadingData->eventHandlerThread.native_handle()) + { + EmscriptenLightweightRunInMainThread(func); + return; + } +#endif QtYieldMutex* const pMutex(static_cast<QtYieldMutex*>(GetYieldMutex())); { @@ -245,6 +261,26 @@ void QtInstance::RunInMainThread(std::function<void()> func) } } +void QtInstance::EmscriptenLightweightRunInMainThread(std::function<void()> func) +{ +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + if (pthread_self() != emscripten_main_runtime_thread_id()) + { + SolarMutexReleaser release; + emscripten_sync_run_in_main_runtime_thread( + EM_FUNC_SIG_RETURN_VALUE_V | EM_FUNC_SIG_WITH_N_PARAMETERS(1) + | EM_FUNC_SIG_SET_PARAM(0, EM_FUNC_SIG_PARAM_P), + +[](void* pf) { + SolarMutexGuard g; + (*static_cast<std::function<void()>*>(pf))(); + }, + &func); + return; + } +#endif + func(); +} + OUString QtInstance::constructToolkitID(std::u16string_view sTKname) { OUString sID(sTKname + OUString::Concat(u" (")); @@ -266,6 +302,11 @@ QtInstance::QtInstance(std::unique_ptr<QApplication>& pQApp) , m_bUpdateFonts(false) , m_pActivePopup(nullptr) { +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + comphelper::emscriptenthreading::setUp(); + m_emscriptenThreadingData = &comphelper::emscriptenthreading::getData(); +#endif + ImplSVData* pSVData = ImplGetSVData(); const OUString sToolkit = "qt" + OUString::number(QT_VERSION_MAJOR); pSVData->maAppData.mxToolkitName = constructToolkitID(sToolkit); @@ -309,6 +350,10 @@ QtInstance::~QtInstance() // force freeing the QApplication before freeing the arguments, // as it uses references to the provided arguments! m_pQApplication.reset(); + +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + comphelper::emscriptenthreading::tearDown(); +#endif } void QtInstance::AfterAppInit() @@ -497,6 +542,26 @@ bool QtInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents) if (bWasEvent) m_aWaitingYieldCond.set(); } +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + else if (pthread_self() == m_emscriptenThreadingData->eventHandlerThread.native_handle()) + { + SolarMutexReleaser release; + struct Args + { + QtInstance* This; + bool bWait; + bool bHandleAllCurrentEvents; + bool& bWasEvent; + }; + (void)emscripten_promise_await(emscripten_proxy_promise( + m_emscriptenThreadingData->proxyingQueue.queue, emscripten_main_runtime_thread_id(), + [](void* p) { + Args const& args = *static_cast<Args*>(p); + args.bWasEvent = args.This->DoYield(args.bWait, args.bHandleAllCurrentEvents); + }, + &o3tl::temporary<Args>({ this, bWait, bHandleAllCurrentEvents, bWasEvent }))); + } +#endif else { { @@ -545,7 +610,20 @@ void QtInstance::TriggerUserEventProcessing() void QtInstance::ProcessEvent(SalUserEvent aEvent) { +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + SolarMutexReleaser release; + (void)emscripten_promise_await( + emscripten_proxy_promise(m_emscriptenThreadingData->proxyingQueue.queue, + m_emscriptenThreadingData->eventHandlerThread.native_handle(), + [](void* p) { + auto& aEvent = *static_cast<SalUserEvent*>(p); + SolarMutexGuard g; + aEvent.m_pFrame->CallCallback(aEvent.m_nEvent, aEvent.m_pData); + }, + &aEvent)); +#else aEvent.m_pFrame->CallCallback(aEvent.m_nEvent, aEvent.m_pData); +#endif } rtl::Reference<QtFilePicker> @@ -601,7 +679,9 @@ QtInstance::CreateClipboard(const css::uno::Sequence<css::uno::Any>& arguments) if (it != m_aClipboards.end()) return it->second; - css::uno::Reference<css::uno::XInterface> xClipboard = QtClipboard::create(sel); + css::uno::Reference<css::uno::XInterface> xClipboard; + EmscriptenLightweightRunInMainThread( + [&sel, &xClipboard] { xClipboard = QtClipboard::create(sel); }); if (xClipboard.is()) m_aClipboards[sel] = xClipboard; diff --git a/vcl/qt5/QtMenu.cxx b/vcl/qt5/QtMenu.cxx index 9b45106338cb..2d677bf6112a 100644 --- a/vcl/qt5/QtMenu.cxx +++ b/vcl/qt5/QtMenu.cxx @@ -769,19 +769,24 @@ QPushButton* QtMenu::ImplAddMenuBarButton(const QIcon& rIcon, const QString& rTo if (!pWidget) { assert(!m_pButtonGroup); - pWidget = new QWidget(mpQMenuBar); + GetQtInstance().EmscriptenLightweightRunInMainThread( + [this, &pWidget] { pWidget = new QWidget(mpQMenuBar); }); assert(!pWidget->layout()); - pLayout = new QHBoxLayout(); + GetQtInstance().EmscriptenLightweightRunInMainThread( + [&pLayout] { pLayout = new QHBoxLayout(); }); pLayout->setContentsMargins(QMargins()); pLayout->setSpacing(0); pWidget->setLayout(pLayout); - m_pButtonGroup = new QButtonGroup(pLayout); + GetQtInstance().EmscriptenLightweightRunInMainThread( + [this, pLayout] { m_pButtonGroup = new QButtonGroup(pLayout); }); m_pButtonGroup->setObjectName(gButtonGroupKey); m_pButtonGroup->setExclusive(false); connect(m_pButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this, &QtMenu::slotMenuBarButtonClicked); - pWidget->show(); - mpQMenuBar->setCornerWidget(pWidget, Qt::TopRightCorner); + GetQtInstance().EmscriptenLightweightRunInMainThread([this, pWidget] { + pWidget->show(); + mpQMenuBar->setCornerWidget(pWidget, Qt::TopRightCorner); + }); } else pLayout = static_cast<QHBoxLayout*>(pWidget->layout()); @@ -792,7 +797,8 @@ QPushButton* QtMenu::ImplAddMenuBarButton(const QIcon& rIcon, const QString& rTo if (pButton) RemoveMenuBarButton(nId); - pButton = new QPushButton(); + GetQtInstance().EmscriptenLightweightRunInMainThread( + [&pButton] { pButton = new QPushButton(); }); // we don't want the button to increase the QMenuBar height, so a fixed size square it is const int nFixedLength = mpQMenuBar->height() - 2 * mpQMenuBar->style()->pixelMetric(QStyle::PM_MenuBarVMargin); @@ -828,7 +834,9 @@ void QtMenu::connectHelpShortcut(QMenu* pMenu) { assert(pMenu); QKeySequence sequence(QKeySequence::HelpContents); - QShortcut* pQShortcut = new QShortcut(sequence, pMenu); + QShortcut* pQShortcut; + GetQtInstance().EmscriptenLightweightRunInMainThread( + [&sequence, pMenu, &pQShortcut] { pQShortcut = new QShortcut(sequence, pMenu); }); connect(pQShortcut, &QShortcut::activated, this, QtMenu::slotShowHelp); connect(pQShortcut, &QShortcut::activatedAmbiguously, this, QtMenu::slotShowHelp); } diff --git a/vcl/qt5/QtTimer.cxx b/vcl/qt5/QtTimer.cxx index 9506801609e9..78856d104281 100644 --- a/vcl/qt5/QtTimer.cxx +++ b/vcl/qt5/QtTimer.cxx @@ -25,6 +25,8 @@ #include <QtWidgets/QApplication> #include <QtCore/QThread> +#include <config_emscripten.h> +#include <config_vclplug.h> #include <vcl/svapp.hxx> #include <sal/log.hxx> @@ -41,7 +43,12 @@ QtTimer::QtTimer() void QtTimer::timeoutActivated() { +#if !(defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD) + //TODO: While the special Emscripten Qt6 JSPI/non-PROXY_TO_PTHREAD mode doesn't lock the + // SolarMutex here, but only when calling pTask->Invoke() in Scheduler::CallbackTaskScheduling, + // that looks too brittle in general, so treat that special mode specially here. SolarMutexGuard aGuard; +#endif if (Application::IsUseSystemEventLoop()) { const ImplSVData* pSVData = ImplGetSVData(); diff --git a/vcl/source/app/scheduler.cxx b/vcl/source/app/scheduler.cxx index eaca69adafa1..6234e6355117 100644 --- a/vcl/source/app/scheduler.cxx +++ b/vcl/source/app/scheduler.cxx @@ -25,6 +25,8 @@ #include <typeinfo> #include <com/sun/star/uno/Exception.hpp> +#include <config_emscripten.h> +#include <config_vclplug.h> #include <sal/log.hxx> #include <sal/types.h> #include <svdata.hxx> @@ -37,6 +39,7 @@ #include <vcl/idle.hxx> #include <saltimer.hxx> #include <salinst.hxx> +#include <comphelper/emscriptenthreading.hxx> #include <comphelper/profilezone.hxx> #include <schedulerimpl.hxx> @@ -358,7 +361,12 @@ void Scheduler::CallbackTaskScheduling() ImplSVData *pSVData = ImplGetSVData(); ImplSchedulerContext &rSchedCtx = pSVData->maSchedCtx; +#if !(defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD) + //TODO: While the special Emscripten Qt6 JSPI/non-PROXY_TO_PTHREAD mode doesn't lock the + // SolarMutex in QtTimer::timeoutActivated, but only down below when calling pTask->Invoke(), + // that looks too brittle in general, so treat that special mode specially here. DBG_TESTSOLARMUTEX(); +#endif SchedulerGuard aSchedulerGuard; if ( !rSchedCtx.mbActive || InfiniteTimeoutMs == rSchedCtx.mnTimerPeriod ) @@ -506,7 +514,23 @@ void Scheduler::CallbackTaskScheduling() { // prepare Scheduler object for deletion after handling pTask->SetDeletionFlags(); +#if defined EMSCRIPTEN && ENABLE_QT6 && HAVE_EMSCRIPTEN_JSPI && !HAVE_EMSCRIPTEN_PROXY_TO_PTHREAD + if (pTask->DecideTransferredExecution()) + { + auto & data = comphelper::emscriptenthreading::getData(); + data.proxyingQueue.proxyAsync(data.eventHandlerThread.native_handle(), [pTask] { + SolarMutexGuard g; + pTask->Invoke(); + }); + } + else + { + SolarMutexGuard g; + pTask->Invoke(); + } +#else pTask->Invoke(); +#endif } } catch (css::uno::Exception&) @@ -693,4 +717,9 @@ Task::~Task() COVERITY_NOEXCEPT_FALSE assert(nullptr == mpSchedulerData || comphelper::IsFuzzing()); } +bool Task::DecideTransferredExecution() +{ + return false; +} + /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/window/dialog.cxx b/vcl/source/window/dialog.cxx index d407626388d6..b65319e9aea5 100644 --- a/vcl/source/window/dialog.cxx +++ b/vcl/source/window/dialog.cxx @@ -548,7 +548,7 @@ void Dialog::ImplLOKNotifier(vcl::Window* pParent) } Dialog::Dialog( WindowType nType ) - : SystemWindow( nType, "vcl::Dialog maLayoutIdle" ) + : SystemWindow( nType, "vcl::Dialog maLayoutIdle", true ) , mnInitFlag(InitFlag::Default) { ImplInitDialogData(); @@ -574,7 +574,7 @@ void Dialog::ImplDeferredInit(vcl::Window* pParent, WinBits nBits) } Dialog::Dialog(vcl::Window* pParent, const OUString& rID, const OUString& rUIXMLDescription) - : SystemWindow(WindowType::DIALOG, "vcl::Dialog maLayoutIdle") + : SystemWindow(WindowType::DIALOG, "vcl::Dialog maLayoutIdle", true) , mnInitFlag(InitFlag::Default) { ImplLOKNotifier(pParent); @@ -583,7 +583,7 @@ Dialog::Dialog(vcl::Window* pParent, const OUString& rID, const OUString& rUIXML } Dialog::Dialog(vcl::Window* pParent, WinBits nStyle, InitFlag eFlag) - : SystemWindow(WindowType::DIALOG, "vcl::Dialog maLayoutIdle") + : SystemWindow(WindowType::DIALOG, "vcl::Dialog maLayoutIdle", true) , mnInitFlag(eFlag) { ImplLOKNotifier(pParent); diff --git a/vcl/source/window/syswin.cxx b/vcl/source/window/syswin.cxx index 6c3e89ca50f1..1601501f0571 100644 --- a/vcl/source/window/syswin.cxx +++ b/vcl/source/window/syswin.cxx @@ -17,10 +17,12 @@ * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ +#include <cassert> #include <memory> #include "menubarwindow.hxx" +#include <comphelper/scopeguard.hxx> #include <o3tl/safeint.hxx> #include <sal/config.h> #include <sal/log.hxx> @@ -66,10 +68,20 @@ SystemWindow::ImplData::ImplData() maMaxOutSize = Size( SHRT_MAX, SHRT_MAX ); } -SystemWindow::SystemWindow(WindowType nType, const char* pIdleDebugName) +bool SystemWindow::LayoutIdle::DecideTransferredExecution() +{ + if (transferState_ == TransferState::NotTransferable) { + return false; + } + parent_.acquire(); // paired with release in ImplHandleLayoutTimerHdl + transferState_ = TransferState::Transferred; + return true; +} + +SystemWindow::SystemWindow(WindowType nType, const char* pIdleDebugName, bool transferableIdle) : Window(nType) , mpImplData(new ImplData) - , maLayoutIdle( pIdleDebugName ) + , maLayoutIdle( pIdleDebugName, *this, transferableIdle ) { mpWindowImpl->mbSysWin = true; mpWindowImpl->mnActivateMode = ActivateModeFlags::GrabFocus; @@ -1036,6 +1048,11 @@ void SystemWindow::setPosSizeOnContainee(Size aSize, Window &rBox) IMPL_LINK_NOARG( SystemWindow, ImplHandleLayoutTimerHdl, Timer*, void ) { + comphelper::ScopeGuard g([this] { + if (maLayoutIdle.wasTransferred()) { + release(); // paired with acquire in LayoutIdle::DecideTransferredExecution + } + }); Window *pBox = GetWindow(GetWindowType::FirstChild); if (!isLayoutEnabled()) {
