From 6e5f6b885866d8c398d6bd62b543e498d1789630 Mon Sep 17 00:00:00 2001 From: Squareys Date: Wed, 29 May 2019 13:45:53 +0200 Subject: [PATCH] Platform: light-weight main loop for EmscriptenApplication Signed-off-by: Squareys --- src/Magnum/Platform/EmscriptenApplication.cpp | 84 +++++++++++++++-- src/Magnum/Platform/EmscriptenApplication.h | 89 +++++++++++++++---- src/Magnum/Platform/Sdl2Application.h | 5 ++ .../Test/EmscriptenApplicationTest.cpp | 10 +++ 4 files changed, 163 insertions(+), 25 deletions(-) diff --git a/src/Magnum/Platform/EmscriptenApplication.cpp b/src/Magnum/Platform/EmscriptenApplication.cpp index 72f2ed4f2..ffa455a01 100644 --- a/src/Magnum/Platform/EmscriptenApplication.cpp +++ b/src/Magnum/Platform/EmscriptenApplication.cpp @@ -265,6 +265,7 @@ bool EmscriptenApplication::tryCreate(const Configuration& configuration) { } setupCallbacks(!!(configuration.windowFlags() & Configuration::WindowFlag::Resizable)); + setupAnimationFrame(!!(configuration.windowFlags() & Configuration::WindowFlag::AlwaysRequestAnimationFrame)); return true; } @@ -353,6 +354,7 @@ bool EmscriptenApplication::tryCreate(const Configuration& configuration, const CORRADE_INTERNAL_ASSERT_OUTPUT(emscripten_webgl_make_context_current(_glContext = context) == EMSCRIPTEN_RESULT_SUCCESS); setupCallbacks(!!(configuration.windowFlags() & Configuration::WindowFlag::Resizable)); + setupAnimationFrame(!!(configuration.windowFlags() & Configuration::WindowFlag::AlwaysRequestAnimationFrame)); /* Return true if the initialization succeeds */ return _context->tryCreate(); @@ -535,6 +537,49 @@ void EmscriptenApplication::setupCallbacks(bool resizable) { } } + +void EmscriptenApplication::setupAnimationFrame(bool forceAnimationFrame) { + if(forceAnimationFrame) { + _callback = [](void* userData) -> int { + auto& app = *static_cast(userData); + + if(app._flags & Flag::ExitRequested) { + app._flags &= ~Flag::LoopActive; + return false; + } + + if(app._flags & Flag::Redraw) { + app._flags &= ~Flag::Redraw; + app.drawEvent(); + } + + return true; + }; + } else { + _callback = [](void* userData) -> int { + auto& app = *static_cast(userData); + + if((app._flags & Flag::Redraw) && !(app._flags & Flag::ExitRequested)) { + app._flags &= ~Flag::Redraw; + app.drawEvent(); + } + + /* If redraw is requested, we will not cancel the already requested + animation frame. + If ForceAnimationFrame is set, we will request an animation frame + even if redraw is not requested. */ + if((app._flags & Flag::Redraw) && !(app._flags & Flag::ExitRequested)) { + return true; + } + + /* Cancel last requested animation frame and make redraw() + requestAnimationFrame again next time */ + app._flags &= ~Flag::LoopActive; + return false; + }; + } +} + void EmscriptenApplication::startTextInput() { _flags |= Flag::TextInputActive; } @@ -562,18 +607,41 @@ EmscriptenApplication::GLConfiguration::GLConfiguration(): #endif int EmscriptenApplication::exec() { - emscripten_set_main_loop_arg([](void* userData) { - auto& app = *static_cast(userData); - if(!(app._flags & Flag::Redraw)) return; - - app._flags &= ~Flag::Redraw; - app.drawEvent(); - }, this, 0, true); + redraw(); return 0; } +void EmscriptenApplication::redraw() { + _flags |= Flag::Redraw; + + /* Loop already running, no need to start, + Note that should javascript runtimes ever be multithreaded, we + will have a reentrancy issue here. */ + if(_flags & Flag::LoopActive) return; + + /* Start requestAnimationFrame loop */ + _flags |= Flag::LoopActive; + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension" + EM_ASM({ + /* Animation frame callback */ + var drawEvent = function() { + var id = window.requestAnimationFrame(drawEvent); + + /* Call our callback via function pointer returning int with two + int params */ + if(!dynCall('ii', $0, [$1])) { + window.cancelAnimationFrame(id); + } + }; + + window.requestAnimationFrame(drawEvent); + }, _callback, this); + #pragma GCC diagnostic pop +} + void EmscriptenApplication::exit(int) { - emscripten_cancel_main_loop(); + _flags |= Flag::ExitRequested; } EmscriptenApplication::MouseEvent::Button EmscriptenApplication::MouseEvent::button() const { diff --git a/src/Magnum/Platform/EmscriptenApplication.h b/src/Magnum/Platform/EmscriptenApplication.h index 67da4dd57..9aa595c23 100644 --- a/src/Magnum/Platform/EmscriptenApplication.h +++ b/src/Magnum/Platform/EmscriptenApplication.h @@ -155,6 +155,36 @@ Unlike desktop platforms, the browser has no concept of application exit code, so the return value of @ref exec() is always @cpp 0 @ce and whatever is passed to @ref exit(int) is ignored. +@subsection Platform-EmscriptenApplication-browser-main-loop Main loop implementation + +Magnum application implementations default to redrawing only when needed to +save power and while this is simple to implement efficiently on desktop apps +where the application has the full control over the main loop, it's harder in +the callback-based browser environment. + +@ref Sdl2Application makes use of @m_class{m-doc-external} [emscripten_set_main_loop()](https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_set_main_loop), +which periodically calls @m_class{m-doc-external} [window.requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) +in order to maintain a steady frame rate. For apps that need to redraw only +when needed this means the callback will be called 60 times per second only to +be a no-op. While that's still significantly more efficient than drawing +everything each time, it still means the browser has to wake up 60 times per +second to do nothing. + +@ref EmscriptenApplication instead makes use of `requestAnimationFrame()` +directly --- on initialization and on @ref redraw(), an animation frame will be +requested and the callback set up. The callback will immediately schedule +another animation frame, but cancel that request after @ref drawEvent() if +@ref redraw() was not requested. Note that due to the way Emscripten internals +work, this also requires the class instance to be stored as a global variable +instead of a local variable in @cpp main() @ce. The +@ref MAGNUM_EMSCRIPTENAPPLICATION_MAIN() macro handles this in a portable way +for you. + +For testing purposes or for more predictable behavior for example when the +application has to redraw all the time anyway this can be disabled using +@ref Configuration::WindowFlag::AlwaysRequestAnimationFrame. Setting the flag +will make the main loop behave equivalently to @ref Sdl2Application. + @section Platform-EmscriptenApplication-webgl WebGL-specific behavior While WebGL itself requires all extensions to be @@ -322,8 +352,13 @@ class EmscriptenApplication { protected: /* Nobody will need to have (and delete) EmscriptenApplication*, thus - this is faster than public pure virtual destructor */ - ~EmscriptenApplication(); + just making it protected would be faster than public pure virtual + destructor. However, because we store it in a Pointer in + MAGNUM_EMSCRIPTENAPPLICATION_MAIN(), Clang complains that + "delete called on non-final 'MyApplication' that has virtual + functions but non-virtual destructor", so we have to mark it virtual + anyway. */ + virtual ~EmscriptenApplication(); #ifdef MAGNUM_TARGET_GL /** @@ -467,7 +502,7 @@ class EmscriptenApplication { void swapBuffers(); /** @copydoc Sdl2Application::redraw() */ - void redraw() { _flags |= Flag::Redraw; } + void redraw(); private: /** @copydoc GlfwApplication::viewportEvent(ViewportEvent&) */ @@ -581,7 +616,9 @@ class EmscriptenApplication { private: enum class Flag: UnsignedByte { Redraw = 1 << 0, - TextInputActive = 1 << 1 + TextInputActive = 1 << 1, + ExitRequested = 1 << 2, + LoopActive = 1 << 3 }; typedef Containers::EnumSet Flags; @@ -589,6 +626,7 @@ class EmscriptenApplication { /* Sorry, but can't use Configuration::WindowFlags here :( */ void setupCallbacks(bool resizable); + void setupAnimationFrame(bool ForceAnimationFrame); Vector2 _devicePixelRatio, _dpiScaling; Vector2i _lastKnownCanvasSize; @@ -603,6 +641,9 @@ class EmscriptenApplication { /* These are saved from command-line arguments */ bool _verboseLog{}; Vector2 _commandLineDpiScaling; + + /* Animation frame callback */ + int (*_callback)(void*); }; CORRADE_ENUMSET_OPERATORS(EmscriptenApplication::Flags) @@ -846,7 +887,22 @@ class EmscriptenApplication::Configuration { * * Implement @ref viewportEvent() to react to the resizing events. */ - Resizable = 1 << 1 + Resizable = 1 << 1, + + /** + * Always request the next animation frame. Disables the + * idle-efficient main loop described in + * @ref Platform-EmscriptenApplication-browser-main-loop and + * unconditionally schedules @m_class{m-doc-external} [window.requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame), + * matching the behavior of @ref Sdl2Application. Useful for + * testing or for simpler internal state when your app is going to + * redraw all the time anyway. + * + * Note that this does not affect how @ref drawEvent() is executed + * --- it depends on @ref redraw() being called independently of + * this flag being set. + */ + AlwaysRequestAnimationFrame = 1 << 2 }; /** @@ -1481,24 +1537,23 @@ class EmscriptenApplication::TextInputEvent { See @ref Magnum::Platform::EmscriptenApplication "Platform::EmscriptenApplication" for usage information. This macro abstracts out platform-specific entry point -code. See -@ref portability-applications for more information. - -@code{.cpp} -int main(int argc, char** argv) { - className app({argc, argv}); - return app.exec(); -} -@endcode +code. See @ref portability-applications for more information. When no other application header is included this macro is also aliased to @cpp MAGNUM_APPLICATION_MAIN() @ce. + +Compared to for example @ref MAGNUM_SDL2APPLICATION_MAIN(), the macro +instantiates the application instance as a global variable instead of a local +variable inside @cpp main() @ce. This is in order to support the +@ref Platform-EmscriptenApplication-browser-main-loop "idle-efficient main loop", +as otherwise the local scope would end before any event callback has a chance +to happen. */ #define MAGNUM_EMSCRIPTENAPPLICATION_MAIN(className) \ + namespace { Corrade::Containers::Pointer emscriptenApplicationInstance ; } \ int main(int argc, char** argv) { \ - className app({argc, argv}); \ - app.exec(); \ - return 0; \ + emscriptenApplicationInstance.reset(new className{{argc, argv}}); \ + return emscriptenApplicationInstance->exec(); \ } #ifndef DOXYGEN_GENERATING_OUTPUT diff --git a/src/Magnum/Platform/Sdl2Application.h b/src/Magnum/Platform/Sdl2Application.h index 9eeeb4bb1..0a8c973cf 100644 --- a/src/Magnum/Platform/Sdl2Application.h +++ b/src/Magnum/Platform/Sdl2Application.h @@ -278,6 +278,11 @@ If you enable @ref Configuration::WindowFlag::Resizable, the canvas will be resized when size of the canvas changes and you get @ref viewportEvent(). If the flag is not enabled, no canvas resizing is performed. +@note While this implementation supports Esmcripten and is going to continue + supporting it for the foreseeable future, @ref EmscriptenApplication is now + the preferred application implementation for the web. It offers a broader + range of features, more efficient idle behavior and smaller code size. + @subsection Platform-Sdl2Application-usage-gles OpenGL ES specifics For OpenGL ES, SDL2 defaults to a "desktop GLES" context of the system driver. diff --git a/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp b/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp index 8a4033cf2..fa693113f 100644 --- a/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp +++ b/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp @@ -53,9 +53,14 @@ struct EmscriptenApplicationTest: Platform::Application { } virtual void drawEvent() override { + Debug() << "draw event"; GL::defaultFramebuffer.clear(GL::FramebufferClear::Color); swapBuffers(); + + if(_redraw) { + redraw(); + } } #ifdef MAGNUM_TARGET_GL @@ -96,6 +101,10 @@ struct EmscriptenApplicationTest: Platform::Application { if(event.key() == KeyEvent::Key::F1) { Debug{} << "starting text input"; startTextInput(); + } else if(event.key() == KeyEvent::Key::F2) { + _redraw = !_redraw; + Debug{} << "redrawing" << (_redraw ? "enabled" : "disabled"); + if(_redraw) redraw(); } else if(event.key() == KeyEvent::Key::Esc) { Debug{} << "stopping text input"; stopTextInput(); @@ -130,6 +139,7 @@ struct EmscriptenApplicationTest: Platform::Application { private: bool _fullscreen = false; + bool _redraw = false; }; }}}