Merge branch 'master' into 21055

Conflicts:
	libraries/gl/src/gl/OffscreenQmlSurface.cpp
	libraries/gl/src/gl/OffscreenQmlSurface.h
This commit is contained in:
David Rowe 2016-10-07 14:44:23 +13:00
commit a1458a26fd
22 changed files with 792 additions and 516 deletions

View file

@ -61,6 +61,7 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::set<recording::Deck>();
DependencyManager::set<recording::Recorder>();
DependencyManager::set<RecordingScriptingInterface>();
DependencyManager::set<ScriptCache>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();

View file

@ -32,11 +32,13 @@
#include <AbstractUriHandler.h>
#include <AccountManager.h>
#include <NetworkAccessManager.h>
#include <GLMHelpers.h>
#include "OffscreenGLCanvas.h"
#include "GLHelpers.h"
#include "GLLogging.h"
#include "TextureRecycler.h"
#include "Context.h"
QString fixupHifiUrl(const QString& urlString) {
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
@ -114,257 +116,8 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) {
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1);
static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2);
static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3);
static const QEvent::Type STOP = QEvent::Type(QEvent::User + 4);
class RawTextureRecycler {
public:
using TexturePtr = GLuint;
RawTextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
void setSize(const uvec2& size);
void clear();
TexturePtr getNextTexture();
void recycleTexture(GLuint texture);
private:
struct TexInfo {
TexturePtr _tex { 0 };
uvec2 _size;
bool _active { false };
TexInfo() {}
TexInfo(TexturePtr tex, const uvec2& size) : _tex(tex), _size(size) {}
};
using Map = std::map<GLuint, TexInfo>;
using Queue = std::queue<TexturePtr>;
Map _allTextures;
Queue _readyTextures;
uvec2 _size { 1920, 1080 };
bool _useMipmaps;
};
void RawTextureRecycler::setSize(const uvec2& size) {
if (size == _size) {
return;
}
_size = size;
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
std::set<Map::key_type> toDelete;
std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) {
if (!item.second._active && item.second._size != _size) {
toDelete.insert(item.first);
}
});
std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) {
_allTextures.erase(key);
});
}
void RawTextureRecycler::clear() {
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
_allTextures.clear();
}
RawTextureRecycler::TexturePtr RawTextureRecycler::getNextTexture() {
if (_readyTextures.empty()) {
TexturePtr newTexture;
glGenTextures(1, &newTexture);
glBindTexture(GL_TEXTURE_2D, newTexture);
if (_useMipmaps) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
_allTextures[newTexture] = TexInfo { newTexture, _size };
_readyTextures.push(newTexture);
}
TexturePtr result = _readyTextures.front();
_readyTextures.pop();
auto& item = _allTextures[result];
item._active = true;
return result;
}
void RawTextureRecycler::recycleTexture(GLuint texture) {
Q_ASSERT(_allTextures.count(texture));
auto& item = _allTextures[texture];
Q_ASSERT(item._active);
item._active = false;
if (item._size != _size) {
// Buh-bye
_allTextures.erase(texture);
return;
}
_readyTextures.push(item._tex);
}
class OffscreenQmlRenderThread : public QThread {
public:
OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext);
virtual ~OffscreenQmlRenderThread() = default;
virtual void run() override;
virtual bool event(QEvent *e) override;
protected:
class Queue : private QQueue<QEvent*> {
public:
void add(QEvent::Type type);
QEvent* take();
private:
QMutex _mutex;
QWaitCondition _waitCondition;
bool _isWaiting{ false };
};
friend class OffscreenQmlSurface;
QJsonObject getGLContextData();
Queue _queue;
QMutex _mutex;
QWaitCondition _waitCondition;
std::atomic<bool> _rendering { false };
QJsonObject _glData;
QMutex _glMutex;
QWaitCondition _glWait;
private:
// Event-driven methods
void init();
void render();
void resize();
void cleanup();
// Helper methods
void setupFbo();
bool allowNewFrame(uint8_t fps);
bool fetchTexture(OffscreenQmlSurface::TextureAndFence& textureAndFence);
void releaseTexture(const OffscreenQmlSurface::TextureAndFence& textureAndFence);
// Texture management
std::mutex _textureMutex;
GLuint _latestTexture { 0 };
GLsync _latestTextureFence { 0 };
std::list<OffscreenQmlSurface::TextureAndFence> _returnedTextures;
// Rendering members
OffscreenGLCanvas _canvas;
OffscreenQmlSurface* _surface{ nullptr };
QQuickWindow* _quickWindow{ nullptr };
QMyQuickRenderControl* _renderControl{ nullptr };
GLuint _fbo { 0 };
GLuint _depthStencil { 0 };
RawTextureRecycler _textures { true };
uint64_t _lastRenderTime{ 0 };
uvec2 _size{ 1920, 1080 };
QSize _newSize;
bool _quit{ false };
};
void OffscreenQmlRenderThread::Queue::add(QEvent::Type type) {
QMutexLocker locker(&_mutex);
enqueue(new QEvent(type));
if (_isWaiting) {
_waitCondition.wakeOne();
}
}
QEvent* OffscreenQmlRenderThread::Queue::take() {
QMutexLocker locker(&_mutex);
while (isEmpty()) {
_isWaiting = true;
_waitCondition.wait(&_mutex);
_isWaiting = false;
}
QEvent* e = dequeue();
return e;
}
OffscreenQmlRenderThread::OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) {
_canvas.setObjectName("OffscreenQmlRenderCanvas");
qCDebug(glLogging) << "Building QML Renderer";
if (!_canvas.create(shareContext)) {
qWarning("Failed to create OffscreenGLCanvas");
_quit = true;
return;
};
_renderControl = new QMyQuickRenderControl();
QQuickWindow::setDefaultAlphaBuffer(true);
// Create a QQuickWindow that is associated with our render control.
// This window never gets created or shown, meaning that it will never get an underlying native (platform) window.
// NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events
// NOTE: Must be created on the rendering thread or it will refuse to render,
// so we wait until after its ctor to move object/context to this thread.
_quickWindow = new QQuickWindow(_renderControl);
_quickWindow->setColor(QColor(255, 255, 255, 0));
_quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
// We can prepare, but we must wait to start() the thread until after the ctor
_renderControl->prepareThread(this);
_canvas.getContextObject()->moveToThread(this);
moveToThread(this);
_queue.add(INIT);
}
void OffscreenQmlRenderThread::run() {
qCDebug(glLogging) << "Starting QML Renderer thread";
while (!_quit) {
QEvent* e = _queue.take();
event(e);
delete e;
}
}
bool OffscreenQmlRenderThread::event(QEvent *e) {
switch (int(e->type())) {
case INIT:
init();
return true;
case RENDER:
render();
return true;
case RESIZE:
resize();
return true;
case STOP:
cleanup();
return true;
default:
return QObject::event(e);
}
}
void OffscreenQmlRenderThread::setupFbo() {
void OffscreenQmlSurface::setupFbo() {
_canvas->makeCurrent();
_textures.setSize(_size);
if (_depthStencil) {
glDeleteRenderbuffers(1, &_depthStencil);
@ -382,41 +135,12 @@ void OffscreenQmlRenderThread::setupFbo() {
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
_canvas->doneCurrent();
}
QJsonObject OffscreenQmlRenderThread::getGLContextData() {
_glMutex.lock();
if (_glData.isEmpty()) {
_glWait.wait(&_glMutex);
}
_glMutex.unlock();
return _glData;
}
void OffscreenQmlRenderThread::init() {
qCDebug(glLogging) << "Initializing QML Renderer";
if (!_canvas.makeCurrent()) {
qWarning("Failed to make context current on QML Renderer Thread");
_quit = true;
return;
}
_glMutex.lock();
_glData = ::getGLContextData();
_glMutex.unlock();
_glWait.wakeAll();
connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender);
connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate);
_renderControl->initialize(_canvas.getContext());
setupFbo();
}
void OffscreenQmlRenderThread::cleanup() {
void OffscreenQmlSurface::cleanup() {
_canvas->makeCurrent();
_renderControl->invalidate();
if (_depthStencil) {
glDeleteRenderbuffers(1, &_depthStencil);
_depthStencil = 0;
@ -427,65 +151,17 @@ void OffscreenQmlRenderThread::cleanup() {
}
_textures.clear();
_canvas.doneCurrent();
_canvas.getContextObject()->moveToThread(QCoreApplication::instance()->thread());
_quit = true;
_canvas->doneCurrent();
}
void OffscreenQmlRenderThread::resize() {
// Lock _newSize changes
{
QMutexLocker locker(&_mutex);
// Update our members
if (_quickWindow) {
_quickWindow->setGeometry(QRect(QPoint(), _newSize));
_quickWindow->contentItem()->setSize(_newSize);
}
// Qt bug in 5.4 forces this check of pixel ratio,
// even though we're rendering offscreen.
qreal pixelRatio = 1.0;
if (_renderControl && _renderControl->_renderWindow) {
pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
}
uvec2 newOffscreenSize = toGlm(_newSize * pixelRatio);
if (newOffscreenSize == _size) {
void OffscreenQmlSurface::render() {
if (_paused) {
return;
}
qCDebug(glLogging) << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio;
_size = newOffscreenSize;
}
_canvas->makeCurrent();
_textures.setSize(_size);
setupFbo();
}
void OffscreenQmlRenderThread::render() {
// Ensure we always release the main thread
Finally releaseMainThread([this] {
_waitCondition.wakeOne();
});
if (_surface->_paused) {
return;
}
_rendering = true;
Finally unmarkRenderingFlag([this] {
_rendering = false;
});
{
QMutexLocker locker(&_mutex);
_renderControl->sync();
releaseMainThread.trigger();
}
_quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y));
// Clear out any pending textures to be returned
@ -507,7 +183,6 @@ void OffscreenQmlRenderThread::render() {
}
}
try {
GLuint texture = _textures.getNextTexture();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
@ -521,56 +196,48 @@ void OffscreenQmlRenderThread::render() {
{
std::unique_lock<std::mutex> lock(_textureMutex);
// If the most recent texture was unused, we can directly recycle it
if (_latestTextureFence) {
}
if (_latestTexture) {
_textures.recycleTexture(_latestTexture);
glDeleteSync(_latestTextureFence);
_latestTexture = 0;
_latestTextureFence = 0;
if (_latestTextureAndFence.first) {
_textures.recycleTexture(_latestTextureAndFence.first);
glDeleteSync(static_cast<GLsync>(_latestTextureAndFence.second));
_latestTextureAndFence = { 0, 0 };
}
_latestTextureFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
_latestTexture = texture;
_latestTextureAndFence = { texture, glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0) };
// Fence will be used in another thread / context, so a flush is required
glFlush();
}
_quickWindow->resetOpenGLState();
_lastRenderTime = usecTimestampNow();
} catch (std::runtime_error& error) {
qWarning() << "Failed to render QML: " << error.what();
}
_canvas->doneCurrent();
}
bool OffscreenQmlRenderThread::fetchTexture(OffscreenQmlSurface::TextureAndFence& textureAndFence) {
bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) {
textureAndFence = { 0, 0 };
std::unique_lock<std::mutex> lock(_textureMutex);
if (0 == _latestTexture) {
if (0 == _latestTextureAndFence.first) {
return false;
}
// Ensure writes to the latest texture are complete before before returning it for reading
Q_ASSERT(0 != _latestTextureFence);
textureAndFence = { _latestTexture, _latestTextureFence };
_latestTextureFence = 0;
_latestTexture = 0;
textureAndFence = _latestTextureAndFence;
_latestTextureAndFence = { 0, 0 };
return true;
}
void OffscreenQmlRenderThread::releaseTexture(const OffscreenQmlSurface::TextureAndFence& textureAndFence) {
void OffscreenQmlSurface::releaseTexture(const TextureAndFence& textureAndFence) {
std::unique_lock<std::mutex> lock(_textureMutex);
_returnedTextures.push_back(textureAndFence);
}
bool OffscreenQmlRenderThread::allowNewFrame(uint8_t fps) {
bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) {
// If we already have a pending texture, don't render another one
// i.e. don't render faster than the consumer context, since it wastes
// GPU cycles on producing output that will never be seen
{
std::unique_lock<std::mutex> lock(_textureMutex);
if (0 != _latestTexture) {
if (0 != _latestTextureAndFence.first) {
return false;
}
}
@ -588,33 +255,46 @@ OffscreenQmlSurface::~OffscreenQmlSurface() {
QObject::disconnect(&_updateTimer);
QObject::disconnect(qApp);
qCDebug(glLogging) << "Stopping QML Renderer Thread " << _renderer->currentThreadId();
_renderer->_queue.add(STOP);
if (!_renderer->wait(MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) {
qWarning() << "Failed to shut down the QML Renderer Thread";
}
delete _rootItem;
delete _renderer;
delete _qmlComponent;
delete _qmlEngine;
cleanup();
_canvas->deleteLater();
_rootItem->deleteLater();
_qmlComponent->deleteLater();
_qmlEngine->deleteLater();
_quickWindow->deleteLater();
}
void OffscreenQmlSurface::onAboutToQuit() {
_paused = true;
QObject::disconnect(&_updateTimer);
}
void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
qCDebug(glLogging) << "Building QML surface";
_renderer = new OffscreenQmlRenderThread(this, shareContext);
_renderer->moveToThread(_renderer);
_renderer->setObjectName("QML Renderer Thread");
_renderer->start();
_renderControl = new QMyQuickRenderControl();
_renderer->_renderControl->_renderWindow = _proxyWindow;
QQuickWindow::setDefaultAlphaBuffer(true);
connect(_renderer->_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged);
// Create a QQuickWindow that is associated with our render control.
// This window never gets created or shown, meaning that it will never get an underlying native (platform) window.
// NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events
// NOTE: Must be created on the rendering thread or it will refuse to render,
// so we wait until after its ctor to move object/context to this thread.
_quickWindow = new QQuickWindow(_renderControl);
_quickWindow->setColor(QColor(255, 255, 255, 0));
_quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
_renderControl->_renderWindow = _proxyWindow;
_canvas = new OffscreenGLCanvas();
if (!_canvas->create(shareContext)) {
qFatal("Failed to create OffscreenGLCanvas");
return;
};
connect(_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged);
// Create a QML engine.
_qmlEngine = new QQmlEngine;
@ -625,13 +305,26 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
importList.insert(importList.begin(), PathUtils::resourcesPath());
_qmlEngine->setImportPathList(importList);
if (!_qmlEngine->incubationController()) {
_qmlEngine->setIncubationController(_renderer->_quickWindow->incubationController());
_qmlEngine->setIncubationController(_quickWindow->incubationController());
}
_qmlEngine->rootContext()->setContextProperty("GL", _renderer->getGLContextData());
// FIXME
_qmlEngine->rootContext()->setContextProperty("GL", _glData);
_qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow()));
_qmlComponent = new QQmlComponent(_qmlEngine);
connect(_renderControl, &QQuickRenderControl::renderRequested, [this] { _render = true; });
connect(_renderControl, &QQuickRenderControl::sceneChanged, [this] { _render = _polish = true; });
if (!_canvas->makeCurrent()) {
qWarning("Failed to make context current for QML Renderer");
return;
}
_glData = ::getGLContextData();
_renderControl->initialize(_canvas->getContext());
setupFbo();
// When Quick says there is a need to render, we will not render immediately. Instead,
// a timer with a small interval is used to get better performance.
QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick);
@ -646,7 +339,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
if (!_renderer || !_renderer->_quickWindow) {
if (!_quickWindow) {
return;
}
@ -662,7 +355,7 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
std::max(static_cast<int>(scale * newSize.height()), 10));
}
QSize currentSize = _renderer->_quickWindow->geometry().size();
QSize currentSize = _quickWindow->geometry().size();
if (newSize == currentSize && !forceResize) {
return;
}
@ -673,12 +366,26 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
_rootItem->setSize(newSize);
}
{
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_newSize = newSize;
// Update our members
_quickWindow->setGeometry(QRect(QPoint(), newSize));
_quickWindow->contentItem()->setSize(newSize);
// Qt bug in 5.4 forces this check of pixel ratio,
// even though we're rendering offscreen.
qreal pixelRatio = 1.0;
if (_renderControl && _renderControl->_renderWindow) {
pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
}
_renderer->_queue.add(RESIZE);
uvec2 newOffscreenSize = toGlm(newSize * pixelRatio);
if (newOffscreenSize == _size) {
return;
}
qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio;
_size = newOffscreenSize;
_textures.setSize(_size);
setupFbo();
}
QQuickItem* OffscreenQmlSurface::getRootItem() {
@ -710,15 +417,6 @@ void OffscreenQmlSurface::clearCache() {
getRootContext()->engine()->clearComponentCache();
}
void OffscreenQmlSurface::requestUpdate() {
_polish = true;
_render = true;
}
void OffscreenQmlSurface::requestRender() {
_render = true;
}
QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f) {
disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0);
if (_qmlComponent->isError()) {
@ -786,8 +484,8 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
}
// The root item is ready. Associate it with the window.
_rootItem = newItem;
_rootItem->setParentItem(_renderer->_quickWindow->contentItem());
_rootItem->setSize(_renderer->_quickWindow->renderTargetSize());
_rootItem->setParentItem(_quickWindow->contentItem());
_rootItem->setSize(_quickWindow->renderTargetSize());
return _rootItem;
}
@ -797,34 +495,22 @@ void OffscreenQmlSurface::updateQuick() {
// b) already rendering a frame
// c) rendering too fast
// then skip this
if (!_renderer || _renderer->_rendering || !_renderer->allowNewFrame(_maxFps)) {
if (!allowNewFrame(_maxFps)) {
return;
}
if (_polish) {
_renderer->_renderControl->polishItems();
_renderControl->polishItems();
_polish = false;
}
if (_render) {
PROFILE_RANGE(__FUNCTION__);
// Lock the GUI size while syncing
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_queue.add(RENDER);
// FIXME need to find a better way to handle the render lockout than this locking of the main thread
_renderer->_waitCondition.wait(&(_renderer->_mutex));
render();
_render = false;
}
}
bool OffscreenQmlSurface::fetchTexture(TextureAndFence& texture) {
return _renderer->fetchTexture(texture);
}
void OffscreenQmlSurface::releaseTexture(const TextureAndFence& texture) {
_renderer->releaseTexture(texture);
}
QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) {
vec2 sourceSize;
if (dynamic_cast<QWidget*>(sourceObject)) {
@ -834,7 +520,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec
}
vec2 offscreenPosition = toGlm(sourcePosition);
offscreenPosition /= sourceSize;
offscreenPosition *= vec2(toGlm(_renderer->_quickWindow->size()));
offscreenPosition *= vec2(toGlm(_quickWindow->size()));
return QPointF(offscreenPosition.x, offscreenPosition.y);
}
@ -848,7 +534,7 @@ QPointF OffscreenQmlSurface::mapToVirtualScreen(const QPointF& originalPoint, QO
//
bool OffscreenQmlSurface::filterEnabled(QObject* originalDestination, QEvent* event) const {
if (_renderer->_quickWindow == originalDestination) {
if (_quickWindow == originalDestination) {
return false;
}
// Only intercept events while we're in an active state
@ -866,7 +552,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
// Don't intercept our own events, or we enter an infinite recursion
QObject* recurseTest = originalDestination;
while (recurseTest) {
Q_ASSERT(recurseTest != _rootItem && recurseTest != _renderer->_quickWindow);
Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow);
recurseTest = recurseTest->parent();
}
#endif
@ -884,7 +570,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
case QEvent::KeyPress:
case QEvent::KeyRelease: {
event->ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, event)) {
if (QCoreApplication::sendEvent(_quickWindow, event)) {
return event->isAccepted();
}
break;
@ -898,7 +584,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
wheelEvent->delta(), wheelEvent->buttons(),
wheelEvent->modifiers(), wheelEvent->orientation());
mappedEvent.ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
return mappedEvent.isAccepted();
}
break;
@ -919,7 +605,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
_qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos);
}
mappedEvent.ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
return mappedEvent.isAccepted();
}
break;
@ -938,7 +624,7 @@ void OffscreenQmlSurface::pause() {
void OffscreenQmlSurface::resume() {
_paused = false;
requestRender();
_render = true;
getRootItem()->setProperty("eventBridge", QVariant::fromValue(this));
getRootContext()->setContextProperty("webEntity", this);
@ -950,8 +636,8 @@ bool OffscreenQmlSurface::isPaused() const {
void OffscreenQmlSurface::setProxyWindow(QWindow* window) {
_proxyWindow = window;
if (_renderer && _renderer->_renderControl) {
_renderer->_renderControl->_renderWindow = window;
if (_renderControl) {
_renderControl->_renderWindow = window;
}
}
@ -960,11 +646,11 @@ QObject* OffscreenQmlSurface::getEventHandler() {
}
QQuickWindow* OffscreenQmlSurface::getWindow() {
return _renderer->_quickWindow;
return _quickWindow;
}
QSize OffscreenQmlSurface::size() const {
return _renderer->_quickWindow->geometry().size();
return _quickWindow->geometry().size();
}
QQmlContext* OffscreenQmlSurface::getRootContext() {

View file

@ -9,24 +9,27 @@
#ifndef hifi_OffscreenQmlSurface_h
#define hifi_OffscreenQmlSurface_h
#include <QTimer>
#include <QUrl>
#include <atomic>
#include <functional>
#include <QtCore/QJsonObject>
#include <QTimer>
#include <QUrl>
#include <GLMHelpers.h>
#include <ThreadHelpers.h>
#include "TextureRecycler.h"
class QWindow;
class QMyQuickRenderControl;
class OffscreenGLCanvas;
class QOpenGLContext;
class QQmlEngine;
class QQmlContext;
class QQmlComponent;
class QQuickWindow;
class QQuickItem;
class OffscreenQmlRenderThread;
class OffscreenQmlSurface : public QObject {
Q_OBJECT
Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged)
@ -88,8 +91,6 @@ signals:
void focusTextChanged(bool focusText);
public slots:
void requestUpdate();
void requestRender();
void onAboutToQuit();
void focusDestroyed(QObject *obj);
@ -109,24 +110,44 @@ protected:
private:
QObject* finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f);
QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject);
void setupFbo();
bool allowNewFrame(uint8_t fps);
void render();
void cleanup();
QJsonObject getGLContextData();
private slots:
void updateQuick();
void onFocusObjectChanged(QObject* newFocus);
private:
friend class OffscreenQmlRenderThread;
OffscreenQmlRenderThread* _renderer{ nullptr };
QQmlEngine* _qmlEngine{ nullptr };
QQmlComponent* _qmlComponent{ nullptr };
QQuickItem* _rootItem{ nullptr };
QQuickWindow* _quickWindow { nullptr };
QMyQuickRenderControl* _renderControl{ nullptr };
QQmlEngine* _qmlEngine { nullptr };
QQmlComponent* _qmlComponent { nullptr };
QQuickItem* _rootItem { nullptr };
OffscreenGLCanvas* _canvas { nullptr };
QJsonObject _glData;
QTimer _updateTimer;
bool _render{ false };
bool _polish{ true };
bool _paused{ true };
uint32_t _fbo { 0 };
uint32_t _depthStencil { 0 };
uint64_t _lastRenderTime { 0 };
uvec2 _size { 1920, 1080 };
TextureRecycler _textures { true };
// Texture management
std::mutex _textureMutex;
TextureAndFence _latestTextureAndFence { 0, 0 };
std::list<TextureAndFence> _returnedTextures;
bool _render { false };
bool _polish { true };
bool _paused { true };
bool _focusText { false };
uint8_t _maxFps{ 60 };
MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } };
uint8_t _maxFps { 60 };
MouseTranslator _mouseTranslator { [](const QPointF& p) { return p.toPoint(); } };
QWindow* _proxyWindow { nullptr };
QQuickItem* _currentFocusItem { nullptr };

View file

@ -0,0 +1,83 @@
//
// Created by Bradley Austin Davis on 2016-10-05
// Copyright 2015 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 "TextureRecycler.h"
#include "Config.h"
#include <set>
void TextureRecycler::setSize(const uvec2& size) {
if (size == _size) {
return;
}
_size = size;
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
std::set<Map::key_type> toDelete;
std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) {
if (!item.second._active && item.second._size != _size) {
toDelete.insert(item.first);
}
});
std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) {
_allTextures.erase(key);
});
}
void TextureRecycler::clear() {
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
_allTextures.clear();
}
uint32_t TextureRecycler::getNextTexture() {
if (_readyTextures.empty()) {
uint32_t newTexture;
glGenTextures(1, &newTexture);
glBindTexture(GL_TEXTURE_2D, newTexture);
if (_useMipmaps) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
_allTextures.emplace(std::piecewise_construct, std::forward_as_tuple(newTexture), std::forward_as_tuple(newTexture, _size));
_readyTextures.push(newTexture);
}
uint32_t result = _readyTextures.front();
_readyTextures.pop();
auto& item = _allTextures[result];
item._active = true;
return result;
}
void TextureRecycler::recycleTexture(GLuint texture) {
Q_ASSERT(_allTextures.count(texture));
auto& item = _allTextures[texture];
Q_ASSERT(item._active);
item._active = false;
if (item._size != _size) {
// Buh-bye
_allTextures.erase(texture);
return;
}
_readyTextures.push(item._tex);
}

View file

@ -0,0 +1,47 @@
//
// Created by Bradley Austin Davis on 2015-04-04
// Copyright 2015 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
//
#pragma once
#ifndef hifi_TextureRecycler_h
#define hifi_TextureRecycler_h
#include <atomic>
#include <queue>
#include <map>
#include <GLMHelpers.h>
class TextureRecycler {
public:
TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
void setSize(const uvec2& size);
void clear();
uint32_t getNextTexture();
void recycleTexture(uint32_t texture);
private:
struct TexInfo {
const uint32_t _tex{ 0 };
const uvec2 _size;
bool _active { false };
TexInfo() {}
TexInfo(uint32_t tex, const uvec2& size) : _tex(tex), _size(size) {}
TexInfo(const TexInfo& other) : _tex(other._tex), _size(other._size) {}
};
using Map = std::map<uint32_t, TexInfo>;
using Queue = std::queue<uint32_t>;
Map _allTextures;
Queue _readyTextures;
uvec2 _size{ 1920, 1080 };
bool _useMipmaps;
};
#endif

View file

@ -394,12 +394,14 @@ void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, Message
auto senderNode = node.toStrongRef();
if (!senderNode) {
qCWarning(asset_client) << "Got completed asset for node that no longer exists";
return;
}
// Check if we have any pending requests for this node
auto messageMapIt = _pendingRequests.find(senderNode);
if (messageMapIt == _pendingRequests.end()) {
qCWarning(asset_client) << "Got completed asset for a node that doesn't have any pending requests";
return;
}
@ -409,6 +411,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, Message
// Check if we have this pending request
auto requestIt = messageCallbackMap.find(messageID);
if (requestIt == messageCallbackMap.end()) {
qCWarning(asset_client) << "Got completed asset for a request that doesn't exist";
return;
}
@ -416,6 +419,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer<Node>& node, Message
auto& message = callbacks.message;
if (!message) {
qCWarning(asset_client) << "Got completed asset for a message that doesn't exist";
return;
}

View file

@ -107,9 +107,11 @@ void AssetRequest::start() {
auto assetClient = DependencyManager::get<AssetClient>();
auto that = QPointer<AssetRequest>(this); // Used to track the request's lifetime
auto hash = _hash;
_assetRequestID = assetClient->getAsset(_hash, start, end,
[this, that, start, end](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
[this, that, hash, start, end](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
if (!that) {
qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
// If the request is dead, return
return;
}

View file

@ -49,6 +49,7 @@ public:
const State& getState() const { return _state; }
const Error& getError() const { return _error; }
QUrl getUrl() const { return ::getATPUrl(_hash); }
QString getHash() const { return _hash; }
signals:
void finished(AssetRequest* thisRequest);

View file

@ -14,6 +14,7 @@
#include "AssetClient.h"
#include "AssetUtils.h"
#include "MappingRequest.h"
#include <QtCore/qloggingcategory.h>
AssetResourceRequest::~AssetResourceRequest() {
if (_assetMappingRequest) {
@ -23,6 +24,10 @@ AssetResourceRequest::~AssetResourceRequest() {
if (_assetRequest) {
_assetRequest->deleteLater();
}
if (_sendTimer) {
cleanupTimer();
}
}
bool AssetResourceRequest::urlIsAssetHash() const {
@ -32,6 +37,24 @@ bool AssetResourceRequest::urlIsAssetHash() const {
return hashRegex.exactMatch(_url.toString());
}
void AssetResourceRequest::setupTimer() {
Q_ASSERT(!_sendTimer);
static const int TIMEOUT_MS = 2000;
_sendTimer = new QTimer(this);
connect(_sendTimer, &QTimer::timeout, this, &AssetResourceRequest::onTimeout);
_sendTimer->setSingleShot(true);
_sendTimer->start(TIMEOUT_MS);
}
void AssetResourceRequest::cleanupTimer() {
Q_ASSERT(_sendTimer);
disconnect(_sendTimer, 0, this, 0);
_sendTimer->deleteLater();
_sendTimer = nullptr;
}
void AssetResourceRequest::doSend() {
// We'll either have a hash or an ATP path to a file (that maps to a hash)
if (urlIsAssetHash()) {
@ -58,6 +81,8 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
Q_ASSERT(_state == InProgress);
Q_ASSERT(request == _assetMappingRequest);
cleanupTimer();
switch (request->getError()) {
case MappingRequest::NoError:
// we have no error, we should have a resulting hash - use that to send of a request for that asset
@ -93,6 +118,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
_assetMappingRequest = nullptr;
});
setupTimer();
_assetMappingRequest->start();
}
@ -102,12 +128,14 @@ void AssetResourceRequest::requestHash(const AssetHash& hash) {
auto assetClient = DependencyManager::get<AssetClient>();
_assetRequest = assetClient->createRequest(hash);
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::progress);
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::onDownloadProgress);
connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
Q_ASSERT(_state == InProgress);
Q_ASSERT(req == _assetRequest);
Q_ASSERT(req->getState() == AssetRequest::Finished);
cleanupTimer();
switch (req->getError()) {
case AssetRequest::Error::NoError:
_data = req->getData();
@ -134,9 +162,35 @@ void AssetResourceRequest::requestHash(const AssetHash& hash) {
_assetRequest = nullptr;
});
setupTimer();
_assetRequest->start();
}
void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
Q_ASSERT(_state == InProgress);
// We've received data, so reset the timer
_sendTimer->start();
emit progress(bytesReceived, bytesTotal);
}
void AssetResourceRequest::onTimeout() {
if (_state == InProgress) {
qWarning() << "Asset request timed out: " << _url;
if (_assetRequest) {
disconnect(_assetRequest, 0, this, 0);
_assetRequest->deleteLater();
_assetRequest = nullptr;
}
if (_assetMappingRequest) {
disconnect(_assetMappingRequest, 0, this, 0);
_assetMappingRequest->deleteLater();
_assetMappingRequest = nullptr;
}
_result = Timeout;
_state = Finished;
emit finished();
}
cleanupTimer();
}

View file

@ -28,13 +28,19 @@ protected:
private slots:
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onTimeout();
private:
void setupTimer();
void cleanupTimer();
bool urlIsAssetHash() const;
void requestMappingForPath(const AssetPath& path);
void requestHash(const AssetHash& hash);
QTimer* _sendTimer { nullptr };
GetMappingRequest* _assetMappingRequest { nullptr };
AssetRequest* _assetRequest { nullptr };
};

View file

@ -21,7 +21,6 @@
AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
_engine(engine)
{
}
void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {

View file

@ -13,12 +13,14 @@
#include <QNetworkReply>
#include <QFile>
#include <QPointer>
#include "ScriptEngineLogging.h"
#include "BatchLoader.h"
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#include "ResourceManager.h"
#include "ScriptEngines.h"
#include "ScriptCache.h"
BatchLoader::BatchLoader(const QList<QUrl>& urls)
: QObject(),
@ -38,30 +40,25 @@ void BatchLoader::start() {
for (const auto& rawURL : _urls) {
QUrl url = expandScriptUrl(normalizeScriptURL(rawURL));
auto request = ResourceManager::createResourceRequest(this, url);
if (!request) {
_data.insert(url, QString());
qCDebug(scriptengine) << "Could not load" << url;
continue;
qCDebug(scriptengine) << "Loading script at " << url;
QPointer<BatchLoader> self = this;
DependencyManager::get<ScriptCache>()->getScriptContents(url.toString(), [this, self](const QString& url, const QString& contents, bool isURL, bool success) {
if (!self) {
return;
}
connect(request, &ResourceRequest::finished, this, [=]() {
if (request->getResult() == ResourceRequest::Success) {
_data.insert(url, request->getData());
if (isURL && success) {
_data.insert(url, contents);
qCDebug(scriptengine) << "Loaded: " << url;
} else {
_data.insert(url, QString());
qCDebug(scriptengine) << "Could not load" << url;
}
request->deleteLater();
checkFinished();
});
// If we end up being destroyed before the reply finishes, clean it up
connect(this, &QObject::destroyed, request, &QObject::deleteLater);
qCDebug(scriptengine) << "Loading script at " << url;
request->send();
}, false);
}
checkFinished();
}

View file

@ -25,6 +25,7 @@
#include "ScriptEngines.h"
#include "ScriptEngineLogging.h"
#include <QtCore/QTimer>
ScriptCache::ScriptCache(QObject* parent) {
// nothing to do here...
@ -133,8 +134,11 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
contentAvailable(url.toString(), scriptContent, true, true);
} else {
bool alreadyWaiting = _contentCallbacks.contains(url);
_contentCallbacks.insert(url, contentAvailable);
auto& scriptRequest = _activeScriptRequests[url];
bool alreadyWaiting = scriptRequest.scriptUsers.size() > 0;
scriptRequest.scriptUsers.push_back(contentAvailable);
lock.unlock();
if (alreadyWaiting) {
@ -152,6 +156,9 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
}
}
static const int MAX_RETRIES = 5;
static int START_DELAY_BETWEEN_RETRIES = 200;
void ScriptCache::scriptContentAvailable() {
#ifdef THREAD_DEBUGGING
qCDebug(scriptengine) << "ScriptCache::scriptContentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
@ -160,29 +167,59 @@ void ScriptCache::scriptContentAvailable() {
QUrl url = req->getUrl();
QString scriptContent;
QList<contentAvailableCallback> allCallbacks;
std::vector<contentAvailableCallback> allCallbacks;
bool finished { false };
bool success { false };
{
Lock lock(_containerLock);
allCallbacks = _contentCallbacks.values(url);
_contentCallbacks.remove(url);
Q_ASSERT(req->getState() == ResourceRequest::Finished);
success = req->getResult() == ResourceRequest::Success;
Lock lock(_containerLock);
if (_activeScriptRequests.contains(url)) {
auto& scriptRequest = _activeScriptRequests[url];
if (success) {
allCallbacks = scriptRequest.scriptUsers;
_activeScriptRequests.remove(url);
_scriptCache[url] = scriptContent = req->getData();
finished = true;
qCDebug(scriptengine) << "Done downloading script at:" << url.toString();
} else {
if (scriptRequest.numRetries < MAX_RETRIES) {
++scriptRequest.numRetries;
qDebug() << "Script request failed: " << url;
int timeout = exp(scriptRequest.numRetries) * START_DELAY_BETWEEN_RETRIES;
QTimer::singleShot(timeout, this, [this, url]() {
qDebug() << "Retrying script request: " << url;
auto request = ResourceManager::createResourceRequest(nullptr, url);
Q_ASSERT(request);
// We've already made a request, so the cache must be disabled or it wasn't there, so enabling
// it will do nothing.
request->setCacheEnabled(false);
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable);
request->send();
});
} else {
// Dubious, but retained here because it matches the behavior before fixing the threading
scriptContent = _scriptCache[url];
finished = true;
qCWarning(scriptengine) << "Error loading script from URL " << url;
}
}
}
}
req->deleteLater();
if (!DependencyManager::get<ScriptEngines>()->isStopped()) {
if (finished && !DependencyManager::get<ScriptEngines>()->isStopped()) {
foreach(contentAvailableCallback thisCallback, allCallbacks) {
thisCallback(url.toString(), scriptContent, true, success);
}

View file

@ -15,13 +15,19 @@
#include <mutex>
#include <ResourceCache.h>
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
class ScriptUser {
public:
virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents) = 0;
virtual void errorInLoadingScript(const QUrl& url) = 0;
};
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
class ScriptRequest {
public:
std::vector<contentAvailableCallback> scriptUsers { };
int numRetries { 0 };
};
/// Interface for loading scripts
class ScriptCache : public QObject, public Dependency {
@ -51,7 +57,7 @@ private:
ScriptCache(QObject* parent = NULL);
Mutex _containerLock;
QMultiMap<QUrl, contentAvailableCallback> _contentCallbacks;
QMap<QUrl, ScriptRequest> _activeScriptRequests;
QHash<QUrl, QString> _scriptCache;
QMultiMap<QUrl, ScriptUser*> _scriptUsers;

View file

@ -34,7 +34,6 @@ namespace Setting {
DependencyManager::destroy<Manager>();
//
globalManager->deleteLater();
globalManager.reset();
// quit the settings manager thread and wait on it to make sure it's gone
@ -72,9 +71,9 @@ namespace Setting {
globalManager = DependencyManager::set<Manager>();
QObject::connect(globalManager.data(), SIGNAL(destroyed()), thread, SLOT(quit()));
QObject::connect(thread, SIGNAL(started()), globalManager.data(), SLOT(startTimer()));
QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
QObject::connect(thread, SIGNAL(finished()), globalManager.data(), SLOT(deleteLater()));
globalManager->moveToThread(thread);
thread->start();
qCDebug(shared) << "Settings thread started.";

View file

@ -10,3 +10,6 @@ set_target_properties(vhacd-util PROPERTIES FOLDER "Tools")
add_subdirectory(ice-client)
set_target_properties(ice-client PROPERTIES FOLDER "Tools")
add_subdirectory(ac-client)
set_target_properties(ac-client PROPERTIES FOLDER "Tools")

View file

@ -0,0 +1,3 @@
set(TARGET_NAME ac-client)
setup_hifi_project(Core Widgets)
link_hifi_libraries(shared networking)

View file

@ -0,0 +1,252 @@
//
// ACClientApp.cpp
// tools/ac-client/src
//
// Created by Seth Alves on 2016-10-5
// Copyright 2015 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 <QDataStream>
#include <QThread>
#include <QLoggingCategory>
#include <QCommandLineParser>
#include <NetworkLogging.h>
#include <SharedLogging.h>
#include <AddressManager.h>
#include <DependencyManager.h>
#include <SettingHandle.h>
#include "ACClientApp.h"
ACClientApp::ACClientApp(int argc, char* argv[]) :
QCoreApplication(argc, argv)
{
// parse command-line
QCommandLineParser parser;
parser.setApplicationDescription("High Fidelity AC client");
const QCommandLineOption helpOption = parser.addHelpOption();
const QCommandLineOption verboseOutput("v", "verbose output");
parser.addOption(verboseOutput);
const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1");
parser.addOption(domainAddressOption);
const QCommandLineOption cacheSTUNOption("s", "cache stun-server response");
parser.addOption(cacheSTUNOption);
const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT));
parser.addOption(listenPortOption);
if (!parser.parse(QCoreApplication::arguments())) {
qCritical() << parser.errorText() << endl;
parser.showHelp();
Q_UNREACHABLE();
}
if (parser.isSet(helpOption)) {
parser.showHelp();
Q_UNREACHABLE();
}
_verbose = parser.isSet(verboseOutput);
if (!_verbose) {
QLoggingCategory::setFilterRules("qt.network.ssl.warning=false");
const_cast<QLoggingCategory*>(&networking())->setEnabled(QtDebugMsg, false);
const_cast<QLoggingCategory*>(&networking())->setEnabled(QtInfoMsg, false);
const_cast<QLoggingCategory*>(&networking())->setEnabled(QtWarningMsg, false);
const_cast<QLoggingCategory*>(&shared())->setEnabled(QtDebugMsg, false);
const_cast<QLoggingCategory*>(&shared())->setEnabled(QtInfoMsg, false);
const_cast<QLoggingCategory*>(&shared())->setEnabled(QtWarningMsg, false);
}
QString domainServerAddress = "127.0.0.1:40103";
if (parser.isSet(domainAddressOption)) {
domainServerAddress = parser.value(domainAddressOption);
}
if (_verbose) {
qDebug() << "domain-server address is" << domainServerAddress;
}
int listenPort = INVALID_PORT;
if (parser.isSet(listenPortOption)) {
listenPort = parser.value(listenPortOption).toInt();
}
Setting::preInit();
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
Setting::init();
DependencyManager::set<AccountManager>([&]{ return QString("Mozilla/5.0 (HighFidelityACClient)"); });
DependencyManager::set<AddressManager>();
DependencyManager::set<NodeList>(NodeType::Agent, listenPort);
auto nodeList = DependencyManager::get<NodeList>();
// start the nodeThread so its event loop is running
QThread* nodeThread = new QThread(this);
nodeThread->setObjectName("NodeList Thread");
nodeThread->start();
// make sure the node thread is given highest priority
nodeThread->setPriority(QThread::TimeCriticalPriority);
// setup a timer for domain-server check ins
QTimer* domainCheckInTimer = new QTimer(nodeList.data());
connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn);
domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);
// put the NodeList and datagram processing on the node thread
nodeList->moveToThread(nodeThread);
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
// connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
// connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ACClientApp::domainConnectionRefused);
connect(nodeList.data(), &NodeList::nodeAdded, this, &ACClientApp::nodeAdded);
connect(nodeList.data(), &NodeList::nodeKilled, this, &ACClientApp::nodeKilled);
connect(nodeList.data(), &NodeList::nodeActivated, this, &ACClientApp::nodeActivated);
// connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID);
// connect(nodeList.data(), &NodeList::uuidChanged, this, &ACClientApp::setSessionUUID);
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ACClientApp::notifyPacketVersionMismatch);
nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer
<< NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer);
DependencyManager::get<AddressManager>()->handleLookupString(domainServerAddress, false);
QTimer* doTimer = new QTimer(this);
doTimer->setSingleShot(true);
connect(doTimer, &QTimer::timeout, this, &ACClientApp::timedOut);
doTimer->start(4000);
}
ACClientApp::~ACClientApp() {
}
void ACClientApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) {
qDebug() << "domainConnectionRefused";
}
void ACClientApp::domainChanged(const QString& domainHostname) {
if (_verbose) {
qDebug() << "domainChanged";
}
}
void ACClientApp::nodeAdded(SharedNodePointer node) {
if (_verbose) {
qDebug() << "node added: " << node->getType();
}
}
void ACClientApp::nodeActivated(SharedNodePointer node) {
if (node->getType() == NodeType::EntityServer) {
if (_verbose) {
qDebug() << "saw EntityServer";
}
_sawEntityServer = true;
}
else if (node->getType() == NodeType::AudioMixer) {
if (_verbose) {
qDebug() << "saw AudioMixer";
}
_sawAudioMixer = true;
}
else if (node->getType() == NodeType::AvatarMixer) {
if (_verbose) {
qDebug() << "saw AvatarMixer";
}
_sawAvatarMixer = true;
}
else if (node->getType() == NodeType::AssetServer) {
if (_verbose) {
qDebug() << "saw AssetServer";
}
_sawAssetServer = true;
}
else if (node->getType() == NodeType::MessagesMixer) {
if (_verbose) {
qDebug() << "saw MessagesMixer";
}
_sawMessagesMixer = true;
}
if (_sawEntityServer && _sawAudioMixer && _sawAvatarMixer && _sawAssetServer && _sawMessagesMixer) {
if (_verbose) {
qDebug() << "success";
}
finish(0);
}
}
void ACClientApp::nodeKilled(SharedNodePointer node) {
qDebug() << "nodeKilled";
}
void ACClientApp::timedOut() {
if (_verbose) {
qDebug() << "timed out: " << _sawEntityServer << _sawAudioMixer <<
_sawAvatarMixer << _sawAssetServer << _sawMessagesMixer;
}
finish(1);
}
void ACClientApp::notifyPacketVersionMismatch() {
if (_verbose) {
qDebug() << "packet version mismatch";
}
finish(1);
}
void ACClientApp::printFailedServers() {
if (!_sawEntityServer) {
qDebug() << "EntityServer";
}
if (!_sawAudioMixer) {
qDebug() << "AudioMixer";
}
if (!_sawAvatarMixer) {
qDebug() << "AvatarMixer";
}
if (!_sawAssetServer) {
qDebug() << "AssetServer";
}
if (!_sawMessagesMixer) {
qDebug() << "MessagesMixer";
}
}
void ACClientApp::finish(int exitCode) {
auto nodeList = DependencyManager::get<NodeList>();
// send the domain a disconnect packet, force stoppage of domain-server check-ins
nodeList->getDomainHandler().disconnect();
nodeList->setIsShuttingDown(true);
// tell the packet receiver we're shutting down, so it can drop packets
nodeList->getPacketReceiver().setShouldDropPackets(true);
QThread* nodeThread = DependencyManager::get<NodeList>()->thread();
// remove the NodeList from the DependencyManager
DependencyManager::destroy<NodeList>();
// ask the node thread to quit and wait until it is done
nodeThread->quit();
nodeThread->wait();
printFailedServers();
QCoreApplication::exit(exitCode);
}

View file

@ -0,0 +1,52 @@
//
// ACClientApp.h
// tools/ac-client/src
//
// Created by Seth Alves on 2016-10-5
// Copyright 2016 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_ACClientApp_h
#define hifi_ACClientApp_h
#include <QApplication>
#include <udt/Constants.h>
#include <udt/Socket.h>
#include <ReceivedMessage.h>
#include <NetworkPeer.h>
#include <NodeList.h>
class ACClientApp : public QCoreApplication {
Q_OBJECT
public:
ACClientApp(int argc, char* argv[]);
~ACClientApp();
private slots:
void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo);
void domainChanged(const QString& domainHostname);
void nodeAdded(SharedNodePointer node);
void nodeActivated(SharedNodePointer node);
void nodeKilled(SharedNodePointer node);
void notifyPacketVersionMismatch();
private:
NodeList* _nodeList;
void timedOut();
void printFailedServers();
void finish(int exitCode);
bool _verbose;
bool _sawEntityServer { false };
bool _sawAudioMixer { false };
bool _sawAvatarMixer { false };
bool _sawAssetServer { false };
bool _sawMessagesMixer { false };
};
#endif //hifi_ACClientApp_h

View file

@ -0,0 +1,23 @@
//
// main.cpp
// tools/ice-client/src
//
// Created by Seth Alves on 2016-10-5
// Copyright 2016 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 <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include "ACClientApp.h"
using namespace std;
int main(int argc, char * argv[]) {
ACClientApp app(argc, argv);
return app.exec();
}

View file

@ -178,16 +178,9 @@ void ICEClientApp::doSomething() {
qDebug() << "sending STUN request";
}
_socket->writeDatagram(stunRequestPacket, sizeof(stunRequestPacket), _stunSockAddr);
_stunResponseTimerCanceled = false;
_stunResponseTimer.singleShot(stunResponseTimeoutMilliSeconds, this, [&] {
if (_stunResponseTimerCanceled) {
return;
}
if (_verbose) {
qDebug() << "timeout waiting for stun-server response";
}
QCoreApplication::exit(stunFailureExitStatus);
});
_stunResponseTimer.setSingleShot(true);
connect(&_stunResponseTimer, SIGNAL(timeout()), this, SLOT(stunResponseTimeout()));
_stunResponseTimer.start(stunResponseTimeoutMilliSeconds);
setState(waitForStunResponse);
} else {
@ -215,16 +208,9 @@ void ICEClientApp::doSomething() {
}
sendPacketToIceServer(PacketType::ICEServerQuery, _iceServerAddr, _sessionUUID, peerID);
_iceResponseTimerCanceled = false;
_iceResponseTimer.singleShot(iceResponseTimeoutMilliSeconds, this, [=] {
if (_iceResponseTimerCanceled) {
return;
}
if (_verbose) {
qDebug() << "timeout waiting for ice-server response";
}
QCoreApplication::exit(iceFailureExitStatus);
});
_iceResponseTimer.setSingleShot(true);
connect(&_iceResponseTimer, SIGNAL(timeout()), this, SLOT(iceResponseTimeout()));
_iceResponseTimer.start(iceResponseTimeoutMilliSeconds);
} else if (_state == pause0) {
setState(pause1);
} else if (_state == pause1) {
@ -237,6 +223,20 @@ void ICEClientApp::doSomething() {
}
}
void ICEClientApp::iceResponseTimeout() {
if (_verbose) {
qDebug() << "timeout waiting for ice-server response";
}
QCoreApplication::exit(iceFailureExitStatus);
}
void ICEClientApp::stunResponseTimeout() {
if (_verbose) {
qDebug() << "timeout waiting for stun-server response";
}
QCoreApplication::exit(stunFailureExitStatus);
}
void ICEClientApp::sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr,
const QUuid& clientID, const QUuid& peerID) {
std::unique_ptr<NLPacket> icePacket = NLPacket::create(packetType);
@ -298,7 +298,6 @@ void ICEClientApp::processSTUNResponse(std::unique_ptr<udt::BasePacket> packet)
}
_stunResponseTimer.stop();
_stunResponseTimerCanceled = true;
uint16_t newPublicPort;
QHostAddress newPublicAddress;
@ -331,7 +330,6 @@ void ICEClientApp::processPacket(std::unique_ptr<udt::Packet> packet) {
if (nlPacket->getType() == PacketType::ICEServerPeerInformation) {
// cancel the timeout timer
_iceResponseTimer.stop();
_iceResponseTimerCanceled = true;
QDataStream iceResponseStream(message->getMessage());
if (!_domainServerPeerSet) {

View file

@ -33,6 +33,10 @@ public:
const int stunResponseTimeoutMilliSeconds { 2000 };
const int iceResponseTimeoutMilliSeconds { 2000 };
public slots:
void iceResponseTimeout();
void stunResponseTimeout();
private:
enum State {
lookUpStunServer, // 0
@ -83,9 +87,7 @@ private:
int _state { 0 };
QTimer _stunResponseTimer;
bool _stunResponseTimerCanceled { false };
QTimer _iceResponseTimer;
bool _iceResponseTimerCanceled { false };
int _domainPingCount { 0 };
};