mirror of
https://github.com/overte-org/overte.git
synced 2025-04-07 19:12:28 +02:00
Merge pull request #8867 from jherico/offscreen_textures
Fix offscreen QML texture leak, improve texture sharing for same size surfaces
This commit is contained in:
commit
6279b273fc
11 changed files with 343 additions and 233 deletions
|
@ -98,9 +98,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) {
|
|||
PROFILE_RANGE(__FUNCTION__);
|
||||
|
||||
if (!_uiTexture) {
|
||||
_uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D([](uint32_t recycleTexture, void* recycleFence){
|
||||
DependencyManager::get<OffscreenUi>()->releaseTexture({ recycleTexture, recycleFence });
|
||||
}));
|
||||
_uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda()));
|
||||
_uiTexture->setSource(__FUNCTION__);
|
||||
}
|
||||
// Once we move UI rendering and screen rendering to different
|
||||
|
|
|
@ -49,6 +49,8 @@ Web3DOverlay::~Web3DOverlay() {
|
|||
if (_webSurface) {
|
||||
_webSurface->pause();
|
||||
_webSurface->disconnect(_connection);
|
||||
|
||||
|
||||
// The lifetime of the QML surface MUST be managed by the main thread
|
||||
// Additionally, we MUST use local variables copied by value, rather than
|
||||
// member variables, since they would implicitly refer to a this that
|
||||
|
@ -111,9 +113,7 @@ void Web3DOverlay::render(RenderArgs* args) {
|
|||
|
||||
if (!_texture) {
|
||||
auto webSurface = _webSurface;
|
||||
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D([webSurface](uint32_t recycleTexture, void* recycleFence) {
|
||||
webSurface->releaseTexture({ recycleTexture, recycleFence });
|
||||
}));
|
||||
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda()));
|
||||
_texture->setSource(__FUNCTION__);
|
||||
}
|
||||
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
|
||||
|
|
|
@ -201,10 +201,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
|
|||
|
||||
if (!_texture) {
|
||||
auto webSurface = _webSurface;
|
||||
auto recycler = [webSurface] (uint32_t recycleTexture, void* recycleFence) {
|
||||
webSurface->releaseTexture({ recycleTexture, recycleFence });
|
||||
};
|
||||
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D(recycler));
|
||||
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda()));
|
||||
_texture->setSource(__FUNCTION__);
|
||||
}
|
||||
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
#include "OffscreenQmlSurface.h"
|
||||
#include "Config.h"
|
||||
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <QtQml/QtQml>
|
||||
|
@ -37,9 +36,148 @@
|
|||
#include "OffscreenGLCanvas.h"
|
||||
#include "GLHelpers.h"
|
||||
#include "GLLogging.h"
|
||||
#include "TextureRecycler.h"
|
||||
#include "Context.h"
|
||||
|
||||
struct TextureSet {
|
||||
// The number of surfaces with this size
|
||||
size_t count { 0 };
|
||||
std::list<OffscreenQmlSurface::TextureAndFence> returnedTextures;
|
||||
};
|
||||
|
||||
uint64_t uvec2ToUint64(const uvec2& v) {
|
||||
uint64_t result = v.x;
|
||||
result <<= 32;
|
||||
result |= v.y;
|
||||
return result;
|
||||
}
|
||||
|
||||
class OffscreenTextures {
|
||||
public:
|
||||
GLuint getNextTexture(const uvec2& size) {
|
||||
assert(QThread::currentThread() == qApp->thread());
|
||||
|
||||
recycle();
|
||||
|
||||
++_activeTextureCount;
|
||||
auto sizeKey = uvec2ToUint64(size);
|
||||
assert(_textures.count(sizeKey));
|
||||
auto& textureSet = _textures[sizeKey];
|
||||
if (!textureSet.returnedTextures.empty()) {
|
||||
auto textureAndFence = textureSet.returnedTextures.front();
|
||||
textureSet.returnedTextures.pop_front();
|
||||
waitOnFence(static_cast<GLsync>(textureAndFence.second));
|
||||
return textureAndFence.first;
|
||||
}
|
||||
|
||||
return createTexture(size);
|
||||
}
|
||||
|
||||
void releaseSize(const uvec2& size) {
|
||||
assert(QThread::currentThread() == qApp->thread());
|
||||
auto sizeKey = uvec2ToUint64(size);
|
||||
assert(_textures.count(sizeKey));
|
||||
auto& textureSet = _textures[sizeKey];
|
||||
if (0 == --textureSet.count) {
|
||||
for (const auto& textureAndFence : textureSet.returnedTextures) {
|
||||
destroy(textureAndFence);
|
||||
}
|
||||
_textures.erase(sizeKey);
|
||||
}
|
||||
}
|
||||
|
||||
void acquireSize(const uvec2& size) {
|
||||
assert(QThread::currentThread() == qApp->thread());
|
||||
auto sizeKey = uvec2ToUint64(size);
|
||||
auto& textureSet = _textures[sizeKey];
|
||||
++textureSet.count;
|
||||
}
|
||||
|
||||
// May be called on any thread
|
||||
void releaseTexture(const OffscreenQmlSurface::TextureAndFence & textureAndFence) {
|
||||
--_activeTextureCount;
|
||||
Lock lock(_mutex);
|
||||
_returnedTextures.push_back(textureAndFence);
|
||||
}
|
||||
|
||||
void report() {
|
||||
uint64_t now = usecTimestampNow();
|
||||
if ((now - _lastReport) > USECS_PER_SECOND * 5) {
|
||||
_lastReport = now;
|
||||
qCDebug(glLogging) << "Current offscreen texture count " << _allTextureCount;
|
||||
qCDebug(glLogging) << "Current offscreen active texture count " << _activeTextureCount;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void waitOnFence(GLsync fence) {
|
||||
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(fence);
|
||||
}
|
||||
|
||||
void destroyTexture(GLuint texture) {
|
||||
--_allTextureCount;
|
||||
_textureSizes.erase(texture);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
void destroy(const OffscreenQmlSurface::TextureAndFence& textureAndFence) {
|
||||
waitOnFence(static_cast<GLsync>(textureAndFence.second));
|
||||
destroyTexture(textureAndFence.first);
|
||||
}
|
||||
|
||||
GLuint createTexture(const uvec2& size) {
|
||||
// Need a new texture
|
||||
uint32_t newTexture;
|
||||
glGenTextures(1, &newTexture);
|
||||
++_allTextureCount;
|
||||
_textureSizes[newTexture] = size;
|
||||
glBindTexture(GL_TEXTURE_2D, newTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_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);
|
||||
return newTexture;
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
assert(QThread::currentThread() == qApp->thread());
|
||||
// First handle any global returns
|
||||
std::list<OffscreenQmlSurface::TextureAndFence> returnedTextures;
|
||||
{
|
||||
Lock lock(_mutex);
|
||||
returnedTextures.swap(_returnedTextures);
|
||||
}
|
||||
|
||||
for (auto textureAndFence : returnedTextures) {
|
||||
GLuint texture = textureAndFence.first;
|
||||
uvec2 size = _textureSizes[texture];
|
||||
auto sizeKey = uvec2ToUint64(size);
|
||||
// Textures can be returned after all surfaces of the given size have been destroyed,
|
||||
// in which case we just destroy the texture
|
||||
if (!_textures.count(sizeKey)) {
|
||||
destroy(textureAndFence);
|
||||
continue;
|
||||
}
|
||||
_textures[sizeKey].returnedTextures.push_back(textureAndFence);
|
||||
}
|
||||
}
|
||||
|
||||
using Mutex = std::mutex;
|
||||
using Lock = std::unique_lock<Mutex>;
|
||||
std::atomic<int> _allTextureCount;
|
||||
std::atomic<int> _activeTextureCount;
|
||||
std::unordered_map<uint64_t, TextureSet> _textures;
|
||||
std::unordered_map<GLuint, uvec2> _textureSizes;
|
||||
Mutex _mutex;
|
||||
std::list<OffscreenQmlSurface::TextureAndFence> _returnedTextures;
|
||||
uint64_t _lastReport { 0 };
|
||||
} offscreenTextures;
|
||||
|
||||
class UrlHandler : public QObject {
|
||||
Q_OBJECT
|
||||
|
@ -98,31 +236,10 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) {
|
|||
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
|
||||
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
|
||||
|
||||
void OffscreenQmlSurface::setupFbo() {
|
||||
_canvas->makeCurrent();
|
||||
_textures.setSize(_size);
|
||||
if (_depthStencil) {
|
||||
glDeleteRenderbuffers(1, &_depthStencil);
|
||||
_depthStencil = 0;
|
||||
}
|
||||
glGenRenderbuffers(1, &_depthStencil);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y);
|
||||
|
||||
if (_fbo) {
|
||||
glDeleteFramebuffers(1, &_fbo);
|
||||
_fbo = 0;
|
||||
}
|
||||
glGenFramebuffers(1, &_fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
|
||||
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
_canvas->doneCurrent();
|
||||
}
|
||||
|
||||
void OffscreenQmlSurface::cleanup() {
|
||||
_canvas->makeCurrent();
|
||||
|
||||
_renderControl->invalidate();
|
||||
delete _renderControl; // and invalidate
|
||||
|
||||
if (_depthStencil) {
|
||||
|
@ -134,7 +251,8 @@ void OffscreenQmlSurface::cleanup() {
|
|||
_fbo = 0;
|
||||
}
|
||||
|
||||
_textures.clear();
|
||||
offscreenTextures.releaseSize(_size);
|
||||
|
||||
_canvas->doneCurrent();
|
||||
}
|
||||
|
||||
|
@ -148,26 +266,7 @@ void OffscreenQmlSurface::render() {
|
|||
_renderControl->sync();
|
||||
_quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y));
|
||||
|
||||
// Clear out any pending textures to be returned
|
||||
{
|
||||
std::list<OffscreenQmlSurface::TextureAndFence> returnedTextures;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_textureMutex);
|
||||
returnedTextures.swap(_returnedTextures);
|
||||
}
|
||||
if (!returnedTextures.empty()) {
|
||||
for (const auto& textureAndFence : returnedTextures) {
|
||||
GLsync fence = static_cast<GLsync>(textureAndFence.second);
|
||||
if (fence) {
|
||||
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(fence);
|
||||
}
|
||||
_textures.recycleTexture(textureAndFence.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint texture = _textures.getNextTexture();
|
||||
GLuint texture = offscreenTextures.getNextTexture(_size);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
|
||||
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
|
||||
PROFILE_RANGE("qml_render->rendercontrol")
|
||||
|
@ -177,12 +276,11 @@ void OffscreenQmlSurface::render() {
|
|||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_textureMutex);
|
||||
// If the most recent texture was unused, we can directly recycle it
|
||||
if (_latestTextureAndFence.first) {
|
||||
_textures.recycleTexture(_latestTextureAndFence.first);
|
||||
glDeleteSync(static_cast<GLsync>(_latestTextureAndFence.second));
|
||||
offscreenTextures.releaseTexture(_latestTextureAndFence);
|
||||
_latestTextureAndFence = { 0, 0 };
|
||||
}
|
||||
|
||||
|
@ -199,7 +297,6 @@ void OffscreenQmlSurface::render() {
|
|||
bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) {
|
||||
textureAndFence = { 0, 0 };
|
||||
|
||||
std::unique_lock<std::mutex> lock(_textureMutex);
|
||||
if (0 == _latestTextureAndFence.first) {
|
||||
return false;
|
||||
}
|
||||
|
@ -210,20 +307,18 @@ bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) {
|
|||
return true;
|
||||
}
|
||||
|
||||
void OffscreenQmlSurface::releaseTexture(const TextureAndFence& textureAndFence) {
|
||||
std::unique_lock<std::mutex> lock(_textureMutex);
|
||||
_returnedTextures.push_back(textureAndFence);
|
||||
std::function<void(uint32_t, void*)> OffscreenQmlSurface::getDiscardLambda() {
|
||||
return [](uint32_t texture, void* fence) {
|
||||
offscreenTextures.releaseTexture({ texture, static_cast<GLsync>(fence) });
|
||||
};
|
||||
}
|
||||
|
||||
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 != _latestTextureAndFence.first) {
|
||||
return false;
|
||||
}
|
||||
if (0 != _latestTextureAndFence.first) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto minRenderInterval = USECS_PER_SECOND / fps;
|
||||
|
@ -307,7 +402,6 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
|
|||
}
|
||||
_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.
|
||||
|
@ -367,9 +461,40 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
|
|||
}
|
||||
|
||||
qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio;
|
||||
|
||||
_canvas->makeCurrent();
|
||||
|
||||
// Release hold on the textures of the old size
|
||||
if (uvec2() != _size) {
|
||||
// If the most recent texture was unused, we can directly recycle it
|
||||
if (_latestTextureAndFence.first) {
|
||||
offscreenTextures.releaseTexture(_latestTextureAndFence);
|
||||
_latestTextureAndFence = { 0, 0 };
|
||||
}
|
||||
offscreenTextures.releaseSize(_size);
|
||||
}
|
||||
|
||||
_size = newOffscreenSize;
|
||||
_textures.setSize(_size);
|
||||
setupFbo();
|
||||
|
||||
// Acquire the new texture size
|
||||
if (uvec2() != _size) {
|
||||
offscreenTextures.acquireSize(_size);
|
||||
if (_depthStencil) {
|
||||
glDeleteRenderbuffers(1, &_depthStencil);
|
||||
_depthStencil = 0;
|
||||
}
|
||||
glGenRenderbuffers(1, &_depthStencil);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y);
|
||||
if (!_fbo) {
|
||||
glGenFramebuffers(1, &_fbo);
|
||||
}
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
|
||||
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
_canvas->doneCurrent();
|
||||
}
|
||||
|
||||
QQuickItem* OffscreenQmlSurface::getRootItem() {
|
||||
|
@ -421,7 +546,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
|
|||
QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll();
|
||||
javaScriptToInject = webChannelStr + createGlobalEventBridgeStr;
|
||||
} else {
|
||||
qWarning() << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
|
||||
qCWarning(glLogging) << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
|
||||
}
|
||||
|
||||
QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp);
|
||||
|
@ -429,7 +554,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
|
|||
if (_qmlComponent->isError()) {
|
||||
QList<QQmlError> errorList = _qmlComponent->errors();
|
||||
foreach(const QQmlError& error, errorList)
|
||||
qWarning() << error.url() << error.line() << error;
|
||||
qCWarning(glLogging) << error.url() << error.line() << error;
|
||||
if (!_rootItem) {
|
||||
qFatal("Unable to finish loading QML root");
|
||||
}
|
||||
|
@ -474,6 +599,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
|
|||
}
|
||||
|
||||
void OffscreenQmlSurface::updateQuick() {
|
||||
offscreenTextures.report();
|
||||
// If we're
|
||||
// a) not set up
|
||||
// b) already rendering a frame
|
||||
|
|
|
@ -10,16 +10,16 @@
|
|||
#define hifi_OffscreenQmlSurface_h
|
||||
|
||||
#include <atomic>
|
||||
#include <queue>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QUrl>
|
||||
|
||||
#include <GLMHelpers.h>
|
||||
#include <ThreadHelpers.h>
|
||||
#include "TextureRecycler.h"
|
||||
|
||||
class QWindow;
|
||||
class QMyQuickRenderControl;
|
||||
|
@ -30,6 +30,11 @@ class QQmlContext;
|
|||
class QQmlComponent;
|
||||
class QQuickWindow;
|
||||
class QQuickItem;
|
||||
|
||||
// GPU resources are typically buffered for one copy being used by the renderer,
|
||||
// one copy in flight, and one copy being used by the receiver
|
||||
#define GPU_RESOURCE_BUFFER_SIZE 3
|
||||
|
||||
class OffscreenQmlSurface : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged)
|
||||
|
@ -82,9 +87,8 @@ public:
|
|||
// when the texture is safe to read.
|
||||
// Returns false if no new texture is available
|
||||
bool fetchTexture(TextureAndFence& textureAndFence);
|
||||
// Release a previously acquired texture, along with a fence which indicates when reads from the
|
||||
// texture have completed.
|
||||
void releaseTexture(const TextureAndFence& textureAndFence);
|
||||
|
||||
static std::function<void(uint32_t, void*)> getDiscardLambda();
|
||||
|
||||
signals:
|
||||
void focusObjectChanged(QObject* newFocus);
|
||||
|
@ -133,14 +137,10 @@ private:
|
|||
uint32_t _fbo { 0 };
|
||||
uint32_t _depthStencil { 0 };
|
||||
uint64_t _lastRenderTime { 0 };
|
||||
uvec2 _size { 1920, 1080 };
|
||||
TextureRecycler _textures { true };
|
||||
uvec2 _size;
|
||||
|
||||
// Texture management
|
||||
std::mutex _textureMutex;
|
||||
TextureAndFence _latestTextureAndFence { 0, 0 };
|
||||
std::list<TextureAndFence> _returnedTextures;
|
||||
|
||||
|
||||
bool _render { false };
|
||||
bool _polish { true };
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
//
|
||||
// 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();
|
||||
}
|
||||
|
||||
void TextureRecycler::addTexture() {
|
||||
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 TextureRecycler::getNextTexture() {
|
||||
while (_allTextures.size() < _textureCount) {
|
||||
addTexture();
|
||||
}
|
||||
|
||||
if (_readyTextures.empty()) {
|
||||
addTexture();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
//
|
||||
// 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>
|
||||
|
||||
// GPU resources are typically buffered for one copy being used by the renderer,
|
||||
// one copy in flight, and one copy being used by the receiver
|
||||
#define GPU_RESOURCE_BUFFER_SIZE 3
|
||||
|
||||
class TextureRecycler {
|
||||
public:
|
||||
TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
|
||||
void setSize(const uvec2& size);
|
||||
void setTextureCount(uint8_t textureCount);
|
||||
void clear();
|
||||
uint32_t getNextTexture();
|
||||
void recycleTexture(uint32_t texture);
|
||||
|
||||
private:
|
||||
void addTexture();
|
||||
|
||||
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;
|
||||
uint8_t _textureCount { GPU_RESOURCE_BUFFER_SIZE };
|
||||
};
|
||||
|
||||
#endif
|
|
@ -115,6 +115,7 @@ namespace gpu {
|
|||
GPUObject* getGPUObject() const { return _gpuObject.get(); }
|
||||
|
||||
friend class Backend;
|
||||
friend class Texture;
|
||||
};
|
||||
|
||||
namespace gl {
|
||||
|
|
|
@ -287,6 +287,22 @@ Texture::Texture():
|
|||
Texture::~Texture()
|
||||
{
|
||||
_textureCPUCount--;
|
||||
if (getUsage().isExternal()) {
|
||||
Texture::ExternalUpdates externalUpdates;
|
||||
{
|
||||
Lock lock(_externalMutex);
|
||||
_externalUpdates.swap(externalUpdates);
|
||||
}
|
||||
for (const auto& update : externalUpdates) {
|
||||
assert(_externalRecycler);
|
||||
_externalRecycler(update.first, update.second);
|
||||
}
|
||||
// Force the GL object to be destroyed here
|
||||
// If we let the normal destructor do it, then it will be
|
||||
// cleared after the _externalRecycler has been destroyed,
|
||||
// resulting in leaked texture memory
|
||||
gpuObject.setGPUObject(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) {
|
||||
|
@ -935,8 +951,20 @@ Vec3u Texture::evalMipDimensions(uint16 level) const {
|
|||
return glm::max(dimensions, Vec3u(1));
|
||||
}
|
||||
|
||||
void Texture::setExternalRecycler(const ExternalRecycler& recycler) {
|
||||
Lock lock(_externalMutex);
|
||||
_externalRecycler = recycler;
|
||||
}
|
||||
|
||||
Texture::ExternalRecycler Texture::getExternalRecycler() const {
|
||||
Lock lock(_externalMutex);
|
||||
Texture::ExternalRecycler result = _externalRecycler;
|
||||
return result;
|
||||
}
|
||||
|
||||
void Texture::setExternalTexture(uint32 externalId, void* externalFence) {
|
||||
Lock lock(_externalMutex);
|
||||
assert(_externalRecycler);
|
||||
_externalUpdates.push_back({ externalId, externalFence });
|
||||
}
|
||||
|
||||
|
|
|
@ -466,8 +466,8 @@ public:
|
|||
void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); }
|
||||
|
||||
void setExternalTexture(uint32 externalId, void* externalFence);
|
||||
void setExternalRecycler(const ExternalRecycler& recycler) { _externalRecycler = recycler; }
|
||||
ExternalRecycler getExternalRecycler() const { return _externalRecycler; }
|
||||
void setExternalRecycler(const ExternalRecycler& recycler);
|
||||
ExternalRecycler getExternalRecycler() const;
|
||||
|
||||
const GPUObjectPointer gpuObject {};
|
||||
|
||||
|
|
105
scripts/developer/tests/webSpawnTool.js
Normal file
105
scripts/developer/tests/webSpawnTool.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
// webSpawnTool.js
|
||||
//
|
||||
// Stress tests the rendering of web surfaces over time
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
ENTITY_SPAWNER = function (properties) {
|
||||
properties = properties || {};
|
||||
var RADIUS = properties.radius || 5.0; // Spawn within this radius (square)
|
||||
var TEST_ENTITY_NAME = properties.entityName || "WebEntitySpawnTest";
|
||||
var NUM_ENTITIES = properties.count || 10000; // number of entities to spawn
|
||||
var ENTITY_SPAWN_LIMIT = properties.spawnLimit || 1;
|
||||
var ENTITY_SPAWN_INTERVAL = properties.spawnInterval || properties.interval || 2;
|
||||
var ENTITY_LIFETIME = properties.lifetime || 10; // Entity timeout (when/if we crash, we need the entities to delete themselves)
|
||||
|
||||
function makeEntity(properties) {
|
||||
var entity = Entities.addEntity(properties);
|
||||
return {
|
||||
destroy: function () {
|
||||
Entities.deleteEntity(entity)
|
||||
},
|
||||
getAge: function () {
|
||||
return Entities.getEntityProperties(entity).age;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function randomPositionXZ(center, radius) {
|
||||
return {
|
||||
x: center.x + (Math.random() * radius * 2.0) - radius,
|
||||
y: center.y,
|
||||
z: center.z + (Math.random() * radius * 2.0) - radius
|
||||
};
|
||||
}
|
||||
|
||||
var entities = [];
|
||||
var entitiesToCreate = 0;
|
||||
var entitiesSpawned = 0;
|
||||
var spawnTimer = 0.0;
|
||||
var keepAliveTimer = 0.0;
|
||||
|
||||
function clear () {
|
||||
var ids = Entities.findEntities(MyAvatar.position, 50);
|
||||
var that = this;
|
||||
ids.forEach(function(id) {
|
||||
var properties = Entities.getEntityProperties(id);
|
||||
if (properties.name == TEST_ENTITY_NAME) {
|
||||
Entities.deleteEntity(id);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
function createEntities () {
|
||||
entitiesToCreate = NUM_ENTITIES;
|
||||
Script.update.connect(spawnEntities);
|
||||
}
|
||||
|
||||
function spawnEntities (dt) {
|
||||
if (entitiesToCreate <= 0) {
|
||||
Script.update.disconnect(spawnEntities);
|
||||
print("Finished spawning entities");
|
||||
}
|
||||
else if ((spawnTimer -= dt) < 0.0){
|
||||
spawnTimer = ENTITY_SPAWN_INTERVAL;
|
||||
|
||||
var n = Math.min(entitiesToCreate, ENTITY_SPAWN_LIMIT);
|
||||
print("Spawning " + n + " entities (" + (entitiesSpawned += n) + ")");
|
||||
|
||||
entitiesToCreate -= n;
|
||||
|
||||
var center = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: RADIUS * -1.5 }));
|
||||
for (; n > 0; --n) {
|
||||
entities.push(makeEntity({
|
||||
type: "Web",
|
||||
sourceUrl: "https://www.reddit.com/r/random/",
|
||||
name: TEST_ENTITY_NAME,
|
||||
position: randomPositionXZ(center, RADIUS),
|
||||
rotation: MyAvatar.orientation,
|
||||
dimensions: { x: .8 + Math.random() * 0.8, y: 0.45 + Math.random() * 0.45, z: 0.01 },
|
||||
lifetime: ENTITY_LIFETIME
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function despawnEntities () {
|
||||
print("despawning entities");
|
||||
entities.forEach(function (entity) {
|
||||
entity.destroy();
|
||||
});
|
||||
entities = [];
|
||||
}
|
||||
|
||||
function init () {
|
||||
Script.update.disconnect(init);
|
||||
clear();
|
||||
createEntities();
|
||||
Script.scriptEnding.connect(despawnEntities);
|
||||
}
|
||||
Script.update.connect(init);
|
||||
};
|
||||
|
||||
ENTITY_SPAWNER();
|
Loading…
Reference in a new issue