mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-08 18:02:13 +02:00
creating workload lib
This commit is contained in:
parent
714e3f445d
commit
be38a4bc71
7 changed files with 492 additions and 0 deletions
|
@ -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}
|
||||
)
|
||||
|
||||
|
|
5
libraries/workload/CMakeLists.txt
Normal file
5
libraries/workload/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
set(TARGET_NAME workload)
|
||||
setup_hifi_library()
|
||||
|
||||
link_hifi_libraries(shared)
|
||||
|
96
libraries/workload/src/workload/Space.cpp
Normal file
96
libraries/workload/src/workload/Space.cpp
Normal file
|
@ -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 <algorithm>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
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<Space::View>& views) {
|
||||
_views = views;
|
||||
}
|
||||
|
||||
void Space::recategorizeProxiesAndGetChanges(std::vector<Space::Change>& 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
libraries/workload/src/workload/Space.h
Normal file
81
libraries/workload/src/workload/Space.h
Normal file
|
@ -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 <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
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; // <x,y,z> = 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<View>& views);
|
||||
|
||||
uint32_t getNumObjects() const { return (uint32_t)(_proxies.size() - _freeIndices.size()); }
|
||||
|
||||
void recategorizeProxiesAndGetChanges(std::vector<Change>& 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<Proxy> _proxies;
|
||||
std::vector<View> _views;
|
||||
std::vector<int32_t> _freeIndices;
|
||||
};
|
||||
|
||||
} // namespace workload
|
||||
|
||||
#endif // hifi_workload_Space_h
|
8
tests/workload/CMakeLists.txt
Normal file
8
tests/workload/CMakeLists.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
# Declare dependencies
|
||||
macro (setup_testcase_dependencies)
|
||||
link_hifi_libraries(shared workload)
|
||||
package_libraries_for_deployment()
|
||||
endmacro ()
|
||||
|
||||
setup_hifi_testcase()
|
272
tests/workload/src/SpaceTests.cpp
Normal file
272
tests/workload/src/SpaceTests.cpp
Normal file
|
@ -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 <iostream>
|
||||
|
||||
#include <workload/Space.h>
|
||||
#include <StreamUtils.h>
|
||||
#include <SharedUtil.h>
|
||||
|
||||
|
||||
const float INV_SQRT_3 = 1.0f / sqrtf(3.0f);
|
||||
|
||||
QTEST_MAIN(SpaceTests)
|
||||
|
||||
void SpaceTests::testOverlaps() {
|
||||
workload::Space space;
|
||||
using Changes = std::vector<workload::Space::Change>;
|
||||
using Views = std::vector<workload::Space::View>;
|
||||
|
||||
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<workload::Space::Sphere>& 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<glm::vec3>& 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<float>& 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<uint64_t> timeToAddAll;
|
||||
std::vector<uint64_t> timeToRemoveAll;
|
||||
std::vector<uint64_t> timeToMoveView;
|
||||
std::vector<uint64_t> timeToMoveProxies;
|
||||
for (uint32_t i = 0; i < numTests; ++i) {
|
||||
|
||||
workload::Space space;
|
||||
|
||||
{ // build the views
|
||||
std::vector<glm::vec3> 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<workload::Space::View> 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<workload::Space::Sphere> proxySpheres;
|
||||
generateSpheres(n, proxySpheres);
|
||||
std::vector<int32_t> 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<glm::vec3> 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<workload::Space::View> 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<workload::Space::Change> 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<workload::Space::Sphere> 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
|
29
tests/workload/src/SpaceTests.h
Normal file
29
tests/workload/src/SpaceTests.h
Normal file
|
@ -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 <QtTest/QtTest>
|
||||
|
||||
//#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
|
Loading…
Reference in a new issue