Fix offscreen QML texture leak, improve texture sharing for same size surfaces

This commit is contained in:
Brad Davis 2016-10-20 11:34:40 -07:00
parent 98e7d6d0eb
commit 9523660027
11 changed files with 344 additions and 233 deletions

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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,150 @@
#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());
assert(textures.count(size));
recycle();
++_activeTextureCount;
auto sizeKey = uvec2ToUint64(size);
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(QOpenGLContext::currentContext());
assert(QThread::currentThread() == qApp->thread());
assert(textures.count(size));
auto sizeKey = uvec2ToUint64(size);
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());
assert(textures.count(size));
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,28 +238,6 @@ 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();
@ -134,7 +252,8 @@ void OffscreenQmlSurface::cleanup() {
_fbo = 0;
}
_textures.clear();
offscreenTextures.releaseSize(_size);
_canvas->doneCurrent();
}
@ -148,26 +267,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 +277,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 +298,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 +308,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 +403,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 +462,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 +547,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 +555,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 +600,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

View file

@ -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 };

View file

@ -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);
}

View file

@ -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

View file

@ -115,6 +115,7 @@ namespace gpu {
GPUObject* getGPUObject() const { return _gpuObject.get(); }
friend class Backend;
friend class Texture;
};
namespace gl {

View file

@ -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 });
}

View file

@ -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 {};

View 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();