From 39d352e1a7bdaeddd937183a54314ad7cceed02d Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 26 Sep 2016 11:27:50 -0700 Subject: [PATCH 01/12] display location/connection status in address bar when it doesn't have focus --- interface/resources/qml/AddressBarDialog.qml | 50 +++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 0bbe053897..1164a0f01e 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -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(); } } From 621e3fc00037b9ed2e8201d0417b6d451220cab0 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 30 Sep 2016 09:06:17 -0700 Subject: [PATCH 02/12] Expose triggerOption to scripts, plus _physicsEnabled Now any menu item can be triggered. Plus you can do a Window.isPhysicsEnabled to detect when we are done loading assets and such. Whee! --- interface/src/scripting/MenuScriptingInterface.cpp | 5 +++++ interface/src/scripting/MenuScriptingInterface.h | 2 ++ interface/src/scripting/WindowScriptingInterface.cpp | 4 ++++ interface/src/scripting/WindowScriptingInterface.h | 1 + libraries/ui/src/ui/Menu.h | 3 ++- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index 7eb80b5946..2fa7470561 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -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)); +} + diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index 03ff4b512a..5b8a437529 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -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); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index c528c26b99..0f9dd698fd 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -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(); +} diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 9303636a1f..f4a89ae221 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -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); diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 895e40fe68..ee60a031c3 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -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; } From 76d9d4c635b2349bf48057cfcfd0c55bbe6f56b8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 3 Oct 2016 10:16:06 -0700 Subject: [PATCH 03/12] fix canRez logic in tribble script: only need canRezTemp, and fail hard rather than retry --- .../developer/tests/performance/tribbles.js | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/scripts/developer/tests/performance/tribbles.js b/scripts/developer/tests/performance/tribbles.js index c9ae347a82..4c04f8b5b7 100644 --- a/scripts/developer/tests/performance/tribbles.js +++ b/scripts/developer/tests/performance/tribbles.js @@ -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); +} From 70a63417cfc71d1218de7fe1f22f09bf4a0548a4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 3 Oct 2016 11:40:50 -0700 Subject: [PATCH 04/12] point to tribbles.js on aws For now, anyways. Just temporary until dev is complete. --- scripts/developer/tests/performance/domain-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index eceffa278b..67da45e1b6 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -173,7 +173,7 @@ You would want to say 'no' (and make other preparations) if you were testing the 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'); // FIXME: replace with AWS Script.setTimeout(continuation, 3000); } else { continuation(); From 43c29500bf8483a5c974f03053fa77e43a3f142b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 3 Oct 2016 12:03:23 -0700 Subject: [PATCH 05/12] ac script to provide an on-demand crowd, and an interface script to summon it --- .../tests/performance/crowd-agent.js | 95 +++++++++++++++++++ scripts/developer/tests/performance/summon.js | 94 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 scripts/developer/tests/performance/crowd-agent.js create mode 100644 scripts/developer/tests/performance/summon.js diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js new file mode 100644 index 0000000000..5df576cf99 --- /dev/null +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -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'); +}); diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js new file mode 100644 index 0000000000..cb3093ca5b --- /dev/null +++ b/scripts/developer/tests/performance/summon.js @@ -0,0 +1,94 @@ +"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, before we start the tribbles. From 8388e1044aa78afee9ce6e7e9889975b218f5936 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 3 Oct 2016 14:09:52 -0700 Subject: [PATCH 06/12] cleanup to use script-driven cache cleaning --- .../tests/performance/domain-check.js | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index 67da45e1b6..1a42683d92 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -25,10 +25,13 @@ 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); @@ -114,17 +117,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 +139,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 +147,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,9 +158,12 @@ 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); } } From 327fcc970b85e7acecc88527fb6d395269a9ef16 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 3 Oct 2016 15:15:58 -0700 Subject: [PATCH 07/12] Remove frame lag from near grabbed physical objects. In the game loop, physics occurs before avatar update. Before this PR, when the avatar is moved during avatar update, near grabbed objects will not pick up this move until one frame later, when the physics is run on the next update. After this PR, near grabbed objects are adjusted to reflect any position or rotation change that occurred during the avatar update. --- interface/src/avatar/AvatarActionHold.cpp | 52 ++++++++++++++++++++++- interface/src/avatar/AvatarActionHold.h | 3 ++ interface/src/avatar/MyAvatar.cpp | 30 ++++++++++++- interface/src/avatar/MyAvatar.h | 9 ++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 8b8f8e8c2e..5f41bf40d4 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -25,14 +25,25 @@ AvatarActionHold::AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntit { _type = ACTION_TYPE_HOLD; _measuredLinearVelocities.resize(AvatarActionHold::velocitySmoothFrames); + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + if (myAvatar) { + myAvatar->addHoldAction(this); + } + #if WANT_DEBUG - qDebug() << "AvatarActionHold::AvatarActionHold"; + qDebug() << "AvatarActionHold::AvatarActionHold" << (void*)this; #endif } AvatarActionHold::~AvatarActionHold() { + auto myAvatar = DependencyManager::get()->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(physicsInfo); + btRigidBody* rigidBody = motionState ? motionState->getRigidBody() : nullptr; + if (!rigidBody) { + return; + } + auto avatarManager = DependencyManager::get(); + auto holdingAvatar = std::static_pointer_cast(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); +} diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index bfa392172d..f0b42111ed 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -15,6 +15,7 @@ #include #include +#include #include #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); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 24dbc62318..20c4f41568 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -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,25 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } } + +// thread-safe +void MyAvatar::addHoldAction(AvatarActionHold* holdAction) { + std::lock_guard guard(_holdActionsMutex); + _holdActions.push_back(holdAction); +} + +// thread-safe +void MyAvatar::removeHoldAction(AvatarActionHold* holdAction) { + std::lock_guard 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) { + std::lock_guard guard(_holdActionsMutex); + for (auto& holdAction : _holdActions) { + holdAction->lateAvatarUpdate(prePhysicsPose, postUpdatePose); + } +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c4ffc08cbc..71f185c6ed 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -26,6 +26,7 @@ #include "MyCharacterController.h" #include +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 _holdActions; + float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f }; float AUDIO_ENERGY_CONSTANT { 0.000001f }; float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f }; From ca3572f991660352ebd14d467513f03902322b88 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 3 Oct 2016 14:13:10 -0700 Subject: [PATCH 08/12] Support external GL textures with proper fencing --- interface/src/ui/ApplicationOverlay.cpp | 43 ++++---- interface/src/ui/ApplicationOverlay.h | 2 +- interface/src/ui/overlays/Web3DOverlay.cpp | 22 +++-- interface/src/ui/overlays/Web3DOverlay.h | 2 +- .../src/RenderableWebEntityItem.cpp | 20 ++-- .../src/RenderableWebEntityItem.h | 4 +- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 99 ++++++++++++++++--- libraries/gl/src/gl/OffscreenQmlSurface.h | 12 ++- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 28 ++++-- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 4 +- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 35 ++++++- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 39 ++++++++ libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 1 + .../src/gpu/gl41/GL41BackendTexture.cpp | 8 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 1 + .../src/gpu/gl45/GL45BackendTexture.cpp | 9 +- libraries/gpu/src/gpu/Batch.cpp | 19 ++-- libraries/gpu/src/gpu/Batch.h | 5 - libraries/gpu/src/gpu/Texture.cpp | 23 +++++ libraries/gpu/src/gpu/Texture.h | 21 +++- 20 files changed, 308 insertions(+), 89 deletions(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 197fb5b58d..f4937c4459 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -40,15 +40,6 @@ ApplicationOverlay::ApplicationOverlay() auto geometryCache = DependencyManager::get(); _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(); - 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->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()->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(); + + OffscreenQmlSurface::TextureAndFence newTextureAndFence; + bool newTextureAvailable = offscreenUi->fetchTexture(newTextureAndFence); + if (newTextureAvailable) { + _uiTexture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second); + } + auto geometryCache = DependencyManager::get(); + 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) { diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index b7a0529f92..d20b569457 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -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; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 46fc2dfc36..8f213e7740 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -80,9 +80,6 @@ void Web3DOverlay::render(RenderArgs* args) { _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 +94,21 @@ 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()->getWhiteTexture()); + if (!_texture) { + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([this](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(); if (color.a < OPAQUE_ALPHA_THRESHOLD) { diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index a828626715..0bb9e5e030 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -43,7 +43,7 @@ public: private: OffscreenQmlSurface* _webSurface{ nullptr }; QMetaObject::Connection _connection; - uint32_t _texture{ 0 }; + gpu::TexturePointer _texture; QString _url; float _dpi; vec2 _resolution{ 640, 480 }; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 86bce87ba2..3c339b7b7d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -140,9 +140,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 +214,31 @@ void RenderableWebEntityItem::render(RenderArgs* args) { // without worrying about excessive overhead. _webSurface->resize(QSize(windowSize.x, windowSize.y)); + if (!_texture) { + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([this](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); + } + 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; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 47808c4262..ea9ddd0c12 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -80,8 +80,8 @@ private: OffscreenQmlSurface* _webSurface{ nullptr }; 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 }; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index d1c884f264..797f297488 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -34,7 +34,6 @@ #include #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 _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 returnedTextures; + { + std::unique_lock lock(_textureMutex); + returnedTextures.swap(_returnedTextures); + } + if (!returnedTextures.empty()) { + for (const auto& textureAndFence : returnedTextures) { + GLsync fence = static_cast(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 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 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 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 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 diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index a9a77f2941..fa2346dd2f 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -71,8 +71,17 @@ public: QPointF mapToVirtualScreen(const QPointF& originalPoint, QObject* originalWidget); bool eventFilter(QObject* originalDestination, QEvent* event) override; + using TextureAndFence = std::pair; + // 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 }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index f070dfe637..c082c00609 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -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> 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 programsTrash; { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index af4851a1b0..f99d34393c 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -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> _buffersTrash; mutable std::list> _texturesTrash; + mutable std::list> _externalTexturesTrash; mutable std::list _framebuffersTrash; mutable std::list _shadersTrash; mutable std::list _programsTrash; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 56ff4166ea..b39bc58bac 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -136,6 +136,7 @@ float GLTexture::getMemoryPressure() { // Create the texture and allocate storage GLTexture::GLTexture(const std::weak_ptr& 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,38 @@ GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& tex Backend::setGPUObject(texture, this); } +GLTexture::GLTexture(const std::weak_ptr& 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(); + assert(recycler); + backend->releaseExternalTexture(_id, recycler); + } else if (_id) { backend->releaseTexture(_id, _size); } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 4375d0644f..9091a63086 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -32,6 +32,42 @@ public: template 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(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(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 _syncState { GLSyncState::Idle }; GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); void setSyncState(GLSyncState syncState) { _syncState = syncState; } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 5d03997b44..3f1f45624a 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -42,6 +42,7 @@ public: using Parent = GLTexture; GLuint allocate(); public: + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); protected: diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 3fb729711d..5594be36b6 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -38,7 +38,13 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transf return GL41Texture::sync(*this, texture, transfer); } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) : GLTexture(backend, texture, allocate(), transferrable) {} +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId) { +} + +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(), transferrable) { +} void GL41Texture::generateMips() const { withPreservedTexture([&] { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index f1c30b9382..059156b4a3 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -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& backend, const Texture& texture, GLuint externalId); GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); ~GL45Texture(); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 36b7b4886f..c59c945531 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -243,6 +243,10 @@ GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { return GL45Texture::getId(*this, texture, transfer); } +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId), _sparseInfo(*this), _transferState(*this) { +} + GL45Texture::GL45Texture(const std::weak_ptr& 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& 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 { diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 8d3f019168..c15da61800 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -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(); } -} \ No newline at end of file +} diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 8a52eef4ea..ad8155774d 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -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, diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 44804abebe..924f5145b9 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -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; +} \ No newline at end of file diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 61d03c070c..9c3e88c67a 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -163,6 +163,10 @@ public: static void setEnableSparseTextures(bool enabled); static void setEnableIncrementalTextureTransfers(bool enabled); + using ExternalRecycler = std::function; + using ExternalIdAndFence = std::pair; + using ExternalUpdates = std::list; + 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 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 _externalUpdates; + ExternalRecycler _externalRecycler; + + // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; From 8c5028158fe8a3e874f2da09e1d2825c992ffa2a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 3 Oct 2016 16:42:50 -0700 Subject: [PATCH 09/12] Fix crash on destroying web entities and overlays --- interface/src/ui/overlays/Web3DOverlay.cpp | 12 +++++-- interface/src/ui/overlays/Web3DOverlay.h | 2 +- .../src/RenderableWebEntityItem.cpp | 33 +++++++++++-------- .../src/RenderableWebEntityItem.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 7 ++-- 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 8f213e7740..be564a768e 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -73,7 +73,12 @@ 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(new OffscreenQmlSurface(), deleter); _webSurface->create(currentContext); _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/")); _webSurface->load("WebView.qml"); @@ -95,8 +100,9 @@ void Web3DOverlay::render(RenderArgs* args) { } if (!_texture) { - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([this](uint32_t recycleTexture, void* recycleFence) { - _webSurface->releaseTexture({ recycleTexture, recycleFence }); + auto webSurface = _webSurface; + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([webSurface](uint32_t recycleTexture, void* recycleFence) { + webSurface->releaseTexture({ recycleTexture, recycleFence }); })); _texture->setSource(__FUNCTION__); } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 0bb9e5e030..1e75bbbb06 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -41,7 +41,7 @@ public: virtual Web3DOverlay* createClone() const override; private: - OffscreenQmlSurface* _webSurface{ nullptr }; + QSharedPointer _webSurface; QMetaObject::Connection _connection; gpu::TexturePointer _texture; QString _url; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 3c339b7b7d..e415062e5c 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -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(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) { @@ -215,9 +227,11 @@ void RenderableWebEntityItem::render(RenderArgs* args) { _webSurface->resize(QSize(windowSize.x, windowSize.y)); if (!_texture) { - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([this](uint32_t recycleTexture, void* recycleFence) { - _webSurface->releaseTexture({ recycleTexture, recycleFence }); - })); + 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; @@ -352,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(); } } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index ea9ddd0c12..b7caaae68c 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -78,7 +78,7 @@ private: void destroyWebSurface(); glm::vec2 getWindowSize() const; - OffscreenQmlSurface* _webSurface{ nullptr }; + QSharedPointer _webSurface; QMetaObject::Connection _connection; gpu::TexturePointer _texture; ivec2 _lastPress { INT_MIN }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index b39bc58bac..649065ab84 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -182,8 +182,11 @@ GLTexture::~GLTexture() { if (backend) { if (_external) { auto recycler = _gpuObject.getExternalRecycler(); - assert(recycler); - backend->releaseExternalTexture(_id, recycler); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; + } } else if (_id) { backend->releaseTexture(_id, _size); } From 5ea4d48e360506019eb27b006bfad1ce5026a08d Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 4 Oct 2016 10:23:10 -0700 Subject: [PATCH 10/12] poll for ready, and include physics in definition of ready, based on #8723 --- .../tests/performance/domain-check.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/scripts/developer/tests/performance/domain-check.js b/scripts/developer/tests/performance/domain-check.js index 1a42683d92..f085c3f685 100644 --- a/scripts/developer/tests/performance/domain-check.js +++ b/scripts/developer/tests/performance/domain-check.js @@ -20,7 +20,7 @@ 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))); } @@ -76,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); @@ -96,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); @@ -170,7 +175,7 @@ You would want to say 'no' (and make other preparations) if you were testing the function maybeRunTribbles(continuation) { if (Window.confirm("Run tribbles?\n\n\ At most, only one participant should say yes.")) { - Script.load('http://cdn.highfidelity.com/davidkelly/production/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(); From dcd425a165802880321ea23a439c1854495c1159 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 4 Oct 2016 10:56:44 -0700 Subject: [PATCH 11/12] Add an entityTree lock around the AvatarHoldAction::lateAvatarUpdate calls --- interface/src/avatar/MyAvatar.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 20c4f41568..19346e51db 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2280,8 +2280,18 @@ void MyAvatar::removeHoldAction(AvatarActionHold* holdAction) { } void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose) { - std::lock_guard guard(_holdActionsMutex); - for (auto& holdAction : _holdActions) { - holdAction->lateAvatarUpdate(prePhysicsPose, 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 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); + } + }); } } From cdd658d4dd842b0a708e6880f56544c6ee220332 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 4 Oct 2016 11:31:49 -0700 Subject: [PATCH 12/12] report results --- scripts/developer/tests/performance/summon.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/developer/tests/performance/summon.js b/scripts/developer/tests/performance/summon.js index cb3093ca5b..2eb1fbe301 100644 --- a/scripts/developer/tests/performance/summon.js +++ b/scripts/developer/tests/performance/summon.js @@ -91,4 +91,13 @@ Script.scriptEnding.connect(function () { }, 500); }); -messageSend({key: 'HELO'}); // Ask agents to report in now, before we start the tribbles. +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);