Merge branch 'master' of https://github.com/highfidelity/hifi into hdr

This commit is contained in:
samcake 2016-10-05 09:31:55 -07:00
commit e6572a42e3
34 changed files with 758 additions and 182 deletions

View file

@ -15,6 +15,7 @@ import "styles"
import "windows"
import "hifi"
import "hifi/toolbars"
import "styles-uit" as HifiStyles
import "controls-uit" as HifiControls
Window {
@ -168,7 +169,24 @@ Window {
}
}
// FIXME replace with TextField
HifiStyles.RalewayLight {
id: notice;
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.50;
anchors {
top: parent.top
topMargin: parent.inputAreaStep + 12
left: addressLine.left
right: addressLine.right
}
}
HifiStyles.FiraSansRegular {
id: location;
font.pixelSize: addressLine.font.pixelSize;
color: "gray";
clip: true;
anchors.fill: addressLine;
visible: !addressLine.activeFocus;
}
TextInput {
id: addressLine
focus: true
@ -179,14 +197,12 @@ Window {
right: parent.right
leftMargin: forwardArrow.width
rightMargin: forwardArrow.width / 2
topMargin: parent.inputAreaStep + hifi.layout.spacing
bottomMargin: parent.inputAreaStep + hifi.layout.spacing
topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing)
bottomMargin: parent.inputAreaStep
}
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75
helperText: "Go to: place, @user, /path, network address"
helperPixelSize: font.pixelSize * 0.75
helperItalic: true
onTextChanged: filterChoicesByText()
onActiveFocusChanged: updateLocationText(focus)
}
}
@ -344,12 +360,24 @@ Window {
});
}
onVisibleChanged: {
if (visible) {
addressLine.forceActiveFocus()
fillDestinations();
function updateLocationText(focus) {
addressLine.text = "";
if (focus) {
notice.text = "Go to a place, @user, path or network address";
notice.color = "gray";
} else {
addressLine.text = ""
notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected";
notice.color = AddressManager.isConnected ? "gray" : "crimson";
// Display hostname, which includes ip address, localhost, and other non-placenames.
location.text = (AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '');
}
}
onVisibleChanged: {
focus = false;
updateLocationText(false);
if (visible) {
fillDestinations();
}
}

View file

@ -25,14 +25,25 @@ AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntit
{
_type = ACTION_TYPE_HOLD;
_measuredLinearVelocities.resize(AvatarActionHold::velocitySmoothFrames);
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
if (myAvatar) {
myAvatar->addHoldAction(this);
}
#if WANT_DEBUG
qDebug() << "AvatarActionHold::AvatarActionHold";
qDebug() << "AvatarActionHold::AvatarActionHold" << (void*)this;
#endif
}
AvatarActionHold::~AvatarActionHold() {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
if (myAvatar) {
myAvatar->removeHoldAction(this);
}
#if WANT_DEBUG
qDebug() << "AvatarActionHold::~AvatarActionHold";
qDebug() << "AvatarActionHold::~AvatarActionHold" << (void*)this;
#endif
}
@ -460,3 +471,40 @@ void AvatarActionHold::deserialize(QByteArray serializedArguments) {
forceBodyNonStatic();
}
void AvatarActionHold::lateAvatarUpdate(const AnimPose& prePhysicsRoomPose, const AnimPose& postAvatarUpdateRoomPose) {
auto ownerEntity = _ownerEntity.lock();
if (!ownerEntity) {
return;
}
void* physicsInfo = ownerEntity->getPhysicsInfo();
if (!physicsInfo) {
return;
}
ObjectMotionState* motionState = static_cast<ObjectMotionState*>(physicsInfo);
btRigidBody* rigidBody = motionState ? motionState->getRigidBody() : nullptr;
if (!rigidBody) {
return;
}
auto avatarManager = DependencyManager::get<AvatarManager>();
auto holdingAvatar = std::static_pointer_cast<Avatar>(avatarManager->getAvatarBySessionID(_holderID));
if (!holdingAvatar || !holdingAvatar->isMyAvatar()) {
return;
}
btTransform worldTrans = rigidBody->getWorldTransform();
AnimPose worldBodyPose(glm::vec3(1), bulletToGLM(worldTrans.getRotation()), bulletToGLM(worldTrans.getOrigin()));
// transform the body transform into sensor space with the prePhysics sensor-to-world matrix.
// then transform it back into world uisng the postAvatarUpdate sensor-to-world matrix.
AnimPose newWorldBodyPose = postAvatarUpdateRoomPose * prePhysicsRoomPose.inverse() * worldBodyPose;
worldTrans.setOrigin(glmToBullet(newWorldBodyPose.trans));
worldTrans.setRotation(glmToBullet(newWorldBodyPose.rot));
rigidBody->setWorldTransform(worldTrans);
bool positionSuccess;
ownerEntity->setPosition(bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset(), positionSuccess, false);
bool orientationSuccess;
ownerEntity->setOrientation(bulletToGLM(worldTrans.getRotation()), orientationSuccess, false);
}

View file

@ -15,6 +15,7 @@
#include <QUuid>
#include <EntityItem.h>
#include <AnimPose.h>
#include <ObjectActionSpring.h>
#include "avatar/MyAvatar.h"
@ -41,6 +42,8 @@ public:
virtual void prepareForPhysicsSimulation() override;
void lateAvatarUpdate(const AnimPose& prePhysicsRoomPose, const AnimPose& postAvatarUpdateRoomPose);
private:
void doKinematicUpdate(float deltaTimeStep);

View file

@ -45,6 +45,7 @@
#include "Application.h"
#include "devices/Faceshift.h"
#include "AvatarManager.h"
#include "AvatarActionHold.h"
#include "Menu.h"
#include "MyAvatar.h"
#include "Physics.h"
@ -1309,6 +1310,8 @@ void MyAvatar::prepareForPhysicsSimulation() {
} else {
_follow.deactivate();
}
_prePhysicsRoomPose = AnimPose(_sensorToWorldMatrix);
}
void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) {
@ -1549,8 +1552,11 @@ void MyAvatar::postUpdate(float deltaTime) {
DebugDraw::getInstance().updateMyAvatarPos(getPosition());
DebugDraw::getInstance().updateMyAvatarRot(getOrientation());
}
AnimPose postUpdateRoomPose(_sensorToWorldMatrix);
updateHoldActions(_prePhysicsRoomPose, postUpdateRoomPose);
}
void MyAvatar::preDisplaySide(RenderArgs* renderArgs) {
@ -2257,3 +2263,35 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
}
}
}
// thread-safe
void MyAvatar::addHoldAction(AvatarActionHold* holdAction) {
std::lock_guard<std::mutex> guard(_holdActionsMutex);
_holdActions.push_back(holdAction);
}
// thread-safe
void MyAvatar::removeHoldAction(AvatarActionHold* holdAction) {
std::lock_guard<std::mutex> guard(_holdActionsMutex);
auto iter = std::find(std::begin(_holdActions), std::end(_holdActions), holdAction);
if (iter != std::end(_holdActions)) {
_holdActions.erase(iter);
}
}
void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose) {
EntityTreeRenderer* entityTreeRenderer = qApp->getEntities();
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
if (entityTree) {
// to prevent actions from adding or removing themselves from the _holdActions vector
// while we are iterating, we need to enter a critical section.
std::lock_guard<std::mutex> guard(_holdActionsMutex);
// lateAvatarUpdate will modify entity position & orientation, so we need an entity write lock
entityTree->withWriteLock([&] {
for (auto& holdAction : _holdActions) {
holdAction->lateAvatarUpdate(prePhysicsPose, postUpdatePose);
}
});
}
}

View file

@ -26,6 +26,7 @@
#include "MyCharacterController.h"
#include <ThreadSafeValueCache.h>
class AvatarActionHold;
class ModelItemID;
enum DriveKeys {
@ -277,6 +278,10 @@ public:
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
void addHoldAction(AvatarActionHold* holdAction); // thread-safe
void removeHoldAction(AvatarActionHold* holdAction); // thread-safe
void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose);
public slots:
void increaseSize();
void decreaseSize();
@ -488,6 +493,10 @@ private:
bool _hmdLeanRecenterEnabled = true;
AnimPose _prePhysicsRoomPose;
std::mutex _holdActionsMutex;
std::vector<AvatarActionHold*> _holdActions;
float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f };
float AUDIO_ENERGY_CONSTANT { 0.000001f };
float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f };

View file

@ -125,3 +125,8 @@ void MenuScriptingInterface::setIsOptionChecked(const QString& menuOption, bool
Q_ARG(const QString&, menuOption),
Q_ARG(bool, isChecked));
}
void MenuScriptingInterface::triggerOption(const QString& menuOption) {
QMetaObject::invokeMethod(Menu::getInstance(), "triggerOption", Q_ARG(const QString&, menuOption));
}

View file

@ -48,6 +48,8 @@ public slots:
bool isOptionChecked(const QString& menuOption);
void setIsOptionChecked(const QString& menuOption, bool isChecked);
void triggerOption(const QString& menuOption);
signals:
void menuItemEvent(const QString& menuItem);

View file

@ -206,3 +206,7 @@ void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) {
void WindowScriptingInterface::shareSnapshot(const QString& path) {
qApp->shareSnapshot(path);
}
bool WindowScriptingInterface::isPhysicsEnabled() {
return qApp->isPhysicsEnabled();
}

View file

@ -54,6 +54,7 @@ public slots:
void copyToClipboard(const QString& text);
void takeSnapshot(bool notify = true, float aspectRatio = 0.0f);
void shareSnapshot(const QString& path);
bool isPhysicsEnabled();
signals:
void domainChanged(const QString& domainHostname);

View file

@ -40,15 +40,6 @@ ApplicationOverlay::ApplicationOverlay()
auto geometryCache = DependencyManager::get<GeometryCache>();
_domainStatusBorder = geometryCache->allocateID();
_magnifierBorder = geometryCache->allocateID();
// Once we move UI rendering and screen rendering to different
// threads, we need to use a sync object to deteremine when
// the current UI texture is no longer being read from, and only
// then release it back to the UI for re-use
auto offscreenUi = DependencyManager::get<OffscreenUi>();
connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [&](GLuint textureId) {
_uiTexture = textureId;
});
}
ApplicationOverlay::~ApplicationOverlay() {
@ -96,18 +87,32 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) {
void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) {
PROFILE_RANGE(__FUNCTION__);
if (_uiTexture) {
gpu::Batch& batch = *renderArgs->_batch;
auto geometryCache = DependencyManager::get<GeometryCache>();
geometryCache->useSimpleDrawPipeline(batch);
batch.setProjectionTransform(mat4());
batch.setModelTransform(Transform());
batch.resetViewTransform();
batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _uiTexture);
geometryCache->renderUnitQuad(batch, glm::vec4(1));
if (!_uiTexture) {
_uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D([](uint32_t recycleTexture, void* recycleFence){
DependencyManager::get<OffscreenUi>()->releaseTexture({ recycleTexture, recycleFence });
}));
_uiTexture->setSource(__FUNCTION__);
}
// Once we move UI rendering and screen rendering to different
// threads, we need to use a sync object to deteremine when
// the current UI texture is no longer being read from, and only
// then release it back to the UI for re-use
auto offscreenUi = DependencyManager::get<OffscreenUi>();
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
bool newTextureAvailable = offscreenUi->fetchTexture(newTextureAndFence);
if (newTextureAvailable) {
_uiTexture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second);
}
auto geometryCache = DependencyManager::get<GeometryCache>();
gpu::Batch& batch = *renderArgs->_batch;
geometryCache->useSimpleDrawPipeline(batch);
batch.setProjectionTransform(mat4());
batch.setModelTransform(Transform());
batch.resetViewTransform();
batch.setResourceTexture(0, _uiTexture);
geometryCache->renderUnitQuad(batch, glm::vec4(1));
}
void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) {

View file

@ -40,13 +40,13 @@ private:
float _alpha{ 1.0f };
float _trailingAudioLoudness{ 0.0f };
uint32_t _uiTexture{ 0 };
int _domainStatusBorder;
int _magnifierBorder;
ivec2 _previousBorderSize{ -1 };
gpu::TexturePointer _uiTexture;
gpu::TexturePointer _overlayDepthTexture;
gpu::TexturePointer _overlayColorTexture;
gpu::FramebufferPointer _overlayFramebuffer;

View file

@ -73,16 +73,18 @@ void Web3DOverlay::render(RenderArgs* args) {
QOpenGLContext * currentContext = QOpenGLContext::currentContext();
QSurface * currentSurface = currentContext->surface();
if (!_webSurface) {
_webSurface = new OffscreenQmlSurface();
auto deleter = [](OffscreenQmlSurface* webSurface) {
AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
webSurface->deleteLater();
});
};
_webSurface = QSharedPointer<OffscreenQmlSurface>(new OffscreenQmlSurface(), deleter);
_webSurface->create(currentContext);
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/"));
_webSurface->load("WebView.qml");
_webSurface->resume();
_webSurface->getRootItem()->setProperty("url", _url);
_webSurface->resize(QSize(_resolution.x, _resolution.y));
_connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) {
_texture = textureId;
});
currentContext->makeCurrent(currentSurface);
}
@ -97,14 +99,22 @@ void Web3DOverlay::render(RenderArgs* args) {
transform.postScale(vec3(getDimensions(), 1.0f));
}
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
if (_texture) {
batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture);
} else {
batch.setResourceTexture(0, DependencyManager::get<TextureCache>()->getWhiteTexture());
if (!_texture) {
auto webSurface = _webSurface;
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D([webSurface](uint32_t recycleTexture, void* recycleFence) {
webSurface->releaseTexture({ recycleTexture, recycleFence });
}));
_texture->setSource(__FUNCTION__);
}
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
bool newTextureAvailable = _webSurface->fetchTexture(newTextureAndFence);
if (newTextureAvailable) {
_texture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second);
}
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
batch.setResourceTexture(0, _texture);
batch.setModelTransform(transform);
auto geometryCache = DependencyManager::get<GeometryCache>();
if (color.a < OPAQUE_ALPHA_THRESHOLD) {

View file

@ -41,9 +41,9 @@ public:
virtual Web3DOverlay* createClone() const override;
private:
OffscreenQmlSurface* _webSurface{ nullptr };
QSharedPointer<OffscreenQmlSurface> _webSurface;
QMetaObject::Connection _connection;
uint32_t _texture{ 0 };
gpu::TexturePointer _texture;
QString _url;
float _dpi;
vec2 _resolution{ 640, 480 };

View file

@ -129,7 +129,19 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) {
// Save the original GL context, because creating a QML surface will create a new context
QOpenGLContext * currentContext = QOpenGLContext::currentContext();
QSurface * currentSurface = currentContext->surface();
_webSurface = new OffscreenQmlSurface();
auto deleter = [](OffscreenQmlSurface* webSurface) {
AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
webSurface->deleteLater();
});
};
_webSurface = QSharedPointer<OffscreenQmlSurface>(new OffscreenQmlSurface(), deleter);
// 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
// is no longer valid
_webSurface->create(currentContext);
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/"));
_webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) {
@ -140,9 +152,6 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) {
_webSurface->getRootItem()->setProperty("url", _sourceUrl);
_webSurface->getRootContext()->setContextProperty("desktop", QVariant());
_webSurface->getRootContext()->setContextProperty("webEntity", _webEntityAPIHelper);
_connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) {
_texture = textureId;
});
// Restore the original GL context
currentContext->makeCurrent(currentSurface);
@ -217,20 +226,33 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
// without worrying about excessive overhead.
_webSurface->resize(QSize(windowSize.x, windowSize.y));
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->setSource(__FUNCTION__);
}
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
bool newTextureAvailable = _webSurface->fetchTexture(newTextureAndFence);
if (newTextureAvailable) {
_texture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second);
}
PerformanceTimer perfTimer("RenderableWebEntityItem::render");
Q_ASSERT(getType() == EntityTypes::Web);
static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f);
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
bool success;
batch.setModelTransform(getTransformToCenter(success));
if (!success) {
return;
}
if (_texture) {
batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture);
}
batch.setResourceTexture(0, _texture);
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
@ -344,16 +366,7 @@ void RenderableWebEntityItem::destroyWebSurface() {
_mouseMoveConnection = QMetaObject::Connection();
QObject::disconnect(_hoverLeaveConnection);
_hoverLeaveConnection = QMetaObject::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
// is no longer valid
auto webSurface = _webSurface;
AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
webSurface->deleteLater();
});
_webSurface = nullptr;
_webSurface.reset();
}
}

View file

@ -78,10 +78,10 @@ private:
void destroyWebSurface();
glm::vec2 getWindowSize() const;
OffscreenQmlSurface* _webSurface{ nullptr };
QSharedPointer<OffscreenQmlSurface> _webSurface;
QMetaObject::Connection _connection;
uint32_t _texture{ 0 };
ivec2 _lastPress{ INT_MIN };
gpu::TexturePointer _texture;
ivec2 _lastPress { INT_MIN };
bool _pressed{ false };
QTouchEvent _lastTouchEvent { QEvent::TouchUpdate };
uint64_t _lastRenderTime{ 0 };

View file

@ -34,7 +34,6 @@
#include <NetworkAccessManager.h>
#include "OffscreenGLCanvas.h"
#include "GLEscrow.h"
#include "GLHelpers.h"
#include "GLLogging.h"
@ -265,6 +264,14 @@ private:
// 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;
@ -274,7 +281,6 @@ private:
GLuint _fbo { 0 };
GLuint _depthStencil { 0 };
RawTextureRecycler _textures { true };
GLTextureEscrow _escrow;
uint64_t _lastRenderTime{ 0 };
uvec2 _size{ 1920, 1080 };
@ -406,9 +412,6 @@ void OffscreenQmlRenderThread::init() {
_renderControl->initialize(_canvas.getContext());
setupFbo();
_escrow.setRecycler([this](GLuint texture){
_textures.recycleTexture(texture);
});
}
void OffscreenQmlRenderThread::cleanup() {
@ -485,27 +488,93 @@ void OffscreenQmlRenderThread::render() {
_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);
}
}
}
try {
GLuint texture = _textures.getNextTexture();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
PROFILE_RANGE("qml_render->rendercontrol")
_renderControl->render();
PROFILE_RANGE("qml_render->rendercontrol")
_renderControl->render();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, texture);
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 (_latestTextureFence) {
}
if (_latestTexture) {
_textures.recycleTexture(_latestTexture);
glDeleteSync(_latestTextureFence);
_latestTexture = 0;
_latestTextureFence = 0;
}
_latestTextureFence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
_latestTexture = texture;
// Fence will be used in another thread / context, so a flush is required
glFlush();
}
_quickWindow->resetOpenGLState();
_escrow.submit(texture);
_lastRenderTime = usecTimestampNow();
} catch (std::runtime_error& error) {
qWarning() << "Failed to render QML: " << error.what();
}
}
bool OffscreenQmlRenderThread::fetchTexture(OffscreenQmlSurface::TextureAndFence& textureAndFence) {
textureAndFence = { 0, 0 };
std::unique_lock<std::mutex> lock(_textureMutex);
if (0 == _latestTexture) {
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;
return true;
}
void OffscreenQmlRenderThread::releaseTexture(const OffscreenQmlSurface::TextureAndFence& textureAndFence) {
std::unique_lock<std::mutex> lock(_textureMutex);
_returnedTextures.push_back(textureAndFence);
}
bool OffscreenQmlRenderThread::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) {
return false;
}
}
auto minRenderInterval = USECS_PER_SECOND / fps;
auto lastInterval = usecTimestampNow() - _lastRenderTime;
return (lastInterval > minRenderInterval);
@ -726,13 +795,18 @@ void OffscreenQmlSurface::updateQuick() {
// 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 = false;
}
}
if (_renderer->_escrow.fetchSignaledAndRelease(_currentTexture)) {
emit textureUpdated(_currentTexture);
}
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) {
@ -752,7 +826,6 @@ QPointF OffscreenQmlSurface::mapToVirtualScreen(const QPointF& originalPoint, QO
return _mouseTranslator(originalPoint);
}
///////////////////////////////////////////////////////
//
// Event handling customization

View file

@ -71,8 +71,17 @@ public:
QPointF mapToVirtualScreen(const QPointF& originalPoint, QObject* originalWidget);
bool eventFilter(QObject* originalDestination, QEvent* event) override;
using TextureAndFence = std::pair<uint32_t, void*>;
// Checks to see if a new texture is available. If one is, the function returns true and
// textureAndFence will be populated with the texture ID and a fence which will be signalled
// 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);
signals:
void textureUpdated(unsigned int texture);
void focusObjectChanged(QObject* newFocus);
void focusTextChanged(bool focusText);
@ -100,7 +109,6 @@ private:
QQmlComponent* _qmlComponent{ nullptr };
QQuickItem* _rootItem{ nullptr };
QTimer _updateTimer;
uint32_t _currentTexture{ 0 };
bool _render{ false };
bool _polish{ true };
bool _paused{ true };

View file

@ -119,8 +119,6 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] =
(&::gpu::gl::GLBackend::do_startNamedCall),
(&::gpu::gl::GLBackend::do_stopNamedCall),
(&::gpu::gl::GLBackend::do_glActiveBindTexture),
(&::gpu::gl::GLBackend::do_glUniform1i),
(&::gpu::gl::GLBackend::do_glUniform1f),
(&::gpu::gl::GLBackend::do_glUniform2f),
@ -388,14 +386,6 @@ void GLBackend::do_popProfileRange(const Batch& batch, size_t paramOffset) {
// As long as we don;t use several versions of shaders we can avoid this more complex code path
// #define GET_UNIFORM_LOCATION(shaderUniformLoc) _pipeline._programShader->getUniformLocation(shaderUniformLoc, isStereo());
#define GET_UNIFORM_LOCATION(shaderUniformLoc) shaderUniformLoc
void GLBackend::do_glActiveBindTexture(const Batch& batch, size_t paramOffset) {
glActiveTexture(batch._params[paramOffset + 2]._uint);
glBindTexture(
GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._uint),
batch._params[paramOffset + 0]._uint);
(void)CHECK_GL_ERROR();
}
void GLBackend::do_glUniform1i(const Batch& batch, size_t paramOffset) {
if (_pipeline._program == 0) {
@ -568,6 +558,11 @@ void GLBackend::releaseBuffer(GLuint id, Size size) const {
_buffersTrash.push_back({ id, size });
}
void GLBackend::releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const {
Lock lock(_trashMutex);
_externalTexturesTrash.push_back({ id, recycler });
}
void GLBackend::releaseTexture(GLuint id, Size size) const {
Lock lock(_trashMutex);
_texturesTrash.push_back({ id, size });
@ -662,6 +657,19 @@ void GLBackend::recycle() const {
}
}
{
std::list<std::pair<GLuint, Texture::ExternalRecycler>> externalTexturesTrash;
{
Lock lock(_trashMutex);
std::swap(_externalTexturesTrash, externalTexturesTrash);
}
for (auto pair : externalTexturesTrash) {
auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
pair.second(pair.first, fence);
decrementTextureGPUCount();
}
}
{
std::list<GLuint> programsTrash;
{

View file

@ -130,8 +130,6 @@ public:
// TODO: As long as we have gl calls explicitely issued from interface
// code, we need to be able to record and batch these calls. THe long
// term strategy is to get rid of any GL calls in favor of the HIFI GPU API
virtual void do_glActiveBindTexture(const Batch& batch, size_t paramOffset) final;
virtual void do_glUniform1i(const Batch& batch, size_t paramOffset) final;
virtual void do_glUniform1f(const Batch& batch, size_t paramOffset) final;
virtual void do_glUniform2f(const Batch& batch, size_t paramOffset) final;
@ -170,6 +168,7 @@ public:
virtual bool isTextureReady(const TexturePointer& texture);
virtual void releaseBuffer(GLuint id, Size size) const;
virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const;
virtual void releaseTexture(GLuint id, Size size) const;
virtual void releaseFramebuffer(GLuint id) const;
virtual void releaseShader(GLuint id) const;
@ -194,6 +193,7 @@ protected:
mutable Mutex _trashMutex;
mutable std::list<std::pair<GLuint, Size>> _buffersTrash;
mutable std::list<std::pair<GLuint, Size>> _texturesTrash;
mutable std::list<std::pair<GLuint, Texture::ExternalRecycler>> _externalTexturesTrash;
mutable std::list<GLuint> _framebuffersTrash;
mutable std::list<GLuint> _shadersTrash;
mutable std::list<GLuint> _programsTrash;

View file

@ -136,6 +136,7 @@ float GLTexture::getMemoryPressure() {
// Create the texture and allocate storage
GLTexture::GLTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint id, bool transferrable) :
GLObject(backend, texture, id),
_external(false),
_source(texture.source()),
_storageStamp(texture.getStamp()),
_target(getGLTextureType(texture)),
@ -152,10 +153,41 @@ GLTexture::GLTexture(const std::weak_ptr<GLBackend>& backend, const Texture& tex
Backend::setGPUObject(texture, this);
}
GLTexture::GLTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint id) :
GLObject(backend, texture, id),
_external(true),
_source(texture.source()),
_storageStamp(0),
_target(getGLTextureType(texture)),
_internalFormat(GL_RGBA8),
// FIXME force mips to 0?
_maxMip(texture.maxMip()),
_minMip(texture.minMip()),
_virtualSize(0),
_transferrable(false)
{
Backend::setGPUObject(texture, this);
// FIXME Is this necessary?
//withPreservedTexture([this] {
// syncSampler();
// if (_gpuObject.isAutogenerateMips()) {
// generateMips();
// }
//});
}
GLTexture::~GLTexture() {
if (_id) {
auto backend = _backend.lock();
if (backend) {
auto backend = _backend.lock();
if (backend) {
if (_external) {
auto recycler = _gpuObject.getExternalRecycler();
if (recycler) {
backend->releaseExternalTexture(_id, recycler);
} else {
qWarning() << "No recycler available for texture " << _id << " possible leak";
}
} else if (_id) {
backend->releaseTexture(_id, _size);
}
}

View file

@ -32,6 +32,42 @@ public:
template <typename GLTextureType>
static GLTextureType* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) {
const Texture& texture = *texturePointer;
// Special case external textures
if (texture.getUsage().isExternal()) {
Texture::ExternalUpdates updates = texture.getUpdates();
if (!updates.empty()) {
Texture::ExternalRecycler recycler = texture.getExternalRecycler();
Q_ASSERT(recycler);
// Discard any superfluous updates
while (updates.size() > 1) {
const auto& update = updates.front();
// Superfluous updates will never have been read, but we want to ensure the previous
// writes to them are complete before they're written again, so return them with the
// same fences they arrived with. This can happen on any thread because no GL context
// work is involved
recycler(update.first, update.second);
updates.pop_front();
}
// The last texture remaining is the one we'll use to create the GLTexture
const auto& update = updates.front();
// Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence
if (update.second) {
GLsync fence = static_cast<GLsync>(update.second);
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(fence);
}
// Create the new texture object (replaces any previous texture object)
new GLTextureType(backend.shared_from_this(), texture, update.first);
}
// Return the texture object (if any) associated with the texture, without extensive logic
// (external textures are
return Backend::getGPUObject<GLTextureType>(texture);
}
if (!texture.isDefined()) {
// NO texture definition yet so let's avoid thinking
return nullptr;
@ -110,6 +146,8 @@ public:
~GLTexture();
// Is this texture generated outside the GPU library?
const bool _external;
const GLuint& _texture { _id };
const std::string _source;
const Stamp _storageStamp;
@ -159,6 +197,7 @@ protected:
std::atomic<GLSyncState> _syncState { GLSyncState::Idle };
GLTexture(const std::weak_ptr<gl::GLBackend>& backend, const Texture& texture, GLuint id, bool transferrable);
GLTexture(const std::weak_ptr<gl::GLBackend>& backend, const Texture& texture, GLuint id);
void setSyncState(GLSyncState syncState) { _syncState = syncState; }

View file

@ -42,6 +42,7 @@ public:
using Parent = GLTexture;
GLuint allocate();
public:
GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& buffer, GLuint externalId);
GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& buffer, bool transferrable);
protected:

View file

@ -38,7 +38,13 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transf
return GL41Texture::sync<GL41Texture>(*this, texture, transfer);
}
GL41Texture::GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, bool transferrable) : GLTexture(backend, texture, allocate(), transferrable) {}
GL41Texture::GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint externalId)
: GLTexture(backend, texture, externalId) {
}
GL41Texture::GL41Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, bool transferrable)
: GLTexture(backend, texture, allocate(), transferrable) {
}
void GL41Texture::generateMips() const {
withPreservedTexture([&] {

View file

@ -33,6 +33,7 @@ public:
static const uint32_t DEFAULT_PAGE_DIMENSION = 128;
static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF;
public:
GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint externalId);
GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, bool transferrable);
~GL45Texture();

View file

@ -243,6 +243,10 @@ GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) {
return GL45Texture::getId<GL45Texture>(*this, texture, transfer);
}
GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint externalId)
: GLTexture(backend, texture, externalId), _sparseInfo(*this), _transferState(*this) {
}
GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, bool transferrable)
: GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this), _transferState(*this) {
@ -252,7 +256,10 @@ GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture&
}
GL45Texture::~GL45Texture() {
qCDebug(gpugl45logging) << "Destroying texture " << _id << " from source " << _source.c_str();
// External textures cycle very quickly, so don't spam the log with messages about them.
if (!_gpuObject.getUsage().isExternal()) {
qCDebug(gpugl45logging) << "Destroying texture " << _id << " from source " << _source.c_str();
}
if (_sparseInfo.sparse) {
// Remove this texture from the candidate list of derezzable textures
{

View file

@ -294,6 +294,11 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) {
void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) {
if (texture && texture->getUsage().isExternal()) {
auto recycler = texture->getExternalRecycler();
Q_ASSERT(recycler);
}
ADD_COMMAND(setResourceTexture);
_params.emplace_back(_textures.cache(texture));
@ -506,18 +511,6 @@ void Batch::popProfileRange() {
#endif
}
#define GL_TEXTURE0 0x84C0
void Batch::_glActiveBindTexture(uint32 unit, uint32 target, uint32 texture) {
// clean the cache on the texture unit we are going to use so the next call to setResourceTexture() at the same slot works fine
setResourceTexture(unit - GL_TEXTURE0, nullptr);
ADD_COMMAND(glActiveBindTexture);
_params.emplace_back(texture);
_params.emplace_back(target);
_params.emplace_back(unit);
}
void Batch::_glUniform1i(int32 location, int32 v0) {
if (location < 0) {
return;
@ -680,4 +673,4 @@ void Batch::flush() {
}
buffer->flush();
}
}
}

View file

@ -229,9 +229,6 @@ public:
// term strategy is to get rid of any GL calls in favor of the HIFI GPU API
// For now, instead of calling the raw gl Call, use the equivalent call on the batch so the call is beeing recorded
// THe implementation of these functions is in GLBackend.cpp
void _glActiveBindTexture(unsigned int unit, unsigned int target, unsigned int texture);
void _glUniform1i(int location, int v0);
void _glUniform1f(int location, float v0);
void _glUniform2f(int location, float v0, float v1);
@ -314,8 +311,6 @@ public:
// TODO: As long as we have gl calls explicitely issued from interface
// code, we need to be able to record and batch these calls. THe long
// term strategy is to get rid of any GL calls in favor of the HIFI GPU API
COMMAND_glActiveBindTexture,
COMMAND_glUniform1i,
COMMAND_glUniform1f,
COMMAND_glUniform2f,

View file

@ -238,6 +238,16 @@ bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Si
return allocated == size;
}
Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) {
Texture* tex = new Texture();
tex->_type = TEX_2D;
tex->_maxMip = 0;
tex->_sampler = sampler;
tex->setUsage(Usage::Builder().withExternal().withColor());
tex->setExternalRecycler(recycler);
return tex;
}
Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) {
return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler);
}
@ -925,3 +935,16 @@ Vec3u Texture::evalMipDimensions(uint16 level) const {
return glm::max(dimensions, Vec3u(1));
}
void Texture::setExternalTexture(uint32 externalId, void* externalFence) {
Lock lock(_externalMutex);
_externalUpdates.push_back({ externalId, externalFence });
}
Texture::ExternalUpdates Texture::getUpdates() const {
Texture::ExternalUpdates result;
{
Lock lock(_externalMutex);
_externalUpdates.swap(result);
}
return result;
}

View file

@ -163,6 +163,10 @@ public:
static void setEnableSparseTextures(bool enabled);
static void setEnableIncrementalTextureTransfers(bool enabled);
using ExternalRecycler = std::function<void(uint32, void*)>;
using ExternalIdAndFence = std::pair<uint32, void*>;
using ExternalUpdates = std::list<ExternalIdAndFence>;
class Usage {
public:
enum FlagBit {
@ -170,7 +174,7 @@ public:
NORMAL, // Texture is a normal map
ALPHA, // Texture has an alpha channel
ALPHA_MASK, // Texture alpha channel is a Mask 0/1
EXTERNAL,
NUM_FLAGS,
};
typedef std::bitset<NUM_FLAGS> Flags;
@ -196,6 +200,7 @@ public:
Builder& withNormal() { _flags.set(NORMAL); return (*this); }
Builder& withAlpha() { _flags.set(ALPHA); return (*this); }
Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); }
Builder& withExternal() { _flags.set(EXTERNAL); return (*this); }
};
Usage(const Builder& builder) : Usage(builder._flags) {}
@ -204,6 +209,7 @@ public:
bool isAlpha() const { return _flags[ALPHA]; }
bool isAlphaMask() const { return _flags[ALPHA_MASK]; }
bool isExternal() const { return _flags[EXTERNAL]; }
bool operator==(const Usage& usage) { return (_flags == usage._flags); }
@ -293,6 +299,7 @@ public:
static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler());
static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler());
static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler());
static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler());
Texture();
Texture(const Texture& buf); // deep copy of the sysmem texture
@ -458,9 +465,21 @@ public:
// Only callable by the Backend
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; }
const GPUObjectPointer gpuObject {};
ExternalUpdates getUpdates() const;
protected:
// Should only be accessed internally or by the backend sync function
mutable Mutex _externalMutex;
mutable std::list<ExternalIdAndFence> _externalUpdates;
ExternalRecycler _externalRecycler;
// Not strictly necessary, but incredibly useful for debugging
std::string _source;
std::unique_ptr< Storage > _storage;

View file

@ -62,7 +62,6 @@ public:
MenuWrapper* getMenu(const QString& menuName);
MenuWrapper* getSubMenuFromName(const QString& menuName, MenuWrapper* menu);
void triggerOption(const QString& menuOption);
QAction* getActionForOption(const QString& menuOption);
QAction* addActionToQMenuAndActionHash(MenuWrapper* destinationMenu,
@ -112,6 +111,8 @@ public slots:
void toggleDeveloperMenus();
void toggleAdvancedMenus();
void triggerOption(const QString& menuOption);
static bool isSomeSubmenuShown() { return _isSomeSubmenuShown; }

View file

@ -0,0 +1,95 @@
"use strict";
/*jslint vars: true, plusplus: true*/
/*global Agent, Avatar, Script, Entities, Vec3, Quat, print*/
//
// crowd-agent.js
// scripts/developer/tests/performance/
//
// Created by Howard Stearns on 9/29/16.
// 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
//
// Add this to domain-settings scripts url with n instances. It will lie dormant until
// a script like summon.js calls up to n avatars to be around you.
var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd";
print('crowd-agent version 1');
/* Observations:
- File urls for AC scripts silently fail. Use a local server (e.g., python SimpleHTTPServer) for development.
- URLs are cached regardless of server headers. Must use cache-defeating query parameters.
- JSON.stringify(Avatar) silently fails (even when Agent.isAvatar)
*/
function startAgent(parameters) { // Can also be used to update.
print('crowd-agent starting params', JSON.stringify(parameters), JSON.stringify(Agent));
Agent.isAvatar = true;
if (parameters.position) {
Avatar.position = parameters.position;
}
if (parameters.orientation) {
Avatar.orientation = parameters.orientation;
}
if (parameters.skeletonModelURL) {
Avatar.skeletonModelURL = parameters.skeletonModelURL;
}
if (parameters.animationData) {
data = parameters.animationData;
Avatar.startAnimation(data.url, data.fps || 30, 1.0, (data.loopFlag === undefined) ? true : data.loopFlag, false, data.startFrame || 0, data.endFrame);
}
print('crowd-agent avatars started');
}
function stopAgent(parameters) {
Agent.isAvatar = false;
print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent));
}
function messageSend(message) {
Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message));
}
function messageHandler(channel, messageString, senderID) {
if (channel !== MESSAGE_CHANNEL) {
return;
}
print('crowd-agent message', channel, messageString, senderID);
if (Agent.sessionUUID === senderID) { // ignore my own
return;
}
var message = {};
try {
message = JSON.parse(messageString);
} catch (e) {
print(e);
}
switch (message.key) {
case "HELO":
messageSend({key: 'hello'}); // Allow the coordinator to count responses and make assignments.
break;
case 'hello': // ignore responses (e.g., from other agents)
break;
case "SUMMON":
if (message.rcpt === Agent.sessionUUID) {
startAgent(message);
}
break;
case "STOP":
if (message.rcpt === Agent.sessionUUID) {
stopAgent(message);
}
break;
default:
print("crowd-agent received unrecognized message:", channel, messageString, senderID);
}
}
Messages.subscribe(MESSAGE_CHANNEL);
Messages.messageReceived.connect(messageHandler);
Script.scriptEnding.connect(function () {
print('crowd-agent shutting down');
Messages.messageReceived.disconnect(messageHandler);
Messages.unsubscribe(MESSAGE_CHANNEL);
print('crowd-agent unsubscribed');
});

View file

@ -20,15 +20,18 @@ var EXPECTED_HMD_FRAMERATE = 90;
var MAXIMUM_LOAD_TIME = 60; // seconds
var MINIMUM_AVATARS = 25; // FIXME: not implemented yet. Requires agent scripts. Idea is to have them organize themselves to the right number.
var version = 1;
var version = 2;
function debug() {
print.apply(null, [].concat.apply(['hrs fixme', version], [].map.call(arguments, JSON.stringify)));
}
var emptyishPlace = 'empty';
var cachePlaces = ['localhost', 'Welcome'];
var isInCachePlace = cachePlaces.indexOf(location.hostname) >= 0;
var defaultPlace = isInCachePlace ? 'Playa' : location.hostname;
function isNowIn(place) { // true if currently in specified place
return location.hostname.toLowerCase() === place.toLowerCase();
}
var cachePlaces = ['dev-Welcome', 'localhost']; // For now, list the lighter weight one first.
var isInCachePlace = cachePlaces.some(isNowIn);
var defaultPlace = isInCachePlace ? 'dev-Playa' : location.hostname;
var prompt = "domain-check.js version " + version + "\n\nWhat place should we enter?";
debug(cachePlaces, isInCachePlace, defaultPlace, prompt);
var entryPlace = Window.prompt(prompt, defaultPlace);
@ -73,10 +76,17 @@ function startTwirl(targetRotation, degreesPerUpdate, interval, strafeDistance,
function doLoad(place, continuationWithLoadTime) { // Go to place and call continuationWithLoadTime(loadTimeInSeconds)
var start = Date.now(), timeout, onDownloadUpdate, finishedTwirl = false, loadTime;
// There are two ways to learn of changes: connect to change signals, or poll.
// Until we get reliable results, we'll poll.
var POLL_INTERVAL = 500, poll;
function setHandlers() {
//Stats.downloadsPendingChanged.connect(onDownloadUpdate); downloadsChanged, and physics...
poll = Script.setInterval(onDownloadUpdate, POLL_INTERVAL);
}
function clearHandlers() {
debug('clearHandlers');
Stats.downloadsPendingChanged.disconnect(onDownloadUpdate);
Stats.downloadsChanged.disconnect(onDownloadUpdate);
//Stats.downloadsPendingChanged.disconnect(onDownloadUpdate); downloadsChanged, and physics..
Script.clearInterval(poll);
}
function waitForLoad(flag) {
debug('entry', place, 'initial downloads/pending', Stats.downloads, Stats.downloadsPending);
@ -93,13 +103,11 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti
continuationWithLoadTime(loadTime);
}
});
Stats.downloadsPendingChanged.connect(onDownloadUpdate);
Stats.downloadsChanged.connect(onDownloadUpdate);
setHandlers();
}
function isLoading() {
// FIXME: This tells us when download are completed, but it doesn't tell us when the objects are parsed and loaded.
// We really want something like _physicsEnabled, but that isn't signalled.
return Stats.downloads || Stats.downloadsPending;
// FIXME: We should also confirm that textures have loaded.
return Stats.downloads || Stats.downloadsPending || !Window.isPhysicsEnabled();
}
onDownloadUpdate = function onDownloadUpdate() {
debug('update downloads/pending', Stats.downloads, Stats.downloadsPending);
@ -114,17 +122,9 @@ function doLoad(place, continuationWithLoadTime) { // Go to place and call conti
}
};
function doit() {
debug('go', place);
location.hostChanged.connect(waitForLoad);
location.handleLookupString(place);
}
if (location.placename.toLowerCase() === place.toLowerCase()) {
location.handleLookupString(emptyishPlace);
Script.setTimeout(doit, 1000);
} else {
doit();
}
debug('go', place);
location.hostChanged.connect(waitForLoad);
location.handleLookupString(place);
}
var config = Render.getConfig("Stats");
@ -144,6 +144,7 @@ function doRender(continuation) {
});
}
var TELEPORT_PAUSE = 500;
function maybePrepareCache(continuation) {
var prepareCache = Window.confirm("Prepare cache?\n\n\
Should we start with all and only those items cached that are encountered when visiting:\n" + cachePlaces.join(', ') + "\n\
@ -151,8 +152,6 @@ If 'yes', cache will be cleared and we will visit these two, with a turn in each
You would want to say 'no' (and make other preparations) if you were testing these places.");
if (prepareCache) {
location.handleLookupString(emptyishPlace);
Window.alert("Please do menu Edit->Reload Content (Clears all caches) and THEN press 'ok'.");
function loadNext() {
var place = cachePlaces.shift();
doLoad(place, function (prepTime) {
@ -164,16 +163,19 @@ You would want to say 'no' (and make other preparations) if you were testing the
}
});
}
loadNext();
location.handleLookupString(cachePlaces[cachePlaces.length - 1]);
Menu.triggerOption("Reload Content (Clears all caches)");
Script.setTimeout(loadNext, TELEPORT_PAUSE);
} else {
continuation();
location.handleLookupString(isNowIn(cachePlaces[0]) ? cachePlaces[1] : cachePlaces[0]);
Script.setTimeout(continuation, TELEPORT_PAUSE);
}
}
function maybeRunTribbles(continuation) {
if (Window.confirm("Run tribbles?\n\n\
At most, only one participant should say yes.")) {
Script.load('http://howard-stearns.github.io/models/scripts/tests/performance/tribbles.js'); // FIXME: replace with AWS
Script.load('http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/tribbles.js');
Script.setTimeout(continuation, 3000);
} else {
continuation();

View file

@ -0,0 +1,103 @@
"use strict";
/*jslint vars: true, plusplus: true*/
/*global Agent, Avatar, Script, Entities, Vec3, Quat, print*/
//
// crowd-agent.js
// scripts/developer/tests/performance/
//
// Created by Howard Stearns on 9/29/16.
// 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
//
// See crowd-agent.js
var version = 1;
var label = "summon";
function debug() {
print.apply(null, [].concat.apply([label, version], [].map.call(arguments, JSON.stringify)));
}
var MINIMUM_AVATARS = 25;
var DENSITY = 0.3; // square meters per person. Some say 10 sq ft is arm's length (0.9m^2), 4.5 is crowd (0.4m^2), 2.5 is mosh pit (0.2m^2).
var spread = Math.sqrt(MINIMUM_AVATARS * DENSITY); // meters
var turnSpread = 90; // How many degrees should turn from front range over.
function coord() { return (Math.random() * spread) - (spread / 2); } // randomly distribute a coordinate zero += spread/2.
var summonedAgents = [];
var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd";
function messageSend(message) {
Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message));
}
function messageHandler(channel, messageString, senderID) {
if (channel !== MESSAGE_CHANNEL) {
return;
}
debug('message', channel, messageString, senderID);
if (MyAvatar.sessionUUID === senderID) { // ignore my own
return;
}
var message = {}, avatarIdentifiers;
try {
message = JSON.parse(messageString);
} catch (e) {
print(e);
}
switch (message.key) {
case "hello":
// There can be avatars we've summoned that do not yet appear in the AvatarList.
avatarIdentifiers = AvatarList.getAvatarIdentifiers().filter(function (id) { return summonedAgents.indexOf(id) === -1; });
debug('present', avatarIdentifiers, summonedAgents);
if ((summonedAgents.length + avatarIdentifiers.length) < MINIMUM_AVATARS ) {
summonedAgents.push(senderID);
messageSend({
key: 'SUMMON',
rcpt: senderID,
position: Vec3.sum(MyAvatar.position, {x: coord(), y: 0, z: coord()}),
orientation: Quat.fromPitchYawRollDegrees(0, Quat.safeEulerAngles(MyAvatar.orientation).y + (turnSpread * (Math.random() - 0.5)), 0)/*,
// No need to specify skeletonModelURL
//skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/being_of_light/being_of_light.fbx",
//skeletonModelURL: "file:///c:/Program Files/High Fidelity Release/resources/meshes/defaultAvatar_full.fst"/,
animationData: { // T-pose until we get animations working again.
"url": "file:///C:/Program Files/High Fidelity Release/resources/avatar/animations/idle.fbx",
//"url": "file:///c:/Program Files/High Fidelity Release/resources/avatar/animations/walk_fwd.fbx",
"startFrame": 0.0,
"endFrame": 300.0,
"timeScale": 1.0,
"loopFlag": true
}*/
});
}
break;
case "HELO":
Window.alert("Someone else is summoning avatars.");
break;
default:
print("crowd-agent received unrecognized message:", messageString);
}
}
Messages.subscribe(MESSAGE_CHANNEL);
Messages.messageReceived.connect(messageHandler);
Script.scriptEnding.connect(function () {
debug('stopping agents', summonedAgents);
summonedAgents.forEach(function (id) { messageSend({key: 'STOP', rcpt: id}); });
debug('agents stopped');
Script.setTimeout(function () {
Messages.messageReceived.disconnect(messageHandler);
Messages.unsubscribe(MESSAGE_CHANNEL);
debug('unsubscribed');
}, 500);
});
messageSend({key: 'HELO'}); // Ask agents to report in now.
Script.setTimeout(function () {
if (0 === summonedAgents.length) {
Window.alert("No agents reported.\n\Please run " + MINIMUM_AVATARS + " instances of\n\
http://cdn.highfidelity.com/davidkelly/production/scripts/tests/performance/crowd-agent.js\n\
on your domain server.");
} else if (summonedAgents.length < MINIMUM_AVATARS) {
Window.alert("Only " + summonedAgents.length + " of the expected " + MINIMUM_AVATARS + " agents reported in.");
}
}, 5000);

View file

@ -54,42 +54,46 @@ function randomVector(range) {
};
}
Script.setInterval(function () {
if (!Entities.serversExist() || !Entities.canRez()) {
return;
}
if (totalCreated >= NUMBER_TO_CREATE) {
print("Created " + totalCreated + " tribbles.");
Script.stop();
}
if (!Entities.canRezTmp()) {
Window.alert("Cannot create temp objects here.");
Script.stop();
} else {
Script.setInterval(function () {
if (!Entities.serversExist()) {
return;
}
if (totalCreated >= NUMBER_TO_CREATE) {
print("Created " + totalCreated + " tribbles.");
Script.stop();
}
var i, numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0);
var parameters = JSON.stringify({
moveTimeout: MOVE_TIMEOUT,
moveRate: MOVE_RATE,
editTimeout: EDIT_TIMEOUT,
editRate: EDIT_RATE,
debug: {flow: false, send: false, receive: false}
});
for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) {
Entities.addEntity({
userData: parameters,
type: TYPE,
name: "tribble-" + totalCreated,
position: Vec3.sum(center, randomVector({ x: RANGE, y: RANGE, z: RANGE })),
dimensions: {x: SIZE, y: SIZE, z: SIZE},
color: {red: Math.random() * 255, green: Math.random() * 255, blue: Math.random() * 255},
velocity: VELOCITY,
angularVelocity: Vec3.multiply(Math.random(), ANGULAR_VELOCITY),
damping: DAMPING,
angularDamping: ANGULAR_DAMPING,
gravity: GRAVITY,
collisionsWillMove: true,
lifetime: LIFETIME,
script: Script.resolvePath("tribbleEntity.js")
var i, numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0);
var parameters = JSON.stringify({
moveTimeout: MOVE_TIMEOUT,
moveRate: MOVE_RATE,
editTimeout: EDIT_TIMEOUT,
editRate: EDIT_RATE,
debug: {flow: false, send: false, receive: false}
});
for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) {
Entities.addEntity({
userData: parameters,
type: TYPE,
name: "tribble-" + totalCreated,
position: Vec3.sum(center, randomVector({ x: RANGE, y: RANGE, z: RANGE })),
dimensions: {x: SIZE, y: SIZE, z: SIZE},
color: {red: Math.random() * 255, green: Math.random() * 255, blue: Math.random() * 255},
velocity: VELOCITY,
angularVelocity: Vec3.multiply(Math.random(), ANGULAR_VELOCITY),
damping: DAMPING,
angularDamping: ANGULAR_DAMPING,
gravity: GRAVITY,
collisionsWillMove: true,
lifetime: LIFETIME,
script: Script.resolvePath("tribbleEntity.js")
});
totalCreated++;
}
}, SCRIPT_INTERVAL);
totalCreated++;
}
}, SCRIPT_INTERVAL);
}