From b7ad20249df5c23cf59c242f14f6406b6603f621 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 4 Mar 2021 16:14:37 -0800 Subject: [PATCH] Improve the Settings serialization API, expose to JS. This not only makes it easier to expose to JavaScript, it also paves the way for a much more efficient implementation. --- libs/viewer/include/viewer/RemoteServer.h | 16 +++-- libs/viewer/include/viewer/Settings.h | 37 +++++----- libs/viewer/src/AutomationEngine.cpp | 3 +- libs/viewer/src/AutomationSpec.cpp | 6 +- libs/viewer/src/RemoteServer.cpp | 48 +++++++++---- libs/viewer/src/Settings.cpp | 84 ++++++++++++++++------- libs/viewer/tests/test_settings.cpp | 14 ++-- samples/gltf_viewer.cpp | 3 +- web/filament-js/jsbindings.cpp | 4 ++ web/samples/remote.html | 13 +++- 10 files changed, 153 insertions(+), 75 deletions(-) diff --git a/libs/viewer/include/viewer/RemoteServer.h b/libs/viewer/include/viewer/RemoteServer.h index ff3d2c325f..c7ab5dd7e2 100644 --- a/libs/viewer/include/viewer/RemoteServer.h +++ b/libs/viewer/include/viewer/RemoteServer.h @@ -17,6 +17,8 @@ #ifndef VIEWER_REMOTE_SERVER_H #define VIEWER_REMOTE_SERVER_H +#include + #include #include @@ -25,7 +27,8 @@ class CivetServer; namespace filament { namespace viewer { -class WsHandler; +class MessageSender; +class MessageReceiver; /** * Encapsulates a message sent from the web client. @@ -54,24 +57,27 @@ class RemoteServer { public: RemoteServer(int port = 8082); ~RemoteServer(); - bool isValid() const { return mCivetServer; } + bool isValid() const { return mMessageSender; } char const* peekIncomingLabel() const; ReceivedMessage const* peekReceivedMessage() const; ReceivedMessage const* acquireReceivedMessage(); void releaseReceivedMessage(ReceivedMessage const* message); + void sendMessage(const Settings& settings); + void sendMessage(const char* label, const char* buffer, size_t bufsize); private: void enqueueReceivedMessage(ReceivedMessage* message); void setIncomingMessage(ReceivedMessage* message); - CivetServer* mCivetServer = nullptr; - WsHandler* mWsHandler = nullptr; + MessageSender* mMessageSender = nullptr; + MessageReceiver* mMessageReceiver = nullptr; size_t mNextMessageUid = 0; size_t mOldestMessageUid = 0; static const size_t kMessageCapacity = 4; ReceivedMessage* mReceivedMessages[kMessageCapacity] = {}; ReceivedMessage* mIncomingMessage = nullptr; + JsonSerializer mSerializer; mutable std::mutex mReceivedMessagesMutex; - friend class WsHandler; + friend class MessageReceiver; }; } // namespace viewer diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h index eefe5bfec4..9b5d076c65 100644 --- a/libs/viewer/include/viewer/Settings.h +++ b/libs/viewer/include/viewer/Settings.h @@ -51,12 +51,6 @@ using ToneMapping = filament::ColorGrading::ToneMapping; using VignetteOptions = filament::View::VignetteOptions; using VsmShadowOptions = filament::View::VsmShadowOptions; -// Reads the given JSON blob and updates the corresponding fields in the given Settings object. -// - The given JSON blob need not specify all settings. -// - Returns true if successful. -// - This function writes warnings and error messages into the utils log. -bool readJson(const char* jsonChunk, size_t size, Settings* out); - // These functions push all editable property values to their respective Filament objects. void applySettings(const ViewSettings& settings, View* dest); void applySettings(const MaterialSettings& settings, MaterialInstance* dest); @@ -64,19 +58,24 @@ void applySettings(const MaterialSettings& settings, MaterialInstance* dest); // Creates a new ColorGrading object based on the given settings. ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine); -// Generates human-readable JSON strings from settings objects. -std::string writeJson(const AmbientOcclusionOptions& in); -std::string writeJson(const BloomOptions& in); -std::string writeJson(const ColorGradingSettings& in); -std::string writeJson(const DepthOfFieldOptions& in); -std::string writeJson(const DynamicLightingSettings& in); -std::string writeJson(const FogOptions& in); -std::string writeJson(const MaterialSettings& in); -std::string writeJson(const RenderQuality& in); -std::string writeJson(const Settings& in); -std::string writeJson(const TemporalAntiAliasingOptions& in); -std::string writeJson(const ViewSettings& in); -std::string writeJson(const VignetteOptions& in); +class JsonSerializer { +public: + JsonSerializer(); + ~JsonSerializer(); + + // Writes a human-readable JSON string into an internal buffer and returns the result. + const std::string& writeJson(const Settings& in); + + // Reads the given JSON blob and updates the corresponding fields in the given Settings object. + // - The given JSON blob need not specify all settings. + // - Returns true if successful. + // - This function writes warnings and error messages into the utils log. + bool readJson(const char* jsonChunk, size_t size, Settings* out); + +private: + struct Context; + Context* context; +}; struct ColorGradingSettings { bool enabled = true; diff --git a/libs/viewer/src/AutomationEngine.cpp b/libs/viewer/src/AutomationEngine.cpp index e1505e69f5..4e95a0cfbe 100644 --- a/libs/viewer/src/AutomationEngine.cpp +++ b/libs/viewer/src/AutomationEngine.cpp @@ -122,7 +122,8 @@ void AutomationEngine::terminate() { } void AutomationEngine::exportSettings(const Settings& settings, const char* filename) { - std::string contents = writeJson(settings); + JsonSerializer serializer; + std::string contents = serializer.writeJson(settings); std::ofstream out(filename); if (!out) { gStatus = "Failed to export settings file."; diff --git a/libs/viewer/src/AutomationSpec.cpp b/libs/viewer/src/AutomationSpec.cpp index 4bdda6f43d..faafddd383 100644 --- a/libs/viewer/src/AutomationSpec.cpp +++ b/libs/viewer/src/AutomationSpec.cpp @@ -104,7 +104,8 @@ static int parseBaseSettings(jsmntok_t const* tokens, int i, const char* jsonChu } // Now that we have a complete JSON string, apply this property change. - readJson(json.c_str(), json.size(), out); + JsonSerializer serializer; + serializer.readJson(json.c_str(), json.size(), out); } return i; } @@ -195,6 +196,7 @@ static int parseAutomationSpec(jsmntok_t const* tokens, int i, const char* jsonC } size_t caseIndex = 0; + JsonSerializer serializer; while (true) { if (VERBOSE) { slog.i << " Generating test case " << caseIndex << io::endl; @@ -228,7 +230,7 @@ static int parseAutomationSpec(jsmntok_t const* tokens, int i, const char* jsonC if (VERBOSE) { slog.i << " Applying " << jsonString.c_str() << io::endl; } - if (!readJson(jsonString.c_str(), jsonString.size(), &testCase)) { + if (!serializer.readJson(jsonString.c_str(), jsonString.size(), &testCase)) { return -1; } } diff --git a/libs/viewer/src/RemoteServer.cpp b/libs/viewer/src/RemoteServer.cpp index b75848d779..6b8cb40a72 100644 --- a/libs/viewer/src/RemoteServer.cpp +++ b/libs/viewer/src/RemoteServer.cpp @@ -27,10 +27,16 @@ using namespace utils; namespace filament { namespace viewer { -class WsHandler : public CivetWebSocketHandler { +class MessageSender : public CivetServer { +public: + MessageSender(const char** options) : CivetServer(options) {} + void sendMessage(const char* label, const char* buffer, size_t bufsize); +}; + +class MessageReceiver : public CivetWebSocketHandler { public: - WsHandler(RemoteServer* server) : mServer(server) {} - ~WsHandler() { delete mReceivedMessage; } + MessageReceiver(RemoteServer* server) : mServer(server) {} + ~MessageReceiver() { delete mReceivedMessage; } bool handleData(CivetServer* server, struct mg_connection*, int, char* , size_t) override; private: RemoteServer* mServer; @@ -47,22 +53,22 @@ RemoteServer::RemoteServer(int port) { }; std::string portString = std::to_string(port); kServerOptions[1] = portString.c_str(); - mCivetServer = new CivetServer(kServerOptions); - if (!mCivetServer->getContext()) { + mMessageSender = new MessageSender(kServerOptions); + if (!mMessageSender->getContext()) { slog.e << "Unable to start RemoteServer, see civetweb.txt for details." << io::endl; - delete mCivetServer; - mCivetServer = nullptr; - mWsHandler = nullptr; + delete mMessageSender; + mMessageSender = nullptr; + mMessageReceiver = nullptr; return; } - mWsHandler = new WsHandler(this); - mCivetServer->addWebSocketHandler("", mWsHandler); + mMessageReceiver = new MessageReceiver(this); + mMessageSender->addWebSocketHandler("", mMessageReceiver); slog.i << "RemoteServer listening at ws://localhost:" << port << io::endl; } RemoteServer::~RemoteServer() { - delete mCivetServer; - delete mWsHandler; + delete mMessageSender; + delete mMessageReceiver; for (auto msg : mReceivedMessages) { releaseReceivedMessage(msg); } @@ -122,8 +128,17 @@ void RemoteServer::releaseReceivedMessage(ReceivedMessage const* message) { } } +void RemoteServer::sendMessage(const Settings& settings) { + const auto& json = mSerializer.writeJson(settings); + mMessageSender->sendMessage("settings.json", json.c_str(), json.size() + 1); +} + +void RemoteServer::sendMessage(const char* label, const char* buffer, size_t bufsize) { + mMessageSender->sendMessage(label, buffer, bufsize); +} + // NOTE: This is invoked off the main thread. -bool WsHandler::handleData(CivetServer* server, struct mg_connection* conn, int bits, +bool MessageReceiver::handleData(CivetServer* server, struct mg_connection* conn, int bits, char* data, size_t size) { const bool final = bits & 0x80; const int opcode = bits & 0xf; @@ -162,5 +177,12 @@ bool WsHandler::handleData(CivetServer* server, struct mg_connection* conn, int return true; } +void MessageSender::sendMessage(const char* label, const char* buffer, size_t bufsize) { + for (auto iter : connections) { + mg_websocket_write(iter.first, 0x80, label, strlen(label) + 1); + mg_websocket_write(iter.first, 0x80, buffer, bufsize); + } +} + } // namespace viewer } // namespace filament diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index 6d797f7e33..62d5fad1ce 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -30,6 +30,19 @@ using namespace utils; namespace filament { namespace viewer { +static std::string writeJson(const Settings& in); +static std::string writeJson(const AmbientOcclusionOptions& in); +static std::string writeJson(const BloomOptions& in); +static std::string writeJson(const ColorGradingSettings& in); +static std::string writeJson(const DepthOfFieldOptions& in); +static std::string writeJson(const DynamicLightingSettings& in); +static std::string writeJson(const FogOptions& in); +static std::string writeJson(const MaterialSettings& in); +static std::string writeJson(const RenderQuality& in); +static std::string writeJson(const TemporalAntiAliasingOptions& in); +static std::string writeJson(const ViewSettings& in); +static std::string writeJson(const VignetteOptions& in); + // Compares a JSON string token against a C string. int compare(jsmntok_t tok, const char* jsonChunk, const char* str) { size_t slen = strlen(str); @@ -669,32 +682,6 @@ int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) return i; } -bool readJson(const char* jsonChunk, size_t size, Settings* out) { - jsmn_parser parser = { 0, 0, 0 }; - - int tokenCount = jsmn_parse(&parser, jsonChunk, size, nullptr, 0); - if (tokenCount <= 0) { - slog.e << "Badly formed JSON." << io::endl; - return false; - } - - jsmntok_t* tokens = (jsmntok_t*) malloc(sizeof(jsmntok_t) * tokenCount); - assert(tokens); - - jsmn_init(&parser); - tokenCount = jsmn_parse(&parser, jsonChunk, size, tokens, tokenCount); - - if (tokenCount <= 0) { - free(tokens); - slog.e << "Badly formed JSON." << io::endl; - return false; - } - - int i = parse(tokens, 0, jsonChunk, out); - free(tokens); - return i >= 0; -} - void applySettings(const ViewSettings& settings, View* dest) { dest->setSampleCount(settings.sampleCount); dest->setAntiAliasing(settings.antiAliasing); @@ -1089,5 +1076,50 @@ bool ColorGradingSettings::operator==(const ColorGradingSettings &rhs) const { scale == rhs.scale; } +// TODO: This can be made faster by ditching ostringstream and using snprintf. +// At the very least, we should stash the ostringstream here in the context object and re-use it. +struct JsonSerializer::Context { + std::string buffer; +}; + +JsonSerializer::JsonSerializer() { + context = new JsonSerializer::Context(); +} + +JsonSerializer::~JsonSerializer() { + delete context; +} + +const std::string& JsonSerializer::writeJson(const Settings& in) { + context->buffer = viewer::writeJson(in); + return context->buffer; +} + +bool JsonSerializer::readJson(const char* jsonChunk, size_t size, Settings* out) { + jsmn_parser parser = { 0, 0, 0 }; + + int tokenCount = jsmn_parse(&parser, jsonChunk, size, nullptr, 0); + if (tokenCount <= 0) { + slog.e << "Badly formed JSON." << io::endl; + return false; + } + + jsmntok_t* tokens = (jsmntok_t*) malloc(sizeof(jsmntok_t) * tokenCount); + assert(tokens); + + jsmn_init(&parser); + tokenCount = jsmn_parse(&parser, jsonChunk, size, tokens, tokenCount); + + if (tokenCount <= 0) { + free(tokens); + slog.e << "Badly formed JSON." << io::endl; + return false; + } + + int i = parse(tokens, 0, jsonChunk, out); + free(tokens); + return i >= 0; +} + } // namespace viewer } // namespace filament diff --git a/libs/viewer/tests/test_settings.cpp b/libs/viewer/tests/test_settings.cpp index 400cdea76e..1273839a5d 100644 --- a/libs/viewer/tests/test_settings.cpp +++ b/libs/viewer/tests/test_settings.cpp @@ -148,24 +148,26 @@ static const char* JSON_TEST_AUTOMATION = R"TXT([{ }])TXT"; TEST_F(ViewSettingsTest, JsonTestDefaults) { + JsonSerializer serializer; Settings settings1 = {0}; - ASSERT_TRUE(readJson(JSON_TEST_DEFAULTS, strlen(JSON_TEST_DEFAULTS), &settings1)); + ASSERT_TRUE(serializer.readJson(JSON_TEST_DEFAULTS, strlen(JSON_TEST_DEFAULTS), &settings1)); ASSERT_TRUE(settings1.view.bloom.threshold); Settings settings2; - ASSERT_TRUE(readJson("{}", strlen("{}"), &settings2)); - ASSERT_FALSE(readJson("{ badly_formed }", strlen("{ badly_formed }"), &settings2)); + ASSERT_TRUE(serializer.readJson("{}", strlen("{}"), &settings2)); + ASSERT_FALSE(serializer.readJson("{ badly_formed }", strlen("{ badly_formed }"), &settings2)); Settings settings3; - ASSERT_EQ(writeJson(settings2), writeJson(settings3)); + ASSERT_EQ(serializer.writeJson(settings2), serializer.writeJson(settings3)); } TEST_F(ViewSettingsTest, JsonTestMaterial) { + JsonSerializer serializer; Settings settings = {0}; std::string js = "{" + std::string(JSON_TEST_MATERIAL) + "}"; - ASSERT_TRUE(readJson(js.c_str(), js.size(), &settings)); - std::string serialized = writeJson(settings); + ASSERT_TRUE(serializer.readJson(js.c_str(), js.size(), &settings)); + std::string serialized = serializer.writeJson(settings); ASSERT_PRED_FORMAT2(testing::IsSubstring, "\"baz\": [1, 2, 3]", serialized); } diff --git a/samples/gltf_viewer.cpp b/samples/gltf_viewer.cpp index 32e487034e..5528bf9812 100644 --- a/samples/gltf_viewer.cpp +++ b/samples/gltf_viewer.cpp @@ -252,7 +252,8 @@ static bool loadSettings(const char* filename, Settings* out) { if (!in.read(json.data(), contentSize)) { return false; } - return readJson(json.data(), contentSize, out); + JsonSerializer serializer; + return serializer.readJson(json.data(), contentSize, out); } static void createGroundPlane(Engine* engine, Scene* scene, App& app) { diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index bc058bfaba..75ab9a6f83 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -1830,6 +1830,10 @@ class_("gltfio$ResourceLoader") class_("Settings"); +class_("JsonSerializer") + .constructor<>() + .function("writeJson", &JsonSerializer::writeJson); + class_("SimpleViewer") .constructor() .function("renderUserInterface", &SimpleViewer::renderUserInterface, allow_raw_pointers()) diff --git a/web/samples/remote.html b/web/samples/remote.html index dc4f52421c..e0e01595a6 100644 --- a/web/samples/remote.html +++ b/web/samples/remote.html @@ -130,6 +130,8 @@ class App { this.renderer = engine.createRenderer(); this.renderer.setClearOptions({clearColor: kBackgroundColor, clear: true}); this.camera = engine.createCamera(Filament.EntityManager.get().create()); + this.serializer = new Filament.JsonSerializer(); + this.previousSettingsJson = ""; view.setScene(scene); view.setCamera(this.camera); @@ -150,8 +152,6 @@ class App { Object.seal(this); - const settings = this.simpleViewer.getSettings(); // TODO: do something with this - this.canvas.addEventListener("pointermove", e => { this.mouseX = e.offsetX * window.devicePixelRatio; this.mouseY = e.offsetY * window.devicePixelRatio; @@ -266,8 +266,17 @@ class App { if (this.mouseButtonEvents.length > 0) { this.mouseButton = this.mouseButtonEvents.shift(); } + this.simpleViewer.renderUserInterface(dt, this.uiview, window.devicePixelRatio, this.mouseX, this.mouseY, this.mouseButton, this.mouseWheelY); + + const settingsJson = this.serializer.writeJson(this.simpleViewer.getSettings()).slice(); + + if (this.previousSettingsJson != settingsJson) { + console.info("Settings have changed."); + this.previousSettingsJson = settingsJson; + } + this.mouseWheelY = 0; this.renderer.beginFrame(this.swapChain);