diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 728f1467e0..c5800959f0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -211,6 +211,7 @@ link_hifi_libraries( render-utils entities-renderer avatars-renderer ui qml auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins + workload ${PLATFORM_GL_BACKEND} ) diff --git a/libraries/workload/CMakeLists.txt b/libraries/workload/CMakeLists.txt new file mode 100644 index 0000000000..3fcf00e0e9 --- /dev/null +++ b/libraries/workload/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME workload) +setup_hifi_library() + +link_hifi_libraries(shared) + diff --git a/libraries/workload/src/workload/Space.cpp b/libraries/workload/src/workload/Space.cpp new file mode 100644 index 0000000000..075f9fe477 --- /dev/null +++ b/libraries/workload/src/workload/Space.cpp @@ -0,0 +1,96 @@ +// +// Space.h +// libraries/shared/src/ +// +// Created by Andrew Meadows 2018.01.30 +// Copyright 2018 High Fidelity, Inc. +// +// Originally from lighthouse3d. Modified to utilize glm::vec3 and clean up to our coding standards +// Simple plane class. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Space.h" + +#include + +#include + +using namespace workload; + +int32_t Space::createProxy(const Space::Sphere& newSphere) { + if (_freeIndices.empty()) { + _proxies.emplace_back(Space::Proxy(newSphere)); + return (int32_t)_proxies.size() - 1; + } else { + int32_t index = _freeIndices.back(); + _freeIndices.pop_back(); + _proxies[index].sphere = newSphere; + _proxies[index].region = Space::REGION_UNKNOWN; + _proxies[index].prevRegion = Space::REGION_UNKNOWN; + return index; + } +} + +void Space::deleteProxy(int32_t proxyId) { + if (proxyId >= (int32_t)_proxies.size() || _proxies.empty()) { + return; + } + if (proxyId == (int32_t)_proxies.size() - 1) { + // remove proxy on back + _proxies.pop_back(); + if (!_freeIndices.empty()) { + // remove any freeIndices on back + std::sort(_freeIndices.begin(), _freeIndices.end()); + while(!_freeIndices.empty() && _freeIndices.back() == (int32_t)_proxies.size() - 1) { + _freeIndices.pop_back(); + _proxies.pop_back(); + } + } + } else { + _proxies[proxyId].region = Space::REGION_INVALID; + _freeIndices.push_back(proxyId); + } +} + +void Space::updateProxy(int32_t proxyId, const Space::Sphere& newSphere) { + if (proxyId >= (int32_t)_proxies.size()) { + return; + } + _proxies[proxyId].sphere = newSphere; + // TODO: when view is not changing it would be faster to recategorize each Proxy that changes. + // Otherwise, we would want to just update all changed objects, adjust the view, and then comute changes. +} + +void Space::setViews(const std::vector& views) { + _views = views; +} + +void Space::recategorizeProxiesAndGetChanges(std::vector& changes) { + uint32_t numProxies = _proxies.size(); + uint32_t numViews = _views.size(); + for (uint32_t i = 0; i < numProxies; ++i) { + Proxy& proxy = _proxies[i]; + if (proxy.region < Space::REGION_INVALID) { + uint8_t region = Space::REGION_UNKNOWN; + for (uint32_t j = 0; j < numViews; ++j) { + float distance2 = glm::distance2(_views[j].center, glm::vec3(_proxies[i].sphere)); + for (uint8_t c = 0; c < region; ++c) { + float touchDistance = _views[j].radiuses[c] + _proxies[i].sphere.w; + if (distance2 < touchDistance * touchDistance) { + region = c; + break; + } + } + } + proxy.prevRegion = proxy.region; + proxy.region = region; + if (proxy.region != proxy.prevRegion) { + changes.emplace_back(Space::Change((int32_t)i, proxy.region, proxy.prevRegion)); + } + } + } +} + diff --git a/libraries/workload/src/workload/Space.h b/libraries/workload/src/workload/Space.h new file mode 100644 index 0000000000..717c15e159 --- /dev/null +++ b/libraries/workload/src/workload/Space.h @@ -0,0 +1,81 @@ +// +// Space.h +// libraries/workload/src/workload +// +// Created by Andrew Meadows 2018.01.30 +// Copyright 2018 High Fidelity, Inc. +// +// Originally from lighthouse3d. Modified to utilize glm::vec3 and clean up to our coding standards +// Simple plane class. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_workload_Space_h +#define hifi_workload_Space_h + +#include +#include + +namespace workload { + +class Space { +public: + static const uint8_t REGION_NEAR = 0; + static const uint8_t REGION_MIDDLE = 1; + static const uint8_t REGION_FAR = 2; + static const uint8_t REGION_UNKNOWN = 3; + static const uint8_t REGION_INVALID = 4; + + using Sphere = glm::vec4; // = center, w = radius + + class Proxy { + public: + Proxy(const Sphere& s) : sphere(s) {} + Sphere sphere; + uint8_t region { REGION_UNKNOWN }; + uint8_t prevRegion { REGION_UNKNOWN }; + }; + + class View { + public: + View(const glm::vec3& pos, float nearRadius, float midRadius, float farRadius) : center(pos) { + radiuses[0] = nearRadius; + radiuses[1] = midRadius; + radiuses[2] = farRadius; + } + glm::vec3 center { 0.0f, 0.0f, 0.0f }; // these init values are not important + float radiuses[3] { 1.0f, 2.0f, 3.0f }; // these init values are not important + }; + + class Change { + public: + Change(int32_t i, uint32_t c, uint32_t p) : proxyId(i), region(c), prevRegion(p) {} + int32_t proxyId { -1 }; + uint8_t region { 0 }; + uint8_t prevRegion { 0 }; + }; + + Space() {} + + int32_t createProxy(const Sphere& sphere); + void deleteProxy(int32_t proxyId); + void updateProxy(int32_t proxyId, const Sphere& sphere); + void setViews(const std::vector& views); + + uint32_t getNumObjects() const { return (uint32_t)(_proxies.size() - _freeIndices.size()); } + + void recategorizeProxiesAndGetChanges(std::vector& changes); + +private: + // NOTE: double-buffering proxy.category and .prevRegion in their own arrays is NOT faster + // (performance is within the noise) than leaving them as data members of Proxy. + std::vector _proxies; + std::vector _views; + std::vector _freeIndices; +}; + +} // namespace workload + +#endif // hifi_workload_Space_h diff --git a/tests/workload/CMakeLists.txt b/tests/workload/CMakeLists.txt new file mode 100644 index 0000000000..53ee7acba1 --- /dev/null +++ b/tests/workload/CMakeLists.txt @@ -0,0 +1,8 @@ + +# Declare dependencies +macro (setup_testcase_dependencies) + link_hifi_libraries(shared workload) + package_libraries_for_deployment() +endmacro () + +setup_hifi_testcase() diff --git a/tests/workload/src/SpaceTests.cpp b/tests/workload/src/SpaceTests.cpp new file mode 100644 index 0000000000..17b6911899 --- /dev/null +++ b/tests/workload/src/SpaceTests.cpp @@ -0,0 +1,272 @@ +// +// SpaceTests.cpp +// tests/physics/src +// +// Created by Andrew Meadows on 2017.01.26 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "SpaceTests.h" + +#include + +#include +#include +#include + + +const float INV_SQRT_3 = 1.0f / sqrtf(3.0f); + +QTEST_MAIN(SpaceTests) + +void SpaceTests::testOverlaps() { + workload::Space space; + using Changes = std::vector; + using Views = std::vector; + + glm::vec3 viewCenter(0.0f, 0.0f, 0.0f); + float near = 1.0f; + float mid = 2.0f; + float far = 3.0f; + + Views views; + views.push_back(workload::Space::View(viewCenter, near, mid, far)); + space.setViews(views); + + int32_t proxyId = 0; + const float DELTA = 0.001f; + float proxyRadius = 0.5f; + glm::vec3 proxyPosition = viewCenter + glm::vec3(0.0f, 0.0f, far + proxyRadius + DELTA); + workload::Space::Sphere proxySphere(proxyPosition, proxyRadius); + + { // create very_far proxy + proxyId = space.createProxy(proxySphere); + QVERIFY(space.getNumObjects() == 1); + + Changes changes; + space.recategorizeProxiesAndGetChanges(changes); + QVERIFY(changes.size() == 0); + } + + { // move proxy far + float newRadius = 1.0f; + glm::vec3 newPosition = viewCenter + glm::vec3(0.0f, 0.0f, far + newRadius - DELTA); + workload::Space::Sphere newSphere(newPosition, newRadius); + space.updateProxy(proxyId, newSphere); + Changes changes; + space.recategorizeProxiesAndGetChanges(changes); + QVERIFY(changes.size() == 1); + QVERIFY(changes[0].proxyId == proxyId); + QVERIFY(changes[0].region == workload::Space::REGION_FAR); + QVERIFY(changes[0].prevRegion == workload::Space::REGION_UNKNOWN); + } + + { // move proxy mid + float newRadius = 1.0f; + glm::vec3 newPosition = viewCenter + glm::vec3(0.0f, 0.0f, mid + newRadius - DELTA); + workload::Space::Sphere newSphere(newPosition, newRadius); + space.updateProxy(proxyId, newSphere); + Changes changes; + space.recategorizeProxiesAndGetChanges(changes); + QVERIFY(changes.size() == 1); + QVERIFY(changes[0].proxyId == proxyId); + QVERIFY(changes[0].region == workload::Space::REGION_MIDDLE); + QVERIFY(changes[0].prevRegion == workload::Space::REGION_FAR); + } + + { // move proxy near + float newRadius = 1.0f; + glm::vec3 newPosition = viewCenter + glm::vec3(0.0f, 0.0f, near + newRadius - DELTA); + workload::Space::Sphere newSphere(newPosition, newRadius); + space.updateProxy(proxyId, newSphere); + Changes changes; + space.recategorizeProxiesAndGetChanges(changes); + QVERIFY(changes.size() == 1); + QVERIFY(changes[0].proxyId == proxyId); + QVERIFY(changes[0].region == workload::Space::REGION_NEAR); + QVERIFY(changes[0].prevRegion == workload::Space::REGION_MIDDLE); + } + + { // delete proxy + // NOTE: atm deleting a proxy doesn't result in a "Change" + space.deleteProxy(proxyId); + Changes changes; + space.recategorizeProxiesAndGetChanges(changes); + QVERIFY(changes.size() == 0); + QVERIFY(space.getNumObjects() == 0); + } +} + +#ifdef MANUAL_TEST + +const float WORLD_WIDTH = 1000.0f; +const float MIN_RADIUS = 1.0f; +const float MAX_RADIUS = 100.0f; + +float randomFloat() { + return 2.0f * ((float)rand() / (float)RAND_MAX) - 1.0f; +} + +glm::vec3 randomVec3() { + glm::vec3 v(randomFloat(), randomFloat(), randomFloat()); + return v; +} + +void generateSpheres(uint32_t numProxies, std::vector& spheres) { + spheres.reserve(numProxies); + for (uint32_t i = 0; i < numProxies; ++i) { + workload::Space::Sphere sphere( + WORLD_WIDTH * randomFloat(), + WORLD_WIDTH * randomFloat(), + WORLD_WIDTH * randomFloat(), + (MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS)) * randomFloat()); + spheres.push_back(sphere); + } +} + +void generatePositions(uint32_t numProxies, std::vector& positions) { + positions.reserve(numProxies); + for (uint32_t i = 0; i < numProxies; ++i) { + positions.push_back(WORLD_WIDTH * randomVec3()); + } +} + +void generateRadiuses(uint32_t numRadiuses, std::vector& radiuses) { + radiuses.reserve(numRadiuses); + for (uint32_t i = 0; i < numRadiuses; ++i) { + radiuses.push_back(MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * randomFloat()); + } +} + +void SpaceTests::benchmark() { + uint32_t numProxies[] = { 100, 1000, 10000, 100000 }; + uint32_t numTests = 4; + std::vector timeToAddAll; + std::vector timeToRemoveAll; + std::vector timeToMoveView; + std::vector timeToMoveProxies; + for (uint32_t i = 0; i < numTests; ++i) { + + workload::Space space; + + { // build the views + std::vector viewPositions; + viewPositions.push_back(glm::vec3(0.0f, 0.0f, 0.0f)); + viewPositions.push_back(glm::vec3(0.0f, 0.0f, 0.1f * WORLD_WIDTH)); + float radius0 = 0.25f * WORLD_WIDTH; + float radius1 = 0.50f * WORLD_WIDTH; + float radius2 = 0.75f * WORLD_WIDTH; + std::vector views; + views.push_back(workload::Space::View(viewPositions[0], radius0, radius1, radius2)); + views.push_back(workload::Space::View(viewPositions[1], radius0, radius1, radius2)); + space.setViews(views); + } + + // build the proxies + uint32_t n = numProxies[i]; + std::vector proxySpheres; + generateSpheres(n, proxySpheres); + std::vector proxyKeys; + proxyKeys.reserve(n); + + // measure time to put proxies in the space + uint64_t startTime = usecTimestampNow(); + for (uint32_t j = 0; j < n; ++j) { + int32_t key = space.createProxy(proxySpheres[j]); + proxyKeys.push_back(key); + } + uint64_t usec = usecTimestampNow() - startTime; + timeToAddAll.push_back(usec); + + { + // build the views + std::vector viewPositions; + viewPositions.push_back(glm::vec3(1.0f, 2.0f, 3.0f)); + viewPositions.push_back(glm::vec3(1.0f, 2.0f, 3.0f + 0.1f * WORLD_WIDTH)); + float radius0 = 0.25f * WORLD_WIDTH; + float radius1 = 0.50f * WORLD_WIDTH; + float radius2 = 0.75f * WORLD_WIDTH; + std::vector views; + views.push_back(workload::Space::View(viewPositions[0], radius0, radius1, radius2)); + views.push_back(workload::Space::View(viewPositions[1], radius0, radius1, radius2)); + space.setViews(views); + } + + // measure time to recategorize everything + std::vector changes; + startTime = usecTimestampNow(); + space.recategorizeProxiesAndGetChanges(changes); + usec = usecTimestampNow() - startTime; + timeToMoveView.push_back(usec); + + // move every 10th proxy around + const float proxySpeed = 1.0f; + std::vector newSpheres; + uint32_t numMovingProxies = n / 10; + uint32_t jstep = n / numMovingProxies; + uint32_t maxJ = numMovingProxies * jstep - 1; + glm::vec3 direction; + for (uint32_t j = 0; j < maxJ - jstep; j += jstep) { + glm::vec3 position = (glm::vec3)proxySpheres[j]; + glm::vec3 destination = (glm::vec3)proxySpheres[j + jstep]; + direction = glm::normalize(destination - position); + glm::vec3 newPosition = position + proxySpeed * direction; + newSpheres.push_back(workload::Space::Sphere(newPosition, proxySpheres[j].w)); + } + glm::vec3 position = (glm::vec3)proxySpheres[maxJ = jstep]; + glm::vec3 destination = (glm::vec3)proxySpheres[0]; + direction = glm::normalize(destination - position); + direction = position + proxySpeed * direction; + newSpheres.push_back(workload::Space::Sphere(direction, proxySpheres[0].w)); + uint32_t k = 0; + startTime = usecTimestampNow(); + for (uint32_t j = 0; j < maxJ; j += jstep) { + space.updateProxy(proxyKeys[j], newSpheres[k++]); + } + changes.clear(); + space.recategorizeProxiesAndGetChanges(changes); + usec = usecTimestampNow() - startTime; + timeToMoveProxies.push_back(usec); + + // measure time to remove proxies from space + startTime = usecTimestampNow(); + for (uint32_t j = 0; j < n; ++j) { + space.deleteProxy(proxyKeys[j]); + } + usec = usecTimestampNow() - startTime; + timeToRemoveAll.push_back(usec); + } + + std::cout << "[numProxies, timeToAddAll] = [" << std::endl; + for (uint32_t i = 0; i < timeToAddAll.size(); ++i) { + uint32_t n = numProxies[i]; + std::cout << " " << n << ", " << timeToAddAll[i] << std::endl; + } + std::cout << "];" << std::endl; + + std::cout << "[numProxies, timeToMoveView] = [" << std::endl; + for (uint32_t i = 0; i < timeToMoveView.size(); ++i) { + uint32_t n = numProxies[i]; + std::cout << " " << n << ", " << timeToMoveView[i] << std::endl; + } + std::cout << "];" << std::endl; + + std::cout << "[numProxies, timeToMoveProxies] = [" << std::endl; + for (uint32_t i = 0; i < timeToMoveProxies.size(); ++i) { + uint32_t n = numProxies[i]; + std::cout << " " << n << "/10, " << timeToMoveProxies[i] << std::endl; + } + std::cout << "];" << std::endl; + + std::cout << "[numProxies, timeToRemoveAll] = [" << std::endl; + for (uint32_t i = 0; i < timeToRemoveAll.size(); ++i) { + uint32_t n = numProxies[i]; + std::cout << " " << n << ", " << timeToRemoveAll[i] << std::endl; + } + std::cout << "];" << std::endl; +} + +#endif // MANUAL_TEST diff --git a/tests/workload/src/SpaceTests.h b/tests/workload/src/SpaceTests.h new file mode 100644 index 0000000000..153b89dfc5 --- /dev/null +++ b/tests/workload/src/SpaceTests.h @@ -0,0 +1,29 @@ +// +// SpaceTests.h +// tests/physics/src +// +// Created by Andrew Meadows on 2017.01.26 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_workload_SpaceTests_h +#define hifi_workload_SpaceTests_h + +#include + +//#define MANUAL_TEST + +class SpaceTests : public QObject { + Q_OBJECT + +private slots: + void testOverlaps(); +#ifdef MANUAL_TEST + void benchmark(); +#endif // MANUAL_TEST +}; + +#endif // hifi_workload_SpaceTests_h