diff options
author | Konstantin Käfer <mail@kkaefer.com> | 2015-04-17 15:43:20 +0200 |
---|---|---|
committer | Konstantin Käfer <mail@kkaefer.com> | 2015-04-17 15:43:20 +0200 |
commit | 7827d4954cea5b4fcfce3f245aae2e8c66e0ccb2 (patch) | |
tree | 4db146c25b77fb118e732118ca021859752cc076 | |
parent | cd496645d435a192af3060f06fbe9f2baca76d1a (diff) | |
parent | 9072df709d30ed0ed2c913482e1e6bae6a3d5dd5 (diff) | |
download | qtlocation-mapboxgl-7827d4954cea5b4fcfce3f245aae2e8c66e0ccb2.tar.gz |
Merge pull request #1272 from mapbox/1272-render-still-images-on-thread
Render still/static images on a separate thread as well, aligning it with the way continuous render mode works
25 files changed, 446 insertions, 158 deletions
diff --git a/android/cpp/jni.cpp b/android/cpp/jni.cpp index b6e46ed5ac..628f24a15b 100644 --- a/android/cpp/jni.cpp +++ b/android/cpp/jni.cpp @@ -283,13 +283,6 @@ void JNICALL nativeResume(JNIEnv *env, jobject obj, jlong nativeMapViewPtr) { nativeMapView->resume(); } -void JNICALL nativeRun(JNIEnv *env, jobject obj, jlong nativeMapViewPtr) { - mbgl::Log::Debug(mbgl::Event::JNI, "nativeRun"); - assert(nativeMapViewPtr != 0); - NativeMapView *nativeMapView = reinterpret_cast<NativeMapView *>(nativeMapViewPtr); - nativeMapView->getMap().run(); -} - void JNICALL nativeUpdate(JNIEnv *env, jobject obj, jlong nativeMapViewPtr) { mbgl::Log::Debug(mbgl::Event::JNI, "nativeUpdate"); assert(nativeMapViewPtr != 0); @@ -936,7 +929,7 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { // NOTE: if you get java.lang.UnsatisfiedLinkError you likely forgot to set the size of the // array correctly (too large) - std::array<JNINativeMethod, 63> methods = {{ // Can remove the extra brace in C++14 + std::array<JNINativeMethod, 62> methods = {{ // Can remove the extra brace in C++14 {"nativeCreate", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeCreate)}, {"nativeDestroy", "(J)V", reinterpret_cast<void *>(&nativeDestroy)}, @@ -951,7 +944,6 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { {"nativeStop", "(J)V", reinterpret_cast<void *>(&nativeStop)}, {"nativePause", "(J)V", reinterpret_cast<void *>(&nativePause)}, {"nativeResume", "(J)V", reinterpret_cast<void *>(&nativeResume)}, - {"nativeRun", "(J)V", reinterpret_cast<void *>(&nativeRun)}, {"nativeUpdate", "(J)V", reinterpret_cast<void *>(&nativeUpdate)}, {"nativeTerminate", "(J)V", reinterpret_cast<void *>(&nativeTerminate)}, {"nativeResize", "(JIIFII)V", diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java index d7ca950fbf..4dcd5ef8ed 100644 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java @@ -89,10 +89,6 @@ class NativeMapView { nativeResume(mNativeMapViewPtr); } - public void run() { - nativeRun(mNativeMapViewPtr); - } - public void update() { nativeUpdate(mNativeMapViewPtr); } @@ -398,8 +394,6 @@ class NativeMapView { private native void nativeResume(long nativeMapViewPtr); - private native void nativeRun(long nativeMapViewPtr); - private native void nativeUpdate(long nativeMapViewPtr); private native void nativeTerminate(long nativeMapViewPtr); diff --git a/bin/render.cpp b/bin/render.cpp index 440256b38c..78cb5b59f5 100644 --- a/bin/render.cpp +++ b/bin/render.cpp @@ -1,4 +1,5 @@ #include <mbgl/map/map.hpp> +#include <mbgl/map/still_image.hpp> #include <mbgl/util/image.hpp> #include <mbgl/util/std.hpp> #include <mbgl/util/io.hpp> @@ -19,12 +20,13 @@ namespace po = boost::program_options; +#include <uv.h> + #include <cassert> #include <cstdlib> #include <iostream> int main(int argc, char *argv[]) { - std::string style_path; double lat = 0, lon = 0; double zoom = 0; @@ -33,7 +35,7 @@ int main(int argc, char *argv[]) { int width = 512; int height = 512; double pixelRatio = 1.0; - std::string output = "out.png"; + static std::string output = "out.png"; std::string cache_file = "cache.sqlite"; std::vector<std::string> classes; std::string token; @@ -81,6 +83,8 @@ int main(int argc, char *argv[]) { HeadlessView view; Map map(view, fileSource); + map.start(Map::Mode::Static); + // Set access token if present if (token.size()) { map.setAccessToken(std::string(token)); @@ -93,14 +97,24 @@ int main(int argc, char *argv[]) { map.setLatLngZoom({ lat, lon }, zoom); map.setBearing(bearing); - // Run the loop. It will terminate when we don't have any further listeners. - map.run(); + uv_async_t *async = new uv_async_t; + uv_async_init(uv_default_loop(), async, [](uv_async_t *as, int) { + std::unique_ptr<const StillImage> image(reinterpret_cast<const StillImage *>(as->data)); + uv_close(reinterpret_cast<uv_handle_t *>(as), [](uv_handle_t *handle) { + delete reinterpret_cast<uv_async_t *>(handle); + }); + + const std::string png = util::compress_png(image->width, image->height, image->pixels.get()); + util::write_file(output, png); + }); + + map.renderStill([async](std::unique_ptr<const StillImage> image) { + async->data = const_cast<StillImage *>(image.release()); + uv_async_send(async); + }); - // Get the data from the GPU. - auto pixels = view.readPixels(); + // This loop will terminate once the async was fired. + uv_run(uv_default_loop(), UV_RUN_DEFAULT); - const unsigned int w = width * pixelRatio; - const unsigned int h = height * pixelRatio; - const std::string image = util::compress_png(w, h, pixels.get()); - util::write_file(output, image); + map.stop(); } diff --git a/bin/render.gyp b/bin/render.gyp index 316a7f3ed0..b205e7a959 100644 --- a/bin/render.gyp +++ b/bin/render.gyp @@ -28,15 +28,18 @@ 'variables' : { 'cflags_cc': [ '<@(glfw3_cflags)', + '<@(uv_cflags)', '<@(boost_cflags)', ], 'ldflags': [ '<@(glfw3_ldflags)', + '<@(uv_ldflags)', '<@(boost_ldflags)', '-lboost_program_options' ], 'libraries': [ '<@(glfw3_static_libs)', + '<@(uv_static_libs)', ], }, diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index 8c7f5f3961..992c12b83e 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -42,16 +42,24 @@ class EnvironmentScope; class AnnotationManager; class MapData; class Worker; +class StillImage; class Map : private util::noncopyable { friend class View; public: + enum class Mode : uint8_t { + None, // we're not doing any processing + Continuous, // continually updating map + Static, // a once-off static image. + }; + explicit Map(View&, FileSource&); ~Map(); // Start the map render thread. It is asynchronous. - void start(bool startPaused = false); + void start(bool startPaused = false, Mode mode = Mode::Continuous); + inline void start(Mode renderMode) { start(false, renderMode); } // Stop the map render thread. This call will block until the map rendering thread stopped. // The optional callback function will be invoked repeatedly until the map thread is stopped. @@ -65,10 +73,8 @@ public: // Resumes a paused render thread void resume(); - // Runs the map event loop. ONLY run this function when you want to get render a single frame - // with this map object. It will *not* spawn a separate thread and instead block until the - // frame is completely rendered. - void run(); + using StillImageCallback = std::function<void(std::unique_ptr<const StillImage>)>; + void renderStill(StillImageCallback callback); // Triggers a synchronous or asynchronous render. void renderSync(); @@ -167,6 +173,11 @@ public: inline AnnotationManager& getAnnotationManager() const { return *annotationManager; } private: + // Runs the map event loop. ONLY run this function when you want to get render a single frame + // with this map object. It will *not* spawn a separate thread and instead block until the + // frame is completely rendered. + void run(); + // This may only be called by the View object. void resize(uint16_t width, uint16_t height, float ratio = 1); void resize(uint16_t width, uint16_t height, float ratio, uint16_t fbWidth, uint16_t fbHeight); @@ -203,12 +214,6 @@ private: size_t sourceCacheSize; - enum class Mode : uint8_t { - None, // we're not doing any processing - Continuous, // continually updating map - Static, // a once-off static image. - }; - Mode mode = Mode::None; const std::unique_ptr<Environment> env; @@ -260,6 +265,7 @@ private: std::mutex mutexTask; std::queue<std::function<void()>> tasks; + StillImageCallback callback; }; } diff --git a/include/mbgl/map/still_image.hpp b/include/mbgl/map/still_image.hpp new file mode 100644 index 0000000000..b9ce0620fa --- /dev/null +++ b/include/mbgl/map/still_image.hpp @@ -0,0 +1,21 @@ +#ifndef MBGL_MAP_STILL_IMAGE +#define MBGL_MAP_STILL_IMAGE + +#include <mbgl/util/noncopyable.hpp> + +#include <string> +#include <cstdint> + +namespace mbgl { + +class StillImage : util::noncopyable { +public: + uint16_t width = 0; + uint16_t height = 0; + using Pixel = uint32_t; + std::unique_ptr<Pixel[]> pixels; +}; + +} + +#endif diff --git a/include/mbgl/map/update.hpp b/include/mbgl/map/update.hpp index 3aa871bf03..727122af62 100644 --- a/include/mbgl/map/update.hpp +++ b/include/mbgl/map/update.hpp @@ -1,6 +1,8 @@ #ifndef MBGL_MAP_UPDATE #define MBGL_MAP_UPDATE +#include <cstdint> + namespace mbgl { using UpdateType = uint32_t; @@ -12,6 +14,7 @@ enum class Update : UpdateType { DefaultTransitionDuration = 1 << 2, Classes = 1 << 3, Zoom = 1 << 4, + RenderStill = 1 << 5, }; } diff --git a/include/mbgl/map/view.hpp b/include/mbgl/map/view.hpp index 9b29227d8b..8e21233b72 100644 --- a/include/mbgl/map/view.hpp +++ b/include/mbgl/map/view.hpp @@ -3,9 +3,12 @@ #include <mbgl/util/chrono.hpp> +#include <memory> + namespace mbgl { class Map; +class StillImage; enum MapChange : uint8_t { MapChangeRegionWillChange = 0, @@ -40,6 +43,14 @@ public: // (i.e. map->renderSync() or map->renderAsync() must be called as a result of this) virtual void invalidate() = 0; + // Called from the render (=GL) thread. Signals that the contents of the contents + // may be discarded. The default is a no-op. + virtual void discard(); + + // Reads the pixel data from the current framebuffer. If your View implementation + // doesn't support reading from the framebuffer, return a null pointer. + virtual std::unique_ptr<StillImage> readStillImage(); + // Notifies a watcher of map x/y/scale/rotation changes. // Must only be called from the same thread that caused the change. // Must not be called from the render thread. diff --git a/include/mbgl/platform/default/headless_view.hpp b/include/mbgl/platform/default/headless_view.hpp index 5ba6709a4f..3262afc463 100644 --- a/include/mbgl/platform/default/headless_view.hpp +++ b/include/mbgl/platform/default/headless_view.hpp @@ -24,32 +24,47 @@ class HeadlessDisplay; class HeadlessView : public View { public: - HeadlessView(); - HeadlessView(std::shared_ptr<HeadlessDisplay> display); + HeadlessView(uint16_t width = 256, uint16_t height = 256, float pixelRatio = 1); + HeadlessView(std::shared_ptr<HeadlessDisplay> display, uint16_t width = 256, uint16_t height = 256, float pixelRatio = 1); ~HeadlessView(); - void createContext(); - void loadExtensions(); - void resize(uint16_t width, uint16_t height, float pixelRatio); - std::unique_ptr<uint32_t[]> readPixels(); void activate() override; void deactivate() override; void notify() override; void invalidate() override; + void discard() override; + std::unique_ptr<StillImage> readStillImage() override; private: + void createContext(); + void loadExtensions(); void clearBuffers(); + bool isActive(); private: - std::shared_ptr<HeadlessDisplay> display_; - uint16_t width_; - uint16_t height_; - float pixelRatio_; + std::shared_ptr<HeadlessDisplay> display; + + struct Dimensions { + inline Dimensions(uint16_t width = 0, uint16_t height = 0, float pixelRatio = 0); + inline uint16_t pixelWidth() const { return width * pixelRatio; } + inline uint16_t pixelHeight() const { return height * pixelRatio; } + + uint16_t width = 0; + uint16_t height = 0; + float pixelRatio = 0; + }; + + // These are the values that represent the state of the current framebuffer. + Dimensions current; + + // These are the values that will be used after the next discard() event. + std::mutex prospectiveMutex; + Dimensions prospective; #if MBGL_USE_CGL - CGLContextObj glContext; + CGLContextObj glContext = nullptr; #endif #if MBGL_USE_GLX @@ -64,6 +79,8 @@ private: GLuint fbo = 0; GLuint fboDepthStencil = 0; GLuint fboColor = 0; + + std::thread::id thread; }; } diff --git a/src/mbgl/storage/request.hpp b/include/mbgl/storage/request.hpp index 00157329be..00157329be 100644 --- a/src/mbgl/storage/request.hpp +++ b/include/mbgl/storage/request.hpp diff --git a/platform/default/headless_view.cpp b/platform/default/headless_view.cpp index df85ee714c..f8622604e3 100644 --- a/platform/default/headless_view.cpp +++ b/platform/default/headless_view.cpp @@ -1,7 +1,10 @@ + #include <mbgl/platform/default/headless_view.hpp> #include <mbgl/platform/default/headless_display.hpp> #include <mbgl/platform/log.hpp> +#include <mbgl/map/still_image.hpp> + #include <mbgl/util/std.hpp> #include <stdexcept> @@ -36,17 +39,17 @@ CGLProc CGLGetProcAddress(const char *proc) { namespace mbgl { - -HeadlessView::HeadlessView() - : display_(std::make_shared<HeadlessDisplay>()) { - createContext(); - loadExtensions(); +HeadlessView::HeadlessView(uint16_t width, uint16_t height, float pixelRatio) + : display(std::make_shared<HeadlessDisplay>()) { + resize(width, height, pixelRatio); } -HeadlessView::HeadlessView(std::shared_ptr<HeadlessDisplay> display) - : display_(display) { - createContext(); - loadExtensions(); +HeadlessView::HeadlessView(std::shared_ptr<HeadlessDisplay> display_, + uint16_t width, + uint16_t height, + float pixelRatio) + : display(display_) { + resize(width, height, pixelRatio); } void HeadlessView::loadExtensions() { @@ -54,8 +57,6 @@ void HeadlessView::loadExtensions() { return; } - activate(); - pthread_once(&loadGLExtensions, [] { const char *extensionPtr = reinterpret_cast<const char *>(MBGL_CHECK_ERROR(glGetString(GL_EXTENSIONS))); @@ -95,17 +96,15 @@ void HeadlessView::loadExtensions() { gl::isDepth24Supported = true; extensionsLoaded = true; - - deactivate(); } void HeadlessView::createContext() { - if (!display_) { + if (!display) { throw std::runtime_error("Display is not set"); } #if MBGL_USE_CGL - CGLError error = CGLCreateContext(display_->pixelFormat, NULL, &glContext); + CGLError error = CGLCreateContext(display->pixelFormat, NULL, &glContext); if (error != kCGLNoError) { throw std::runtime_error(std::string("Error creating GL context object:") + CGLErrorString(error) + "\n"); } @@ -117,8 +116,8 @@ void HeadlessView::createContext() { #endif #if MBGL_USE_GLX - xDisplay = display_->xDisplay; - fbConfigs = display_->fbConfigs; + xDisplay = display->xDisplay; + fbConfigs = display->fbConfigs; if (!glContext) { // Try to create a legacy context @@ -147,17 +146,34 @@ void HeadlessView::createContext() { #endif } +bool HeadlessView::isActive() { + return std::this_thread::get_id() == thread; +} + void HeadlessView::resize(const uint16_t width, const uint16_t height, const float pixelRatio) { - clearBuffers(); + std::lock_guard<std::mutex> lock(prospectiveMutex); + prospective = { width, height, pixelRatio }; +} - width_ = width; - height_ = height; - pixelRatio_ = pixelRatio; +HeadlessView::Dimensions::Dimensions(uint16_t width_, uint16_t height_, float pixelRatio_) + : width(width_), height(height_), pixelRatio(pixelRatio_) { +} - const unsigned int w = width_ * pixelRatio_; - const unsigned int h = height_ * pixelRatio_; +void HeadlessView::discard() { + assert(isActive()); - activate(); + { // Obtain the new values. + std::lock_guard<std::mutex> lock(prospectiveMutex); + if (current.pixelWidth() == prospective.pixelWidth() && current.pixelHeight() == prospective.pixelHeight()) { + return; + } + current = prospective; + } + + clearBuffers(); + + const unsigned int w = current.width * current.pixelRatio; + const unsigned int h = current.height * current.pixelRatio; // Create depth/stencil buffer MBGL_CHECK_ERROR(glGenRenderbuffersEXT(1, &fboDepthStencil)); @@ -193,35 +209,36 @@ void HeadlessView::resize(const uint16_t width, const uint16_t height, const flo throw std::runtime_error(error.str()); } - View::resize(width, height, pixelRatio, w, h); - - deactivate(); + View::resize(current.width, current.height, current.pixelRatio, w, h); } -std::unique_ptr<uint32_t[]> HeadlessView::readPixels() { - const unsigned int w = width_ * pixelRatio_; - const unsigned int h = height_ * pixelRatio_; +std::unique_ptr<StillImage> HeadlessView::readStillImage() { + assert(isActive()); - auto pixels = util::make_unique<uint32_t[]>(w * h); + const unsigned int w = current.pixelWidth(); + const unsigned int h = current.pixelHeight(); - activate(); - MBGL_CHECK_ERROR(glReadPixels(0, 0, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, pixels.get())); - deactivate(); + auto image = util::make_unique<StillImage>(); + image->width = w; + image->height = h; + image->pixels = util::make_unique<uint32_t[]>(w * h); + + MBGL_CHECK_ERROR(glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels.get())); const int stride = w * 4; auto tmp = util::make_unique<char[]>(stride); - char *rgba = reinterpret_cast<char *>(pixels.get()); - for (int i = 0, j = height_ - 1; i < j; i++, j--) { + char *rgba = reinterpret_cast<char *>(image->pixels.get()); + for (int i = 0, j = h - 1; i < j; i++, j--) { std::memcpy(tmp.get(), rgba + i * stride, stride); std::memcpy(rgba + i * stride, rgba + j * stride, stride); std::memcpy(rgba + j * stride, tmp.get(), stride); } - return pixels; + return image; } void HeadlessView::clearBuffers() { - activate(); + assert(isActive()); MBGL_CHECK_ERROR(glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)); @@ -239,12 +256,12 @@ void HeadlessView::clearBuffers() { MBGL_CHECK_ERROR(glDeleteRenderbuffersEXT(1, &fboDepthStencil)); fboDepthStencil = 0; } - - deactivate(); } HeadlessView::~HeadlessView() { + activate(); clearBuffers(); + deactivate(); #if MBGL_USE_CGL CGLDestroyContext(glContext); @@ -265,6 +282,15 @@ void HeadlessView::notify() { } void HeadlessView::activate() { + if (thread != std::thread::id()) { + throw std::runtime_error("OpenGL context was already current"); + } + thread = std::this_thread::get_id(); + + if (!glContext) { + createContext(); + } + #if MBGL_USE_CGL CGLError error = CGLSetCurrentContext(glContext); if (error != kCGLNoError) { @@ -277,9 +303,17 @@ void HeadlessView::activate() { throw std::runtime_error("Switching OpenGL context failed.\n"); } #endif + + loadExtensions(); + discard(); } void HeadlessView::deactivate() { + if (thread == std::thread::id()) { + throw std::runtime_error("OpenGL context was not current"); + } + thread = std::thread::id(); + #if MBGL_USE_CGL CGLError error = CGLSetCurrentContext(nullptr); if (error != kCGLNoError) { diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index adaf22d60f..5e7473820e 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -2,6 +2,7 @@ #include <mbgl/map/environment.hpp> #include <mbgl/map/view.hpp> #include <mbgl/map/map_data.hpp> +#include <mbgl/map/still_image.hpp> #include <mbgl/platform/platform.hpp> #include <mbgl/map/source.hpp> #include <mbgl/renderer/painter.hpp> @@ -79,7 +80,7 @@ Map::Map(View& view_, FileSource& fileSource_) } Map::~Map() { - if (mode == Mode::Continuous) { + if (mode != Mode::None) { stop(); } @@ -111,13 +112,13 @@ Worker& Map::getWorker() { return *workers; } -void Map::start(bool startPaused) { +void Map::start(bool startPaused, Mode renderMode) { assert(Environment::currentlyOn(ThreadType::Main)); assert(mode == Mode::None); // When starting map rendering in another thread, we perform async/continuously // updated rendering. Only in these cases, we attach the async handlers. - mode = Mode::Continuous; + mode = renderMode; // Reset the flag. isStopped = false; @@ -143,6 +144,10 @@ void Map::start(bool startPaused) { }); asyncUpdate = util::make_unique<uv::async>(env->loop, [this] { + // Whenever we call triggerUpdate(), we ref() the asyncUpdate handle to make sure that all + // of the calls actually get triggered. + asyncUpdate->unref(); + update(); }); @@ -184,15 +189,15 @@ void Map::start(bool startPaused) { triggerUpdate(); } -void Map::stop(std::function<void ()> callback) { +void Map::stop(std::function<void ()> cb) { assert(Environment::currentlyOn(ThreadType::Main)); - assert(mode == Mode::Continuous); + assert(mode != Mode::None); asyncTerminate->send(); resume(); - if (callback) { + if (cb) { // Wait until the render thread stopped. We are using this construct instead of plainly // relying on the thread_join because the system might need to run things in the current // thread that is required for the render thread to terminate correctly. This is for example @@ -200,7 +205,7 @@ void Map::stop(std::function<void ()> callback) { // thread (== main thread) is blocked. The callback function should use an efficient waiting // function to avoid a busy waiting loop. while (!isStopped) { - callback(); + cb(); } } @@ -231,7 +236,7 @@ void Map::pause(bool waitForPause) { void Map::resume() { assert(Environment::currentlyOn(ThreadType::Main)); - assert(mode == Mode::Continuous); + assert(mode != Mode::None); mutexRun.lock(); pausing = false; @@ -239,21 +244,28 @@ void Map::resume() { mutexRun.unlock(); } -void Map::run() { - ThreadType threadType = ThreadType::Map; - std::string threadName("Map"); +void Map::renderStill(StillImageCallback fn) { + assert(Environment::currentlyOn(ThreadType::Main)); - if (mode == Mode::None) { - mode = Mode::Static; + if (mode != Mode::Static) { + throw util::Exception("Map is not in static render mode"); + } - // FIXME: Threads should have only one purpose. When running on Static mode, - // we are currently not spawning a Map thread and running the code on the - // Main thread, thus, the Main thread in this case is both Main and Map thread. - threadType = static_cast<ThreadType>(static_cast<uint8_t>(threadType) | static_cast<uint8_t>(ThreadType::Main)); - threadName += "andMain"; + if (callback) { + throw util::Exception("Map is currently rendering an image"); } - EnvironmentScope mapScope(*env, threadType, threadName); + assert(mode == Mode::Static); + + callback = std::move(fn); + + triggerUpdate(Update::RenderStill); +} + +void Map::run() { + EnvironmentScope mapScope(*env, ThreadType::Map, "Map"); + assert(Environment::currentlyOn(ThreadType::Map)); + assert(mode != Mode::None); if (mode == Mode::Continuous) { checkForPause(); @@ -261,11 +273,8 @@ void Map::run() { auto styleInfo = data->getStyleInfo(); - if (mode == Mode::Static && !style && (styleInfo.url.empty() && styleInfo.json.empty())) { - throw util::Exception("Style is not set"); - } - view.activate(); + view.discard(); workers = util::make_unique<Worker>(env->loop, 4); @@ -274,24 +283,47 @@ void Map::run() { if (mode == Mode::Continuous) { terminating = false; - while(!terminating) { + while (!terminating) { uv_run(env->loop, UV_RUN_DEFAULT); checkForPause(); } + } else if (mode == Mode::Static) { + terminating = false; + while (!terminating) { + uv_run(env->loop, UV_RUN_DEFAULT); + + // After the loop terminated, these async handles may have been deleted if the terminate() + // callback was fired. In this case, we are exiting the loop. + if (asyncTerminate && asyncUpdate) { + // Otherwise, loop termination means that we have acquired and parsed all resources + // required for this map image and we can now proceed to rendering. + render(); + auto image = view.readStillImage(); + + // We are moving the callback out of the way and empty it in case the callback function + // starts the next map image render. + assert(callback); + StillImageCallback cb; + std::swap(cb, callback); + + // Now we can finally invoke the callback function with the map image we rendered. + cb(std::move(image)); + + // To prepare for the next event loop run, we have to make sure the async handles keep + // the loop alive. + asyncTerminate->ref(); + asyncUpdate->ref(); + asyncInvoke->ref(); + asyncRender->ref(); + } + } } else { - uv_run(env->loop, UV_RUN_DEFAULT); + abort(); } // Run the event loop once more to make sure our async delete handlers are called. uv_run(env->loop, UV_RUN_ONCE); - // If the map rendering wasn't started asynchronously, we perform one render - // *after* all events have been processed. - if (mode == Mode::Static) { - render(); - mode = Mode::None; - } - view.deactivate(); } @@ -309,14 +341,14 @@ void Map::renderSync() { void Map::triggerUpdate(const Update u) { updated |= static_cast<UpdateType>(u); - if (mode == Mode::Static) { - prepare(); - } else if (asyncUpdate) { + if (asyncUpdate) { + asyncUpdate->ref(); asyncUpdate->send(); } } void Map::triggerRender() { + assert(mode == Mode::Continuous); assert(asyncRender); asyncRender->send(); } @@ -328,12 +360,8 @@ void Map::invokeTask(std::function<void()>&& fn) { tasks.emplace(::std::forward<std::function<void()>>(fn)); } - // TODO: Once we have aligned static and continuous rendering, this should always dispatch - // to the async queue. if (asyncInvoke) { asyncInvoke->send(); - } else { - processTasks(); } } @@ -775,6 +803,19 @@ void Map::prepare() { painter->setDebug(data->getDebug()); } + if (u & static_cast<UpdateType>(Update::RenderStill)) { + // Triggers a view resize. + view.discard(); + + // Whenever we trigger an image render, we are unrefing all async handles so the loop will + // eventually terminate. However, it'll stay alive as long as there are pending requests + // (like work requests or HTTP requests). + asyncTerminate->unref(); + asyncUpdate->unref(); + asyncInvoke->unref(); + asyncRender->unref(); + } + if (style) { if (u & static_cast<UpdateType>(Update::DefaultTransitionDuration)) { style->setDefaultTransitionDuration(data->getDefaultTransitionDuration()); @@ -805,6 +846,8 @@ void Map::prepare() { void Map::render() { assert(Environment::currentlyOn(ThreadType::Map)); + view.discard(); + // Cleanup OpenGL objects that we abandoned since the last render call. env->performCleanup(); diff --git a/src/mbgl/map/view.cpp b/src/mbgl/map/view.cpp index 95c31f93e3..fddcf3acdd 100644 --- a/src/mbgl/map/view.cpp +++ b/src/mbgl/map/view.cpp @@ -1,5 +1,6 @@ #include <mbgl/map/view.hpp> #include <mbgl/map/map.hpp> +#include <mbgl/map/still_image.hpp> namespace mbgl { @@ -8,6 +9,14 @@ void View::initialize(Map *map_) { map = map_; } +void View::discard() { + // no-op +} + +std::unique_ptr<StillImage> View::readStillImage() { + return nullptr; +} + void View::resize(uint16_t width, uint16_t height, float ratio, uint16_t fbWidth, uint16_t fbHeight) { assert(map); map->resize(width, height, ratio, fbWidth, fbHeight); diff --git a/src/mbgl/platform/log.cpp b/src/mbgl/platform/log.cpp index 2a761c6260..380113d7dc 100644 --- a/src/mbgl/platform/log.cpp +++ b/src/mbgl/platform/log.cpp @@ -42,7 +42,8 @@ void Log::record(EventSeverity severity, Event event, int64_t code) { } void Log::record(EventSeverity severity, Event event, int64_t code, const std::string &msg) { - if (currentObserver && currentObserver->onRecord(severity, event, code, msg)) { + if (currentObserver && severity != EventSeverity::Debug && + currentObserver->onRecord(severity, event, code, msg)) { return; } diff --git a/src/mbgl/style/style.cpp b/src/mbgl/style/style.cpp index 2fb3ccabcb..0a2a53075b 100644 --- a/src/mbgl/style/style.cpp +++ b/src/mbgl/style/style.cpp @@ -5,7 +5,6 @@ #include <mbgl/style/style_parser.hpp> #include <mbgl/style/style_bucket.hpp> #include <mbgl/util/constants.hpp> -#include <mbgl/util/error.hpp> #include <mbgl/util/std.hpp> #include <mbgl/util/uv_detail.hpp> #include <mbgl/platform/log.hpp> @@ -75,7 +74,7 @@ void Style::loadJSON(const uint8_t *const data) { doc.Parse<0>((const char *const)data); if (doc.HasParseError()) { Log::Error(Event::ParseStyle, "Error parsing style JSON at %i: %s", doc.GetErrorOffset(), doc.GetParseError()); - throw error::style_parse(doc.GetErrorOffset(), doc.GetParseError()); + return; } StyleParser parser; diff --git a/src/mbgl/util/error.hpp b/src/mbgl/util/error.hpp deleted file mode 100644 index 09fa8d3e21..0000000000 --- a/src/mbgl/util/error.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MBGL_UTIL_ERROR -#define MBGL_UTIL_ERROR - -#include <stdexcept> -#include <string> - -namespace mbgl { -namespace error { - -struct style_parse : std::exception { - inline style_parse(size_t offset_, const char *msg_) : offset(offset_), msg(msg_) {} - inline const char* what() const noexcept { return msg.c_str(); } - const size_t offset; - const std::string msg; -}; -} - -} - -#endif
\ No newline at end of file diff --git a/test/api/repeated_render.cpp b/test/api/repeated_render.cpp new file mode 100644 index 0000000000..16688ec36f --- /dev/null +++ b/test/api/repeated_render.cpp @@ -0,0 +1,63 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include <mbgl/map/map.hpp> +#include <mbgl/map/still_image.hpp> +#include <mbgl/platform/default/headless_view.hpp> +#include <mbgl/platform/default/headless_display.hpp> +#include <mbgl/storage/default_file_source.hpp> +#include <mbgl/util/image.hpp> +#include <mbgl/util/io.hpp> + +#include <future> + +TEST(API, RepeatedRender) { + using namespace mbgl; + + const auto style = util::read_file("test/fixtures/api/water.json"); + + auto display = std::make_shared<mbgl::HeadlessDisplay>(); + HeadlessView view(display); + DefaultFileSource fileSource(nullptr); + + Log::setObserver(util::make_unique<FixtureLogObserver>()); + + Map map(view, fileSource); + + map.start(Map::Mode::Static); + + { + view.resize(128, 512, 1); + map.setStyleJSON(style, "test/suite"); + std::promise<std::unique_ptr<const StillImage>> promise; + map.renderStill([&promise](std::unique_ptr<const StillImage> image) { + promise.set_value(std::move(image)); + }); + auto result = promise.get_future().get(); + ASSERT_EQ(128, result->width); + ASSERT_EQ(512, result->height); + const std::string png = util::compress_png(result->width, result->height, result->pixels.get()); + util::write_file("test/fixtures/api/1.png", png); + } + + { + view.resize(512, 512, 2); + map.setStyleJSON(style, "TEST_DATA/suite"); + std::promise<std::unique_ptr<const StillImage>> promise; + map.renderStill([&promise](std::unique_ptr<const StillImage> image) { + promise.set_value(std::move(image)); + }); + auto result = promise.get_future().get(); + ASSERT_EQ(1024, result->width); + ASSERT_EQ(1024, result->height); + const std::string png = util::compress_png(result->width, result->height, result->pixels.get()); + util::write_file("test/fixtures/api/2.png", png); + } + + map.stop(); + + auto observer = Log::removeObserver(); + auto flo = dynamic_cast<FixtureLogObserver*>(observer.get()); + auto unchecked = flo->unchecked(); + EXPECT_TRUE(unchecked.empty()) << unchecked; +} diff --git a/test/api/set_style.cpp b/test/api/set_style.cpp new file mode 100644 index 0000000000..973989d1c8 --- /dev/null +++ b/test/api/set_style.cpp @@ -0,0 +1,33 @@ +#include "../fixtures/util.hpp" +#include "../fixtures/fixture_log_observer.hpp" + +#include <mbgl/map/map.hpp> +#include <mbgl/platform/default/headless_view.hpp> +#include <mbgl/platform/default/headless_display.hpp> +#include <mbgl/storage/default_file_source.hpp> + + +TEST(API, SetStyle) { + using namespace mbgl; + + auto display = std::make_shared<mbgl::HeadlessDisplay>(); + HeadlessView view(display); + DefaultFileSource fileSource(nullptr); + + Log::setObserver(util::make_unique<FixtureLogObserver>()); + + Map map(view, fileSource); + + map.start(Map::Mode::Static); + + map.setStyleJSON("invalid", "test/suite"); + + map.stop(); + + auto observer = Log::removeObserver(); + auto flo = dynamic_cast<FixtureLogObserver*>(observer.get()); + EXPECT_EQ(1ul, flo->count({ EventSeverity::Error, Event::ParseStyle, -1, + "Error parsing style JSON at 0: Expect either an object or array at root" })); + auto unchecked = flo->unchecked(); + EXPECT_TRUE(unchecked.empty()) << unchecked; +} diff --git a/test/fixtures/api/1.png b/test/fixtures/api/1.png Binary files differnew file mode 100644 index 0000000000..69e3c8ca95 --- /dev/null +++ b/test/fixtures/api/1.png diff --git a/test/fixtures/api/2.png b/test/fixtures/api/2.png Binary files differnew file mode 100644 index 0000000000..2a28c639a5 --- /dev/null +++ b/test/fixtures/api/2.png diff --git a/test/fixtures/api/water.json b/test/fixtures/api/water.json new file mode 100644 index 0000000000..2bcce11992 --- /dev/null +++ b/test/fixtures/api/water.json @@ -0,0 +1,25 @@ +{ + "version": 7, + "name": "Water", + "sources": { + "mapbox": { + "type": "vector", + "url": "asset://TEST_DATA/fixtures/tiles/streets.json" + } + }, + "layers": [{ + "id": "background", + "type": "background", + "paint": { + "background-color": "red" + } + }, { + "id": "water", + "type": "fill", + "source": "mapbox", + "source-layer": "water", + "paint": { + "fill-color": "blue" + } + }] +} diff --git a/test/fixtures/tiles/streets.json b/test/fixtures/tiles/streets.json new file mode 100644 index 0000000000..a848a1bf72 --- /dev/null +++ b/test/fixtures/tiles/streets.json @@ -0,0 +1,14 @@ +{ + "bounds": [ -180, -85.0511, 180, 85.0511 ], + "center": [ 0, 0, 0 ], + "description": "", + "format": "pbf", + "id": "streets", + "maskLevel": 8, + "maxzoom": 15, + "minzoom": 0, + "name": "Streets", + "scheme": "xyz", + "tilejson": "2.0.0", + "tiles": [ "asset://TEST_DATA/fixtures/tiles/streets/{z}-{x}-{y}.vector.pbf" ] +} diff --git a/test/fixtures/tiles/streets/0-0-0.vector.pbf b/test/fixtures/tiles/streets/0-0-0.vector.pbf Binary files differnew file mode 100644 index 0000000000..a0f049ad43 --- /dev/null +++ b/test/fixtures/tiles/streets/0-0-0.vector.pbf diff --git a/test/headless/headless.cpp b/test/headless/headless.cpp index f4c8accf61..4ce9b668f1 100644 --- a/test/headless/headless.cpp +++ b/test/headless/headless.cpp @@ -2,6 +2,7 @@ #include "../fixtures/fixture_log_observer.hpp" #include <mbgl/map/map.hpp> +#include <mbgl/map/still_image.hpp> #include <mbgl/util/image.hpp> #include <mbgl/util/std.hpp> @@ -15,6 +16,8 @@ #include <mbgl/platform/default/headless_display.hpp> #include <mbgl/storage/default_file_source.hpp> +#include <uv.h> + #include <dirent.h> void rewriteLocalScheme(rapidjson::Value &value, rapidjson::Document::AllocatorType &allocator) { @@ -142,6 +145,8 @@ TEST_P(HeadlessTest, render) { DefaultFileSource fileSource(nullptr); Map map(view, fileSource); + map.start(Map::Mode::Static); + map.setClasses(classes); map.setStyleJSON(style, "test/suite"); @@ -149,16 +154,34 @@ TEST_P(HeadlessTest, render) { map.setLatLngZoom(mbgl::LatLng(latitude, longitude), zoom); map.setBearing(bearing); - // Run the loop. It will terminate when we don't have any further listeners. - map.run(); - - const unsigned int w = width * pixelRatio; - const unsigned int h = height * pixelRatio; - - auto pixels = view.readPixels(); + struct Data { + std::string path; + std::unique_ptr<const StillImage> image; + }; - const std::string image = util::compress_png(w, h, pixels.get()); - util::write_file(actual_image, image); + uv_async_t *async = new uv_async_t; + async->data = new Data { "test/suite/tests/" + base + "/" + name + "/actual.png", nullptr }; + uv_async_init(uv_default_loop(), async, [](uv_async_t *as, int) { + auto data = std::unique_ptr<Data>(reinterpret_cast<Data *>(as->data)); + as->data = nullptr; + uv_close(reinterpret_cast<uv_handle_t *>(as), [](uv_handle_t *handle) { + delete reinterpret_cast<uv_async_t *>(handle); + }); + + assert(data); + const std::string png = util::compress_png(data->image->width, data->image->height, data->image->pixels.get()); + util::write_file(data->path, png); + }); + + map.renderStill([async](std::unique_ptr<const StillImage> image) { + reinterpret_cast<Data *>(async->data)->image = std::move(image); + uv_async_send(async); + }); + + // This loop will terminate once the async was fired. + uv_run(uv_default_loop(), UV_RUN_DEFAULT); + + map.stop(); } } diff --git a/test/test.gyp b/test/test.gyp index d9e234fe09..2e89db58b0 100644 --- a/test/test.gyp +++ b/test/test.gyp @@ -35,6 +35,9 @@ 'fixtures/fixture_log_observer.hpp', 'fixtures/fixture_log_observer.cpp', + 'api/set_style.cpp', + 'api/repeated_render.cpp', + 'headless/headless.cpp', 'miscellaneous/clip_ids.cpp', |