Merge pull request #7399 from zzmp/fix/qml-thread

Move QML rendering to a defined thread
This commit is contained in:
Brad Hefta-Gaub 2016-03-21 15:31:42 -07:00
commit a6bf84b90e
4 changed files with 271 additions and 261 deletions

View file

@ -85,7 +85,6 @@ namespace MenuOption {
const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene";
const QString EchoLocalAudio = "Echo Local Audio";
const QString EchoServerAudio = "Echo Server Audio";
const QString Enable3DTVMode = "Enable 3DTV Mode";
const QString EnableCharacterController = "Enable avatar collisions";
const QString EnableInverseKinematics = "Enable Inverse Kinematics";
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";

View file

@ -15,8 +15,9 @@
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickWindow>
#include <QtQuick/QQuickRenderControl>
#include <QtCore/QWaitCondition>
#include <QtCore/QThread>
#include <QtCore/QMutex>
#include <QtCore/QWaitCondition>
#include <shared/NsightHelpers.h>
#include <PerfStat.h>
@ -47,11 +48,10 @@ protected:
private:
QWindow* _renderWindow{ nullptr };
friend class OffscreenQmlRenderer;
friend class OffscreenQmlRenderThread;
friend class OffscreenQmlSurface;
};
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
@ -60,268 +60,281 @@ 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 OffscreenQmlRenderer : public OffscreenGLCanvas {
friend class OffscreenQmlSurface;
class OffscreenQmlRenderThread : public QThread {
public:
OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext);
virtual ~OffscreenQmlRenderThread() = default;
OffscreenQmlRenderer(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) {
if (!OffscreenGLCanvas::create(shareContext)) {
static const char* error = "Failed to create OffscreenGLCanvas";
qWarning() << error;
throw error;
};
virtual void run() override;
virtual bool event(QEvent *e) override;
_renderControl = new QMyQuickRenderControl();
protected:
class Queue : public QQueue<QEvent*> {
public:
void add(QEvent::Type type);
QEvent* take();
// Create a QQuickWindow that is associated with out render control. Note that this
// window never gets created or shown, meaning that it will never get an underlying
// native (platform) window.
QQuickWindow::setDefaultAlphaBuffer(true);
// Weirdness... QQuickWindow NEEDS to be created on the rendering thread, or it will refuse to render
// because it retains an internal 'context' object that retains the thread it was created on,
// regardless of whether you later move it to another thread.
_quickWindow = new QQuickWindow(_renderControl);
_quickWindow->setColor(QColor(255, 255, 255, 0));
_quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
// Qt 5.5
_renderControl->prepareThread(&_thread);
getContextObject()->moveToThread(&_thread);
moveToThread(&_thread);
_thread.setObjectName("QML Thread");
_thread.start();
post(INIT);
}
bool event(QEvent *e) {
switch (int(e->type())) {
case INIT:
{
QMutexLocker lock(&_mutex);
init();
}
return true;
case RENDER:
{
QMutexLocker lock(&_mutex);
render(&lock);
}
return true;
case RESIZE:
{
QMutexLocker lock(&_mutex);
resize();
}
return true;
case STOP:
{
QMutexLocker lock(&_mutex);
cleanup();
}
return true;
default:
return QObject::event(e);
}
}
void post(const QEvent::Type& type) {
QCoreApplication::postEvent(this, new QEvent(type));
}
private:
QMutex _mutex;
QWaitCondition _waitCondition;
bool _isWaiting{ false };
};
friend class OffscreenQmlSurface;
Queue _queue;
QMutex _mutex;
QWaitCondition _waitCondition;
private:
// Event-driven methods
void init();
void render();
void resize();
void cleanup();
void setupFbo() {
using namespace oglplus;
_textures.setSize(_size);
_depthStencil.reset(new Renderbuffer());
Context::Bound(Renderbuffer::Target::Renderbuffer, *_depthStencil)
.Storage(
PixelDataInternalFormat::DepthComponent,
_size.x, _size.y);
_fbo.reset(new Framebuffer());
_fbo->Bind(Framebuffer::Target::Draw);
_fbo->AttachRenderbuffer(Framebuffer::Target::Draw,
FramebufferAttachment::Depth, *_depthStencil);
DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
}
void init() {
connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender);
connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate);
if (!makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
_renderControl->initialize(getContext());
setupFbo();
_escrow.setRecycler([this](GLuint texture){
_textures.recycleTexture(texture);
});
doneCurrent();
}
void cleanup() {
if (!makeCurrent()) {
qFatal("Failed to make context current on render thread");
return;
}
_renderControl->invalidate();
_fbo.reset();
_depthStencil.reset();
_textures.clear();
doneCurrent();
getContextObject()->moveToThread(QCoreApplication::instance()->thread());
_thread.quit();
_cond.wakeOne();
}
void resize() {
// 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);
_textures.setSize(newOffscreenSize);
if (newOffscreenSize == _size) {
return;
}
_size = newOffscreenSize;
// Clear out any fbos with the old size
if (!makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
qDebug() << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio;
setupFbo();
doneCurrent();
}
void render(QMutexLocker *lock) {
if (_surface->_paused) {
return;
}
if (!makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
_renderControl->sync();
_cond.wakeOne();
lock->unlock();
using namespace oglplus;
_quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y));
try {
PROFILE_RANGE("qml_render")
TexturePtr texture = _textures.getNextTexture();
_fbo->Bind(Framebuffer::Target::Draw);
_fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0);
_fbo->Complete(Framebuffer::Target::Draw);
{
_renderControl->render();
// FIXME The web browsers seem to be leaving GL in an error state.
// Need a debug context with sync logging to figure out why.
// for now just clear the errors
glGetError();
}
// FIXME probably unecessary
DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
_quickWindow->resetOpenGLState();
_escrow.submit(GetName(*texture));
_lastRenderTime = usecTimestampNow();
} catch (std::runtime_error& error) {
qWarning() << "Failed to render QML " << error.what();
}
}
void aboutToQuit() {
QMutexLocker lock(&_quitMutex);
_quit = true;
}
static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2;
void stop() {
if (_thread.isRunning()) {
qDebug() << "Stopping QML render thread " << _thread.currentThreadId();
{
QMutexLocker lock(&_mutex);
post(STOP);
}
auto start = usecTimestampNow();
auto now = usecTimestampNow();
bool shutdownClean = false;
while (now - start < (MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) {
QMutexLocker lock(&_mutex);
if (_cond.wait(&_mutex, MSECS_PER_SECOND)) {
shutdownClean = true;
break;
}
now = usecTimestampNow();
}
if (!shutdownClean) {
qWarning() << "Failed to shut down the QML render thread";
}
} else {
qDebug() << "QML render thread already completed";
}
}
bool allowNewFrame(uint8_t fps) {
auto minRenderInterval = USECS_PER_SECOND / fps;
auto lastInterval = usecTimestampNow() - _lastRenderTime;
return (lastInterval > minRenderInterval);
}
// Helper methods
void setupFbo();
bool allowNewFrame(uint8_t fps);
// Rendering members
OffscreenGLCanvas _canvas;
OffscreenQmlSurface* _surface{ nullptr };
QQuickWindow* _quickWindow{ nullptr };
QMyQuickRenderControl* _renderControl{ nullptr };
QThread _thread;
QMutex _mutex;
QWaitCondition _cond;
QMutex _quitMutex;
QSize _newSize;
bool _quit;
FramebufferPtr _fbo;
RenderbufferPtr _depthStencil;
uvec2 _size{ 1920, 1080 };
uint64_t _lastRenderTime{ 0 };
TextureRecycler _textures;
GLTextureEscrow _escrow;
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) {
if (!_canvas.create(shareContext)) {
static const char* error = "Failed to create OffscreenGLCanvas";
qWarning() << error;
throw error;
};
_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() {
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() {
using namespace oglplus;
_textures.setSize(_size);
_depthStencil.reset(new Renderbuffer());
Context::Bound(Renderbuffer::Target::Renderbuffer, *_depthStencil)
.Storage(
PixelDataInternalFormat::DepthComponent,
_size.x, _size.y);
_fbo.reset(new Framebuffer());
_fbo->Bind(Framebuffer::Target::Draw);
_fbo->AttachRenderbuffer(Framebuffer::Target::Draw,
FramebufferAttachment::Depth, *_depthStencil);
DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
}
void OffscreenQmlRenderThread::init() {
connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender);
connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate);
if (!_canvas.makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
_renderControl->initialize(_canvas.getContext());
setupFbo();
_escrow.setRecycler([this](GLuint texture){
_textures.recycleTexture(texture);
});
_canvas.doneCurrent();
}
void OffscreenQmlRenderThread::cleanup() {
if (!_canvas.makeCurrent()) {
qFatal("Failed to make context current on render thread");
return;
}
_renderControl->invalidate();
_fbo.reset();
_depthStencil.reset();
_textures.clear();
_canvas.doneCurrent();
_canvas.getContextObject()->moveToThread(QCoreApplication::instance()->thread());
_quit = true;
}
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);
_textures.setSize(newOffscreenSize);
if (newOffscreenSize == _size) {
return;
}
_size = newOffscreenSize;
// Clear out any fbos with the old size
if (!_canvas.makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
qDebug() << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio;
locker.unlock();
setupFbo();
_canvas.doneCurrent();
}
void OffscreenQmlRenderThread::render() {
if (_surface->_paused) {
_waitCondition.wakeOne();
return;
}
if (!_canvas.makeCurrent()) {
qWarning("Failed to make context current on render thread");
return;
}
QMutexLocker locker(&_mutex);
_renderControl->sync();
_waitCondition.wakeOne();
locker.unlock();
using namespace oglplus;
_quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y));
try {
PROFILE_RANGE("qml_render")
TexturePtr texture = _textures.getNextTexture();
_fbo->Bind(Framebuffer::Target::Draw);
_fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0);
_fbo->Complete(Framebuffer::Target::Draw);
{
_renderControl->render();
// FIXME The web browsers seem to be leaving GL in an error state.
// Need a debug context with sync logging to figure out why.
// for now just clear the errors
glGetError();
}
// FIXME probably unecessary
DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
_quickWindow->resetOpenGLState();
_escrow.submit(GetName(*texture));
_lastRenderTime = usecTimestampNow();
} catch (std::runtime_error& error) {
qWarning() << "Failed to render QML " << error.what();
}
}
bool OffscreenQmlRenderThread::allowNewFrame(uint8_t fps) {
auto minRenderInterval = USECS_PER_SECOND / fps;
auto lastInterval = usecTimestampNow() - _lastRenderTime;
return (lastInterval > minRenderInterval);
}
OffscreenQmlSurface::OffscreenQmlSurface() {
}
static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2;
OffscreenQmlSurface::~OffscreenQmlSurface() {
QObject::disconnect(&_updateTimer);
QObject::disconnect(qApp);
_renderer->stop();
qDebug() << "Stopping QML render thread " << _renderer->currentThreadId();
_renderer->_queue.add(STOP);
if (!_renderer->wait(MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) {
qWarning() << "Failed to shut down the QML render thread";
}
delete _rootItem;
delete _renderer;
delete _qmlComponent;
@ -330,15 +343,16 @@ OffscreenQmlSurface::~OffscreenQmlSurface() {
void OffscreenQmlSurface::onAboutToQuit() {
QObject::disconnect(&_updateTimer);
// Disconnecting the update timer is insufficient, since the renderer
// may attempting to render already, so we need to explicitly tell the renderer
// to stop
_renderer->aboutToQuit();
}
void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
_renderer = new OffscreenQmlRenderer(this, shareContext);
_renderer = new OffscreenQmlRenderThread(this, shareContext);
_renderer->moveToThread(_renderer);
_renderer->setObjectName("QML Renderer Thread");
_renderer->start();
_renderer->_renderControl->_renderWindow = _proxyWindow;
// Create a QML engine.
_qmlEngine = new QQmlEngine;
if (!_qmlEngine->incubationController()) {
@ -387,11 +401,11 @@ void OffscreenQmlSurface::resize(const QSize& newSize_) {
}
{
QMutexLocker _locker(&(_renderer->_mutex));
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_newSize = newSize;
}
_renderer->post(RESIZE);
_renderer->_queue.add(RESIZE);
}
QQuickItem* OffscreenQmlSurface::getRootItem() {
@ -491,14 +505,11 @@ void OffscreenQmlSurface::updateQuick() {
}
if (_render) {
QMutexLocker lock(&(_renderer->_mutex));
_renderer->post(RENDER);
while (!_renderer->_cond.wait(&(_renderer->_mutex), 100)) {
if (_renderer->_quit) {
return;
}
qApp->processEvents();
}
// Lock the GUI size while syncing
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_queue.add(RENDER);
_renderer->_waitCondition.wait(&(_renderer->_mutex));
_render = false;
}

View file

@ -26,7 +26,7 @@ class QQmlComponent;
class QQuickWindow;
class QQuickItem;
class OffscreenQmlRenderer;
class OffscreenQmlRenderThread;
class OffscreenQmlSurface : public QObject {
Q_OBJECT
@ -84,8 +84,8 @@ private slots:
void updateQuick();
private:
friend class OffscreenQmlRenderer;
OffscreenQmlRenderer* _renderer{ nullptr };
friend class OffscreenQmlRenderThread;
OffscreenQmlRenderThread* _renderer{ nullptr };
QQmlEngine* _qmlEngine{ nullptr };
QQmlComponent* _qmlComponent{ nullptr };
QQuickItem* _rootItem{ nullptr };

View file

@ -76,7 +76,7 @@ QUrl ResourceManager::normalizeURL(const QUrl& originalUrl) {
}
void ResourceManager::init() {
_thread.setObjectName("Ressource Manager Thread");
_thread.setObjectName("Resource Manager Thread");
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_thread);