diff --git a/libs/viewer/CMakeLists.txt b/libs/viewer/CMakeLists.txt index 13869b4f89..223652848b 100644 --- a/libs/viewer/CMakeLists.txt +++ b/libs/viewer/CMakeLists.txt @@ -8,13 +8,15 @@ set(PUBLIC_HDR_DIR include) # Sources and headers # ================================================================================================== set(PUBLIC_HDRS - include/viewer/SimpleViewer.h + include/viewer/Automation.h include/viewer/Settings.h + include/viewer/SimpleViewer.h ) set(SRCS - src/SimpleViewer.cpp + src/Automation.cpp src/Settings.cpp + src/SimpleViewer.cpp ) # ================================================================================================== diff --git a/libs/viewer/include/viewer/Automation.h b/libs/viewer/include/viewer/Automation.h new file mode 100644 index 0000000000..5eff6e5c09 --- /dev/null +++ b/libs/viewer/include/viewer/Automation.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by mIcable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VIEWER_AUTOMATION_H +#define VIEWER_AUTOMATION_H + +#include +#include + +#include + +namespace filament { +namespace viewer { + +// Named list of settings permutations. +struct AutomationSpec { + std::string name; + std::vector cases; +}; + +// Consumes a JSON string and produces a list of automation specs using a counting algorithm. +// +// Each top-level item in the JSON is an object with "name", "base" and "permute". +// The "base" object specifies a single set of changes to apply to default settings. +// The optional "permute" object specifies a cross product of changes to apply to the base. +// See the unit test for an example. +// +// - Returns true if successful. +// - This function writes warnings and error messages into the utils log. +bool generate(const char* jsonChunk, size_t size, std::vector* out); + +} // namespace viewer +} // namespace filament + +#endif // VIEWER_AUTOMATION_H diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h index fddb29e7d7..20f16a4fb4 100644 --- a/libs/viewer/include/viewer/Settings.h +++ b/libs/viewer/include/viewer/Settings.h @@ -46,7 +46,6 @@ using ToneMapping = filament::ColorGrading::ToneMapping; using VignetteOptions = filament::View::VignetteOptions; // 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. diff --git a/libs/viewer/include/viewer/SimpleViewer.h b/libs/viewer/include/viewer/SimpleViewer.h index 4d4601f379..8ca43b2f05 100644 --- a/libs/viewer/include/viewer/SimpleViewer.h +++ b/libs/viewer/include/viewer/SimpleViewer.h @@ -37,7 +37,7 @@ namespace filament { namespace viewer { /** - * \class SimpleViewer SimpleViewer.h gltfio/SimpleViewer.h + * \class SimpleViewer SimpleViewer.h viewer/SimpleViewer.h * \brief Manages the state for a simple glTF viewer with imgui controls and a tree view. * * This is a utility that can be used across multiple platforms, including web. diff --git a/libs/viewer/src/Automation.cpp b/libs/viewer/src/Automation.cpp new file mode 100644 index 0000000000..5129b70b5b --- /dev/null +++ b/libs/viewer/src/Automation.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by mIcable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define JSMN_HEADER + +#include + +#include "parse.h" + +#include + +#include + +#include + +using namespace utils; + +using std::vector; + +static const bool VERBOSE = false; + +namespace filament { +namespace viewer { + +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, std::string* val) { + CHECK_TOKTYPE(tokens[i], JSMN_STRING); + *val = STR(tokens[i], jsonChunk); + return i + 1; +} + +static int parseBaseSettings(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + for (int j = 0; j < size; ++j, i += 2) { + std::stringstream dk(STR(tokens[i], jsonChunk)); + std::string token; + std::string prefix; + int depth = 0; + + // Expand "foo.bar.baz" into "foo: { bar: { baz: " + while (getline(dk, token, '.')) { + prefix += "{ \"" + token + "\": "; + depth++; + } + std::string json = prefix + STR(tokens[i + 1], jsonChunk); + for (int d = 0; d < depth; d++) { json += " } "; } + if (VERBOSE) { + slog.i << " Base: " << json.c_str() << io::endl; + } + + // Now that we have a complete JSON string, apply this property change. + readJson(json.c_str(), json.size(), out); + } + return i; +} + +static int parsePermutationsSpec(jsmntok_t const* tokens, int i, const char* jsonChunk, + vector>* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + out->resize(size); + for (int j = 0; j < size; ++j) { + std::stringstream dk(STR(tokens[i], jsonChunk)); + std::string token; + std::string prefix; + int depth = 0; + + // Expand "foo.bar.baz" into "foo: { bar: { baz: " + while (getline(dk, token, '.')) { + prefix += "{ \"" + token + "\": "; + depth++; + } + ++i; + + // Build a complete JSON string for each requested property value. + const jsmntok_t valueArray = tokens[i++]; + CHECK_TOKTYPE(valueArray, JSMN_ARRAY); + vector& spec = (*out)[j]; + spec.resize(valueArray.size); + for (int k = 0; k < valueArray.size; k++, i++) { + std::string json = prefix + STR(tokens[i], jsonChunk); + for (int d = 0; d < depth; d++) { json += " } "; } + spec[k] = json; + } + } + return i; +} + +static int parseAutomationSpec(jsmntok_t const* tokens, int i, const char* jsonChunk, + AutomationSpec* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + Settings base; + vector> permute; + for (int j = 0; j < size; ++j) { + const jsmntok_t tok = tokens[i]; + CHECK_KEY(tok); + if (0 == compare(tok, jsonChunk, "name")) { + i = parse(tokens, i + 1, jsonChunk, &out->name); + if (VERBOSE) { + slog.i << "Building spec [" << out->name << "]" << io::endl; + } + } else if (0 == compare(tok, jsonChunk, "base")) { + i = parseBaseSettings(tokens, i + 1, jsonChunk, &base); + } else if (0 == compare(tok, jsonChunk, "permute")) { + i = parsePermutationsSpec(tokens, i + 1, jsonChunk, &permute); + } else { + slog.w << "Invalid automation key: '" << STR(tok, jsonChunk) << "'" << io::endl; + i = parse(tokens, i + 1); + } + if (i < 0) { + slog.e << "Invalid automation value: '" << STR(tok, jsonChunk) << "'" << io::endl; + return i; + } + } + + // Determine the number of permutations. + size_t caseCount = 1; + size_t propIndex = 0; + vector::const_iterator> iters(permute.size()); + for (const auto& prop : permute) { + caseCount *= prop.size(); + if (VERBOSE) { + for (const auto& s : prop) { + slog.i << " Perm: " << s.c_str() << io::endl; + } + } + iters[propIndex++] = prop.begin(); + } + out->cases.resize(caseCount); + + if (VERBOSE) { + slog.i << " Case count: " << caseCount << io::endl; + } + + size_t caseIndex = 0; + while (true) { + + // Append a copy of the current Settings object to the case list. + if (VERBOSE) { + slog.i << " Appending case " << caseIndex << io::endl; + } + out->cases[caseIndex++] = base; + + // Leave early if there are no permutations. + if (iters.empty()) { + return i; + } + + // Use a basic counting algorithm to generate the next test case. + // Bump the first digit, if it rolls back to 0 then bump the next digit, etc. + // In this case, the "digit" is an iterator into a vector of JSON strings. + propIndex = 0; + for (auto& iter : iters) { + const auto& prop = permute[propIndex++]; + if (++iter != prop.end()) { + break; + } + iter = prop.begin(); + + // Check if all permutations have been generated. + if (propIndex == permute.size()) { + assert(caseIndex == out->cases.size()); + return i; + } + } + + // Apply changes to the settings object. + for (const auto& iter : iters) { + const std::string& jsonString = *iter; + if (VERBOSE) { + slog.i << " Applying " << jsonString.c_str() << io::endl; + } + if (!readJson(jsonString.c_str(), jsonString.size(), &base)) { + return -1; + } + } + } + + return i; +} + +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, + vector* out) { + CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + int size = tokens[i++].size; + out->resize(size); + for (int j = 0; j < size && i >= 0; ++j) { + i = parseAutomationSpec(tokens, i, jsonChunk, &out->at(j)); + } + return i; +} + +bool generate(const char* jsonChunk, size_t size, vector* out) { + jsmn_parser parser = { 0, 0, 0 }; + + int tokenCount = jsmn_parse(&parser, jsonChunk, size, nullptr, 0); + if (tokenCount <= 0) { + 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); + return false; + } + + int i = parse(tokens, 0, jsonChunk, out); + free(tokens); + return i >= 0; +} + +} // namespace viewer +} // namespace filament diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index e22e4b4d6e..d688b5ea43 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -18,16 +18,12 @@ #include -#include -#include +#include "parse.h" #include -#include - -#define CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return -1; } -#define CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return -1; } -#define STR(tok, jsonChunk) std::string(jsonChunk + tok.start, tok.end - tok.start) +#include +#include using namespace utils; @@ -35,14 +31,14 @@ namespace filament { namespace viewer { // Compares a JSON string token against a C string. -static int compare(jsmntok_t tok, const char* jsonChunk, const char* str) { +int compare(jsmntok_t tok, const char* jsonChunk, const char* str) { size_t slen = strlen(str); size_t tlen = tok.end - tok.start; return (slen == tlen) ? strncmp(jsonChunk + tok.start, str, slen) : 128; } // Skips over an unused token. -static int parse(jsmntok_t const* tokens, int i) { +int parse(jsmntok_t const* tokens, int i) { int end = i + 1; while (i < end) { switch (tokens[i].type) { @@ -573,7 +569,7 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ViewSett return i; } -static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) { +int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) { CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); int size = tokens[i++].size; for (int j = 0; j < size; ++j) { diff --git a/libs/viewer/src/parse.h b/libs/viewer/src/parse.h new file mode 100644 index 0000000000..0b294cce9b --- /dev/null +++ b/libs/viewer/src/parse.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by mIcable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace filament { +namespace viewer { + +#define CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return -1; } +#define CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return -1; } +#define STR(tok, jsonChunk) std::string(jsonChunk + tok.start, tok.end - tok.start) + +struct Settings; + +int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out); +int parse(jsmntok_t const* tokens, int i); +int compare(jsmntok_t tok, const char* jsonChunk, const char* str); + +} // namespace viewer +} // namespace filament diff --git a/libs/viewer/tests/test_settings.cpp b/libs/viewer/tests/test_settings.cpp index 53db43346b..d5cee3696e 100644 --- a/libs/viewer/tests/test_settings.cpp +++ b/libs/viewer/tests/test_settings.cpp @@ -14,12 +14,15 @@ * limitations under the License. */ +#include #include #include using namespace filament::viewer; +using std::vector; + class ViewSettingsTest : public testing::Test {}; static const char* JSON_TEST_DEFAULTS = R"TXT( @@ -127,6 +130,30 @@ static const char* JSON_TEST_DEFAULTS = R"TXT( } )TXT"; +static const char* JSON_AUTOMATION_TEST = R"TXT([ + { + "name": "ppoff", + "base": { + "view.postProcessingEnabled": false + } + }, + { + "name": "viewopts", + "base": { + "view.postProcessingEnabled": true + } + "permute": { + "view.dithering": ["NONE", "TEMPORAL"], + "view.sampleCount": [1, 4], + "view.taa.enabled": [false, true], + "view.antiAliasing": ["FXAA", "NONE"], + "view.ssao.enabled": [false, true], + "view.bloom.enabled": [false, true] + } + } +] +)TXT"; + TEST_F(ViewSettingsTest, JsonTestDefaults) { Settings settings1 = {0}; ASSERT_TRUE(readJson(JSON_TEST_DEFAULTS, strlen(JSON_TEST_DEFAULTS), &settings1)); @@ -135,10 +162,18 @@ TEST_F(ViewSettingsTest, JsonTestDefaults) { Settings settings2; ASSERT_TRUE(readJson("{}", strlen("{}"), &settings2)); - ASSERT_EQ(writeJson(settings1), writeJson(settings2)); + ASSERT_FALSE(readJson("{ badly_formed }", strlen("{ badly_formed }"), &settings2)); Settings settings3; - ASSERT_EQ(writeJson(settings1), writeJson(settings3)); + ASSERT_EQ(writeJson(settings2), writeJson(settings3)); +} + +TEST_F(ViewSettingsTest, AutomationSpec) { + vector specs; + ASSERT_TRUE(generate(JSON_AUTOMATION_TEST, strlen(JSON_AUTOMATION_TEST), &specs)); + ASSERT_EQ(specs.size(), 2); + ASSERT_EQ(specs[0].cases.size(), 1); + ASSERT_EQ(specs[1].cases.size(), 1 << 6); } int main(int argc, char** argv) {