summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Käfer <mail@kkaefer.com>2015-04-17 15:43:20 +0200
committerKonstantin Käfer <mail@kkaefer.com>2015-04-17 15:43:20 +0200
commit7827d4954cea5b4fcfce3f245aae2e8c66e0ccb2 (patch)
tree4db146c25b77fb118e732118ca021859752cc076
parentcd496645d435a192af3060f06fbe9f2baca76d1a (diff)
parent9072df709d30ed0ed2c913482e1e6bae6a3d5dd5 (diff)
downloadqtlocation-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
-rw-r--r--android/cpp/jni.cpp10
-rw-r--r--android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/views/NativeMapView.java6
-rw-r--r--bin/render.cpp34
-rw-r--r--bin/render.gyp3
-rw-r--r--include/mbgl/map/map.hpp28
-rw-r--r--include/mbgl/map/still_image.hpp21
-rw-r--r--include/mbgl/map/update.hpp3
-rw-r--r--include/mbgl/map/view.hpp11
-rw-r--r--include/mbgl/platform/default/headless_view.hpp39
-rw-r--r--include/mbgl/storage/request.hpp (renamed from src/mbgl/storage/request.hpp)0
-rw-r--r--platform/default/headless_view.cpp114
-rw-r--r--src/mbgl/map/map.cpp121
-rw-r--r--src/mbgl/map/view.cpp9
-rw-r--r--src/mbgl/platform/log.cpp3
-rw-r--r--src/mbgl/style/style.cpp3
-rw-r--r--src/mbgl/util/error.hpp20
-rw-r--r--test/api/repeated_render.cpp63
-rw-r--r--test/api/set_style.cpp33
-rw-r--r--test/fixtures/api/1.pngbin0 -> 12529 bytes
-rw-r--r--test/fixtures/api/2.pngbin0 -> 115197 bytes
-rw-r--r--test/fixtures/api/water.json25
-rw-r--r--test/fixtures/tiles/streets.json14
-rw-r--r--test/fixtures/tiles/streets/0-0-0.vector.pbfbin0 -> 482553 bytes
-rw-r--r--test/headless/headless.cpp41
-rw-r--r--test/test.gyp3
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
new file mode 100644
index 0000000000..69e3c8ca95
--- /dev/null
+++ b/test/fixtures/api/1.png
Binary files differ
diff --git a/test/fixtures/api/2.png b/test/fixtures/api/2.png
new file mode 100644
index 0000000000..2a28c639a5
--- /dev/null
+++ b/test/fixtures/api/2.png
Binary files differ
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
new file mode 100644
index 0000000000..a0f049ad43
--- /dev/null
+++ b/test/fixtures/tiles/streets/0-0-0.vector.pbf
Binary files differ
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',