wasm: improve asyncify support

Rewrite the event dispatcher to use QWasmSuspendResumeControl
for asyncify suspend/resume.

This includes using the following helper classes which provides
additional functionality on top of QWasmSuspendResumeControl.

  - QWasmTimer: manages native timers on the main thread
  - QWasmEventHandler and qstdweb::EventCallback: input events
  - QWasmAnimationFrameHandler: animation frame events

Initialization differs slightly, depending on if QtGui and
the QPA machinery is in use, or of the app is a QtCore only
application. In the former case, QWasmSuspendResumeControl is
created early by QWasmIntegration in order to support registering
event handlers at startup, before the event dispatcher has
been created.

processEvents() now actually processes native events. This is
done by running a suspend-resume loop until the native event
queue has been exhausted. If WaitForMoreEvents is specified then
processEvents() will, in addition, also suspend and wait for
additional native events.

Timers on secondary threads are now managed by modifying the
wait condition timeout, instead of proxying timers to the main
thread. In effect secondary threads will now sleep until the
next timer should fire, and then wake up and process that timer.

Change-Id: I20e8afb6b67c64a7c52dbd89e9c50ffadba39594
Reviewed-by: Jøger Hansegård <joger.hansegard@qt.io>
This commit is contained in:
Morten Sørvig 2024-12-02 15:15:43 +01:00
parent 9df167a013
commit 89d5b96b45
10 changed files with 362 additions and 467 deletions

View File

@ -3,23 +3,20 @@
#include "qeventdispatcher_wasm_p.h"
#include <QtCore/private/qabstracteventdispatcher_p.h> // for qGlobalPostedEventsCount()
#include <QtCore/qcoreapplication.h>
#include <QtCore/qthread.h>
#include <QtCore/qscopedvaluerollback.h>
#include <QtCore/private/qobject_p.h>
#include <QtCore/private/qwasmglobal_p.h>
#include <QtCore/private/qstdweb_p.h>
#include <QtCore/private/qwasmsocket_p.h>
#include "emscripten.h"
#include <emscripten/html5.h>
#include <emscripten/threading.h>
#include <emscripten/val.h>
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<QWasmSuspendResumeControl> QEventDispatcherWasm::g_mainThreadSuspendResumeControl;
#if QT_CONFIG(thread)
Q_CONSTINIT QVector<QEventDispatcherWasm *> 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<QWasmSuspendResumeControl> 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<QWasmSuspendResumeControl>();
}
// Zero-timer used on wake() calls
m_wakeupTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl.get(), [](){ onWakeup(); });
// Timer set to fire at the next Qt timer timeout
m_nativeTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl.get(), []() { onTimer(); });
// Timer used when suspending to process native events
m_suspendTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl.get(), []() { onProcessNativeEventsResume(); });
} else {
#if QT_CONFIG(thread)
std::lock_guard<std::mutex> lock(g_staticDataMutex);
g_secondaryThreadEventDispatchers.append(this);
#endif
}
m_timerInfo = std::make_unique<QTimerInfoList>();
}
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<std::mutex> 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<QEventDispatcherWasm *>(
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<std::mutex> 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<int, std::micro>::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<std::mutex> 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<std::chrono::milliseconds>(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<std::chrono::milliseconds> milliWait;
if (nanoWait.has_value())
milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
secondaryThreadWait(milliWait);
}
}
void QEventDispatcherWasm::asyncifyWait(std::optional<std::chrono::milliseconds> 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<std::chrono::milliseconds> timeout)
{
#if QT_CONFIG(thread)
Q_ASSERT(QThread::currentThread() == thread());
using namespace std::chrono_literals;
std::unique_lock<std::mutex> 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<std::chrono::nanoseconds> wait = m_timerInfo->timerWait();
const auto toWaitDuration = duration_cast<milliseconds>(wait.value_or(0ms));
const auto newTargetTimePoint = m_timerInfo->currentTime + toWaitDuration;
auto epochNsecs = newTargetTimePoint.time_since_epoch();
auto newTargetTime = std::chrono::duration_cast<std::chrono::milliseconds>(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<QEventDispatcherWasm *>(context) == g_mainThreadEventDispatcher) {
g_mainThreadEventDispatcher->m_timerTargetTime = 0ms;
g_mainThreadEventDispatcher->processTimers();
// Clear any timer if there are no active timers
const std::optional<std::chrono::nanoseconds> 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<std::mutex> lock(g_staticDataMutex);
if (g_secondaryThreadEventDispatchers.contains(context)) {
QEventDispatcherWasm *eventDispatcher = reinterpret_cast<QEventDispatcherWasm *>(context);
eventDispatcher->m_timerTargetTime = 0ms;
eventDispatcher->m_processTimers = true;
eventDispatcher->wakeUp();
}
#endif
auto milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*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<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(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<void(void)> fn)
void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier)
{
fn();
QWasmSocket::unregisterSocketNotifier(notifier);
}
void QEventDispatcherWasm::runOnMainThread(std::function<void(void)> fn)
void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite,
bool *selectForRead, bool *selectForWrite, bool *socketDisconnect)
{
#if QT_CONFIG(thread)
qstdweb::runTaskOnMainThread<void>(fn, &g_proxyingQueue);
#else
qstdweb::runTaskOnMainThread<void>(fn);
#endif
}
QEventDispatcherWasm *eventDispatcher = static_cast<QEventDispatcherWasm *>(
QAbstractEventDispatcher::instance(QThread::currentThread()));
// Runs a function asynchronously. Main thread only.
void QEventDispatcherWasm::runAsync(std::function<void(void)> fn)
{
trampoline(new std::function<void(void)>(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<void(void)> fn)
{
void *context = new std::function<void(void)>(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

View File

@ -17,6 +17,7 @@
#include "qabstracteventdispatcher.h"
#include "private/qtimerinfo_unix_p.h"
#include "private/qwasmsuspendresumecontrol_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtCore/qwaitcondition.h>
@ -24,8 +25,7 @@
#include <mutex>
#include <optional>
#include <tuple>
#include <emscripten/proxying.h>
#include <memory>
QT_BEGIN_NAMESPACE
@ -36,7 +36,7 @@ class Q_CORE_EXPORT QEventDispatcherWasm : public QAbstractEventDispatcherV2
{
Q_OBJECT
public:
QEventDispatcherWasm();
QEventDispatcherWasm(std::shared_ptr<QWasmSuspendResumeControl> suspendResume = std::shared_ptr<QWasmSuspendResumeControl>());
~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<void(void)> 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<std::chrono::milliseconds> timeout);
bool secondaryThreadWait(std::optional<std::chrono::milliseconds> timeout);
void processTimers();
void updateNativeTimer();
static void callProcessTimers(void *eventDispatcher);
static void run(std::function<void(void)> fn);
static void runAsync(std::function<void(void)> fn);
static void runOnMainThreadAsync(std::function<void(void)> fn);
static QEventDispatcherWasm *g_mainThreadEventDispatcher;
static std::shared_ptr<QWasmSuspendResumeControl> 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<QTimerInfoList> m_timerInfo;
std::chrono::time_point<std::chrono::steady_clock> m_timerTargetTime;
std::unique_ptr<QWasmTimer> m_nativeTimer;
std::unique_ptr<QWasmTimer> m_wakeupTimer;
std::unique_ptr<QWasmTimer> 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<QEventDispatcherWasm *> 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

View File

@ -9,8 +9,9 @@
#include <QtCore/qfile.h>
#include <QtCore/qmimedata.h>
#include <emscripten/bind.h>
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <emscripten/html5.h>
#include <emscripten/threading.h>
@ -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<std::function<void(emscripten::val)> *>(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<void(emscripten::val)> &fn)
:QWasmEventHandler(element, name, fn)
{
m_element.call<void>("removeEventListener", m_eventName, m_eventListener);
}
EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function<void(emscripten::val)> &handler)
:m_element(element)
,m_eventName(name)
,m_handler(std::make_unique<std::function<void(emscripten::val)>>(handler))
{
uintptr_t handlerUint = reinterpret_cast<uintptr_t>(m_handler.get()); // FIXME: pass pointer directly instead
m_eventListener = emscripten::val::module_property("QtEventListener").new_(handlerUint);
m_element.call<void>("addEventListener", m_eventName, m_eventListener);
}
EMSCRIPTEN_BINDINGS(qtStdwebCalback) {
emscripten::class_<EventListener>("QtEventListener")
.constructor<uintptr_t>()
.function("handleEvent", &EventListener::handleEvent);
}
namespace Promise {

View File

@ -19,6 +19,7 @@
#include <QtCore/qglobal.h>
#include "QtCore/qhash.h"
#include "QtCore/qiodevice.h"
#include "QtCore/private/qwasmsuspendresumecontrol_p.h"
#include <emscripten/val.h>
@ -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<void(emscripten::val)> &fn);
private:
emscripten::val m_element = emscripten::val::undefined();
std::string m_eventName;
std::unique_ptr<std::function<void(emscripten::val)>> m_handler;
emscripten::val m_eventListener = emscripten::val::undefined();
};
struct PromiseCallbacks

View File

@ -5,6 +5,7 @@
#include "qwasmwindow.h"
#include <private/qeventdispatcher_wasm_p.h>
#include <private/qwasmsuspendresumecontrol_p.h>
#include <qpa/qwindowsysteminterface.h>
@ -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<QWasmCompositor *>(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<QWasmScreen *>(parent());
}
QWasmAnimationFrameHandler::QWasmAnimationFrameHandler(std::function<void(double)> handler)
{
auto argCastWrapper = [handler](val arg){ handler(arg.as<double>()); };
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<ReturnType>("requestAnimationFrame", handler));
}
void QWasmAnimationFrameHandler::cancelAnimationFrame(int64_t id)
{
val::global("window").call<void>("cancelAnimationFrame", double(id));
}

View File

@ -11,6 +11,8 @@
#include <QMap>
#include <tuple>
#include <emscripten/val.h>
QT_BEGIN_NAMESPACE
class QWasmWindow;
@ -18,6 +20,18 @@ class QWasmScreen;
enum class QWasmWindowTreeNodeChangeType;
class QWasmAnimationFrameHandler
{
public:
QWasmAnimationFrameHandler(std::function<void(double)> 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<QWasmWindow *, std::tuple<QRect, UpdateRequestDeliveryType>> m_requestUpdateWindows;
int m_requestAnimationFrameId = -1;
QWasmAnimationFrameHandler m_animationFrameHandler;
int64_t m_requestAnimationFrameId = -1;
bool m_inDeliverUpdateRequest = false;
static bool m_requestUpdateHoldEnabled;
};

View File

@ -8,11 +8,17 @@
QT_BEGIN_NAMESPACE
QWasmEventDispatcher::QWasmEventDispatcher(std::shared_ptr<QWasmSuspendResumeControl> 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);
}

View File

@ -6,12 +6,19 @@
#include <QtCore/private/qeventdispatcher_wasm_p.h>
#include <memory>
QT_BEGIN_NAMESPACE
class QWasmSuspendResumeControl;
class QWasmEventDispatcher : public QEventDispatcherWasm
{
public:
QWasmEventDispatcher(std::shared_ptr<QWasmSuspendResumeControl> suspendResume);
protected:
bool processPostedEvents() override;
bool sendPostedEvents() override;
void onLoaded() override;
};

View File

@ -22,6 +22,7 @@
#include <QtCore/qcoreapplication.h>
#include <qpa/qplatforminputcontextfactory_p.h>
#include <qpa/qwindowsysteminterface_p.h>
#include "private/qwasmsuspendresumecontrol_p.h"
#include <emscripten/bind.h>
#include <emscripten/val.h>
@ -94,6 +95,7 @@ QWasmIntegration::QWasmIntegration()
#if QT_CONFIG(accessibility)
, m_accessibility(new QWasmAccessibility)
#endif
, m_suspendResume(std::make_shared<QWasmSuspendResumeControl>()) // 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

View File

@ -20,6 +20,8 @@
#include <emscripten/html5.h>
#include <emscripten/val.h>
#include <memory>
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<QWasmSuspendResumeControl> m_suspendResume;
#if QT_CONFIG(draganddrop)
std::unique_ptr<QWasmDrag> m_drag;