diff --git a/src/corelib/kernel/qeventdispatcher_wasm.cpp b/src/corelib/kernel/qeventdispatcher_wasm.cpp index f2ee46ce539..0d1b2553fa0 100644 --- a/src/corelib/kernel/qeventdispatcher_wasm.cpp +++ b/src/corelib/kernel/qeventdispatcher_wasm.cpp @@ -3,23 +3,20 @@ #include "qeventdispatcher_wasm_p.h" -#include // for qGlobalPostedEventsCount() #include #include +#include +#include +#include #include #include -#include "emscripten.h" -#include -#include -#include - using namespace std::chrono; using namespace std::chrono_literals; QT_BEGIN_NAMESPACE -// using namespace emscripten; +using emscripten::val; Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher"); Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers"); @@ -30,12 +27,6 @@ Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers"); #define LOCK_GUARD(M) #endif -// Emscripten asyncify currently supports one level of suspend - -// recursion is not permitted. We track the suspend state here -// on order to fail (more) gracefully, but we can of course only -// track Qts own usage of asyncify. -static bool g_is_asyncify_suspended = false; - #if defined(QT_STATIC) static bool useAsyncify() @@ -43,34 +34,6 @@ static bool useAsyncify() return qstdweb::haveAsyncify(); } -// clang-format off -EM_ASYNC_JS(void, qt_asyncify_suspend_js, (), { - if (Module.qtSuspendId === undefined) - Module.qtSuspendId = 0; - ++Module.qtSuspendId; - await new Promise(resolve => { - Module.qtAsyncifyWakeUp = resolve; - }); -}); - -EM_JS(void, qt_asyncify_resume_js, (), { - let wakeUp = Module.qtAsyncifyWakeUp; - if (wakeUp == undefined) - return; - Module.qtAsyncifyWakeUp = undefined; - const suspendId = Module.qtSuspendId; - - // Delayed wakeup with zero-timer. Workaround/fix for - // https://github.com/emscripten-core/emscripten/issues/10515 - setTimeout(() => { - // Another suspend occurred while the timeout was in queue. - if (Module.qtSuspendId !== suspendId) - return; - wakeUp(); - }); -}); -// clang-format on - #else // EM_JS is not supported for side modules; disable asyncify @@ -80,51 +43,17 @@ static bool useAsyncify() return false; } -void qt_asyncify_suspend_js() -{ - Q_UNREACHABLE(); -} - -void qt_asyncify_resume_js() -{ - Q_UNREACHABLE(); -} - #endif // defined(QT_STATIC) -// Suspends the main thread until qt_asyncify_resume() is called. Returns -// false immediately if Qt has already suspended the main thread (recursive -// suspend is not supported by Emscripten). Returns true (after resuming), -// if the thread was suspended. -bool qt_asyncify_suspend() -{ - if (g_is_asyncify_suspended) - return false; - g_is_asyncify_suspended = true; - qt_asyncify_suspend_js(); - return true; -} - -// Wakes any currently suspended main thread. Returns true if the main -// thread was suspended, in which case it will now be asynchronously woken. -void qt_asyncify_resume() -{ - if (!g_is_asyncify_suspended) - return; - g_is_asyncify_suspended = false; - qt_asyncify_resume_js(); -} - - Q_CONSTINIT QEventDispatcherWasm *QEventDispatcherWasm::g_mainThreadEventDispatcher = nullptr; +Q_CONSTINIT std::shared_ptr QEventDispatcherWasm::g_mainThreadSuspendResumeControl; + #if QT_CONFIG(thread) Q_CONSTINIT QVector QEventDispatcherWasm::g_secondaryThreadEventDispatchers; Q_CONSTINIT std::mutex QEventDispatcherWasm::g_staticDataMutex; -emscripten::ProxyingQueue QEventDispatcherWasm::g_proxyingQueue; -pthread_t QEventDispatcherWasm::g_mainThread; #endif -QEventDispatcherWasm::QEventDispatcherWasm() +QEventDispatcherWasm::QEventDispatcherWasm(std::shared_ptr suspendResumeControl) { // QEventDispatcherWasm operates in two main modes: // - On the main thread: @@ -146,28 +75,39 @@ QEventDispatcherWasm::QEventDispatcherWasm() // dispatchers so we set a global pointer to it. Q_ASSERT(g_mainThreadEventDispatcher == nullptr); g_mainThreadEventDispatcher = this; -#if QT_CONFIG(thread) - g_mainThread = pthread_self(); -#endif - // Call the "onLoaded" JavaScript callback, unless startup tasks - // have been registered which should complete first. Run async - // to make sure event dispatcher construction (in particular any - // subclass construction) has completed first. - runAsync(callOnLoadedIfRequired); + if (suspendResumeControl) { + g_mainThreadSuspendResumeControl = suspendResumeControl; + } else { + g_mainThreadSuspendResumeControl = std::make_shared(); + } + + // Zero-timer used on wake() calls + m_wakeupTimer = std::make_unique(g_mainThreadSuspendResumeControl.get(), [](){ onWakeup(); }); + + // Timer set to fire at the next Qt timer timeout + m_nativeTimer = std::make_unique(g_mainThreadSuspendResumeControl.get(), []() { onTimer(); }); + + // Timer used when suspending to process native events + m_suspendTimer = std::make_unique(g_mainThreadSuspendResumeControl.get(), []() { onProcessNativeEventsResume(); }); } else { #if QT_CONFIG(thread) std::lock_guard lock(g_staticDataMutex); g_secondaryThreadEventDispatchers.append(this); #endif } + + m_timerInfo = std::make_unique(); } QEventDispatcherWasm::~QEventDispatcherWasm() { qCDebug(lcEventDispatcher) << "Destroying QEventDispatcherWasm instance" << this; - delete m_timerInfo; + // Reset to ensure destruction before g_mainThreadSuspendResumeControl + m_wakeupTimer.reset(); + m_nativeTimer.reset(); + m_suspendTimer.reset(); #if QT_CONFIG(thread) if (isSecondaryThreadEventDispatcher()) { @@ -176,10 +116,9 @@ QEventDispatcherWasm::~QEventDispatcherWasm() } else #endif { - if (m_timerId > 0) - emscripten_clear_timeout(m_timerId); QWasmSocket::clearSocketNotifiers(); g_mainThreadEventDispatcher = nullptr; + g_mainThreadSuspendResumeControl.reset(); } } @@ -193,6 +132,11 @@ bool QEventDispatcherWasm::isSecondaryThreadEventDispatcher() return this != g_mainThreadEventDispatcher; } +bool QEventDispatcherWasm::isValidEventDispatcher() +{ + return isValidEventDispatcherPointer(this); +} + bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *eventDispatcher) { if (eventDispatcher == g_mainThreadEventDispatcher) @@ -206,31 +150,23 @@ bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *e bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) { - qCDebug(lcEventDispatcher) << "QEventDispatcherWasm::processEvents flags" << flags; - emit awake(); - if (isMainThreadEventDispatcher()) { - if (flags & QEventLoop::DialogExec) - handleDialogExec(); - else if (flags & QEventLoop::ApplicationExec) - handleApplicationExec(); - } + if (!useAsyncify() && isMainThreadEventDispatcher()) + handleNonAsyncifyErrorCases(flags); -#if QT_CONFIG(thread) - { - // Reset wakeUp state: if wakeUp() was called at some point before - // this then processPostedEvents() below will service that call. - std::unique_lock lock(m_mutex); - m_wakeUpCalled = false; - } -#endif + bool didSendEvents = false; - processPostedEvents(); + didSendEvents |= sendPostedEvents(); + if (!isValidEventDispatcher()) + return false; - // The processPostedEvents() call above may process an event which deletes the - // application object and the event dispatcher; stop event processing in that case. - if (!isValidEventDispatcherPointer(this)) + didSendEvents |= sendNativeEvents(flags); + if (!isValidEventDispatcher()) + return false; + + didSendEvents |= sendTimerEvents(); + if (!isValidEventDispatcher()) return false; if (m_interrupted) { @@ -239,16 +175,68 @@ bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) } if (flags & QEventLoop::WaitForMoreEvents) - wait(); + processEventsWait(); - if (m_processTimers) { - m_processTimers = false; - processTimers(); - } + return didSendEvents; +} +bool QEventDispatcherWasm::sendNativeEvents(QEventLoop::ProcessEventsFlags flags) +{ + // TODO: support ExcludeUserInputEvents and ExcludeSocketNotifiers + + // Secondary threads do not support native events + if (!isMainThreadEventDispatcher()) + return false; + + // Can't suspend without asyncify + if (!useAsyncify()) + return false; + + // Send any pending events, and + bool didSendEvents = false; + didSendEvents|= g_mainThreadSuspendResumeControl->sendPendingEvents(); + + // if the processEvents() call is made from an exec() call then we assume + // that the main thread has just resumed, and that it will suspend again + // at the end of processEvents(). This makes the suspend loop below superfluous. + if (flags & QEventLoop::EventLoopExec) + return didSendEvents; + + // Run a suspend-resume loop until all pending native events have + // been processed. Suspending returns control to the browsers'event + // loop and makes it process events. If any event was for us then + // the wasm instance will resume (via event handling code in QWasmSuspendResumeControl + // and process the event. + // + // Set a zero-timer to exit the loop via the m_wakeFromSuspendTimer flag. + // This timer will be added to the end of the native event queue and + // ensures that all pending (at the time of this sendNativeEvents() call) + // native events are processed. + m_wakeFromSuspendTimer = false; + do { + m_suspendTimer->setTimeout(0ms); + g_mainThreadSuspendResumeControl->suspend(); + QScopedValueRollback scoped(m_isSendingNativeEvents, true); + didSendEvents |= g_mainThreadSuspendResumeControl->sendPendingEvents(); + } while (!m_wakeFromSuspendTimer); + + return didSendEvents; +} + +bool QEventDispatcherWasm::sendPostedEvents() +{ + QCoreApplication::sendPostedEvents(); return false; } +bool QEventDispatcherWasm::sendTimerEvents() +{ + int activatedTimers = m_timerInfo->activateTimers(); + if (activatedTimers > 0) + updateNativeTimer(); + return activatedTimers > 0; +} + void QEventDispatcherWasm::registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object) { #ifndef QT_NO_DEBUG @@ -332,181 +320,142 @@ void QEventDispatcherWasm::interrupt() } void QEventDispatcherWasm::wakeUp() -{ - // The event dispatcher thread may be blocked or suspended by - // wait(), or control may have been returned to the browser's - // event loop. Make sure the thread is unblocked or make it - // process events. - bool wasBlocked = wakeEventDispatcherThread(); - - if (!wasBlocked && isMainThreadEventDispatcher()) { - { - LOCK_GUARD(m_mutex); - if (m_pendingProcessEvents) - return; - m_pendingProcessEvents = true; - } - runOnMainThreadAsync([this](){ - QEventDispatcherWasm::callProcessPostedEvents(this); - }); - } -} - -void QEventDispatcherWasm::handleApplicationExec() -{ - // Start the main loop, and then stop it on the first callback. This - // is done for the "simulateInfiniteLoop" functionality where - // emscripten_set_main_loop() throws a JS exception which returns - // control to the browser while preserving the C++ stack. - // - // Note that we don't use asyncify here: Emscripten supports one level of - // asyncify only and we want to reserve that for dialog exec() instead of - // using it for the one qApp exec(). - // When JSPI is used, awaited async calls are allowed to be nested, so we - // proceed normally. - const bool simulateInfiniteLoop = true; - emscripten_set_main_loop([](){ - emscripten_pause_main_loop(); - }, 0, simulateInfiniteLoop); -} - -void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier) -{ - QWasmSocket::registerSocketNotifier(notifier); -} - -void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier) -{ - QWasmSocket::unregisterSocketNotifier(notifier); -} - -void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite, - bool *selectForRead, bool *selectForWrite, bool *socketDisconnect) -{ - QEventDispatcherWasm *eventDispatcher = static_cast( - QAbstractEventDispatcher::instance(QThread::currentThread())); - - if (!eventDispatcher) { - qWarning("QEventDispatcherWasm::socketSelect called without eventdispatcher instance"); - return; - } - - QWasmSocket::waitForSocketState(eventDispatcher, timeout, socket, waitForRead, waitForWrite, - selectForRead, selectForWrite, socketDisconnect); -} - - -void QEventDispatcherWasm::handleDialogExec() -{ - if (!useAsyncify()) { - qWarning() << "Warning: exec() is not supported on Qt for WebAssembly in this configuration. Please build" - << "with asyncify support, or use an asynchronous API like QDialog::open()"; - emscripten_sleep(1); // This call never returns - } - // For the asyncify case we do nothing here and wait for events in wait() -} - -// Blocks/suspends the calling thread. This is possible in two cases: -// - Caller is a secondary thread: block on m_moreEvents -// - Caller is the main thread and asyncify is enabled: suspend using qt_asyncify_suspend() -// Returns false if the wait timed out. -bool QEventDispatcherWasm::wait(int timeout) -{ -#if QT_CONFIG(thread) - using namespace std::chrono_literals; - Q_ASSERT(QThread::currentThread() == thread()); - - if (isSecondaryThreadEventDispatcher()) { - std::unique_lock lock(m_mutex); - - // If wakeUp() was called there might be pending events in the event - // queue which should be processed. Don't block, instead return - // so that the event loop can spin and call processEvents() again. - if (m_wakeUpCalled) - return true; - - auto wait_time = timeout > 0 ? timeout * 1ms : std::chrono::duration::max(); - bool wakeUpCalled = m_moreEvents.wait_for(lock, wait_time, [=] { return m_wakeUpCalled; }); - return wakeUpCalled; - } -#endif - Q_ASSERT(emscripten_is_main_runtime_thread()); - Q_ASSERT(isMainThreadEventDispatcher()); - if (useAsyncify()) { - if (timeout > 0) - qWarning() << "QEventDispatcherWasm asyncify wait with timeout is not supported; timeout will be ignored"; // FIXME - - bool didSuspend = qt_asyncify_suspend(); - if (!didSuspend) { - qWarning("QEventDispatcherWasm: current thread is already suspended; could not asyncify wait for events"); - return false; - } - - return true; - } else { - qWarning("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify"); - Q_UNUSED(timeout); - } - return false; -} - -// Wakes a blocked/suspended event dispatcher thread. Returns true if the -// thread is unblocked or was resumed, false if the thread state could not -// be determined. -bool QEventDispatcherWasm::wakeEventDispatcherThread() { #if QT_CONFIG(thread) if (isSecondaryThreadEventDispatcher()) { std::lock_guard lock(m_mutex); m_wakeUpCalled = true; m_moreEvents.notify_one(); - return true; - } + } else #endif - Q_ASSERT(isMainThreadEventDispatcher()); - if (!g_is_asyncify_suspended) - return false; - runOnMainThread([]() { qt_asyncify_resume(); }); + { + QEventDispatcherWasm *eventDispatcher = this; + qwasmglobal::runOnMainThreadAsync([eventDispatcher]() { + if (isValidEventDispatcherPointer(eventDispatcher)) { + if (!eventDispatcher->m_wakeupTimer->hasTimeout()) + eventDispatcher->m_wakeupTimer->setTimeout(0ms); + } + }); + } +} + +void QEventDispatcherWasm::handleNonAsyncifyErrorCases(QEventLoop::ProcessEventsFlags flags) +{ + Q_ASSERT(!useAsyncify()); + + if (flags & QEventLoop::ApplicationExec) { + // Start the main loop, and then stop it on the first callback. This + // is done for the "simulateInfiniteLoop" functionality where + // emscripten_set_main_loop() throws a JS exception which returns + // control to the browser while preserving the C++ stack. + const bool simulateInfiniteLoop = true; + emscripten_set_main_loop([](){ + emscripten_pause_main_loop(); + }, 0, simulateInfiniteLoop); + } else if (flags & QEventLoop::DialogExec) { + qFatal() << "Calling exec() is not supported on Qt for WebAssembly in this configuration. Please build" + << "with asyncify support, or use an asynchronous API like QDialog::open()"; + } else if (flags & QEventLoop::WaitForMoreEvents) { + qFatal("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify"); + } +} + +// Blocks or suspends the current thread for the given amount of time. +// The event dispatcher does not process events while blocked. TODO: +// make it not process events while blocked. +bool QEventDispatcherWasm::wait(int timeout) +{ + auto tim = timeout > 0 ? std::optional(timeout) : std::nullopt; + if (isSecondaryThreadEventDispatcher()) + return secondaryThreadWait(tim); + if (useAsyncify()) + asyncifyWait(tim); return true; } -// Process event activation callbacks for the main thread event dispatcher. -// Must be called on the main thread. -void QEventDispatcherWasm::callProcessPostedEvents(void *context) +// Waits for more events by blocking or suspending the current thread. Should be called from +// processEvents() only. +void QEventDispatcherWasm::processEventsWait() +{ + if (isMainThreadEventDispatcher()) { + asyncifyWait(std::nullopt); + } else { + auto nanoWait = m_timerInfo->timerWait(); + std::optional milliWait; + if (nanoWait.has_value()) + milliWait = std::chrono::duration_cast(*nanoWait); + secondaryThreadWait(milliWait); + } +} + +void QEventDispatcherWasm::asyncifyWait(std::optional timeout) { Q_ASSERT(emscripten_is_main_runtime_thread()); + Q_ASSERT(isMainThreadEventDispatcher()); + Q_ASSERT(useAsyncify()); + if (timeout.has_value()) + m_suspendTimer->setTimeout(timeout.value()); + g_mainThreadSuspendResumeControl->suspend(); +} - // Bail out if Qt has been shut down. +bool QEventDispatcherWasm::secondaryThreadWait(std::optional timeout) +{ +#if QT_CONFIG(thread) + Q_ASSERT(QThread::currentThread() == thread()); + using namespace std::chrono_literals; + std::unique_lock lock(m_mutex); + + // If wakeUp() was called there might be pending events in the event + // queue which should be processed. Don't block, instead return + // so that the event loop can spin and call processEvents() again. + if (m_wakeUpCalled) { + m_wakeUpCalled = false; + return true; + } + + auto waitTime = timeout.value_or(std::chrono::milliseconds::max()); + bool wakeUpCalled = m_moreEvents.wait_for(lock, waitTime, [this] { return m_wakeUpCalled; }); + m_wakeUpCalled = false; + return wakeUpCalled; +#else + Q_UNREACHABLE(); + return false; +#endif +} + +void QEventDispatcherWasm::onTimer() +{ + Q_ASSERT(emscripten_is_main_runtime_thread()); + if (!g_mainThreadEventDispatcher) + return; + g_mainThreadEventDispatcher->sendTimerEvents(); +} + +void QEventDispatcherWasm::onWakeup() +{ + Q_ASSERT(emscripten_is_main_runtime_thread()); if (!g_mainThreadEventDispatcher) return; - // In the unlikely event that we get a callProcessPostedEvents() call for - // a previous main thread event dispatcher (i.e. the QApplication - // object was deleted and created again): just ignore it and return. - if (context != g_mainThreadEventDispatcher) + // In the case where we are suspending from sendNativeEvents() we don't want + // to call processEvents() again, since we are then already in processEvents() + // and are already awake. + if (g_mainThreadEventDispatcher->m_isSendingNativeEvents) return; - { - LOCK_GUARD(g_mainThreadEventDispatcher->m_mutex); - g_mainThreadEventDispatcher->m_pendingProcessEvents = false; - } - - g_mainThreadEventDispatcher->processPostedEvents(); + g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents); } -bool QEventDispatcherWasm::processPostedEvents() +void QEventDispatcherWasm::onProcessNativeEventsResume() { - QCoreApplication::sendPostedEvents(); - return false; + Q_ASSERT(emscripten_is_main_runtime_thread()); + if (!g_mainThreadEventDispatcher) + return; + g_mainThreadEventDispatcher->m_wakeFromSuspendTimer = true; } -void QEventDispatcherWasm::processTimers() -{ - m_timerInfo->activateTimers(); - updateNativeTimer(); // schedule next native timer, if any -} - -// Updates the native timer based on currently registered Qt timers. +// Updates the native timer based on currently registered Qt timers, +// by setting a timeout equivalent to the shortest timer. // Must be called on the event dispatcher thread. void QEventDispatcherWasm::updateNativeTimer() { @@ -514,84 +463,33 @@ void QEventDispatcherWasm::updateNativeTimer() Q_ASSERT(QThread::currentThread() == thread()); #endif - // Multiplex Qt timers down to a single native timer, maintained - // to have a timeout corresponding to the shortest Qt timer. This - // is done in two steps: first determine the target wakeup time - // on the event dispatcher thread (since this thread has exclusive - // access to m_timerInfo), and then call native API to set the new - // wakeup time on the main thread. + // On secondary threads, the timeout is managed by setting the WaitForMoreEvents + // timeout in processEventsWait(). + if (!isMainThreadEventDispatcher()) + return; - const std::optional wait = m_timerInfo->timerWait(); - const auto toWaitDuration = duration_cast(wait.value_or(0ms)); - const auto newTargetTimePoint = m_timerInfo->currentTime + toWaitDuration; - auto epochNsecs = newTargetTimePoint.time_since_epoch(); - auto newTargetTime = std::chrono::duration_cast(epochNsecs); - auto maintainNativeTimer = [this, wait, toWaitDuration, newTargetTime]() { - Q_ASSERT(emscripten_is_main_runtime_thread()); - - if (!wait) { - if (m_timerId > 0) { - emscripten_clear_timeout(m_timerId); - m_timerId = 0; - m_timerTargetTime = 0ms; - } - return; - } - - if (m_timerTargetTime != 0ms && newTargetTime >= m_timerTargetTime) - return; // existing timer is good - - qCDebug(lcEventDispatcherTimers) - << "Created new native timer with wait" << toWaitDuration.count() << "ms" - << "timeout" << newTargetTime.count() << "ms"; - emscripten_clear_timeout(m_timerId); - m_timerId = emscripten_set_timeout(&QEventDispatcherWasm::callProcessTimers, - toWaitDuration.count(), this); - m_timerTargetTime = newTargetTime; - }; - - // Update the native timer for this thread/dispatcher. This must be - // done on the main thread where we have access to native API. - runOnMainThread([this, maintainNativeTimer]() { - Q_ASSERT(emscripten_is_main_runtime_thread()); - - // "this" may have been deleted, or may be about to be deleted. - // Check if the pointer we have is still a valid event dispatcher, - // and keep the mutex locked while updating the native timer to - // prevent it from being deleted. - LOCK_GUARD(g_staticDataMutex); - if (isValidEventDispatcherPointer(this)) - maintainNativeTimer(); - }); -} - -// Static timer activation callback. Must be called on the main thread -// and will then either process timers on the main thread or wake and -// process timers on a secondary thread. -void QEventDispatcherWasm::callProcessTimers(void *context) -{ - Q_ASSERT(emscripten_is_main_runtime_thread()); - - // Note: "context" may be a stale pointer here, - // take care before casting and dereferencing! - - // Process timers on this thread if this is the main event dispatcher - if (reinterpret_cast(context) == g_mainThreadEventDispatcher) { - g_mainThreadEventDispatcher->m_timerTargetTime = 0ms; - g_mainThreadEventDispatcher->processTimers(); + // Clear any timer if there are no active timers + const std::optional nanoWait = m_timerInfo->timerWait(); + if (!nanoWait.has_value()) { + m_nativeTimer->clearTimeout(); return; } - // Wake and process timers on the secondary thread if this a secondary thread dispatcher -#if QT_CONFIG(thread) - std::lock_guard lock(g_staticDataMutex); - if (g_secondaryThreadEventDispatchers.contains(context)) { - QEventDispatcherWasm *eventDispatcher = reinterpret_cast(context); - eventDispatcher->m_timerTargetTime = 0ms; - eventDispatcher->m_processTimers = true; - eventDispatcher->wakeUp(); - } -#endif + auto milliWait = std::chrono::duration_cast(*nanoWait); + const auto newTargetTime = m_timerInfo->currentTime + milliWait; + + // Keep existing timer if the timeout has not changed. + if (m_nativeTimer->hasTimeout() && newTargetTime == m_timerTargetTime) + return; + + // Clear current and set new timer + qCDebug(lcEventDispatcherTimers) + << "Created new native timer timeout" << milliWait.count() << "ms" + << "previous target time" << m_timerTargetTime.time_since_epoch() + << "new target time" << newTargetTime.time_since_epoch(); + m_nativeTimer->clearTimeout(); + m_nativeTimer->setTimeout(milliWait); + m_timerTargetTime = newTargetTime; } namespace { @@ -635,54 +533,29 @@ void QEventDispatcherWasm::onLoaded() // have valid geometry at startup. } -namespace { - void trampoline(void *context) { - - auto async_fn = [](void *context){ - std::function *fn = reinterpret_cast *>(context); - (*fn)(); - delete fn; - }; - - emscripten_async_call(async_fn, context, 0); - } +void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier) +{ + QWasmSocket::registerSocketNotifier(notifier); } -// Runs a function right away -void QEventDispatcherWasm::run(std::function fn) +void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier) { - fn(); + QWasmSocket::unregisterSocketNotifier(notifier); } -void QEventDispatcherWasm::runOnMainThread(std::function fn) +void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite, + bool *selectForRead, bool *selectForWrite, bool *socketDisconnect) { -#if QT_CONFIG(thread) - qstdweb::runTaskOnMainThread(fn, &g_proxyingQueue); -#else - qstdweb::runTaskOnMainThread(fn); -#endif -} + QEventDispatcherWasm *eventDispatcher = static_cast( + QAbstractEventDispatcher::instance(QThread::currentThread())); -// Runs a function asynchronously. Main thread only. -void QEventDispatcherWasm::runAsync(std::function fn) -{ - trampoline(new std::function(fn)); -} - -// Runs a function on the main thread. The function always runs asynchronously, -// also if the calling thread is the main thread. -void QEventDispatcherWasm::runOnMainThreadAsync(std::function fn) -{ - void *context = new std::function(fn); -#if QT_CONFIG(thread) - if (!emscripten_is_main_runtime_thread()) { - g_proxyingQueue.proxyAsync(g_mainThread, [context]{ - trampoline(context); - }); + if (!eventDispatcher) { + qWarning("QEventDispatcherWasm::socketSelect called without eventdispatcher instance"); return; } -#endif - trampoline(context); + + QWasmSocket::waitForSocketState(eventDispatcher, timeout, socket, waitForRead, waitForWrite, + selectForRead, selectForWrite, socketDisconnect); } QT_END_NAMESPACE diff --git a/src/corelib/kernel/qeventdispatcher_wasm_p.h b/src/corelib/kernel/qeventdispatcher_wasm_p.h index 76abcae69d2..f897cac41a9 100644 --- a/src/corelib/kernel/qeventdispatcher_wasm_p.h +++ b/src/corelib/kernel/qeventdispatcher_wasm_p.h @@ -17,6 +17,7 @@ #include "qabstracteventdispatcher.h" #include "private/qtimerinfo_unix_p.h" +#include "private/qwasmsuspendresumecontrol_p.h" #include #include @@ -24,8 +25,7 @@ #include #include #include - -#include +#include QT_BEGIN_NAMESPACE @@ -36,7 +36,7 @@ class Q_CORE_EXPORT QEventDispatcherWasm : public QAbstractEventDispatcherV2 { Q_OBJECT public: - QEventDispatcherWasm(); + QEventDispatcherWasm(std::shared_ptr suspendResume = std::shared_ptr()); ~QEventDispatcherWasm(); bool processEvents(QEventLoop::ProcessEventsFlags flags) override; @@ -56,44 +56,49 @@ public: static void socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite, bool *selectForRead, bool *selectForWrite, bool *socketDisconnect); - static void runOnMainThread(std::function fn); - static void registerStartupTask(); static void completeStarupTask(); static void callOnLoadedIfRequired(); virtual void onLoaded(); + static void onTimer(); + static void onWakeup(); + static void onProcessNativeEventsResume(); protected: - virtual bool processPostedEvents(); + virtual bool sendPostedEvents(); private: bool isMainThreadEventDispatcher(); bool isSecondaryThreadEventDispatcher(); + bool isValidEventDispatcher(); static bool isValidEventDispatcherPointer(QEventDispatcherWasm *eventDispatcher); - void handleApplicationExec(); - void handleDialogExec(); - bool wait(int timeout = -1); - bool wakeEventDispatcherThread(); - static void callProcessPostedEvents(void *eventDispatcher); + bool sendTimerEvents(); + bool sendNativeEvents(QEventLoop::ProcessEventsFlags flags); + + void handleNonAsyncifyErrorCases(QEventLoop::ProcessEventsFlags flags); + + bool wait(int timeout); + void processEventsWait(); + void asyncifyWait(std::optional timeout); + bool secondaryThreadWait(std::optional timeout); - void processTimers(); void updateNativeTimer(); - static void callProcessTimers(void *eventDispatcher); - - static void run(std::function fn); - static void runAsync(std::function fn); - static void runOnMainThreadAsync(std::function fn); static QEventDispatcherWasm *g_mainThreadEventDispatcher; + static std::shared_ptr g_mainThreadSuspendResumeControl; bool m_interrupted = false; - bool m_processTimers = false; - bool m_pendingProcessEvents = false; - QTimerInfoList *m_timerInfo = new QTimerInfoList(); - long m_timerId = 0; - std::chrono::milliseconds m_timerTargetTime{}; + std::unique_ptr m_timerInfo; + std::chrono::time_point m_timerTargetTime; + + std::unique_ptr m_nativeTimer; + std::unique_ptr m_wakeupTimer; + std::unique_ptr m_suspendTimer; + + bool m_wakeFromSuspendTimer = false; + bool m_isSendingNativeEvents = false; #if QT_CONFIG(thread) std::mutex m_mutex; @@ -102,8 +107,6 @@ private: static QVector g_secondaryThreadEventDispatchers; static std::mutex g_staticDataMutex; - static emscripten::ProxyingQueue g_proxyingQueue; - static pthread_t g_mainThread; // Note on mutex usage: the global g_staticDataMutex protects the global (g_ prefixed) data, // while the per eventdispatcher m_mutex protects the state accociated with blocking and waking diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 970e717212d..406243a33e9 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -9,8 +9,9 @@ #include #include -#include #include +#include +#include #include #include @@ -22,6 +23,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::Literals::StringLiterals; +using emscripten::val; namespace qstdweb { @@ -717,45 +719,11 @@ emscripten::val Uint8Array::constructor_() return emscripten::val::global("Uint8Array"); } -class EventListener { -public: - EventListener(uintptr_t handler) - :m_handler(handler) - { - - } - - // Special function - addEventListender() allows adding an object with a - // handleEvent() function which eceives the event. - void handleEvent(emscripten::val event) { - auto handlerPtr = reinterpret_cast *>(m_handler); - (*handlerPtr)(event); - } - - uintptr_t m_handler; -}; - -// Registers a callback function for a named event on the given element. The event -// name must be the name as returned by the Event.type property: e.g. "load", "error". -EventCallback::~EventCallback() +EventCallback::EventCallback(emscripten::val element, const std::string &name, + const std::function &fn) + :QWasmEventHandler(element, name, fn) { - m_element.call("removeEventListener", m_eventName, m_eventListener); -} -EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function &handler) - :m_element(element) - ,m_eventName(name) - ,m_handler(std::make_unique>(handler)) -{ - uintptr_t handlerUint = reinterpret_cast(m_handler.get()); // FIXME: pass pointer directly instead - m_eventListener = emscripten::val::module_property("QtEventListener").new_(handlerUint); - m_element.call("addEventListener", m_eventName, m_eventListener); -} - -EMSCRIPTEN_BINDINGS(qtStdwebCalback) { - emscripten::class_("QtEventListener") - .constructor() - .function("handleEvent", &EventListener::handleEvent); } namespace Promise { diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index d34b86bce73..95a77560372 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -19,6 +19,7 @@ #include #include "QtCore/qhash.h" #include "QtCore/qiodevice.h" +#include "QtCore/private/qwasmsuspendresumecontrol_p.h" #include @@ -196,21 +197,15 @@ namespace qstdweb { emscripten::val m_uint8Array = emscripten::val::undefined(); }; - class Q_CORE_EXPORT EventCallback + // EventCallback here for source compatibility; prefer using QWasmEventHandler directly + class Q_CORE_EXPORT EventCallback : public QWasmEventHandler { public: EventCallback() = default; - ~EventCallback(); EventCallback(EventCallback const&) = delete; EventCallback& operator=(EventCallback const&) = delete; EventCallback(emscripten::val element, const std::string &name, const std::function &fn); - - private: - emscripten::val m_element = emscripten::val::undefined(); - std::string m_eventName; - std::unique_ptr> m_handler; - emscripten::val m_eventListener = emscripten::val::undefined(); }; struct PromiseCallbacks diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index db1fed65d6c..99cf8885af5 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -5,6 +5,7 @@ #include "qwasmwindow.h" #include +#include #include @@ -14,7 +15,13 @@ using namespace emscripten; bool QWasmCompositor::m_requestUpdateHoldEnabled = false; -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) +: QObject(screen) +, m_animationFrameHandler(QWasmAnimationFrameHandler([this](double frametime){ + Q_UNUSED(frametime); + this->m_requestAnimationFrameId = -1; + this->deliverUpdateRequests(); + })) { QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } @@ -22,7 +29,7 @@ QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) QWasmCompositor::~QWasmCompositor() { if (m_requestAnimationFrameId != -1) - emscripten_cancel_animation_frame(m_requestAnimationFrameId); + m_animationFrameHandler.cancelAnimationFrame(m_requestAnimationFrameId); // TODO(mikolaj.boc): Investigate if m_isEnabled is needed at all. It seems like a frame should // not be generated after this instead. @@ -86,17 +93,7 @@ void QWasmCompositor::requestUpdate() if (m_requestUpdateHoldEnabled) return; - static auto frame = [](double frameTime, void *context) -> EM_BOOL { - Q_UNUSED(frameTime); - - QWasmCompositor *compositor = reinterpret_cast(context); - - compositor->m_requestAnimationFrameId = -1; - compositor->deliverUpdateRequests(); - - return EM_FALSE; - }; - m_requestAnimationFrameId = emscripten_request_animation_frame(frame, this); + m_requestAnimationFrameId = m_animationFrameHandler.requestAnimationFrame(); } void QWasmCompositor::deliverUpdateRequests() @@ -168,3 +165,28 @@ QWasmScreen *QWasmCompositor::screen() { return static_cast(parent()); } + +QWasmAnimationFrameHandler::QWasmAnimationFrameHandler(std::function handler) +{ + auto argCastWrapper = [handler](val arg){ handler(arg.as()); }; + m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(argCastWrapper); +} + +QWasmAnimationFrameHandler::~QWasmAnimationFrameHandler() +{ + QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex); +} + +int64_t QWasmAnimationFrameHandler::requestAnimationFrame() +{ + using ReturnType = double; // FIXME emscripten::val::call() does not support int64_t + val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex); + return int64_t(val::global("window").call("requestAnimationFrame", handler)); +} + +void QWasmAnimationFrameHandler::cancelAnimationFrame(int64_t id) +{ + val::global("window").call("cancelAnimationFrame", double(id)); +} + + diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index 0f24a6690e7..8fc290dda3b 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -11,6 +11,8 @@ #include #include +#include + QT_BEGIN_NAMESPACE class QWasmWindow; @@ -18,6 +20,18 @@ class QWasmScreen; enum class QWasmWindowTreeNodeChangeType; +class QWasmAnimationFrameHandler +{ +public: + QWasmAnimationFrameHandler(std::function handler); + ~QWasmAnimationFrameHandler(); + int64_t requestAnimationFrame(); + void cancelAnimationFrame(int64_t id); + +private: + uint32_t m_handlerIndex; +}; + class QWasmCompositor final : public QObject { Q_OBJECT @@ -51,7 +65,8 @@ private: bool m_isEnabled = true; QMap> m_requestUpdateWindows; - int m_requestAnimationFrameId = -1; + QWasmAnimationFrameHandler m_animationFrameHandler; + int64_t m_requestAnimationFrameId = -1; bool m_inDeliverUpdateRequest = false; static bool m_requestUpdateHoldEnabled; }; diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp index 1f2d3095d65..af549c6664c 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp @@ -8,11 +8,17 @@ QT_BEGIN_NAMESPACE +QWasmEventDispatcher::QWasmEventDispatcher(std::shared_ptr suspendResume) + :QEventDispatcherWasm(suspendResume) +{ + +} + // Note: All event dispatcher functionality is implemented in QEventDispatcherWasm // in QtCore, except for processPostedEvents() below which uses API from QtGui. -bool QWasmEventDispatcher::processPostedEvents() +bool QWasmEventDispatcher::sendPostedEvents() { - QEventDispatcherWasm::processPostedEvents(); + QEventDispatcherWasm::sendPostedEvents(); return QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents); } diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.h b/src/plugins/platforms/wasm/qwasmeventdispatcher.h index cbf10482e3f..d3f342a621e 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.h +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.h @@ -6,12 +6,19 @@ #include +#include + QT_BEGIN_NAMESPACE +class QWasmSuspendResumeControl; + class QWasmEventDispatcher : public QEventDispatcherWasm { +public: + QWasmEventDispatcher(std::shared_ptr suspendResume); + protected: - bool processPostedEvents() override; + bool sendPostedEvents() override; void onLoaded() override; }; diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index 173131ad84a..2798b06f2bd 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "private/qwasmsuspendresumecontrol_p.h" #include #include @@ -94,6 +95,7 @@ QWasmIntegration::QWasmIntegration() #if QT_CONFIG(accessibility) , m_accessibility(new QWasmAccessibility) #endif + , m_suspendResume(std::make_shared()) // create early in order to register event handlers at startup { s_instance = this; @@ -264,7 +266,7 @@ QPlatformFontDatabase *QWasmIntegration::fontDatabase() const QAbstractEventDispatcher *QWasmIntegration::createEventDispatcher() const { - return new QWasmEventDispatcher; + return new QWasmEventDispatcher(m_suspendResume); } QVariant QWasmIntegration::styleHint(QPlatformIntegration::StyleHint hint) const diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index ae281d90557..50bed742c70 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -20,6 +20,8 @@ #include #include +#include + QT_BEGIN_NAMESPACE class QWasmEventTranslator; @@ -33,6 +35,7 @@ class QWasmClipboard; class QWasmAccessibility; class QWasmServices; class QWasmDrag; +class QWasmSuspendResumeControl; class QWasmIntegration : public QObject, public QPlatformIntegration { @@ -104,6 +107,7 @@ private: static QWasmIntegration *s_instance; QWasmInputContext *m_wasmInputContext = nullptr; + std::shared_ptr m_suspendResume; #if QT_CONFIG(draganddrop) std::unique_ptr m_drag;