From d0af2220dc68a23dfa279bcb2471b5e5460600fe Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 13:20:41 -0800 Subject: [PATCH 01/73] Virtual baton. --- examples/libraries/virtualBaton.18.js | 197 ++++++++++++++++++++++++++ examples/tests/testBaton.js | 27 ++++ 2 files changed, 224 insertions(+) create mode 100644 examples/libraries/virtualBaton.18.js create mode 100644 examples/tests/testBaton.js diff --git a/examples/libraries/virtualBaton.18.js b/examples/libraries/virtualBaton.18.js new file mode 100644 index 0000000000..c44bac0760 --- /dev/null +++ b/examples/libraries/virtualBaton.18.js @@ -0,0 +1,197 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +/*global Entities, Script, MyAvatar, Messages, AvatarList, print */ +// +// Created by Howard Stearns +// 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 +// +// Allows cooperating scripts to pass a "virtual baton" between them, +// which is useful when part of a script should only be executed by +// the one participant that is holding this particular baton. +// +// A virtual baton is simply any string agreed upon by the scripts +// that use it. Only one script at a time can hold the baton, and it +// holds it until that script releases it, or the other scripts +// determine that the holding script is not responding. The script +// automatically determines who among claimants has the baton, if anyone, +// and holds an "election" if necessary. +// +// See entityScript/tribble.js as an example, and the functions +// virtualBaton(), claim(), release(). +// + +// Answers a new virtualBaton for the given parameters, of which 'key' +// is required. +virtualBaton = function virtualBaton(options) { + var key = options.key, + channel = "io.highfidelity.virtualBaton:" + key, + exports = options.exports || {}, + timeout = options.timeout || 5, // seconds + claimCallback, + releaseCallback, + // paxos proposer state + nPromises = 0, + nQuorum, + mostRecentInterested, + bestPromise = {number: 0}, + // paxos acceptor state + bestProposal = {number: 0}, + accepted = null; + if (!key) { + throw new Error("A VirtualBaton must specify a key."); + } + function debug() { + print.apply(null, [].map.call(arguments, JSON.stringify)); + } + function send(operation, data) { + debug('baton: send', operation, data); + var message = JSON.stringify({op: operation, data: data}); + Messages.sendMessage(channel, message); + } + function doRelease() { + var callback = releaseCallback, oldAccepted = accepted; + accepted = releaseCallback = undefined; + debug('baton: doRelease', key, callback); + if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. + Messages.messageReceived.disconnect(messageHandler); + Messages.unsubscribe(channel); // Messages currently allow publishing without subscription. + send('release', oldAccepted); // This order is less crufty. + callback(key); // Pass key so that clients may use the same handler for different batons. + } + + // Internally, this uses the Paxos algorith to hold elections. + // Alternatively, we could have the message server pick and maintain a winner, but it would + // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. + // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer + // (such as the server). + function acceptedId() { return accepted && accepted.winner; } + // Paxos makes several tests of one "proposal number" versus another, assuming + // that better proposals from the same proposer have a higher number, + // and different proposers use a different set of numbers. We achieve that + // by dividing the "number" into two parts, and integer and a proposerId, + // which keeps the combined number unique and yet still strictly ordered. + function betterNumber(number, best) { + debug('baton: betterNumber', number, best); + return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); + } + function propose(claim) { + debug('baton: propose', claim); + if (!claimCallback) { return; } // We're not participating. + nPromises = 0; + nQuorum = Math.floor(AvatarList.getAvatarIdentifiers.length / 2) + 1; + bestPromise.proposerId = MyAvatar.sessionUUID; + bestPromise.number++; + bestPromise.winner = claim; + send('prepare!', bestPromise); + // Fixme: set a watchdog that is cancelled when we send accept!, and which propose(claim) when it goes off. + } + + function messageHandler(messageChannel, messageString, senderID) { + if (messageChannel !== channel) { return; } + var message = JSON.parse(messageString), data = message.data; + debug('baton: received from', senderID, message.op, data); + switch (message.op) { + case 'prepare!': + // Optimization: Don't waste time with low future proposals. + // Does not remove the need for betterNumber() to consider proposerId, because + // participants might not receive this prepare! message before their next proposal. + //FIXME bestPromise.number = Math.max(bestPromise.number, data.number); + + if (betterNumber(data, bestProposal)) { + var response = accepted || data; + if (!response.winner && claimCallback) { + // Optimization: Let the proposer know we're interested in the job if the proposer doesn't + // know who else to pick. Avoids needing to start multiple simultaneous proposals. + response.interested = MyAvatar.sessionUUID; + } + bestProposal = data; + send('promise', response); + } // FIXME nack? + break; + case 'promise': + if (data.proposerId !== MyAvatar.sessionUUID) { return; } // Only the proposer needs to do anything. + mostRecentInterested = mostRecentInterested || data.interested; + if (betterNumber(data, bestPromise)) { + bestPromise = data; + } + if (++nPromises >= nQuorum) { + if (!bestPromise.winner) { // we get to pick + bestPromise.winner = claimCallback ? MyAvatar.sessionUUID : mostRecentInterested; + } + send('accept!', bestPromise); + } + break; + case 'accept!': + if (!betterNumber(bestProposal, data)) { + accepted = data; + send('accepted', accepted); + } + // FIXME: start interval (with a little random offset?) that claims if winner is ever not in AvatarList and we still claimCallback + break; + case 'accepted': + accepted = data; + if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not been the proposer. + if (claimCallback) { + var callback = claimCallback; + claimCallback = undefined; + callback(key); + } else { // We won, but are no longer interested. + propose(); // Propose that someone else take the job. + } + } + break; + case 'release': + if (!betterNumber(accepted, data)) { // Unless our data is fresher... + accepted.winner = undefined; // ... allow next proposer to have his way. + } + break; + default: + print("Unrecognized virtualBaton message:", message); + } + } + // Registers an intent to hold the baton: + // Calls onElection(key) once, if you are elected by the scripts + // to be the unique holder of the baton, which may be never. + // Calls onRelease(key) once, if you release the baton held by you, + // whether this is by you calling release(), or by loosing + // an election when you become disconnected. + // You may claim again at any time after the start of onRelease + // being called. Otherwise, you will not participate in further elections. + exports.claim = function claim(onElection, onRelease) { + debug('baton: claim'); + if (claimCallback) { + print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); + return; + } + if (releaseCallback) { + print("Ignoring attempt to claim virtualBaton " + key + ", which is somehow incorrect released, and that should not happen."); + return; + } + claimCallback = onElection; + releaseCallback = onRelease; + Messages.messageReceived.connect(messageHandler); + Messages.subscribe(channel); + propose(MyAvatar.sessionUUID); + }; + + // Release the baton you hold, or just log that you are not holding it. + exports.release = function release(optionalReplacementOnRelease) { + debug('baton: release'); + if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + releaseCallback = optionalReplacementOnRelease; + } + if (acceptedId() !== MyAvatar.sessionUUID) { + print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); + return; + } + doRelease(); + if (!claimCallback) { // No claim set in release callback. + propose(); // We are the distinguished proposer, but we'll pick anyone else interested. + } + }; + + return exports; +}; diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js new file mode 100644 index 0000000000..3a44ecd92a --- /dev/null +++ b/examples/tests/testBaton.js @@ -0,0 +1,27 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; + +Script.include("../libraries/virtualBaton.18.js"); +var TICKER_INTERVAL = 1000; // ms +var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); +var ticker, countDown; + +// Tick every TICKER_INTERVAL. +function gotBaton(key) { + print("gotBaton", key); + countDown = 20; + ticker = Script.startInterval(function () { + print("tick"); + }, 1000); +} +// If we've lost the baton (e.g., to network problems), stop ticking +// but ask for the baton back (waiting indefinitely to get it). +function lostBaton(key) { + print("lostBaton", key); + Script.clearInterval(ticker); + baton.claim(gotBaton, lostBaton); +} +baton.claim(gotBaton, lostBaton); + + From cdff3323fb8c55704bd11567dc45e63b2b2acd59 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 13:25:42 -0800 Subject: [PATCH 02/73] typo --- examples/tests/testBaton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 3a44ecd92a..08c8691d5c 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -11,7 +11,7 @@ var ticker, countDown; function gotBaton(key) { print("gotBaton", key); countDown = 20; - ticker = Script.startInterval(function () { + ticker = Script.setInterval(function () { print("tick"); }, 1000); } From 38443af43750dbbc2d5a5bcacb22903c69f6b827 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 13:36:40 -0800 Subject: [PATCH 03/73] typo --- examples/libraries/{virtualBaton.18.js => virtualBaton.19.js} | 2 +- examples/tests/testBaton.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename examples/libraries/{virtualBaton.18.js => virtualBaton.19.js} (99%) diff --git a/examples/libraries/virtualBaton.18.js b/examples/libraries/virtualBaton.19.js similarity index 99% rename from examples/libraries/virtualBaton.18.js rename to examples/libraries/virtualBaton.19.js index c44bac0760..653681dda6 100644 --- a/examples/libraries/virtualBaton.18.js +++ b/examples/libraries/virtualBaton.19.js @@ -81,7 +81,7 @@ virtualBaton = function virtualBaton(options) { debug('baton: propose', claim); if (!claimCallback) { return; } // We're not participating. nPromises = 0; - nQuorum = Math.floor(AvatarList.getAvatarIdentifiers.length / 2) + 1; + nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; bestPromise.proposerId = MyAvatar.sessionUUID; bestPromise.number++; bestPromise.winner = claim; diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 08c8691d5c..02ac2e5232 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -2,7 +2,7 @@ /*jslint nomen: true, plusplus: true, vars: true*/ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; -Script.include("../libraries/virtualBaton.18.js"); +Script.include("../libraries/virtualBaton.19.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); var ticker, countDown; From 3d01b3ec2b2f90b649951c95367a9298a0cc2edd Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 14:15:03 -0800 Subject: [PATCH 04/73] watchdog --- .../{virtualBaton.19.js => virtualBaton.20.js} | 13 ++++++++++--- examples/tests/testBaton.js | 14 +++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) rename examples/libraries/{virtualBaton.19.js => virtualBaton.20.js} (94%) diff --git a/examples/libraries/virtualBaton.19.js b/examples/libraries/virtualBaton.20.js similarity index 94% rename from examples/libraries/virtualBaton.19.js rename to examples/libraries/virtualBaton.20.js index 653681dda6..76811d2f48 100644 --- a/examples/libraries/virtualBaton.19.js +++ b/examples/libraries/virtualBaton.20.js @@ -29,7 +29,6 @@ virtualBaton = function virtualBaton(options) { var key = options.key, channel = "io.highfidelity.virtualBaton:" + key, exports = options.exports || {}, - timeout = options.timeout || 5, // seconds claimCallback, releaseCallback, // paxos proposer state @@ -37,6 +36,8 @@ virtualBaton = function virtualBaton(options) { nQuorum, mostRecentInterested, bestPromise = {number: 0}, + electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election + electionWatchdog, // paxos acceptor state bestProposal = {number: 0}, accepted = null; @@ -79,6 +80,7 @@ virtualBaton = function virtualBaton(options) { } function propose(claim) { debug('baton: propose', claim); + if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } if (!claimCallback) { return; } // We're not participating. nPromises = 0; nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; @@ -86,9 +88,10 @@ virtualBaton = function virtualBaton(options) { bestPromise.number++; bestPromise.winner = claim; send('prepare!', bestPromise); - // Fixme: set a watchdog that is cancelled when we send accept!, and which propose(claim) when it goes off. + electionWatchdog = Script.setTimeout(function () { + propose(claim); + }, electionTimeout); } - function messageHandler(messageChannel, messageString, senderID) { if (messageChannel !== channel) { return; } var message = JSON.parse(messageString), data = message.data; @@ -134,6 +137,10 @@ virtualBaton = function virtualBaton(options) { case 'accepted': accepted = data; if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not been the proposer. + if (electionWatchdog) { + Script.clearTimeout(electionWatchdog); + electionWatchdog = null; + } if (claimCallback) { var callback = claimCallback; claimCallback = undefined; diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 02ac2e5232..5ba5d99565 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -1,8 +1,18 @@ "use strict"; /*jslint nomen: true, plusplus: true, vars: true*/ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; +// +// Created by Howard Stearns +// 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 +// +// test libraries/virtualBaton.js +// All participants should run the test script. -Script.include("../libraries/virtualBaton.19.js"); + +Script.include("../libraries/virtualBaton.20.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); var ticker, countDown; @@ -23,5 +33,3 @@ function lostBaton(key) { baton.claim(gotBaton, lostBaton); } baton.claim(gotBaton, lostBaton); - - From fbcacbe14ae0f58aedc2c72e9bd9e024a43fa5a6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 14:31:32 -0800 Subject: [PATCH 05/73] simpler betterNumber test. --- examples/libraries/{virtualBaton.20.js => virtualBaton.21.js} | 3 ++- examples/tests/testBaton.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename examples/libraries/{virtualBaton.20.js => virtualBaton.21.js} (98%) diff --git a/examples/libraries/virtualBaton.20.js b/examples/libraries/virtualBaton.21.js similarity index 98% rename from examples/libraries/virtualBaton.20.js rename to examples/libraries/virtualBaton.21.js index 76811d2f48..7cd9281555 100644 --- a/examples/libraries/virtualBaton.20.js +++ b/examples/libraries/virtualBaton.21.js @@ -76,7 +76,8 @@ virtualBaton = function virtualBaton(options) { // which keeps the combined number unique and yet still strictly ordered. function betterNumber(number, best) { debug('baton: betterNumber', number, best); - return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); + //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); + return (number.number || 0) > best.number; } function propose(claim) { debug('baton: propose', claim); diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 5ba5d99565..90f9219d09 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -12,7 +12,7 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // All participants should run the test script. -Script.include("../libraries/virtualBaton.20.js"); +Script.include("../libraries/virtualBaton.21.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); var ticker, countDown; From 03244fbeb58937381e4e05869a378540ec938781 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 15:37:22 -0800 Subject: [PATCH 06/73] fix promise-sending. --- ...{virtualBaton.21.js => virtualBaton.25.js} | 27 ++++++++++--------- examples/tests/testBaton.js | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) rename examples/libraries/{virtualBaton.21.js => virtualBaton.25.js} (91%) diff --git a/examples/libraries/virtualBaton.21.js b/examples/libraries/virtualBaton.25.js similarity index 91% rename from examples/libraries/virtualBaton.21.js rename to examples/libraries/virtualBaton.25.js index 7cd9281555..90b04b8dda 100644 --- a/examples/libraries/virtualBaton.21.js +++ b/examples/libraries/virtualBaton.25.js @@ -40,7 +40,7 @@ virtualBaton = function virtualBaton(options) { electionWatchdog, // paxos acceptor state bestProposal = {number: 0}, - accepted = null; + accepted = {}; if (!key) { throw new Error("A VirtualBaton must specify a key."); } @@ -54,7 +54,8 @@ virtualBaton = function virtualBaton(options) { } function doRelease() { var callback = releaseCallback, oldAccepted = accepted; - accepted = releaseCallback = undefined; + releaseCallback = undefined; + accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; debug('baton: doRelease', key, callback); if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. Messages.messageReceived.disconnect(messageHandler); @@ -68,7 +69,7 @@ virtualBaton = function virtualBaton(options) { // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer // (such as the server). - function acceptedId() { return accepted && accepted.winner; } + function acceptedId() { return accepted && accepted.winner; } // fixme doesn't need to be so fancy any more? // Paxos makes several tests of one "proposal number" versus another, assuming // that better proposals from the same proposer have a higher number, // and different proposers use a different set of numbers. We achieve that @@ -84,14 +85,13 @@ virtualBaton = function virtualBaton(options) { if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } if (!claimCallback) { return; } // We're not participating. nPromises = 0; - nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; + nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! bestPromise.proposerId = MyAvatar.sessionUUID; bestPromise.number++; bestPromise.winner = claim; send('prepare!', bestPromise); - electionWatchdog = Script.setTimeout(function () { - propose(claim); - }, electionTimeout); + function reclaim() { propose(claim); } + electionWatchdog = Script.setTimeout(reclaim, electionTimeout); } function messageHandler(messageChannel, messageString, senderID) { if (messageChannel !== channel) { return; } @@ -105,14 +105,15 @@ virtualBaton = function virtualBaton(options) { //FIXME bestPromise.number = Math.max(bestPromise.number, data.number); if (betterNumber(data, bestProposal)) { - var response = accepted || data; - if (!response.winner && claimCallback) { + bestProposal = data; + if (claimCallback) { // Optimization: Let the proposer know we're interested in the job if the proposer doesn't // know who else to pick. Avoids needing to start multiple simultaneous proposals. - response.interested = MyAvatar.sessionUUID; + accepted.interested = MyAvatar.sessionUUID; } - bestProposal = data; - send('promise', response); + send('promise', accepted.winner ? // data must include proposerId so that proposer catalogs results. + {number: accepted.number, proposerId: data.proposerId, winner: accepted.winner} : + {proposerId: data.proposerId}); } // FIXME nack? break; case 'promise': @@ -137,7 +138,7 @@ virtualBaton = function virtualBaton(options) { break; case 'accepted': accepted = data; - if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not been the proposer. + if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not have been the proposer. if (electionWatchdog) { Script.clearTimeout(electionWatchdog); electionWatchdog = null; diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 90f9219d09..8c92b320b3 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -12,7 +12,7 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // All participants should run the test script. -Script.include("../libraries/virtualBaton.21.js"); +Script.include("../libraries/virtualBaton.25.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); var ticker, countDown; From 59f1cdfc184484212c2d5b09fc3ec06c42622950 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 2 Feb 2016 16:18:33 -0800 Subject: [PATCH 07/73] Reinit bestPromise with each proposal. --- examples/libraries/{virtualBaton.25.js => virtualBaton.27.js} | 4 +--- examples/tests/testBaton.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) rename examples/libraries/{virtualBaton.25.js => virtualBaton.27.js} (98%) diff --git a/examples/libraries/virtualBaton.25.js b/examples/libraries/virtualBaton.27.js similarity index 98% rename from examples/libraries/virtualBaton.25.js rename to examples/libraries/virtualBaton.27.js index 90b04b8dda..9564f3f737 100644 --- a/examples/libraries/virtualBaton.25.js +++ b/examples/libraries/virtualBaton.27.js @@ -86,9 +86,7 @@ virtualBaton = function virtualBaton(options) { if (!claimCallback) { return; } // We're not participating. nPromises = 0; nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! - bestPromise.proposerId = MyAvatar.sessionUUID; - bestPromise.number++; - bestPromise.winner = claim; + bestPromise = {number: ++bestPromise.number, proposerId: MyAvatar.sessionUUID, winner: claim}; send('prepare!', bestPromise); function reclaim() { propose(claim); } electionWatchdog = Script.setTimeout(reclaim, electionTimeout); diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 8c92b320b3..bc6caa6f50 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -12,7 +12,7 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // All participants should run the test script. -Script.include("../libraries/virtualBaton.25.js"); +Script.include("../libraries/virtualBaton.27.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); var ticker, countDown; From 055de61ec64ef24a8e3af6e9fdddf2aef8e14d03 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 3 Feb 2016 11:10:04 -0800 Subject: [PATCH 08/73] update --- ...{virtualBaton.27.js => virtualBaton.29.js} | 76 +++++++++++-------- examples/tests/testBaton.js | 4 +- 2 files changed, 48 insertions(+), 32 deletions(-) rename examples/libraries/{virtualBaton.27.js => virtualBaton.29.js} (75%) diff --git a/examples/libraries/virtualBaton.27.js b/examples/libraries/virtualBaton.29.js similarity index 75% rename from examples/libraries/virtualBaton.27.js rename to examples/libraries/virtualBaton.29.js index 9564f3f737..6cdbbfe849 100644 --- a/examples/libraries/virtualBaton.27.js +++ b/examples/libraries/virtualBaton.29.js @@ -26,18 +26,21 @@ // Answers a new virtualBaton for the given parameters, of which 'key' // is required. virtualBaton = function virtualBaton(options) { - var key = options.key, + var key = options.key, channel = "io.highfidelity.virtualBaton:" + key, exports = options.exports || {}, claimCallback, releaseCallback, // paxos proposer state nPromises = 0, + proposalNumber = 0, nQuorum, mostRecentInterested, bestPromise = {number: 0}, - electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election + electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election. FIXME randomize electionWatchdog, + recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize + recheckWatchdog, // paxos acceptor state bestProposal = {number: 0}, accepted = {}; @@ -45,23 +48,26 @@ virtualBaton = function virtualBaton(options) { throw new Error("A VirtualBaton must specify a key."); } function debug() { - print.apply(null, [].map.call(arguments, JSON.stringify)); + print.apply(print, [].map.call(arguments, JSON.stringify)); // fixme no console + } + function debugFlow() { + if (options.debugFlow) { debug.apply(null, arguments); } } function send(operation, data) { - debug('baton: send', operation, data); + if (options.debugSend) { debug('baton:', MyAvatar.sessionUUID, '=>', '-', operation, data); } var message = JSON.stringify({op: operation, data: data}); Messages.sendMessage(channel, message); } - function doRelease() { + function localRelease() { var callback = releaseCallback, oldAccepted = accepted; releaseCallback = undefined; - accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; - debug('baton: doRelease', key, callback); + accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number. + debugFlow('baton: localRelease', key, !!callback); if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. - Messages.messageReceived.disconnect(messageHandler); - Messages.unsubscribe(channel); // Messages currently allow publishing without subscription. - send('release', oldAccepted); // This order is less crufty. + //Messages.messageReceived.disconnect(messageHandler); + //Messages.unsubscribe(channel); // Messages currently allow publishing without subscription. callback(key); // Pass key so that clients may use the same handler for different batons. + return oldAccepted; } // Internally, this uses the Paxos algorith to hold elections. @@ -69,38 +75,37 @@ virtualBaton = function virtualBaton(options) { // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer // (such as the server). - function acceptedId() { return accepted && accepted.winner; } // fixme doesn't need to be so fancy any more? + function acceptedId() { return accepted && accepted.winner; } // Paxos makes several tests of one "proposal number" versus another, assuming // that better proposals from the same proposer have a higher number, // and different proposers use a different set of numbers. We achieve that // by dividing the "number" into two parts, and integer and a proposerId, // which keeps the combined number unique and yet still strictly ordered. function betterNumber(number, best) { - debug('baton: betterNumber', number, best); + // FIXME restore debug('baton: betterNumber', number, best); //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); return (number.number || 0) > best.number; } - function propose(claim) { - debug('baton: propose', claim); + function propose() { + debugFlow('baton:', MyAvatar.sessionUUID, 'propose', !!claimCallback); if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } if (!claimCallback) { return; } // We're not participating. nPromises = 0; + proposalNumber = Math.max(proposalNumber, bestPromise.number); nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! - bestPromise = {number: ++bestPromise.number, proposerId: MyAvatar.sessionUUID, winner: claim}; - send('prepare!', bestPromise); - function reclaim() { propose(claim); } - electionWatchdog = Script.setTimeout(reclaim, electionTimeout); + send('prepare!', {number: ++proposalNumber, proposerId: MyAvatar.sessionUUID}); + electionWatchdog = Script.setTimeout(propose, electionTimeout); } function messageHandler(messageChannel, messageString, senderID) { if (messageChannel !== channel) { return; } var message = JSON.parse(messageString), data = message.data; - debug('baton: received from', senderID, message.op, data); + if (options.debugReceive) { debug('baton:', senderID, '=>', MyAvatar.sessionUUID, message.op, data); } switch (message.op) { case 'prepare!': // Optimization: Don't waste time with low future proposals. // Does not remove the need for betterNumber() to consider proposerId, because // participants might not receive this prepare! message before their next proposal. - //FIXME bestPromise.number = Math.max(bestPromise.number, data.number); + proposalNumber = Math.max(proposalNumber, data.number); if (betterNumber(data, bestProposal)) { bestProposal = data; @@ -111,7 +116,7 @@ virtualBaton = function virtualBaton(options) { } send('promise', accepted.winner ? // data must include proposerId so that proposer catalogs results. {number: accepted.number, proposerId: data.proposerId, winner: accepted.winner} : - {proposerId: data.proposerId}); + {number: data.number, proposerId: data.proposerId}); } // FIXME nack? break; case 'promise': @@ -130,11 +135,12 @@ virtualBaton = function virtualBaton(options) { case 'accept!': if (!betterNumber(bestProposal, data)) { accepted = data; - send('accepted', accepted); + //send('accepted', accepted); // With the collapsed roles here, do we need this message? Maybe just go to 'accepted' case here? + messageHandler(messageChannel, JSON.stringify({op: 'accepted', data: accepted}), senderID); } - // FIXME: start interval (with a little random offset?) that claims if winner is ever not in AvatarList and we still claimCallback break; case 'accepted': + if (betterNumber(accepted, data)) { return; } accepted = data; if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not have been the proposer. if (electionWatchdog) { @@ -145,7 +151,7 @@ virtualBaton = function virtualBaton(options) { var callback = claimCallback; claimCallback = undefined; callback(key); - } else { // We won, but are no longer interested. + } else if (!releaseCallback) { // We won, but have been released and are no longer interested. propose(); // Propose that someone else take the job. } } @@ -153,12 +159,19 @@ virtualBaton = function virtualBaton(options) { case 'release': if (!betterNumber(accepted, data)) { // Unless our data is fresher... accepted.winner = undefined; // ... allow next proposer to have his way. + if (recheckWatchdog) { + Script.clearInterval(recheckWatchdog); + recheckWatchdog = null; + } } break; default: print("Unrecognized virtualBaton message:", message); } } + Messages.messageReceived.connect(messageHandler); // FIXME MUST BE DONE. quorum will be wrong if no one claims and we only subscrbe with claims + Messages.subscribe(channel); // FIXME + // Registers an intent to hold the baton: // Calls onElection(key) once, if you are elected by the scripts // to be the unique holder of the baton, which may be never. @@ -168,7 +181,7 @@ virtualBaton = function virtualBaton(options) { // You may claim again at any time after the start of onRelease // being called. Otherwise, you will not participate in further elections. exports.claim = function claim(onElection, onRelease) { - debug('baton: claim'); + debugFlow('baton:', MyAvatar.sessionUUID, 'claim'); if (claimCallback) { print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); return; @@ -179,14 +192,14 @@ virtualBaton = function virtualBaton(options) { } claimCallback = onElection; releaseCallback = onRelease; - Messages.messageReceived.connect(messageHandler); - Messages.subscribe(channel); - propose(MyAvatar.sessionUUID); + //Messages.messageReceived.connect(messageHandler); + //Messages.subscribe(channel); + propose(); }; // Release the baton you hold, or just log that you are not holding it. exports.release = function release(optionalReplacementOnRelease) { - debug('baton: release'); + debugFlow('baton:', MyAvatar.sessionUUID, 'release'); if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. releaseCallback = optionalReplacementOnRelease; } @@ -194,7 +207,10 @@ virtualBaton = function virtualBaton(options) { print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); return; } - doRelease(); + var released = localRelease(); + if (released) { + send('release', released); // Let everyone know right away, including old number in case we overlap with reclaim. + } if (!claimCallback) { // No claim set in release callback. propose(); // We are the distinguished proposer, but we'll pick anyone else interested. } diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index bc6caa6f50..9f4e4defca 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -12,9 +12,9 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // All participants should run the test script. -Script.include("../libraries/virtualBaton.27.js"); +Script.include("../libraries/virtualBaton.29.js"); var TICKER_INTERVAL = 1000; // ms -var baton = virtualBaton({key: 'io.highfidelity.testBaton'}); +var baton = virtualBaton({key: 'io.highfidelity.testBaton', debugSend: true, debugFlow: true, debugReceive: true}); var ticker, countDown; // Tick every TICKER_INTERVAL. From 287d91d4b2932b440fdf45cd47a4699b5902b27c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 3 Feb 2016 20:57:35 -0800 Subject: [PATCH 09/73] better --- examples/libraries/virtualBaton.29.js | 220 ---------------------- examples/libraries/virtualBaton.31.js | 252 ++++++++++++++++++++++++++ examples/tests/testBaton.js | 2 +- 3 files changed, 253 insertions(+), 221 deletions(-) delete mode 100644 examples/libraries/virtualBaton.29.js create mode 100644 examples/libraries/virtualBaton.31.js diff --git a/examples/libraries/virtualBaton.29.js b/examples/libraries/virtualBaton.29.js deleted file mode 100644 index 6cdbbfe849..0000000000 --- a/examples/libraries/virtualBaton.29.js +++ /dev/null @@ -1,220 +0,0 @@ -"use strict"; -/*jslint nomen: true, plusplus: true, vars: true */ -/*global Entities, Script, MyAvatar, Messages, AvatarList, print */ -// -// Created by Howard Stearns -// 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 -// -// Allows cooperating scripts to pass a "virtual baton" between them, -// which is useful when part of a script should only be executed by -// the one participant that is holding this particular baton. -// -// A virtual baton is simply any string agreed upon by the scripts -// that use it. Only one script at a time can hold the baton, and it -// holds it until that script releases it, or the other scripts -// determine that the holding script is not responding. The script -// automatically determines who among claimants has the baton, if anyone, -// and holds an "election" if necessary. -// -// See entityScript/tribble.js as an example, and the functions -// virtualBaton(), claim(), release(). -// - -// Answers a new virtualBaton for the given parameters, of which 'key' -// is required. -virtualBaton = function virtualBaton(options) { - var key = options.key, - channel = "io.highfidelity.virtualBaton:" + key, - exports = options.exports || {}, - claimCallback, - releaseCallback, - // paxos proposer state - nPromises = 0, - proposalNumber = 0, - nQuorum, - mostRecentInterested, - bestPromise = {number: 0}, - electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election. FIXME randomize - electionWatchdog, - recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize - recheckWatchdog, - // paxos acceptor state - bestProposal = {number: 0}, - accepted = {}; - if (!key) { - throw new Error("A VirtualBaton must specify a key."); - } - function debug() { - print.apply(print, [].map.call(arguments, JSON.stringify)); // fixme no console - } - function debugFlow() { - if (options.debugFlow) { debug.apply(null, arguments); } - } - function send(operation, data) { - if (options.debugSend) { debug('baton:', MyAvatar.sessionUUID, '=>', '-', operation, data); } - var message = JSON.stringify({op: operation, data: data}); - Messages.sendMessage(channel, message); - } - function localRelease() { - var callback = releaseCallback, oldAccepted = accepted; - releaseCallback = undefined; - accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number. - debugFlow('baton: localRelease', key, !!callback); - if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. - //Messages.messageReceived.disconnect(messageHandler); - //Messages.unsubscribe(channel); // Messages currently allow publishing without subscription. - callback(key); // Pass key so that clients may use the same handler for different batons. - return oldAccepted; - } - - // Internally, this uses the Paxos algorith to hold elections. - // Alternatively, we could have the message server pick and maintain a winner, but it would - // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. - // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer - // (such as the server). - function acceptedId() { return accepted && accepted.winner; } - // Paxos makes several tests of one "proposal number" versus another, assuming - // that better proposals from the same proposer have a higher number, - // and different proposers use a different set of numbers. We achieve that - // by dividing the "number" into two parts, and integer and a proposerId, - // which keeps the combined number unique and yet still strictly ordered. - function betterNumber(number, best) { - // FIXME restore debug('baton: betterNumber', number, best); - //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); - return (number.number || 0) > best.number; - } - function propose() { - debugFlow('baton:', MyAvatar.sessionUUID, 'propose', !!claimCallback); - if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } - if (!claimCallback) { return; } // We're not participating. - nPromises = 0; - proposalNumber = Math.max(proposalNumber, bestPromise.number); - nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! - send('prepare!', {number: ++proposalNumber, proposerId: MyAvatar.sessionUUID}); - electionWatchdog = Script.setTimeout(propose, electionTimeout); - } - function messageHandler(messageChannel, messageString, senderID) { - if (messageChannel !== channel) { return; } - var message = JSON.parse(messageString), data = message.data; - if (options.debugReceive) { debug('baton:', senderID, '=>', MyAvatar.sessionUUID, message.op, data); } - switch (message.op) { - case 'prepare!': - // Optimization: Don't waste time with low future proposals. - // Does not remove the need for betterNumber() to consider proposerId, because - // participants might not receive this prepare! message before their next proposal. - proposalNumber = Math.max(proposalNumber, data.number); - - if (betterNumber(data, bestProposal)) { - bestProposal = data; - if (claimCallback) { - // Optimization: Let the proposer know we're interested in the job if the proposer doesn't - // know who else to pick. Avoids needing to start multiple simultaneous proposals. - accepted.interested = MyAvatar.sessionUUID; - } - send('promise', accepted.winner ? // data must include proposerId so that proposer catalogs results. - {number: accepted.number, proposerId: data.proposerId, winner: accepted.winner} : - {number: data.number, proposerId: data.proposerId}); - } // FIXME nack? - break; - case 'promise': - if (data.proposerId !== MyAvatar.sessionUUID) { return; } // Only the proposer needs to do anything. - mostRecentInterested = mostRecentInterested || data.interested; - if (betterNumber(data, bestPromise)) { - bestPromise = data; - } - if (++nPromises >= nQuorum) { - if (!bestPromise.winner) { // we get to pick - bestPromise.winner = claimCallback ? MyAvatar.sessionUUID : mostRecentInterested; - } - send('accept!', bestPromise); - } - break; - case 'accept!': - if (!betterNumber(bestProposal, data)) { - accepted = data; - //send('accepted', accepted); // With the collapsed roles here, do we need this message? Maybe just go to 'accepted' case here? - messageHandler(messageChannel, JSON.stringify({op: 'accepted', data: accepted}), senderID); - } - break; - case 'accepted': - if (betterNumber(accepted, data)) { return; } - accepted = data; - if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not have been the proposer. - if (electionWatchdog) { - Script.clearTimeout(electionWatchdog); - electionWatchdog = null; - } - if (claimCallback) { - var callback = claimCallback; - claimCallback = undefined; - callback(key); - } else if (!releaseCallback) { // We won, but have been released and are no longer interested. - propose(); // Propose that someone else take the job. - } - } - break; - case 'release': - if (!betterNumber(accepted, data)) { // Unless our data is fresher... - accepted.winner = undefined; // ... allow next proposer to have his way. - if (recheckWatchdog) { - Script.clearInterval(recheckWatchdog); - recheckWatchdog = null; - } - } - break; - default: - print("Unrecognized virtualBaton message:", message); - } - } - Messages.messageReceived.connect(messageHandler); // FIXME MUST BE DONE. quorum will be wrong if no one claims and we only subscrbe with claims - Messages.subscribe(channel); // FIXME - - // Registers an intent to hold the baton: - // Calls onElection(key) once, if you are elected by the scripts - // to be the unique holder of the baton, which may be never. - // Calls onRelease(key) once, if you release the baton held by you, - // whether this is by you calling release(), or by loosing - // an election when you become disconnected. - // You may claim again at any time after the start of onRelease - // being called. Otherwise, you will not participate in further elections. - exports.claim = function claim(onElection, onRelease) { - debugFlow('baton:', MyAvatar.sessionUUID, 'claim'); - if (claimCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); - return; - } - if (releaseCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is somehow incorrect released, and that should not happen."); - return; - } - claimCallback = onElection; - releaseCallback = onRelease; - //Messages.messageReceived.connect(messageHandler); - //Messages.subscribe(channel); - propose(); - }; - - // Release the baton you hold, or just log that you are not holding it. - exports.release = function release(optionalReplacementOnRelease) { - debugFlow('baton:', MyAvatar.sessionUUID, 'release'); - if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. - releaseCallback = optionalReplacementOnRelease; - } - if (acceptedId() !== MyAvatar.sessionUUID) { - print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); - return; - } - var released = localRelease(); - if (released) { - send('release', released); // Let everyone know right away, including old number in case we overlap with reclaim. - } - if (!claimCallback) { // No claim set in release callback. - propose(); // We are the distinguished proposer, but we'll pick anyone else interested. - } - }; - - return exports; -}; diff --git a/examples/libraries/virtualBaton.31.js b/examples/libraries/virtualBaton.31.js new file mode 100644 index 0000000000..5df4710d3e --- /dev/null +++ b/examples/libraries/virtualBaton.31.js @@ -0,0 +1,252 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +/*global Entities, Script, MyAvatar, Messages, AvatarList, print */ +// +// Created by Howard Stearns +// 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 +// +// Allows cooperating scripts to pass a "virtual baton" between them, +// which is useful when part of a script should only be executed by +// the one participant that is holding this particular baton. +// +// A virtual baton is simply any string agreed upon by the scripts +// that use it. Only one script at a time can hold the baton, and it +// holds it until that script releases it, or the other scripts +// determine that the holding script is not responding. The script +// automatically determines who among claimants has the baton, if anyone, +// and holds an "election" if necessary. +// +// See entityScript/tribble.js as an example, and the functions +// virtualBaton(), claim(), release(). +// + +// Answers a new virtualBaton for the given parameters, of which 'key' +// is required. +virtualBaton = function virtualBaton(options) { + // Answer averages (number +/- variability). Avoids having everyone act in lockstep. + function randomize(number, variability) { + var randomPart = number * variability; + return number - (randomPart / 2) + (Math.random() * randomPart); + } + var key = options.key, + useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, + exports = options.exports || {}, + electionTimeout = options.electionTimeout || randomize(1000, 0.2), // ms. If no winner in this time, hold a new election. + claimCallback, + releaseCallback, + ourId = MyAvatar.sessionUUID; // better be stable! + if (!key) { + throw new Error("A VirtualBaton must specify a key."); + } + function debug() { + print.apply(null, [].map.call(arguments, JSON.stringify)); + } + function debugFlow() { + if (options.debugFlow) { debug.apply(null, arguments); } + } + + // Messages: Just synactic sugar for hooking things up to Messages system. + // We create separate subchannel strings for each operation within our general channelKey, instead of using + // a switch in the receiver. + var channelKey = "io.highfidelity.virtualBaton:" + key, + subchannelHandlers = {}, // Message channel string => {function, op} + subchannelKeys = {}; // operation => Message channel string + function subchannelKey(operation) { return channelKey + ':' + operation; } + function receive(operation, handler) { // Record a handler for an operation on our channelKey + var subKey = subchannelKey(operation); + subchannelHandlers[subKey] = {receiver: handler, op: operation}; + subchannelKeys[operation] = subKey; + Messages.subscribe(subKey); + } + function sendHelper(subchannel, data) { + var message = JSON.stringify(data); + Messages.sendMessage(subchannel, message); + } + function send1(operation, destination, data) { // Send data for an operation to just one destination on our channelKey. + if (options.debugSend) { debug('baton:', ourId, '=>', destination, operation, data); } + sendHelper(subchannelKey(operation) + destination, data); + } + function send(operation, data) { // Send data for an operation on our channelKey. + if (options.debugSend) { debug('baton:', ourId, '=>', '-', operation, data); } + sendHelper(subchannelKeys[operation], data); + } + Messages.messageReceived.connect(function (channel, messageString, senderID) { + var handler = subchannelHandlers[channel]; + if (!handler) { return; } + var data = JSON.parse(messageString); + if (options.debugReceive) { debug('baton:', senderID, '=>', ourId, handler.op, data); } + handler.receiver(data); + }); + + // Internally, this uses the Paxos algorith to hold elections. + // Alternatively, we could have the message server pick and maintain a winner, but it would + // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. + // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer + // (such as the server). + // + // Paxos makes several tests of one "proposal number" versus another, assuming + // that better proposals from the same proposer have a higher number, + // and different proposers use a different set of numbers. We achieve that + // by dividing the "number" into two parts, and integer and a proposerId, + // which keeps the combined number unique and yet still strictly ordered. + function betterNumber(number, best) { + // FIXME restore debug('baton: betterNumber', number, best); + //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); + return (number.number || 0) > best.number; + } + // Paxos Proposer behavior + var nPromises = 0, + proposalNumber = 0, + nQuorum, + mostRecentInterested, + bestPromise = {number: 0}, + electionWatchdog, + recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize + recheckWatchdog; + function propose() { + debugFlow('baton:', ourId, 'propose', !!claimCallback); + if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } + if (!claimCallback) { return; } // We're not participating. + electionWatchdog = Script.setTimeout(propose, electionTimeout); + nPromises = 0; + proposalNumber = Math.max(proposalNumber, bestPromise.number); + nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! + send('prepare!', {number: ++proposalNumber, proposerId: ourId}); + } + // We create a distinguished promise subchannel for our id, because promises need only be sent to the proposer. + receive('promise' + ourId, function (data) { + if (data.proposerId !== ourId) { return; } // Only the proposer needs to do anything. + mostRecentInterested = mostRecentInterested || data.interested; + if (betterNumber(data, bestPromise)) { + bestPromise = data; + } + if (++nPromises >= nQuorum) { + var answer = {number: data.proposalNumber, proposerId: data.proposerId, winner: data.winner}; + if (!answer.winner) { // we get to pick + answer.winner = claimCallback ? ourId : mostRecentInterested; + } + send('accept!', answer); + } + }); + // Paxos Acceptor behavior + var bestProposal = {number: 0}, accepted = {}; + function acceptedId() { return accepted && accepted.winner; } + receive('prepare!', function (data) { + if (useOptimizations) { // Don't waste time with low future proposals. + proposalNumber = Math.max(proposalNumber, data.number); + } + if (betterNumber(data, bestProposal)) { + bestProposal = data; + if (claimCallback && useOptimizations) { + // Let the proposer know we're interested in the job if the proposer doesn't + // know who else to pick. Avoids needing to start multiple simultaneous proposals. + accepted.interested = ourId; + } + send1('promise', data.proposerId, + accepted.winner ? // data must include proposerId and number so that proposer catalogs results. + {number: accepted.number, winner: accepted.winner, proposerId: data.proposerId, proposalNumber: data.number} : + {proposerId: data.proposerId, proposalNumber: data.number}); + } // FIXME nack? + }); + receive('accept!', function (data) { + if (!betterNumber(bestProposal, data)) { + bestProposal = data; + if (useOptimizations) { + // The Paxos literature describe every acceptor sending 'accepted' to + // every proposer and learner. In our case, these are the same nodes that received + // the 'accept!' message, so we can send to just the originating proposer and invoke + // our own accepted handler directly. + // Note that this optimization cannot be used with Byzantine Paxos (which needs another + // multi-broadcast to detect lying and collusion). + subchannelHandlers[subchannelKey('accepted') + ourId].receiver(data); + send1('accepted', data.proposerId, data); + } else { + send('accepted', data); + } + } + }); + // Paxos Learner behavior. + receive('accepted' + (useOptimizations ? ourId : ''), function (data) { // See note in 'accept!' regarding use of ourId here. + accepted = data; + if (acceptedId() === ourId) { // Note that we might not have been the proposer. + if (electionWatchdog) { + Script.clearTimeout(electionWatchdog); + electionWatchdog = null; + } + if (claimCallback) { + var callback = claimCallback; + claimCallback = undefined; + callback(key); + } else if (!releaseCallback) { // We won, but have been released and are no longer interested. + propose(); // Propose that someone else take the job. + } + } + }); + + receive('release', function (data) { + if (!betterNumber(accepted, data)) { // Unless our data is fresher... + debugFlow('baton:', ourId, 'handle release', data); + accepted.winner = null; // ... allow next proposer to have his way by making this explicitly null (not undefined). + if (recheckWatchdog) { + Script.clearInterval(recheckWatchdog); + recheckWatchdog = null; + } + } + }); + function localRelease() { + var callback = releaseCallback, oldAccepted = accepted; + releaseCallback = undefined; + accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number. + debugFlow('baton: localRelease', key, !!callback); + if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. + callback(key); // Pass key so that clients may use the same handler for different batons. + return oldAccepted; + } + + // Registers an intent to hold the baton: + // Calls onElection(key) once, if you are elected by the scripts + // to be the unique holder of the baton, which may be never. + // Calls onRelease(key) once, if you release the baton held by you, + // whether this is by you calling release(), or by loosing + // an election when you become disconnected. + // You may claim again at any time after the start of onRelease + // being called. Otherwise, you will not participate in further elections. + exports.claim = function claim(onElection, onRelease) { + debugFlow('baton:', ourId, 'claim'); + if (claimCallback) { + print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); + return; + } + if (releaseCallback) { + print("Ignoring attempt to claim virtualBaton " + key + ", which is somehow incorrect released, and that should not happen."); + return; + } + claimCallback = onElection; + releaseCallback = onRelease; + propose(); + }; + + // Release the baton you hold, or just log that you are not holding it. + exports.release = function release(optionalReplacementOnRelease) { + debugFlow('baton:', ourId, 'release'); + if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + releaseCallback = optionalReplacementOnRelease; + } + if (acceptedId() !== ourId) { + print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); + return; + } + var released = localRelease(); + if (released) { + send('release', released); // Let everyone know right away, including old number in case we overlap with reclaim. + } + if (!claimCallback) { // No claim set in release callback. + propose(); // We are the distinguished proposer, but we'll pick anyone else interested. + } + }; + + return exports; +}; diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js index 9f4e4defca..74fa0d39e4 100644 --- a/examples/tests/testBaton.js +++ b/examples/tests/testBaton.js @@ -12,7 +12,7 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // All participants should run the test script. -Script.include("../libraries/virtualBaton.29.js"); +Script.include("../libraries/virtualBaton.31.js"); var TICKER_INTERVAL = 1000; // ms var baton = virtualBaton({key: 'io.highfidelity.testBaton', debugSend: true, debugFlow: true, debugReceive: true}); var ticker, countDown; From 663dadd1b4c266041ed686d3ba49f14bcbdc3482 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 08:51:42 -0800 Subject: [PATCH 10/73] only disconnect mouse if connected --- examples/junkyard/junkyardClientReset.js | 22 +++++++++---------- .../CellScience/Scripts/clickToRideAndLook.js | 8 ++++--- .../CellScience/Scripts/navigationButton.js | 2 +- .../Scripts/playBackgroundAudio.js | 4 ++-- .../CellScience/importCellScience.js | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/examples/junkyard/junkyardClientReset.js b/examples/junkyard/junkyardClientReset.js index c4dfa19c4f..36c87ea0dc 100644 --- a/examples/junkyard/junkyardClientReset.js +++ b/examples/junkyard/junkyardClientReset.js @@ -13,18 +13,18 @@ // var IMPORT_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/arfs/junkyard.json"; -var PASTE_ENTITIES_LOCATION = {x: 0, y: 0, z: 0}; -reset(); - -function reset() { - // Delete everything and re-import the junkyard arf - var e = Entities.findEntities(MyAvatar.position, 1000); - for (i = 0; i < e.length; i++) { - Entities.deleteEntity(e[i]); - } - importAssetResourceFile(); -} +var PASTE_ENTITIES_LOCATION = {x: 0, y: 20, z: 0}; +// reset(); +// function reset() { +// // Delete everything and re-import the junkyard arf +// var e = Entities.findEntities(MyAvatar.position, 1000); +// for (i = 0; i < e.length; i++) { +// Entities.deleteEntity(e[i]); +// } +// importAssetResourceFile(); +// } + importAssetResourceFile() function importAssetResourceFile() { Clipboard.importEntities(IMPORT_URL); Clipboard.pasteEntities(PASTE_ENTITIES_LOCATION); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js b/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js index 805cdc423b..1ba0206edc 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js @@ -24,7 +24,7 @@ this.buttonImageURL = baseURL + "GUI/GUI_jump_off.png"; this.addExitButton(); this.isRiding = false; - + self.mouseIsConnected = false; if (this.data && this.data.isDynein) { this.rotation = 180; } else { @@ -66,6 +66,7 @@ visible: true }); Controller.mousePressEvent.connect(this.onMousePress); + self.mouseIsConnected = true; Script.update.connect(this.update); } } @@ -136,8 +137,9 @@ // print("unload"); self.reset(); - Controller.mousePressEvent.disconnect(this.onMousePress); + if (self.mouseIsConnected === true) { + Controller.mousePressEvent.disconnect(this.onMousePress); + } } - }); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js index 9e1874ed00..31f6612296 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js @@ -8,7 +8,7 @@ (function() { - var version = 1; + var version = 2; var added = false; this.frame = 0; var utilsScript = Script.resolvePath('utils.js'); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js index 856b9f8f67..ff1f772d4e 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js @@ -8,13 +8,13 @@ (function() { var self = this; var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; - var version = 9; + var version = 10; this.preload = function(entityId) { self.soundPlaying = false; self.entityId = entityId; self.getUserData(); self.soundURL = baseURL + "Audio/" + self.userData.name + ".wav?" + version; - print("Script.clearTimeout creating WAV name location is " + baseURL + "Audio/" + self.userData.name + ".wav"); + print("creating WAV name location is " + baseURL + "Audio/" + self.userData.name + ".wav"); self.soundOptions = { stereo: true, diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index 58888584a9..36b60bf939 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1004; +var version = 1005; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; From 82f9b3f97f21dc202306d34c135f872503bc661e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 11:33:54 -0800 Subject: [PATCH 11/73] increase vol for voiceover --- .../DomainContent/CellScience/Scripts/showButtonToPlaySound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 65fddd7ad3..7e51dcd9d1 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -24,7 +24,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.035 + volume: 0.2 }; this.sound = SoundCache.getSound(this.soundURL); } From 708c6e8d25431dc20713dff56240110ab5b4f460 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 11:37:46 -0800 Subject: [PATCH 12/73] volume --- .../DomainContent/CellScience/Scripts/showButtonToPlaySound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 7e51dcd9d1..277896e1bd 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -24,7 +24,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.2 + volume: 0.135 }; this.sound = SoundCache.getSound(this.soundURL); } From c377254cb0f7b0aa289be43846e42e3b54445140 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 14:34:41 -0800 Subject: [PATCH 13/73] revert junkyardreset --- examples/junkyard/junkyardClientReset.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/junkyard/junkyardClientReset.js b/examples/junkyard/junkyardClientReset.js index 36c87ea0dc..c4dfa19c4f 100644 --- a/examples/junkyard/junkyardClientReset.js +++ b/examples/junkyard/junkyardClientReset.js @@ -13,18 +13,18 @@ // var IMPORT_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/arfs/junkyard.json"; -var PASTE_ENTITIES_LOCATION = {x: 0, y: 20, z: 0}; -// reset(); +var PASTE_ENTITIES_LOCATION = {x: 0, y: 0, z: 0}; +reset(); + +function reset() { + // Delete everything and re-import the junkyard arf + var e = Entities.findEntities(MyAvatar.position, 1000); + for (i = 0; i < e.length; i++) { + Entities.deleteEntity(e[i]); + } + importAssetResourceFile(); +} -// function reset() { -// // Delete everything and re-import the junkyard arf -// var e = Entities.findEntities(MyAvatar.position, 1000); -// for (i = 0; i < e.length; i++) { -// Entities.deleteEntity(e[i]); -// } -// importAssetResourceFile(); -// } - importAssetResourceFile() function importAssetResourceFile() { Clipboard.importEntities(IMPORT_URL); Clipboard.pasteEntities(PASTE_ENTITIES_LOCATION); From 76350a39f8091239b66fd98e40ea4ff6e849ac0b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 14:46:58 -0800 Subject: [PATCH 14/73] increase volume more and fix not having logDebug function --- .../CellScience/Scripts/showButtonToPlaySound.js | 2 +- unpublishedScripts/DomainContent/CellScience/Scripts/utils.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 277896e1bd..0a4c709c69 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -24,7 +24,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.135 + volume: 0.26 }; this.sound = SoundCache.getSound(this.soundURL); } diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js b/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js index a79637acc3..10a0827e9d 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js @@ -20,8 +20,8 @@ getEntityUserData = function(id) { try { results = JSON.parse(properties.userData); } catch (err) { - logDebug(err); - logDebug(properties.userData); + print('error parsing json'); + print('properties are:'+ properties.userData); } } return results ? results : {}; From 0c92c52d267e98ebc7d46fa52519376efab2ef7e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Feb 2016 14:52:50 -0800 Subject: [PATCH 15/73] more volume --- .../DomainContent/CellScience/Scripts/showButtonToPlaySound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 0a4c709c69..e505a532e3 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -24,7 +24,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.26 + volume: 0.5 }; this.sound = SoundCache.getSound(this.soundURL); } From 82789113a833a2ce9d24e77a17452bb2b6633871 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 5 Feb 2016 19:50:08 -0800 Subject: [PATCH 16/73] quiet print messages --- .../CellScience/Scripts/clickToRideAndLook.js | 64 +- .../CellScience/Scripts/moveRandomly.js | 4 +- .../CellScience/Scripts/navigationButton.js | 90 +- .../Scripts/playBackgroundAudio.js | 74 +- .../Scripts/showButtonToPlaySound.js | 190 ++-- .../CellScience/Scripts/showIdentification.js | 75 +- .../CellScience/Scripts/utils.js | 4 +- .../DomainContent/CellScience/Scripts/zoom.js | 93 +- .../CellScience/importCellScience.js | 846 +++++++++--------- 9 files changed, 792 insertions(+), 648 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js b/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js index 1ba0206edc..d6a9b79467 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/clickToRideAndLook.js @@ -18,21 +18,46 @@ var self = this; this.preload = function(entityId) { - this.entityId = entityId; - this.data = JSON.parse(Entities.getEntityProperties(this.entityId, "userData").userData); - this.buttonImageURL = baseURL + "GUI/GUI_jump_off.png"; - this.addExitButton(); - this.isRiding = false; - self.mouseIsConnected = false; - if (this.data && this.data.isDynein) { - this.rotation = 180; - } else { - this.rotation = 0; - } - + this.initialize(entityId); + self.initTimeout = null; } + this.initialize = function(entityId) { + //print(' should initialize' + entityId) + var properties = Entities.getEntityProperties(entityId); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + //print(' no user data yet, try again in one second') + self.initialize(entityId); + }, 1000) + + } else { + //print(' userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + //print(' error parsing json'); + //print(' properties are:' + properties.userData); + return; + } + + self.data = self.userData; + self.buttonImageURL = baseURL + "GUI/GUI_jump_off.png"; + self.addExitButton(); + self.isRiding = false; + self.mouseIsConnected = false; + if (self.data && self.data.isDynein) { + self.rotation = 180; + } else { + self.rotation = 0; + } + } + } + + + this.addExitButton = function() { this.windowDimensions = Controller.getViewportDimensions(); this.buttonWidth = 75; @@ -53,14 +78,16 @@ } this.clickReleaseOnEntity = function(entityId, mouseEvent) { - // print('CLICKED ON MOTOR PROTEIN') + + //print('CLICKED ON MOTOR PROTEIN') + return; if (mouseEvent.isLeftButton && !self.isRiding) { - print("GET ON"); + //print("GET ON"); self.isRiding = true; if (!self.entityId) { self.entityId = entityId; } - self.entityLocation = Entities.getEntityProperties(this.entityId, "position").position; + self.entityLocation = Entities.getEntityProperties(self.entityId, "position").position; self.targetLocation = Vec3.sum(self.entityLocation, TARGET_OFFSET); Overlays.editOverlay(self.exitButton, { visible: true @@ -117,7 +144,7 @@ y: event.y }); if (event.isLeftButton && clickedOverlay === self.exitButton) { - print("GET OFF"); + //print("GET OFF"); Script.update.disconnect(this.update); self.reset(); } @@ -138,7 +165,10 @@ self.reset(); if (self.mouseIsConnected === true) { - Controller.mousePressEvent.disconnect(this.onMousePress); + Controller.mousePressEvent.disconnect(self.onMousePress); + } + if (self.initTimeout !== null) { + Script.clearTimeout(self.initTimeout); } } diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js b/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js index 7a3f2cf8fd..7c880a44b6 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js @@ -40,7 +40,7 @@ z: Math.random() - 0.5 }; - // print("POS magnitude is " + magnitudeV + " and direction is " + directionV.x); + //print("POS magnitude is " + magnitudeV + " and direction is " + directionV.x); Entities.editEntity(self.entityId, { velocity: Vec3.multiply(magnitudeV, Vec3.normalize(directionV)) @@ -60,7 +60,7 @@ y: Math.random() - 0.5, z: Math.random() - 0.5 }; - // print("ROT magnitude is " + magnitudeAV + " and direction is " + directionAV.x); + //print("ROT magnitude is " + magnitudeAV + " and direction is " + directionAV.x); Entities.editEntity(self.entityId, { angularVelocity: Vec3.multiply(magnitudeAV, Vec3.normalize(directionAV)) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js index 31f6612296..2798b58807 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js @@ -1,4 +1,3 @@ - // Copyright 2016 High Fidelity, Inc. // // @@ -8,7 +7,7 @@ (function() { - var version = 2; + var version = 10; var added = false; this.frame = 0; var utilsScript = Script.resolvePath('utils.js'); @@ -19,35 +18,61 @@ this.preload = function(entityId) { this.entityId = entityId; - var mySavedSettings = Settings.getValue(entityId); + this.initialize(entityId); + this.initTimeout = null; + } - if (mySavedSettings.buttons !== undefined) { - // print('NAV preload buttons'+ mySavedSettings.buttons) - mySavedSettings.buttons.forEach(function(b) { - // print('NAV deleting button'+ b) - Overlays.deleteOverlay(b); - }) - Settings.setValue(entityId,'') - } - - - self.getUserData(); - this.buttonImageURL = baseURL + "GUI/GUI_" + self.userData.name + ".png?" + version; - if (self.button === undefined) { - // print('NAV NO BUTTON ADDING ONE!!') - self.button = true; - self.addButton(); + this.initialize = function(entityId) { + // print(' should initialize' + entityId) + var properties = Entities.getEntityProperties(entityId); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + // print(' no user data yet, try again in one second') + self.initialize(entityId); + }, 1000) } else { - // print('NAV SELF ALREADY HAS A BUTTON!!') - } + // print(' userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + // print(' error parsing json'); + // print(' properties are:' + properties.userData); + return; + } + + var mySavedSettings = Settings.getValue(entityId); + + if (mySavedSettings.buttons !== undefined) { + //print(' preload buttons' + mySavedSettings.buttons) + mySavedSettings.buttons.forEach(function(b) { + //print(' deleting button' + b) + Overlays.deleteOverlay(b); + }) + Settings.setValue(entityId, '') + } + + + self.buttonImageURL = baseURL + "GUI/GUI_" + self.userData.name + ".png?" + version; + //print('BUTTON IMAGE URL:' + self.buttonImageURL) + if (self.button === undefined) { + // print('NAV NO BUTTON ADDING ONE!!') + self.button = true; + self.addButton(); + + } else { + // print('NAV SELF ALREADY HAS A BUTTON!!') + } + + } } + + this.addButton = function() { - - self.getUserData(); this.windowDimensions = Controller.getViewportDimensions(); this.buttonWidth = 150; this.buttonHeight = 50; @@ -87,7 +112,7 @@ if (self.frame < 10) { self.frame++; } else { - // this.lookAt(this.userData.target); + // this.lookAt(this.userData.target); } } @@ -107,7 +132,7 @@ } this.lookAtTarget = function() { - self.getUserData(); + var direction = Vec3.normalize(Vec3.subtract(self.userData.entryPoint, self.userData.target)); var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { x: 1, @@ -125,16 +150,6 @@ MyAvatar.headYaw = 0; } - - this.getUserData = function() { - this.properties = Entities.getEntityProperties(this.entityId); - if (self.properties.userData) { - this.userData = JSON.parse(this.properties.userData); - } else { - this.userData = {}; - } - } - var buttonDeleter; var deleterCount = 0; this.unload = function() { @@ -144,6 +159,11 @@ Controller.mousePressEvent.disconnect(this.onClick); // Script.update.disconnect(this.update); + + + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); + } } Controller.mousePressEvent.connect(this.onClick); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js index ff1f772d4e..6edccc211c 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js @@ -12,50 +12,72 @@ this.preload = function(entityId) { self.soundPlaying = false; self.entityId = entityId; - self.getUserData(); - self.soundURL = baseURL + "Audio/" + self.userData.name + ".wav?" + version; - print("creating WAV name location is " + baseURL + "Audio/" + self.userData.name + ".wav"); - - self.soundOptions = { - stereo: true, - loop: true, - localOnly: true, - volume: 0.035 - }; - - this.sound = SoundCache.getSound(self.soundURL); - + this.initTimeout = null; + this.initialize(entityId); } - this.getUserData = function() { - self.properties = Entities.getEntityProperties(self.entityId); - if (self.properties.userData) { - self.userData = JSON.parse(this.properties.userData); + this.initialize = function(entityID) { + //print(' should initialize' + entityID) + var properties = Entities.getEntityProperties(entityID); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + //print(' no user data yet, try again in one second') + self.initialize(entityID); + }, 1000) + } else { - self.userData = {}; + //print(' userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + //print(' error parsing json'); + //print(' properties are:' + properties.userData); + return; + } + + + //print(' USERDATA NAME ' + self.userData.name) + self.soundURL = baseURL + "Audio/" + self.userData.name + ".wav?" + version; + //print(" creating WAV name location is " + baseURL + "Audio/" + self.userData.name + ".wav"); + //print(' self soundURL' + self.soundURL) + + self.soundOptions = { + stereo: true, + loop: true, + localOnly: true, + volume: 0.035 + }; + + self.sound = SoundCache.getSound(self.soundURL); } } this.enterEntity = function(entityID) { - print("entering audio zone"); + //print("entering audio zone"); if (self.sound.downloaded) { - print("playing background audio named " + self.userData.name + "which has been downloaded"); + //print("playing background audio named " + self.userData.name + "which has been downloaded"); this.soundPlaying = Audio.playSound(self.sound, self.soundOptions); } else { - print("sound is not downloaded"); + //print("sound is not downloaded"); } } - this.leaveEntity = function(entityID) { - print("leaving audio area " + self.userData.name); + //print("leaving audio area " + self.userData.name); if (self.soundPlaying !== false) { - print("not null"); - print("Stopped sound " + self.userData.name); + //print("not null"); + //print("Stopped sound " + self.userData.name); self.soundPlaying.stop(); } else { - print("Sound not playing"); + //print("Sound not playing"); + } + } + + this.unload = function() { + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); } } diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index e505a532e3..d37655f09b 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -6,97 +6,117 @@ // (function() { - var baseURL = "https://hifi-content.s3.amazonaws.com/hifi-content/DomainContent/CellScience/"; - var self = this; - this.buttonImageURL = baseURL + "GUI/play_audio.svg?2"; + var baseURL = "https://hifi-content.s3.amazonaws.com/hifi-content/DomainContent/CellScience/"; + var self = this; + this.buttonImageURL = baseURL + "GUI/play_audio.svg?2"; - - - this.preload = function(entityId) { - this.entityId = entityId; - self.addButton(); - this.buttonShowing = false; - self.getUserData(); - this.showDistance = self.userData.showDistance; - this.soundURL = baseURL + "Audio/" + self.userData.soundName + ".wav"; - print("distance = " + self.userData.showDistance + ", sound = " + this.soundURL); - this.soundOptions = { - stereo: true, - loop: false, - localOnly: true, - volume: 0.5 - }; - this.sound = SoundCache.getSound(this.soundURL); - } - - this.addButton = function() { - this.windowDimensions = Controller.getViewportDimensions(); - this.buttonWidth = 100; - this.buttonHeight = 100; - this.buttonPadding = 0; - - this.buttonPositionX = (self.windowDimensions.x - self.buttonPadding) / 2 - self.buttonWidth; - this.buttonPositionY = (self.windowDimensions.y - self.buttonHeight) - (self.buttonHeight + self.buttonPadding); - this.button = Overlays.addOverlay("image", { - x: self.buttonPositionX, - y: self.buttonPositionY, - width: self.buttonWidth, - height: self.buttonHeight, - imageURL: self.buttonImageURL, - visible: false, - alpha: 1.0 - }); - } - - this.getUserData = function() { - this.properties = Entities.getEntityProperties(this.entityId); - if (self.properties.userData) { - this.userData = JSON.parse(this.properties.userData); - } else { - this.userData = {}; + this.preload = function(entityId) { + this.entityId = entityId; + this.initialize(entityId) + this.initTimeout = null; } - } - this.update = function(deltaTime) { + this.initialize = function(entityId) { + print(' should initialize' + entityId) + var properties = Entities.getEntityProperties(entityId); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + print(' no user data yet, try again in one second') + self.initialize(entityId); + }, 1000) - self.distance = Vec3.distance(MyAvatar.position, Entities.getEntityProperties(self.entityId).position); - //print(self.distance); - if (!self.buttonShowing && self.distance < self.userData.showDistance) { - self.buttonShowing = true; - Overlays.editOverlay(self.button, { - visible: true - }); - } else if (self.buttonShowing && self.distance > self.userData.showDistance) { - self.buttonShowing = false; - Overlays.editOverlay(self.button, { - visible: false - }); - } - } - - this.onClick = function(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - if (clickedOverlay === self.button) { - print("button was clicked"); - if (self.sound.downloaded) { - print("play sound"); - Audio.playSound(self.sound, self.soundOptions); } else { - print("not downloaded"); + print(' userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + print(' error parsing json'); + print(' properties are:' + properties.userData); + return; + } + + + + self.addButton(); + self.buttonShowing = false; + self.showDistance = self.userData.showDistance; + self.soundURL = baseURL + "Audio/" + self.userData.soundName + ".wav"; + // print("distance = " + self.userData.showDistance + ", sound = " + self.soundURL); + self.soundOptions = { + stereo: true, + loop: false, + localOnly: true, + volume: 0.75 + }; + self.sound = SoundCache.getSound(this.soundURL); + + + } - } - } - this.unload = function() { - Overlays.deleteOverlay(self.button); - Controller.mousePressEvent.disconnect(this.onClick); - Script.update.disconnect(this.update); - } + this.addButton = function() { + this.windowDimensions = Controller.getViewportDimensions(); + this.buttonWidth = 100; + this.buttonHeight = 100; + this.buttonPadding = 0; - Controller.mousePressEvent.connect(this.onClick); - Script.update.connect(this.update); + this.buttonPositionX = (self.windowDimensions.x - self.buttonPadding) / 2 - self.buttonWidth; + this.buttonPositionY = (self.windowDimensions.y - self.buttonHeight) - (self.buttonHeight + self.buttonPadding); + this.button = Overlays.addOverlay("image", { + x: self.buttonPositionX, + y: self.buttonPositionY, + width: self.buttonWidth, + height: self.buttonHeight, + imageURL: self.buttonImageURL, + visible: false, + alpha: 1.0 + }); + } -}); \ No newline at end of file + this.update = function(deltaTime) { + + self.distance = Vec3.distance(MyAvatar.position, Entities.getEntityProperties(self.entityId).position); + //print(self.distance); + if (!self.buttonShowing && self.distance < self.userData.showDistance) { + self.buttonShowing = true; + Overlays.editOverlay(self.button, { + visible: true + }); + } else if (self.buttonShowing && self.distance > self.userData.showDistance) { + self.buttonShowing = false; + Overlays.editOverlay(self.button, { + visible: false + }); + } + } + + this.onClick = function(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + if (clickedOverlay === self.button) { + print("button was clicked"); + if (self.sound.downloaded) { + print("play sound"); + Audio.playSound(self.sound, self.soundOptions); + } else { + print("not downloaded"); + } + } + } + + this.unload = function() { + Overlays.deleteOverlay(self.button); + Controller.mousePressEvent.disconnect(this.onClick); + Script.update.disconnect(this.update); + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); + } + } + + Controller.mousePressEvent.connect(this.onClick); + Script.update.connect(this.update); + + }); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js index 2e37f3a51f..7c019865e9 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js @@ -14,20 +14,47 @@ this.preload = function(entityId) { this.soundPlaying = null; this.entityId = entityId; - self.getUserData(); - this.labelURL = baseURL + "GUI/labels_" + self.userData.name + ".png?" + version; - this.showDistance = self.userData.showDistance; - this.soundURL = baseURL + "Audio/" + self.userData.name + ".wav"; - this.soundOptions = { - stereo: true, - loop: false, - localOnly: true, - volume: 0.035, - position: this.position - }; - this.sound = SoundCache.getSound(this.soundURL); - this.buttonImageURL = baseURL + "GUI/GUI_audio.png?" + version; - self.addButton(); + self.initTimeout = null; + this.initialize(entityId); + + + } + + this.initialize = function(entityId) { + print(' should initialize' + entityId) + var properties = Entities.getEntityProperties(entityId); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + //print(' no user data yet, try again in one second') + self.initialize(entityId); + }, 1000) + + } else { + //print(' userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + //print(' error parsing json'); + //print(' properties are:' + properties.userData); + return; + } + + self.labelURL = baseURL + "GUI/labels_" + self.userData.name + ".png?" + version; + self.showDistance = self.userData.showDistance; + self.soundURL = baseURL + "Audio/" + self.userData.name + ".wav"; + self.soundOptions = { + stereo: true, + loop: false, + localOnly: true, + volume: 0.035, + position: properties.position + }; + self.sound = SoundCache.getSound(self.soundURL); + self.buttonImageURL = baseURL + "GUI/GUI_audio.png?" + version; + self.addButton(); + + } } this.addButton = function() { @@ -78,9 +105,8 @@ this.enterEntity = function(entityID) { - // self.getUserData(); - print("entering entity and showing" + self.labelURL); - //self.buttonShowing = true; + // print("entering entity and showing" + self.labelURL); + Overlays.editOverlay(self.button, { visible: true }); @@ -92,9 +118,8 @@ this.leaveEntity = function(entityID) { - // self.getUserData(); - // print("leaving entity " + self.userData.name); - //self.buttonShowing = false; + // print("leaving entity " + self.userData.name); + print(Overlays); Overlays.editOverlay(self.button, { visible: false @@ -110,16 +135,16 @@ y: event.y }); if (clickedOverlay == self.button) { - print("button was clicked"); + //print("button was clicked"); if (self.sound.downloaded) { - print("play sound"); + // print("play sound"); Overlays.editOverlay(self.button, { visible: false }); this.soundPlaying = Audio.playSound(self.sound, self.soundOptions); } else { - print("not downloaded"); + // print("not downloaded"); } } } @@ -129,7 +154,9 @@ if (this.soundPlaying !== null) { this.soundPlaying.stop(); } - + if (self.initTimeout !== null) { + Script.clearTimeout(self.initTimeout); + } Controller.mousePressEvent.disconnect(this.onClick); } diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js b/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js index 10a0827e9d..8b1f5c4055 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/utils.js @@ -20,8 +20,8 @@ getEntityUserData = function(id) { try { results = JSON.parse(properties.userData); } catch (err) { - print('error parsing json'); - print('properties are:'+ properties.userData); + // print('error parsing json'); + // print('properties are:'+ properties.userData); } } return results ? results : {}; diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js index 101ce54a5e..8b720b7fec 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js @@ -14,64 +14,76 @@ this.entered = true; this.preload = function(entityID) { - this.entityId = entityID; + this.initialize(entityID); + this.initTimeout = null; + } + this.initialize = function(entityID) { + // print(' should initialize') var properties = Entities.getEntityProperties(entityID); - portalDestination = properties.userData; - animationURL = properties.modelURL; - this.soundOptions = { - stereo: true, - loop: false, - localOnly: false, - position: this.position, - volume: 0.035 - }; - - this.teleportSound = SoundCache.getSound("https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/whoosh.wav"); - //print('Script.clearTimeout PRELOADING A ZOOM ENTITY') - print(" portal destination is " + portalDestination); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + // print(' no user data yet, try again in one second') + self.initialize(entityID); + }, 1000) + } else { + // print(' has userData') + self.portalDestination = properties.userData; + animationURL = properties.modelURL; + self.soundOptions = { + stereo: true, + loop: false, + localOnly: false, + position: properties.position, + volume: 0.5 + }; + + self.teleportSound = SoundCache.getSound("https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/whoosh.wav"); + // print(" portal destination is " + self.portalDestination); + } } this.enterEntity = function(entityID) { - print('Script.clearTimeout ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) + // print(' ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) var data = JSON.parse(Entities.getEntityProperties(this.entityId).userData); - - + // print(' DATA IS::' + data) if (data != null) { - print("Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); + // print("Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); if (self.teleportSound.downloaded) { //print("play sound"); + MyAvatar.position = data.location; Audio.playSound(self.teleportSound, self.soundOptions); + } else { //print("not downloaded"); } - this.lookAt(data.target, data.location); - + // this.lookAt(data.target, data.location); } } - this.lookAt = function(targetPosition, avatarPosition) { - var direction = Vec3.normalize(Vec3.subtract(MyAvatar.position, targetPosition)); + // this.lookAt = function(targetPosition, avatarPosition) { + // print('GOING TO') + // var direction = Vec3.normalize(Vec3.subtract(MyAvatar.position, targetPosition)); - var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { - x: 1, - y: 0, - z: 0 - }); - var yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * 180.0 / Math.PI, { - x: 0, - y: 1, - z: 0 - }); - - MyAvatar.goToLocation(avatarPosition, true, yaw); - MyAvatar.headYaw = 0; - } + // var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { + // x: 1, + // y: 0, + // z: 0 + // }); + // var yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * 180.0 / Math.PI, { + // x: 0, + // y: 1, + // z: 0 + // }); + // print('JBP ZOOM DEBUG YO') + // MyAvatar.goToLocation(avatarPosition, true, yaw); + // MyAvatar.headYaw = 0; + // } @@ -81,9 +93,18 @@ animationSettings: '{ "frameIndex": 1, "running": false }' }); this.entered = false; + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); + } //playSound(); } + this.unload = function() { + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); + } + } + this.hoverEnterEntity = function(entityID) { Entities.editEntity(entityID, { animationURL: animationURL, diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index 36b60bf939..ad18c2db3c 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1005; +var version = 1015; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; @@ -79,398 +79,137 @@ var locations = { }, 1000] }; -var scenes = [ - { - name: "Cells", - objects: "", - location: locations.cells[0], - entryPoint: locations.cells[1], - zone: { - dimensions: { - x: 4000, - y: 4000, - z: 4000 - }, - light: { - r: 255, - g: 200, - b: 200 - }, - intensity: 1.1, - ambient: 0.7, - sun: true, - skybox: "cells_skybox_cross" +var scenes = [{ + name: "Cells", + objects: "", + location: locations.cells[0], + entryPoint: locations.cells[1], + zone: { + dimensions: { + x: 4000, + y: 4000, + z: 4000 }, - instances: [{ - model: "Cell", + light: { + r: 255, + g: 200, + b: 200 + }, + intensity: 1.1, + ambient: 0.7, + sun: true, + skybox: "cells_skybox_cross" + }, + instances: [{ + model: "Cell", + dimensions: { + x: 550, + y: 620, + z: 550 + }, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 500, + number: 10, + userData: JSON.stringify({ + target: locations.cellLayout[1], + location: locations.cellLayout[0], + baseURL: baseLocation + }), + script: "zoom.js?" + version, + visible: true + }], + boundary: { + radius: locations.cells[2], + center: locations.cells[0], + location: locations.cellLayout[1], + target: locations.cellLayout[0] + } +}, { + name: "CellLayout", + objects: cellLayout, + location: locations.cellLayout[0], + entryPoint: locations.cellLayout[1], + zone: { + dimensions: { + x: 4000, + y: 4000, + z: 4000 + }, + light: { + r: 247, + g: 233, + b: 220 + }, + intensity: 2.3, + ambient: 0.7, + sun: true, + skybox: "cosmos_skybox_blurred" + }, + instances: [{ + model: "translation", dimensions: { - x: 550, - y: 620, - z: 550 + x: 10, + y: 16, + z: 10 }, offset: { x: 0, y: 0, z: 0 }, - radius: 500, - number: 10, + radius: 300, + number: 15, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + }, + target: locations.ribosome[1], + location: locations.ribosome[0], + baseURL: baseLocation + }), + script: "zoom.js?" + version, + visible: true + }, { + model: "vesicle", + dimensions: { + x: 60, + y: 60, + z: 60 + }, + randomSize: 10, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1000, + number: 45, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), script: "moveRandomly.js?" + version, visible: true - }], - boundary: { - radius: locations.cells[2], - center: locations.cells[0], - location: locations.cellLayout[1], - target: locations.cellLayout[0] - } - }, { - name: "CellLayout", - objects: cellLayout, - location: locations.cellLayout[0], - entryPoint: locations.cellLayout[1], - zone: { + }, { //golgi vesicles + model: "vesicle", dimensions: { - x: 4000, - y: 4000, - z: 4000 - }, - light: { - r: 247, - g: 233, - b: 220 - }, - intensity: 2.3, - ambient: 0.7, - sun: true, - skybox: "cosmos_skybox_blurred" - }, - instances: [{ - model: "translation", - dimensions: { - x: 10, - y: 16, - z: 10 - }, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 300, - number: 15, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - }, - target: locations.ribosome[1], - location: locations.ribosome[0], - baseURL: baseLocation - }), - script: "zoom.js?" + version, - visible: true - }, { - model: "vesicle", - dimensions: { - x: 60, - y: 60, - z: 60 - }, - randomSize: 10, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 1000, - number: 45, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "moveRandomly.js?" + version, - visible: true - }, { //golgi vesicles - model: "vesicle", - dimensions: { - x: 10, - y: 10, - z: 10 - }, - randomSize: 10, - offset: { - x: -319, - y: 66, - z: 976 - }, - radius: 140, - number: 20, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "", - visible: true - }, { //golgi vesicles - model: "vesicle", - dimensions: { - x: 15, - y: 15, - z: 15 - }, - randomSize: 10, - offset: { - x: -319, - y: 66, - z: 976 - }, - radius: 115, - number: 15, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "moveRandomly.js?" + version, - visible: true - }, { - model: "vesicle", - dimensions: { - x: 50, - y: 50, - z: 50 - }, - randomSize: 10, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 600, - number: 30, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "", - visible: true - }, { //outer vesicles - model: "vesicle", - dimensions: { - x: 60, - y: 60, - z: 60 - }, - randomSize: 10, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 1600, - number: 45, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "", - visible: true - }, { //outer vesicles - model: "vesicle", - dimensions: { - x: 40, - y: 40, - z: 40 - }, - randomSize: 10, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 1400, - number: 45, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "moveRandomly.js?" + version, - visible: true - }, { //outer vesicles - model: "vesicle", - dimensions: { - x: 80, - y: 80, - z: 80 - }, - randomSize: 10, - offset: { - x: 0, - y: 0, - z: 0 - }, - radius: 1800, - number: 45, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "moveRandomly.js?" + version, - visible: true - }, - // {//wigglies - // model:"wiggly", - // dimensions:{x:320,y:40,z:160}, - // randomSize: 10, - // offset:{x:0,y:0,z:0}, - // radius:1800, - // number:50, - // userData:"", - // script:"moveRandomly", - // visible:true - // }, - //// {//wigglies - // model:"wiggly", - // dimensions:{x:640,y:80,z:320}, - // randomSize: 10, - // offset:{x:0,y:0,z:0}, - // radius:2100, - // number:50, - // userData:"", - // script:"moveRandomly", - // visible:true - // }, - { - model: "hexokinase", - dimensions: { - x: 3, - y: 4, - z: 3 - }, - randomSize: 10, - offset: { - x: 236, - y: 8, - z: 771 - }, - radius: 80, - number: 15, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - }, - target: locations.hexokinase[1], - location: locations.hexokinase[0], - baseURL: baseLocation - }), - script: "zoom.js?" + version, - visible: true - }, { - model: "pfructo_kinase", - dimensions: { - x: 3, - y: 4, - z: 3 - }, - randomSize: 10, - offset: { - x: 236, - y: 8, - z: 771 - }, - radius: 60, - number: 15, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "", - visible: true - }, { - model: "glucose_isomerase", - dimensions: { - x: 3, - y: 4, - z: 3 - }, - randomSize: 10, - offset: { - x: 236, - y: 8, - z: 771 - }, - radius: 70, - number: 15, - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }), - script: "", - visible: true - } - // { - // model:"NPC", - // dimensions:{x:20,y:20,z:20}, - // randomSize: 10, - // offset:{x:208.593693,y:6.113100222,z:153.3202277}, - // radius:520, - // number:25, - // userData: "", - // script:"", - // visible:true - // } - - - ], - boundary: { - radius: locations.cellLayout[2], - center: locations.cellLayout[0], - location: locations.cells[1], - target: locations.cells[0] - } - }, { - name: "Ribosome", - objects: "", - location: locations.ribosome[0], - entryPoint: locations.ribosome[1], - zone: { - dimensions: { - x: 4000, - y: 4000, - z: 4000 - }, - light: { - r: 250, - g: 185, - b: 182 - }, - intensity: 0.6, - ambient: 2.9, - sun: true, - skybox: "ribosome_skybox" - }, - instances: [{ - model: "translation_highres", - dimensions: { - x: 500, - y: 500, - z: 200 + x: 10, + y: 10, + z: 10 }, + randomSize: 10, offset: { - x: 0, - y: 0, - z: 0 + x: -319, + y: 66, + z: 976 }, - radius: 1, - number: 1, + radius: 140, + number: 20, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -478,48 +217,201 @@ var scenes = [ }), script: "", visible: true - }], - boundary: { - radius: locations.ribosome[2], - center: locations.ribosome[0], - location: locations.translation[1], - target: locations.translation[0] - } - }, { - name: "Hexokinase", - objects: "", - location: locations.hexokinase[0], - entryPoint: locations.hexokinase[1], - zone: { + }, { //golgi vesicles + model: "vesicle", dimensions: { - x: 4000, - y: 4000, - z: 4000 + x: 15, + y: 15, + z: 15 }, - light: { - r: 255, - g: 255, - b: 255 + randomSize: 10, + offset: { + x: -319, + y: 66, + z: 976 }, - intensity: 0.6, - ambient: 0.6, - sun: true, - skybox: "hexokinase_skybox" - }, - instances: [{ - model: "hexokinase_highres", + radius: 115, + number: 15, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "moveRandomly.js?" + version, + visible: true + }, { + model: "vesicle", dimensions: { - x: 600, - y: 600, - z: 600 + x: 50, + y: 50, + z: 50 }, + randomSize: 10, offset: { x: 0, y: 0, z: 0 }, - radius: 1, - number: 1, + radius: 600, + number: 30, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "", + visible: true + }, { //outer vesicles + model: "vesicle", + dimensions: { + x: 60, + y: 60, + z: 60 + }, + randomSize: 10, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1600, + number: 45, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "", + visible: true + }, { //outer vesicles + model: "vesicle", + dimensions: { + x: 40, + y: 40, + z: 40 + }, + randomSize: 10, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1400, + number: 45, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "moveRandomly.js?" + version, + visible: true + }, { //outer vesicles + model: "vesicle", + dimensions: { + x: 80, + y: 80, + z: 80 + }, + randomSize: 10, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1800, + number: 45, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "moveRandomly.js?" + version, + visible: true + }, + // {//wigglies + // model:"wiggly", + // dimensions:{x:320,y:40,z:160}, + // randomSize: 10, + // offset:{x:0,y:0,z:0}, + // radius:1800, + // number:50, + // userData:"", + // script:"moveRandomly", + // visible:true + // }, + //// {//wigglies + // model:"wiggly", + // dimensions:{x:640,y:80,z:320}, + // randomSize: 10, + // offset:{x:0,y:0,z:0}, + // radius:2100, + // number:50, + // userData:"", + // script:"moveRandomly", + // visible:true + // }, + { + model: "hexokinase", + dimensions: { + x: 3, + y: 4, + z: 3 + }, + randomSize: 10, + offset: { + x: 236, + y: 8, + z: 771 + }, + radius: 80, + number: 15, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + }, + target: locations.hexokinase[1], + location: locations.hexokinase[0], + baseURL: baseLocation + }), + script: "zoom.js?" + version, + visible: true + }, { + model: "pfructo_kinase", + dimensions: { + x: 3, + y: 4, + z: 3 + }, + randomSize: 10, + offset: { + x: 236, + y: 8, + z: 771 + }, + radius: 60, + number: 15, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "", + visible: true + }, { + model: "glucose_isomerase", + dimensions: { + x: 3, + y: 4, + z: 3 + }, + randomSize: 10, + offset: { + x: 236, + y: 8, + z: 771 + }, + radius: 70, + number: 15, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -527,15 +419,126 @@ var scenes = [ }), script: "", visible: true - }], - boundary: { - radius: locations.hexokinase[2], - center: locations.hexokinase[0], - location: locations.mitochondria[1], - target: locations.mitochondria[0] } + // { + // model:"NPC", + // dimensions:{x:20,y:20,z:20}, + // randomSize: 10, + // offset:{x:208.593693,y:6.113100222,z:153.3202277}, + // radius:520, + // number:25, + // userData: "", + // script:"", + // visible:true + // } + + + ], + boundary: { + radius: locations.cellLayout[2], + center: locations.cellLayout[0], + location: locations.cells[1], + target: locations.cells[0] } -]; +}, { + name: "Ribosome", + objects: "", + location: locations.ribosome[0], + entryPoint: locations.ribosome[1], + zone: { + dimensions: { + x: 4000, + y: 4000, + z: 4000 + }, + light: { + r: 250, + g: 185, + b: 182 + }, + intensity: 0.6, + ambient: 2.9, + sun: true, + skybox: "ribosome_skybox" + }, + instances: [{ + model: "translation_highres", + dimensions: { + x: 500, + y: 500, + z: 200 + }, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1, + number: 1, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "", + visible: true + }], + boundary: { + radius: locations.ribosome[2], + center: locations.ribosome[0], + location: locations.translation[1], + target: locations.translation[0] + } +}, { + name: "Hexokinase", + objects: "", + location: locations.hexokinase[0], + entryPoint: locations.hexokinase[1], + zone: { + dimensions: { + x: 4000, + y: 4000, + z: 4000 + }, + light: { + r: 255, + g: 255, + b: 255 + }, + intensity: 0.6, + ambient: 0.6, + sun: true, + skybox: "hexokinase_skybox" + }, + instances: [{ + model: "hexokinase_highres", + dimensions: { + x: 600, + y: 600, + z: 600 + }, + offset: { + x: 0, + y: 0, + z: 0 + }, + radius: 1, + number: 1, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }), + script: "", + visible: true + }], + boundary: { + radius: locations.hexokinase[2], + center: locations.hexokinase[0], + location: locations.mitochondria[1], + target: locations.mitochondria[0] + } +}]; function ImportScene(scene) { @@ -637,6 +640,7 @@ function createLayoutLights() { }) } + function CreateNavigationButton(scene, number) { // print('NAV NAVIGATION CREATING NAV!!' +scene.name + " " + number) @@ -811,7 +815,7 @@ function CreateInstances(scene) { }, idBounds, 150); } - print('Script.clearTimeout SCRIPT AT CREATE ENTITY: ' + script) + //print('SCRIPT AT CREATE ENTITY: ' + script) CreateEntity(scene.instances[i].model, position, rotation, scene.instances[i].dimensions, url, script, scene.instances[i].userData, scene.instances[i].visible); } } @@ -890,7 +894,7 @@ function getPointOnSphereOfRadius(radius, number, totalNumber) { // print("inc " + inc + " off " + off + " y " + y + " r " + r + " phi " + phi); if (isNaN(r)) { - print("r is not a number"); + //print("r is not a number"); r = 1; } @@ -913,7 +917,7 @@ function CreateEntity(name, position, rotation, dimensions, url, script, userDat scriptLocation = baseLocation + "Scripts/" + script; } - print('Script.clearTimeout SCRIPT LOCATION IN CREATE ENTITY' + scriptLocation) + //print(' SCRIPT LOCATION IN CREATE ENTITY' + scriptLocation) Entities.addEntity({ type: "Model", name: name, From 720175f862779b4adb9ef64401238ed2875ac9ae Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 5 Feb 2016 19:51:02 -0800 Subject: [PATCH 17/73] a couple more prints --- .../CellScience/Scripts/showButtonToPlaySound.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index d37655f09b..429fd0b902 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -17,22 +17,22 @@ } this.initialize = function(entityId) { - print(' should initialize' + entityId) + //print(' should initialize' + entityId) var properties = Entities.getEntityProperties(entityId); if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { self.initTimeout = Script.setTimeout(function() { - print(' no user data yet, try again in one second') + // print(' no user data yet, try again in one second') self.initialize(entityId); }, 1000) } else { - print(' userdata before parse attempt' + properties.userData) + //print(' userdata before parse attempt' + properties.userData) self.userData = null; try { self.userData = JSON.parse(properties.userData); } catch (err) { - print(' error parsing json'); - print(' properties are:' + properties.userData); + // print(' error parsing json'); + // print(' properties are:' + properties.userData); return; } @@ -97,12 +97,12 @@ y: event.y }); if (clickedOverlay === self.button) { - print("button was clicked"); + //print("button was clicked"); if (self.sound.downloaded) { - print("play sound"); + //print("play sound"); Audio.playSound(self.sound, self.soundOptions); } else { - print("not downloaded"); + //print("not downloaded"); } } } From dc50efe7ec9895ce0e446eab06907f59352fbb65 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 10:42:43 -0800 Subject: [PATCH 18/73] final? --- .../{tribble.js => tribble.x.34.js} | 37 +- examples/libraries/virtualBaton.31.js | 252 ------------- examples/libraries/virtualBaton.39.js | 354 ++++++++++++++++++ 3 files changed, 386 insertions(+), 257 deletions(-) rename examples/entityScripts/{tribble.js => tribble.x.34.js} (68%) delete mode 100644 examples/libraries/virtualBaton.31.js create mode 100644 examples/libraries/virtualBaton.39.js diff --git a/examples/entityScripts/tribble.js b/examples/entityScripts/tribble.x.34.js similarity index 68% rename from examples/entityScripts/tribble.js rename to examples/entityScripts/tribble.x.34.js index 3f84901344..145adfc325 100644 --- a/examples/entityScripts/tribble.js +++ b/examples/entityScripts/tribble.x.34.js @@ -1,5 +1,6 @@ (function () { // See tests/performance/tribbles.js + Script.include("../libraries/virtualBaton.39.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, @@ -7,7 +8,8 @@ accumulated = 0, increment = {red: 1, green: 1, blue: 1}, hasUpdate = false, - shutdown = false; + shutdown = false, + baton; function nextWavelength(color) { var old = oldColor[color]; if (old === 255) { @@ -35,6 +37,22 @@ Entities.editEntity(entityID, newData); if (!shutdown) { Script.setTimeout(move, nextChange); } } + function startUpdate() { + print('startUpdate', entityID); + hasUpdate = true; + Script.update.connect(update); + } + function stopUpdate() { + print('stopUpdate', entityID, hasUpdate); + if (!hasUpdate) { return; } + hasUpdate = false; + Script.update.disconnect(update); + } + function stopUpdateAndReclaim() { + print('stopUpdateAndReclaim', entityID); + stopUpdate(); + baton.claim(startUpdate, stopUpdateAndReclaim); + } this.preload = function (givenEntityID) { entityID = givenEntityID; var properties = Entities.getEntityProperties(entityID); @@ -45,11 +63,19 @@ moveRate = (moveRate && userData.moveRate) || moveRate; oldColor = properties.color; dimensions = Vec3.multiply(scale, properties.dimensions); + baton = virtualBaton({ + // FIXME: batonName: 'io.highfidelity.tribble:' + entityID, + // If we wanted to have only one tribble change colors, we could do: + batonName: 'io.highfidelity.tribble', + instanceId: entityID + MyAvatar.sessionUUID, + debugFlow: true, + debugSend: true, + debugReceive: true + }); if (editTimeout) { - hasUpdate = true; - Script.update.connect(update); + baton.claim(startUpdate, stopUpdateAndReclaim); if (editTimeout > 0) { - Script.setTimeout(function () { Script.update.disconnect(update); hasUpdate = false; }, editTimeout * 1000); + Script.setTimeout(stopUpdate, editTimeout * 1000); } } if (moveTimeout) { @@ -60,7 +86,8 @@ } }; this.unload = function () { + baton.unload(); shutdown = true; - if (hasUpdate) { Script.update.disconnect(update); } + stopUpdate(); }; }) diff --git a/examples/libraries/virtualBaton.31.js b/examples/libraries/virtualBaton.31.js deleted file mode 100644 index 5df4710d3e..0000000000 --- a/examples/libraries/virtualBaton.31.js +++ /dev/null @@ -1,252 +0,0 @@ -"use strict"; -/*jslint nomen: true, plusplus: true, vars: true */ -/*global Entities, Script, MyAvatar, Messages, AvatarList, print */ -// -// Created by Howard Stearns -// 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 -// -// Allows cooperating scripts to pass a "virtual baton" between them, -// which is useful when part of a script should only be executed by -// the one participant that is holding this particular baton. -// -// A virtual baton is simply any string agreed upon by the scripts -// that use it. Only one script at a time can hold the baton, and it -// holds it until that script releases it, or the other scripts -// determine that the holding script is not responding. The script -// automatically determines who among claimants has the baton, if anyone, -// and holds an "election" if necessary. -// -// See entityScript/tribble.js as an example, and the functions -// virtualBaton(), claim(), release(). -// - -// Answers a new virtualBaton for the given parameters, of which 'key' -// is required. -virtualBaton = function virtualBaton(options) { - // Answer averages (number +/- variability). Avoids having everyone act in lockstep. - function randomize(number, variability) { - var randomPart = number * variability; - return number - (randomPart / 2) + (Math.random() * randomPart); - } - var key = options.key, - useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, - exports = options.exports || {}, - electionTimeout = options.electionTimeout || randomize(1000, 0.2), // ms. If no winner in this time, hold a new election. - claimCallback, - releaseCallback, - ourId = MyAvatar.sessionUUID; // better be stable! - if (!key) { - throw new Error("A VirtualBaton must specify a key."); - } - function debug() { - print.apply(null, [].map.call(arguments, JSON.stringify)); - } - function debugFlow() { - if (options.debugFlow) { debug.apply(null, arguments); } - } - - // Messages: Just synactic sugar for hooking things up to Messages system. - // We create separate subchannel strings for each operation within our general channelKey, instead of using - // a switch in the receiver. - var channelKey = "io.highfidelity.virtualBaton:" + key, - subchannelHandlers = {}, // Message channel string => {function, op} - subchannelKeys = {}; // operation => Message channel string - function subchannelKey(operation) { return channelKey + ':' + operation; } - function receive(operation, handler) { // Record a handler for an operation on our channelKey - var subKey = subchannelKey(operation); - subchannelHandlers[subKey] = {receiver: handler, op: operation}; - subchannelKeys[operation] = subKey; - Messages.subscribe(subKey); - } - function sendHelper(subchannel, data) { - var message = JSON.stringify(data); - Messages.sendMessage(subchannel, message); - } - function send1(operation, destination, data) { // Send data for an operation to just one destination on our channelKey. - if (options.debugSend) { debug('baton:', ourId, '=>', destination, operation, data); } - sendHelper(subchannelKey(operation) + destination, data); - } - function send(operation, data) { // Send data for an operation on our channelKey. - if (options.debugSend) { debug('baton:', ourId, '=>', '-', operation, data); } - sendHelper(subchannelKeys[operation], data); - } - Messages.messageReceived.connect(function (channel, messageString, senderID) { - var handler = subchannelHandlers[channel]; - if (!handler) { return; } - var data = JSON.parse(messageString); - if (options.debugReceive) { debug('baton:', senderID, '=>', ourId, handler.op, data); } - handler.receiver(data); - }); - - // Internally, this uses the Paxos algorith to hold elections. - // Alternatively, we could have the message server pick and maintain a winner, but it would - // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. - // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer - // (such as the server). - // - // Paxos makes several tests of one "proposal number" versus another, assuming - // that better proposals from the same proposer have a higher number, - // and different proposers use a different set of numbers. We achieve that - // by dividing the "number" into two parts, and integer and a proposerId, - // which keeps the combined number unique and yet still strictly ordered. - function betterNumber(number, best) { - // FIXME restore debug('baton: betterNumber', number, best); - //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); - return (number.number || 0) > best.number; - } - // Paxos Proposer behavior - var nPromises = 0, - proposalNumber = 0, - nQuorum, - mostRecentInterested, - bestPromise = {number: 0}, - electionWatchdog, - recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize - recheckWatchdog; - function propose() { - debugFlow('baton:', ourId, 'propose', !!claimCallback); - if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } - if (!claimCallback) { return; } // We're not participating. - electionWatchdog = Script.setTimeout(propose, electionTimeout); - nPromises = 0; - proposalNumber = Math.max(proposalNumber, bestPromise.number); - nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! - send('prepare!', {number: ++proposalNumber, proposerId: ourId}); - } - // We create a distinguished promise subchannel for our id, because promises need only be sent to the proposer. - receive('promise' + ourId, function (data) { - if (data.proposerId !== ourId) { return; } // Only the proposer needs to do anything. - mostRecentInterested = mostRecentInterested || data.interested; - if (betterNumber(data, bestPromise)) { - bestPromise = data; - } - if (++nPromises >= nQuorum) { - var answer = {number: data.proposalNumber, proposerId: data.proposerId, winner: data.winner}; - if (!answer.winner) { // we get to pick - answer.winner = claimCallback ? ourId : mostRecentInterested; - } - send('accept!', answer); - } - }); - // Paxos Acceptor behavior - var bestProposal = {number: 0}, accepted = {}; - function acceptedId() { return accepted && accepted.winner; } - receive('prepare!', function (data) { - if (useOptimizations) { // Don't waste time with low future proposals. - proposalNumber = Math.max(proposalNumber, data.number); - } - if (betterNumber(data, bestProposal)) { - bestProposal = data; - if (claimCallback && useOptimizations) { - // Let the proposer know we're interested in the job if the proposer doesn't - // know who else to pick. Avoids needing to start multiple simultaneous proposals. - accepted.interested = ourId; - } - send1('promise', data.proposerId, - accepted.winner ? // data must include proposerId and number so that proposer catalogs results. - {number: accepted.number, winner: accepted.winner, proposerId: data.proposerId, proposalNumber: data.number} : - {proposerId: data.proposerId, proposalNumber: data.number}); - } // FIXME nack? - }); - receive('accept!', function (data) { - if (!betterNumber(bestProposal, data)) { - bestProposal = data; - if (useOptimizations) { - // The Paxos literature describe every acceptor sending 'accepted' to - // every proposer and learner. In our case, these are the same nodes that received - // the 'accept!' message, so we can send to just the originating proposer and invoke - // our own accepted handler directly. - // Note that this optimization cannot be used with Byzantine Paxos (which needs another - // multi-broadcast to detect lying and collusion). - subchannelHandlers[subchannelKey('accepted') + ourId].receiver(data); - send1('accepted', data.proposerId, data); - } else { - send('accepted', data); - } - } - }); - // Paxos Learner behavior. - receive('accepted' + (useOptimizations ? ourId : ''), function (data) { // See note in 'accept!' regarding use of ourId here. - accepted = data; - if (acceptedId() === ourId) { // Note that we might not have been the proposer. - if (electionWatchdog) { - Script.clearTimeout(electionWatchdog); - electionWatchdog = null; - } - if (claimCallback) { - var callback = claimCallback; - claimCallback = undefined; - callback(key); - } else if (!releaseCallback) { // We won, but have been released and are no longer interested. - propose(); // Propose that someone else take the job. - } - } - }); - - receive('release', function (data) { - if (!betterNumber(accepted, data)) { // Unless our data is fresher... - debugFlow('baton:', ourId, 'handle release', data); - accepted.winner = null; // ... allow next proposer to have his way by making this explicitly null (not undefined). - if (recheckWatchdog) { - Script.clearInterval(recheckWatchdog); - recheckWatchdog = null; - } - } - }); - function localRelease() { - var callback = releaseCallback, oldAccepted = accepted; - releaseCallback = undefined; - accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number. - debugFlow('baton: localRelease', key, !!callback); - if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. - callback(key); // Pass key so that clients may use the same handler for different batons. - return oldAccepted; - } - - // Registers an intent to hold the baton: - // Calls onElection(key) once, if you are elected by the scripts - // to be the unique holder of the baton, which may be never. - // Calls onRelease(key) once, if you release the baton held by you, - // whether this is by you calling release(), or by loosing - // an election when you become disconnected. - // You may claim again at any time after the start of onRelease - // being called. Otherwise, you will not participate in further elections. - exports.claim = function claim(onElection, onRelease) { - debugFlow('baton:', ourId, 'claim'); - if (claimCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); - return; - } - if (releaseCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is somehow incorrect released, and that should not happen."); - return; - } - claimCallback = onElection; - releaseCallback = onRelease; - propose(); - }; - - // Release the baton you hold, or just log that you are not holding it. - exports.release = function release(optionalReplacementOnRelease) { - debugFlow('baton:', ourId, 'release'); - if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. - releaseCallback = optionalReplacementOnRelease; - } - if (acceptedId() !== ourId) { - print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); - return; - } - var released = localRelease(); - if (released) { - send('release', released); // Let everyone know right away, including old number in case we overlap with reclaim. - } - if (!claimCallback) { // No claim set in release callback. - propose(); // We are the distinguished proposer, but we'll pick anyone else interested. - } - }; - - return exports; -}; diff --git a/examples/libraries/virtualBaton.39.js b/examples/libraries/virtualBaton.39.js new file mode 100644 index 0000000000..726a5b2c49 --- /dev/null +++ b/examples/libraries/virtualBaton.39.js @@ -0,0 +1,354 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +/*global Messages, Script, MyAvatar, AvatarList, Entities, print */ + +// Created by Howard Stearns +// 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 +// +// Allows cooperating scripts to pass a "virtual baton" between them, +// which is useful when part of a script should only be executed by +// the one participant that is holding this particular baton. +// +// A virtual baton is simply any string agreed upon by the scripts +// that use it. Only one script at a time can hold the baton, and it +// holds it until that script releases it, or the other scripts +// determine that the holding script is not responding. The script +// automatically determines who among claimants has the baton, if anyone, +// and holds an "election" if necessary. +// +// See entityScript/tribble.js as an example, and the functions +// virtualBaton(), claim(), release(). +// + +// Answers a new virtualBaton for the given parameters, of which 'key' +// is required. +function virtualBatonf(options) { + // Answer averages (number +/- variability). Avoids having everyone act in lockstep. + function randomize(number, variability) { + var allowedDeviation = number * variability; + return number - allowedDeviation + (Math.random() * 2 * allowedDeviation); + } + // Allow testing outside in a harness of High Fidelity. + var globals = options.globals || {}, + messages = globals.Messages || Messages, + myAvatar = globals.MyAvatar || MyAvatar, + avatarList = globals.AvatarList || AvatarList, + entities = globals.Entities || Entities, + timers = globals.Script || Script, + log = globals.print || print; + + var batonName = options.batonName, // The identify of the baton. + // instanceId is the identify of this particular copy of the script among all copies using the same batonName + // in the domain. For example, if you wanted only one entity among multiple entity scripts to hold the baton, + // you could specify virtualBaton({batonName: 'someBatonName', id: MyAvatar.sessionUUID + entityID}). + instanceId = options.instanceId || myAvatar.sessionUUID, + // virtualBaton() returns the exports object with properties. You can pass in an object to be side-effected. + exports = options.exports || {}, + // Handy to set false if we believe the optimizations are wrong, or to use both values in a test harness. + useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, + // + electionTimeout = options.electionTimeout || randomize(500, 0.2), // ms. If no winner in this time, hold a new election. + recheckInterval = options.recheckInterval || randomize(500, 0.2), // ms. Check that winners remain connected. + // If you supply your own instanceId, you can also supply a connectionTest to check that answers + // truthy iff the given id is still valid and connected, and is run at recheckInterval. You can use + // exports.validId (see below), and the default answers truthy if id is valid or a concatenation of two valid + // ids. (This handles the most common cases of instanceId being either (the default) MyAvatar.sessionUUID, an + // entityID, or the concatenation (in either order) of both.) + connectionTest = options.connectionTest || function connectionTest(id) { + var idLength = 38; + if (id.length === idLength) { return exports.validId(id); } + return (id.length === 2 * idLength) && exports.validId(id.slice(0, idLength)) && exports.validId(id.slice(idLength)); + }; + + if (!batonName) { + throw new Error("A virtualBaton must specify a batonName."); + } + // Truthy if id exists as either a connected avatar or valid entity. + exports.validId = function validId(id) { + var avatar = avatarList.getAvatar(id); + if (avatar && (avatar.sessionUUID === id)) { return true; } + var properties = entities.getEntityProperties(id, ['type']); + return properties && properties.type; + }; + + // Various logging, controllable through options. + function debug() { // Display the arguments not just [Object object]. + log.apply(null, [].map.call(arguments, JSON.stringify)); + } + function debugFlow() { + if (options.debugFlow) { debug.apply(null, arguments); } + } + function debugSend(destination, operation, data) { + if (options.debugSend) { debug('baton:', batonName, instanceId, 's=>', destination, operation, data); } + } + function debugReceive(senderID, operation, data) { // senderID is client sessionUUID -- not necessarily instanceID! + if (options.debugReceive) { debug('baton:', batonName, senderID, '=>r', instanceId, operation, data); } + } + + // Messages: Just synactic sugar for hooking things up to Messages system. + // We create separate subchannel strings for each operation within our general channelKey, instead of using + // a switch in the receiver. + var channelKey = "io.highfidelity.virtualBaton:" + batonName, + subchannelHandlers = {}, // Message channel string => {receiver, op} + subchannelKeys = {}; // operation => Message channel string + function subchannelKey(operation) { return channelKey + ':' + operation; } + function receive(operation, handler) { // Record a handler for an operation on our channelKey + var subKey = subchannelKey(operation); + subchannelHandlers[subKey] = {receiver: handler, op: operation}; + subchannelKeys[operation] = subKey; + messages.subscribe(subKey); + } + function sendHelper(subchannel, data) { + var message = JSON.stringify(data); + messages.sendMessage(subchannel, message); + } + function send1(operation, destination, data) { // Send data for an operation to just one destination on our channelKey. + debugSend(destination, operation, data); + sendHelper(subchannelKey(operation) + destination, data); + } + function send(operation, data) { // Send data for an operation on our channelKey. + debugSend('-', operation, data); + sendHelper(subchannelKeys[operation], data); + } + function messageHandler(channel, messageString, senderID) { + var handler = subchannelHandlers[channel]; + if (!handler) { return; } + var data = JSON.parse(messageString); + debugReceive(senderID, handler.op, data); + handler.receiver(data); + } + messages.messageReceived.connect(messageHandler); + + var nPromises = 0, nAccepted = 0, electionWatchdog; + + // It would be great if we had a way to know how many subscribers our channel has. Failing that... + var nNack = 0, previousNSubscribers = 0, lastGathering = 0, thisTimeout = electionTimeout; + function nSubscribers() { // Answer the number of subscribers - or zero to indicate that we have to wait. + // To find nQuorum, we need to know how many scripts are being run using this batonName, which isn't + // the same as the number of clients! + // + // If we overestimate by too much, we may fail to reach consensus, which triggers a new + // election proposal, so we take the number of acceptors to be the max(nPromises, nAccepted) + // + nNack reported in the previous round. + // + // If we understimate by too much, there can be different pockets on the Internet that each + // believe they have agreement on different holders of the baton, which is precisely what + // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' gather stragglers. + + var now = Date.now(), elapsed = now - lastGathering; + if (elapsed >= thisTimeout) { + previousNSubscribers = Math.max(nPromises, nAccepted) + nNack; + lastGathering = now; + } // ...otherwise we use the previous value unchanged. + + // On startup, we do one proposal that we cannot possibly win, so that we'll + // lock things up for timeout to gather the number of responses. + if (!previousNSubscribers) { + var LARGE_INTEGER = Number.MAX_SAFE_INTEGER || (-1 >>> 1); // QT doesn't define the ECMA constant. Max int will do for our purposes. + previousNSubscribers = LARGE_INTEGER; + } + return previousNSubscribers; + } + + // MAIN ALGORITHM + // + // Internally, this uses the Paxos algorith to hold elections. + // Alternatively, we could have the message server pick and maintain a winner, but it would + // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. + // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer + // (such as the server). + function betterNumber(number, best) { + return (number.number || 0) > best.number; + } + // Paxos Proposer behavior + var proposalNumber = 0, + nQuorum = 0, + bestPromise = {number: 0}, + claimCallback, + releaseCallback; + function propose() { // Make a new proposal, so that we learn/update the proposalNumber and winner. + // Even though we send back a 'nack' if the proposal is obsolete, with net work errors + // there's no way to know for certain that we've failed. The electionWatchdog will try a new + // proposal if we have not been accepted by a quorum after election Timeout. + if (electionWatchdog) { + // If we had a means of determining nSubscribers other than by counting, we could just + // timers.clearTimeout(electionWatchdog) and not return. + return; + } + thisTimeout = randomize(electionTimeout, 0.5); // for accurate nSubcribers counting. + electionWatchdog = timers.setTimeout(function () { + electionWatchdog = null; + propose(); + }, thisTimeout); + var nAcceptors = nSubscribers(); + nQuorum = Math.floor(nAcceptors / 2) + 1; + + proposalNumber = Math.max(proposalNumber, bestPromise.number) + 1; + debugFlow('baton:', batonName, instanceId, 'propose', proposalNumber, + 'claim:', !!claimCallback, 'nAcceptors:', nAcceptors, nPromises, nAccepted, nNack); + nPromises = nAccepted = nNack = 0; + send('prepare!', {number: proposalNumber, proposerId: instanceId}); + } + // We create a distinguished promise subchannel for our id, because promises need only be sent to the proposer. + receive('promise' + instanceId, function (data) { + if (betterNumber(data, bestPromise)) { + bestPromise = data; + } + if ((data.proposalNumber === proposalNumber) && (++nPromises >= nQuorum)) { // Note check for not being a previous round + var answer = {number: data.proposalNumber, proposerId: data.proposerId, winner: bestPromise.winner}; // Not data.number. + if (!answer.winner || (answer.winner === instanceId)) { // We get to pick. + answer.winner = claimCallback ? instanceId : null; + } + send('accept!', answer); + } + }); + receive('nack' + instanceId, function (data) { // An acceptor reports more recent data... + if (data.proposalNumber === proposalNumber) { + nNack++; // For updating nQuorum. + // IWBNI if we started our next proposal now (here, in a setTimeout, but we need a decent nNack count. + // Lets save that optimization for another day... + } + }); + // Paxos Acceptor behavior + var bestProposal = {number: 0}, accepted = {}; + function acceptedId() { return accepted && accepted.winner; } + receive('prepare!', function (data) { + var response = {proposalNumber: data.number, proposerId: data.proposerId}; + if (betterNumber(data, bestProposal)) { + bestProposal = data; + if (accepted.winner) { + response.number = accepted.number; + response.winner = accepted.winner; + } + send1('promise', data.proposerId, response); + } else { + send1('nack', data.proposerId, response); + } + }); + receive('accept!', function (data) { + if (!betterNumber(bestProposal, data)) { + bestProposal = accepted = data; // Update both with current data. Might have missed the proposal earlier. + if (useOptimizations) { + // The Paxos literature describe every acceptor sending 'accepted' to + // every proposer and learner. In our case, these are the same nodes that received + // the 'accept!' message, so we can send to just the originating proposer and invoke + // our own accepted handler directly. + // Note that this optimization cannot be used with Byzantine Paxos (which needs another + // multi-broadcast to detect lying and collusion). + debugSend('/', 'accepted', data); + debugReceive(instanceId, 'accepted', data); // direct on next line, which doesn't get logging. + subchannelHandlers[subchannelKey('accepted') + instanceId].receiver(data); + if (data.proposerId !== instanceId) { // i.e., we didn't already do it directly on the line above. + send1('accepted', data.proposerId, data); + } + } else { + send('accepted', data); + } + } else { + send1('nack', data.proposerId, {proposalNumber: data.number}); + } + }); + // Paxos Learner behavior. + function localRelease() { + var callback = releaseCallback; + debugFlow('baton:', batonName, 'localRelease', 'callback:', !!releaseCallback); + if (!releaseCallback) { return; } // Already released, but we might still receive a stale message. That's ok. + releaseCallback = undefined; + callback(batonName); // Pass batonName so that clients may use the same handler for different batons. + } + receive('accepted' + (useOptimizations ? instanceId : ''), function (data) { // See note in 'accept!' regarding use of instanceId here. + if (betterNumber(accepted, data)) { // Especially when !useOptimizations, we can receive other acceptances late. + return; + } + var oldAccepted = accepted; + debugFlow('baton:', batonName, instanceId, 'accepted', data.number, data.winner); + accepted = data; + // If we are proposer, make sure we get a quorum of acceptances. + if ((data.proposerId === instanceId) && (data.number === proposalNumber) && (++nAccepted >= nQuorum)) { + if (electionWatchdog) { + timers.clearTimeout(electionWatchdog); + electionWatchdog = null; + } + } + // If we are the winner -- regardless of whether we were the proposer. + if (acceptedId() === instanceId) { + if (claimCallback) { + var callback = claimCallback; + claimCallback = undefined; + callback(batonName); + } else if (!releaseCallback) { // We won, but have been released and are no longer interested. + // Propose that someone else take the job. + timers.setTimeout(propose, 0); // Asynchronous to queue message handling if some are synchronous and others not. + } + } else if (releaseCallback && (oldAccepted.winner === instanceId)) { // We've been released by someone else! + localRelease(); // This can happen if enough people thought we'd disconnected. + } + }); + + // Registers an intent to hold the baton: + // Calls onElection(batonName) once, if you are elected by the scripts + // to be the unique holder of the baton, which may be never. + // Calls onRelease(batonName) once, if you release the baton held by you, + // whether this is by you calling release(), or by loosing + // an election when you become disconnected. + // You may claim again at any time after the start of onRelease + // being called. Otherwise, you will not participate in further elections. + exports.claim = function claim(onElection, onRelease) { + debugFlow('baton:', batonName, instanceId, 'claim'); + if (claimCallback) { + log("Ignoring attempt to claim virtualBaton " + batonName + ", which is already waiting for claim."); + return; + } + if (releaseCallback) { + log("Ignoring attempt to claim virtualBaton " + batonName + ", which is somehow incorrect released, and that should not happen."); + return; + } + claimCallback = onElection; + releaseCallback = onRelease; + propose(); + return exports; // Allows chaining. e.g., var baton = virtualBaton({batonName: 'foo'}.claim(onClaim, onRelease); + }; + // Release the baton you hold, or just log that you are not holding it. + exports.release = function release(optionalReplacementOnRelease) { + debugFlow('baton:', batonName, instanceId, 'release'); + if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + releaseCallback = optionalReplacementOnRelease; + } + if (acceptedId() !== instanceId) { + log("Ignoring attempt to release virtualBaton " + batonName + ", which is not being held."); + return; + } + localRelease(); + if (!claimCallback) { // No claim set in release callback. + propose(); // We are the distinguished proposer, but we'll pick anyone else interested, else set it to null. + } + return exports; + }; + exports.recheckWatchdog = timers.setInterval(function recheck() { + var holder = acceptedId(); // If we're waiting and we notice the holder is gone, ... + if (holder && claimCallback && !electionWatchdog && !connectionTest(holder)) { + propose(); // ... propose an election. + } + }, recheckInterval); + exports.unload = function unload() { // Disconnect from everything. + messages.messageReceived.disconnect(messageHandler); + timers.clearInterval(exports.recheckWatchdog); + if (electionWatchdog) { timers.clearTimeout(electionWatchdog); } + electionWatchdog = claimCallback = releaseCallback = null; + Object.keys(subchannelHandlers).forEach(messages.unsubscribe); + debugFlow('baton:', batonName, instanceId, 'unload'); + return exports; + }; + + // Gather nAcceptors by making two proposals with some gathering time, even without a claim. + propose(); + return exports; +} +if (typeof module !== 'undefined') { + module.exports = virtualBatonf; +} else { + virtualBaton = virtualBatonf; +} From 7b39700136d88ac685afb3637615b8e65d91ca7f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 13:33:21 -0800 Subject: [PATCH 19/73] one ring to rule --- .../entityScripts/{tribble.x.34.js => tribble.x.35.js} | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) rename examples/entityScripts/{tribble.x.34.js => tribble.x.35.js} (92%) diff --git a/examples/entityScripts/tribble.x.34.js b/examples/entityScripts/tribble.x.35.js similarity index 92% rename from examples/entityScripts/tribble.x.34.js rename to examples/entityScripts/tribble.x.35.js index 145adfc325..a65f91b171 100644 --- a/examples/entityScripts/tribble.x.34.js +++ b/examples/entityScripts/tribble.x.35.js @@ -64,13 +64,10 @@ oldColor = properties.color; dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ - // FIXME: batonName: 'io.highfidelity.tribble:' + entityID, - // If we wanted to have only one tribble change colors, we could do: batonName: 'io.highfidelity.tribble', - instanceId: entityID + MyAvatar.sessionUUID, debugFlow: true, - debugSend: true, - debugReceive: true + debugSend: false, + debugReceive: false }); if (editTimeout) { baton.claim(startUpdate, stopUpdateAndReclaim); From 8f714980df2e371c040fd113209f44f74c34bbd3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 13:49:46 -0800 Subject: [PATCH 20/73] debuggable --- .../entityScripts/{tribble.x.35.js => tribble.x.36.js} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename examples/entityScripts/{tribble.x.35.js => tribble.x.36.js} (95%) diff --git a/examples/entityScripts/tribble.x.35.js b/examples/entityScripts/tribble.x.36.js similarity index 95% rename from examples/entityScripts/tribble.x.35.js rename to examples/entityScripts/tribble.x.36.js index a65f91b171..a4c6f76224 100644 --- a/examples/entityScripts/tribble.x.35.js +++ b/examples/entityScripts/tribble.x.36.js @@ -59,15 +59,16 @@ var userData = properties.userData && JSON.parse(properties.userData); var moveTimeout = userData ? userData.moveTimeout : 0; var editTimeout = userData ? userData.editTimeout : 0; + var debug = (userData && userData.debug) || {}; editRate = (userData && userData.editRate) || editRate; moveRate = (moveRate && userData.moveRate) || moveRate; oldColor = properties.color; dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ batonName: 'io.highfidelity.tribble', - debugFlow: true, - debugSend: false, - debugReceive: false + debugFlow: debug.flow, + debugSend: debug.send, + debugReceive: debug.receive }); if (editTimeout) { baton.claim(startUpdate, stopUpdateAndReclaim); From 1a0017900c3b0d155e1626976966906f04b74f54 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 14:10:55 -0800 Subject: [PATCH 21/73] noisy connectionTest --- examples/entityScripts/{tribble.x.36.js => tribble.x.37.js} | 5 +++++ 1 file changed, 5 insertions(+) rename examples/entityScripts/{tribble.x.36.js => tribble.x.37.js} (94%) diff --git a/examples/entityScripts/tribble.x.36.js b/examples/entityScripts/tribble.x.37.js similarity index 94% rename from examples/entityScripts/tribble.x.36.js rename to examples/entityScripts/tribble.x.37.js index a4c6f76224..05f7154fd1 100644 --- a/examples/entityScripts/tribble.x.36.js +++ b/examples/entityScripts/tribble.x.37.js @@ -66,6 +66,11 @@ dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ batonName: 'io.highfidelity.tribble', + connectionTest: function (id) { + var connected = baton.validId(id); + print(MyAvatar.sessionUUID, id, connected); + return connected; + }, debugFlow: debug.flow, debugSend: debug.send, debugReceive: debug.receive From 8b27b90566734ed56952b6d5dc2430fc088ce6ac Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 14:57:29 -0800 Subject: [PATCH 22/73] Verify validId before reporting a winning reponse to a proposal. --- examples/entityScripts/{tribble.x.37.js => tribble.x.38.js} | 2 +- .../libraries/{virtualBaton.39.js => virtualBaton.40.js} | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) rename examples/entityScripts/{tribble.x.37.js => tribble.x.38.js} (98%) rename examples/libraries/{virtualBaton.39.js => virtualBaton.40.js} (98%) diff --git a/examples/entityScripts/tribble.x.37.js b/examples/entityScripts/tribble.x.38.js similarity index 98% rename from examples/entityScripts/tribble.x.37.js rename to examples/entityScripts/tribble.x.38.js index 05f7154fd1..3031ec6cd6 100644 --- a/examples/entityScripts/tribble.x.37.js +++ b/examples/entityScripts/tribble.x.38.js @@ -1,6 +1,6 @@ (function () { // See tests/performance/tribbles.js - Script.include("../libraries/virtualBaton.39.js"); + Script.include("../libraries/virtualBaton.40.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, diff --git a/examples/libraries/virtualBaton.39.js b/examples/libraries/virtualBaton.40.js similarity index 98% rename from examples/libraries/virtualBaton.39.js rename to examples/libraries/virtualBaton.40.js index 726a5b2c49..8453365c9d 100644 --- a/examples/libraries/virtualBaton.39.js +++ b/examples/libraries/virtualBaton.40.js @@ -219,7 +219,10 @@ function virtualBatonf(options) { var response = {proposalNumber: data.number, proposerId: data.proposerId}; if (betterNumber(data, bestProposal)) { bestProposal = data; - if (accepted.winner) { + // For stability, we don't let any one proposer rule out a disconnnected winner. + // If someone notices that a winner has disconnected (in their recheckWatchdog), + // they call for a new election. To remain chosen, a quorum need to confirm here. + if (accepted.winner && connectionTest(accepted.winner)) { response.number = accepted.number; response.winner = accepted.winner; } From b6472217c4193fce6ffef96478443cf596ea4336 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 16:08:40 -0800 Subject: [PATCH 23/73] Clear accepted/bestPromise .winner when disconnected. --- examples/entityScripts/{tribble.x.38.js => tribble.x.39.js} | 2 +- .../libraries/{virtualBaton.40.js => virtualBaton.41.js} | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) rename examples/entityScripts/{tribble.x.38.js => tribble.x.39.js} (98%) rename examples/libraries/{virtualBaton.40.js => virtualBaton.41.js} (98%) diff --git a/examples/entityScripts/tribble.x.38.js b/examples/entityScripts/tribble.x.39.js similarity index 98% rename from examples/entityScripts/tribble.x.38.js rename to examples/entityScripts/tribble.x.39.js index 3031ec6cd6..804666d344 100644 --- a/examples/entityScripts/tribble.x.38.js +++ b/examples/entityScripts/tribble.x.39.js @@ -1,6 +1,6 @@ (function () { // See tests/performance/tribbles.js - Script.include("../libraries/virtualBaton.40.js"); + Script.include("../libraries/virtualBaton.41.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, diff --git a/examples/libraries/virtualBaton.40.js b/examples/libraries/virtualBaton.41.js similarity index 98% rename from examples/libraries/virtualBaton.40.js rename to examples/libraries/virtualBaton.41.js index 8453365c9d..a472a9fdda 100644 --- a/examples/libraries/virtualBaton.40.js +++ b/examples/libraries/virtualBaton.41.js @@ -219,10 +219,7 @@ function virtualBatonf(options) { var response = {proposalNumber: data.number, proposerId: data.proposerId}; if (betterNumber(data, bestProposal)) { bestProposal = data; - // For stability, we don't let any one proposer rule out a disconnnected winner. - // If someone notices that a winner has disconnected (in their recheckWatchdog), - // they call for a new election. To remain chosen, a quorum need to confirm here. - if (accepted.winner && connectionTest(accepted.winner)) { + if (accepted.winner) { response.number = accepted.number; response.winner = accepted.winner; } @@ -333,6 +330,7 @@ function virtualBatonf(options) { exports.recheckWatchdog = timers.setInterval(function recheck() { var holder = acceptedId(); // If we're waiting and we notice the holder is gone, ... if (holder && claimCallback && !electionWatchdog && !connectionTest(holder)) { + accepted.winner = bestPromise.winner = null; propose(); // ... propose an election. } }, recheckInterval); From e52b910e532469be8da6894c1aa73d4b1d1095c4 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 16:39:29 -0800 Subject: [PATCH 24/73] connectionTest --- examples/entityScripts/{tribble.x.39.js => tribble.x.40.js} | 2 +- examples/libraries/{virtualBaton.41.js => virtualBaton.42.js} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename examples/entityScripts/{tribble.x.39.js => tribble.x.40.js} (98%) rename examples/libraries/{virtualBaton.41.js => virtualBaton.42.js} (99%) diff --git a/examples/entityScripts/tribble.x.39.js b/examples/entityScripts/tribble.x.40.js similarity index 98% rename from examples/entityScripts/tribble.x.39.js rename to examples/entityScripts/tribble.x.40.js index 804666d344..7dd2fe3aa5 100644 --- a/examples/entityScripts/tribble.x.39.js +++ b/examples/entityScripts/tribble.x.40.js @@ -1,6 +1,6 @@ (function () { // See tests/performance/tribbles.js - Script.include("../libraries/virtualBaton.41.js"); + Script.include("../libraries/virtualBaton.42.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, diff --git a/examples/libraries/virtualBaton.41.js b/examples/libraries/virtualBaton.42.js similarity index 99% rename from examples/libraries/virtualBaton.41.js rename to examples/libraries/virtualBaton.42.js index a472a9fdda..fd81671c44 100644 --- a/examples/libraries/virtualBaton.41.js +++ b/examples/libraries/virtualBaton.42.js @@ -219,7 +219,7 @@ function virtualBatonf(options) { var response = {proposalNumber: data.number, proposerId: data.proposerId}; if (betterNumber(data, bestProposal)) { bestProposal = data; - if (accepted.winner) { + if (accepted.winner && connectionTest(accepted.winner)) { response.number = accepted.number; response.winner = accepted.winner; } @@ -330,7 +330,7 @@ function virtualBatonf(options) { exports.recheckWatchdog = timers.setInterval(function recheck() { var holder = acceptedId(); // If we're waiting and we notice the holder is gone, ... if (holder && claimCallback && !electionWatchdog && !connectionTest(holder)) { - accepted.winner = bestPromise.winner = null; + bestPromise.winner = null; // used if the quorum agrees that old winner is not there propose(); // ... propose an election. } }, recheckInterval); From b08f3644474af7185650c33a5fd61b244fd89109 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 18:29:10 -0800 Subject: [PATCH 25/73] yes! --- .../{tribble.x.40.js => tribble.js} | 5 - .../{virtualBaton.42.js => virtualBaton.js} | 47 ++-- examples/tests/performance/tribbles.js | 7 +- tests/mocha/README.md | 5 + tests/mocha/package.json | 11 + tests/mocha/test/testVirtualBaton.js | 220 ++++++++++++++++++ 6 files changed, 265 insertions(+), 30 deletions(-) rename examples/entityScripts/{tribble.x.40.js => tribble.js} (94%) rename examples/libraries/{virtualBaton.42.js => virtualBaton.js} (91%) create mode 100644 tests/mocha/README.md create mode 100644 tests/mocha/package.json create mode 100644 tests/mocha/test/testVirtualBaton.js diff --git a/examples/entityScripts/tribble.x.40.js b/examples/entityScripts/tribble.js similarity index 94% rename from examples/entityScripts/tribble.x.40.js rename to examples/entityScripts/tribble.js index 7dd2fe3aa5..a5aaf4feb6 100644 --- a/examples/entityScripts/tribble.x.40.js +++ b/examples/entityScripts/tribble.js @@ -66,11 +66,6 @@ dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ batonName: 'io.highfidelity.tribble', - connectionTest: function (id) { - var connected = baton.validId(id); - print(MyAvatar.sessionUUID, id, connected); - return connected; - }, debugFlow: debug.flow, debugSend: debug.send, debugReceive: debug.receive diff --git a/examples/libraries/virtualBaton.42.js b/examples/libraries/virtualBaton.js similarity index 91% rename from examples/libraries/virtualBaton.42.js rename to examples/libraries/virtualBaton.js index fd81671c44..b1f813f764 100644 --- a/examples/libraries/virtualBaton.42.js +++ b/examples/libraries/virtualBaton.js @@ -31,7 +31,8 @@ function virtualBatonf(options) { var allowedDeviation = number * variability; return number - allowedDeviation + (Math.random() * 2 * allowedDeviation); } - // Allow testing outside in a harness of High Fidelity. + // Allow testing outside in a harness outside of High Fidelity. + // See sourceCodeSandbox/tests/mocha/test/testVirtualBaton.js var globals = options.globals || {}, messages = globals.Messages || Messages, myAvatar = globals.MyAvatar || MyAvatar, @@ -43,20 +44,20 @@ function virtualBatonf(options) { var batonName = options.batonName, // The identify of the baton. // instanceId is the identify of this particular copy of the script among all copies using the same batonName // in the domain. For example, if you wanted only one entity among multiple entity scripts to hold the baton, - // you could specify virtualBaton({batonName: 'someBatonName', id: MyAvatar.sessionUUID + entityID}). + // you could specify virtualBaton({batonName: 'someBatonName', instanceId: MyAvatar.sessionUUID + entityID}). instanceId = options.instanceId || myAvatar.sessionUUID, // virtualBaton() returns the exports object with properties. You can pass in an object to be side-effected. exports = options.exports || {}, // Handy to set false if we believe the optimizations are wrong, or to use both values in a test harness. useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, - // electionTimeout = options.electionTimeout || randomize(500, 0.2), // ms. If no winner in this time, hold a new election. recheckInterval = options.recheckInterval || randomize(500, 0.2), // ms. Check that winners remain connected. - // If you supply your own instanceId, you can also supply a connectionTest to check that answers - // truthy iff the given id is still valid and connected, and is run at recheckInterval. You can use - // exports.validId (see below), and the default answers truthy if id is valid or a concatenation of two valid - // ids. (This handles the most common cases of instanceId being either (the default) MyAvatar.sessionUUID, an - // entityID, or the concatenation (in either order) of both.) + // If you supply your own instanceId, you might also supply a connectionTest that answers + // truthy iff the given id is still valid and connected, and is run at recheckInterval. You + // can use exports.validId (see below), and the default answers truthy if id is valid or a + // concatenation of two valid ids. (This handles the most common cases of instanceId being + // either (the default) MyAvatar.sessionUUID, an entityID, or the concatenation (in either + // order) of both.) connectionTest = options.connectionTest || function connectionTest(id) { var idLength = 38; if (id.length === idLength) { return exports.validId(id); } @@ -126,7 +127,7 @@ function virtualBatonf(options) { // It would be great if we had a way to know how many subscribers our channel has. Failing that... var nNack = 0, previousNSubscribers = 0, lastGathering = 0, thisTimeout = electionTimeout; - function nSubscribers() { // Answer the number of subscribers - or zero to indicate that we have to wait. + function nSubscribers() { // Answer the number of subscribers. // To find nQuorum, we need to know how many scripts are being run using this batonName, which isn't // the same as the number of clients! // @@ -136,7 +137,7 @@ function virtualBatonf(options) { // // If we understimate by too much, there can be different pockets on the Internet that each // believe they have agreement on different holders of the baton, which is precisely what - // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' gather stragglers. + // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' to gather stragglers. var now = Date.now(), elapsed = now - lastGathering; if (elapsed >= thisTimeout) { @@ -144,8 +145,8 @@ function virtualBatonf(options) { lastGathering = now; } // ...otherwise we use the previous value unchanged. - // On startup, we do one proposal that we cannot possibly win, so that we'll - // lock things up for timeout to gather the number of responses. + // On startup, we do one proposal that we cannot possibly close, so that we'll + // lock things up for the full electionTimeout to gather responses. if (!previousNSubscribers) { var LARGE_INTEGER = Number.MAX_SAFE_INTEGER || (-1 >>> 1); // QT doesn't define the ECMA constant. Max int will do for our purposes. previousNSubscribers = LARGE_INTEGER; @@ -170,7 +171,7 @@ function virtualBatonf(options) { claimCallback, releaseCallback; function propose() { // Make a new proposal, so that we learn/update the proposalNumber and winner. - // Even though we send back a 'nack' if the proposal is obsolete, with net work errors + // Even though we send back a 'nack' if the proposal is obsolete, with network errors // there's no way to know for certain that we've failed. The electionWatchdog will try a new // proposal if we have not been accepted by a quorum after election Timeout. if (electionWatchdog) { @@ -178,7 +179,7 @@ function virtualBatonf(options) { // timers.clearTimeout(electionWatchdog) and not return. return; } - thisTimeout = randomize(electionTimeout, 0.5); // for accurate nSubcribers counting. + thisTimeout = randomize(electionTimeout, 0.5); // Note use in nSubcribers. electionWatchdog = timers.setTimeout(function () { electionWatchdog = null; propose(); @@ -208,7 +209,7 @@ function virtualBatonf(options) { receive('nack' + instanceId, function (data) { // An acceptor reports more recent data... if (data.proposalNumber === proposalNumber) { nNack++; // For updating nQuorum. - // IWBNI if we started our next proposal now (here, in a setTimeout, but we need a decent nNack count. + // IWBNI if we started our next proposal right now/here, but we need a decent nNack count. // Lets save that optimization for another day... } }); @@ -232,7 +233,7 @@ function virtualBatonf(options) { if (!betterNumber(bestProposal, data)) { bestProposal = accepted = data; // Update both with current data. Might have missed the proposal earlier. if (useOptimizations) { - // The Paxos literature describe every acceptor sending 'accepted' to + // The Paxos literature describes every acceptor sending 'accepted' to // every proposer and learner. In our case, these are the same nodes that received // the 'accept!' message, so we can send to just the originating proposer and invoke // our own accepted handler directly. @@ -288,14 +289,16 @@ function virtualBatonf(options) { } }); + // Public Interface + // // Registers an intent to hold the baton: // Calls onElection(batonName) once, if you are elected by the scripts // to be the unique holder of the baton, which may be never. - // Calls onRelease(batonName) once, if you release the baton held by you, - // whether this is by you calling release(), or by loosing + // Calls onRelease(batonName) once, if the baton held by you is released, + // whether this is by you calling release(), or by losing // an election when you become disconnected. // You may claim again at any time after the start of onRelease - // being called. Otherwise, you will not participate in further elections. + // being called. exports.claim = function claim(onElection, onRelease) { debugFlow('baton:', batonName, instanceId, 'claim'); if (claimCallback) { @@ -314,7 +317,7 @@ function virtualBatonf(options) { // Release the baton you hold, or just log that you are not holding it. exports.release = function release(optionalReplacementOnRelease) { debugFlow('baton:', batonName, instanceId, 'release'); - if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + if (optionalReplacementOnRelease) { // If you want to change. releaseCallback = optionalReplacementOnRelease; } if (acceptedId() !== instanceId) { @@ -323,7 +326,7 @@ function virtualBatonf(options) { } localRelease(); if (!claimCallback) { // No claim set in release callback. - propose(); // We are the distinguished proposer, but we'll pick anyone else interested, else set it to null. + propose(); } return exports; }; @@ -348,7 +351,7 @@ function virtualBatonf(options) { propose(); return exports; } -if (typeof module !== 'undefined') { +if (typeof module !== 'undefined') { // Allow testing in nodejs. module.exports = virtualBatonf; } else { virtualBaton = virtualBatonf; diff --git a/examples/tests/performance/tribbles.js b/examples/tests/performance/tribbles.js index da533f490a..a48ded730d 100644 --- a/examples/tests/performance/tribbles.js +++ b/examples/tests/performance/tribbles.js @@ -14,8 +14,8 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // The _TIMEOUT parameters can be 0 for no activity, and -1 to be active indefinitely. // -var NUMBER_TO_CREATE = 200; -var LIFETIME = 60; // seconds +var NUMBER_TO_CREATE = 1; // FIXME 200; +var LIFETIME = 30; // FIXME 60; // seconds var EDIT_RATE = 60; // hz var EDIT_TIMEOUT = -1; var MOVE_RATE = 1; // hz @@ -68,7 +68,8 @@ Script.setInterval(function () { moveTimeout: MOVE_TIMEOUT, moveRate: MOVE_RATE, editTimeout: EDIT_TIMEOUT, - editRate: EDIT_RATE + editRate: EDIT_RATE, + debug: {flow: true, send: true} }); for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) { Entities.addEntity({ diff --git a/tests/mocha/README.md b/tests/mocha/README.md new file mode 100644 index 0000000000..0bda9a20bc --- /dev/null +++ b/tests/mocha/README.md @@ -0,0 +1,5 @@ +mocha tests of javascript code (e.g., from ../../examples/libraries/). +``` +npm install +npm test +``` \ No newline at end of file diff --git a/tests/mocha/package.json b/tests/mocha/package.json new file mode 100644 index 0000000000..0532677f26 --- /dev/null +++ b/tests/mocha/package.json @@ -0,0 +1,11 @@ +{ + "name": "HighFidelityTests", + "version": "1.0.0", + "scripts": { + "test": "mocha" + }, + "license": "Apache 2.0", + "devDependencies": { + "mocha": "^2.2.1" + } +} diff --git a/tests/mocha/test/testVirtualBaton.js b/tests/mocha/test/testVirtualBaton.js new file mode 100644 index 0000000000..c661bfeffc --- /dev/null +++ b/tests/mocha/test/testVirtualBaton.js @@ -0,0 +1,220 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +var assert = require('assert'); +var mocha = require('mocha'), describe = mocha.describe, it = mocha.it, after = mocha.after; +var virtualBaton = require('../../../examples/libraries/virtualBaton.js'); + +describe('temp', function () { + var messageCount = 0, testStart = Date.now(); + function makeMessager(nodes, me, mode) { // shim for High Fidelity Message system + function noopSend(channel, string, source) { } + function hasChannel(node, channel) { return -1 !== node.subscribed.indexOf(channel); } + function sendSync(channel, message, nodes, skip) { + nodes.forEach(function (node) { + if (!hasChannel(node, channel) || (node === skip)) { return; } + node.sender(channel, message, me.name); + }); + } + nodes.forEach(function (node) { + node.sender = node.sender || noopSend; + node.subscribed = node.subscribed || []; + }); + return { + subscriberCount: function () { + var c = 0; + nodes.forEach(function (n) { if (n.subscribed.length) { c++; } }); + return c; + }, + subscribe: function (channel) { + me.subscribed.push(channel); + }, + unsubscribe: function (channel) { + me.subscribed.splice(me.subscribed.indexOf(channel), 1); + }, + sendMessage: function (channel, message) { + if ((mode === 'immediate2Me') && hasChannel(me, channel)) { me.sender(channel, message, me.name); } + if (mode === 'immediate') { + sendSync(channel, message, nodes, null); + } else { + process.nextTick(function () { + sendSync(channel, message, nodes, (mode === 'immediate2Me') ? me : null); + }); + } + }, + messageReceived: { + connect: function (f) { + me.sender = function (c, m, i) { messageCount++; f(c, m, i); }; + }, + disconnect: function () { + me.sender = noopSend; + } + } + }; + } + var debug = {}; //{flow: true, send: false, receive: false}; + function makeBaton(testKey, nodes, node, debug, mode, optimize) { + debug = debug || {}; + var baton = virtualBaton({ + batonName: testKey, + debugSend: debug.send, + debugReceive: debug.receive, + debugFlow: debug.flow, + useOptimizations: optimize, + connectionTest: function (id) { return baton.validId(id); }, + globals: { + Messages: makeMessager(nodes, node, mode), + MyAvatar: {sessionUUID: node.name}, + Script: { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval + }, + AvatarList: { + getAvatar: function (id) { return {sessionUUID: id}; } + }, + Entities: {getEntityProperties: function () { }}, + print: console.log + } + }); + return baton; + } + function noRelease(batonName) { assert.ok(!batonName, "should not release"); } + function defineABunch(mode, optimize) { + function makeKey(prefix) { return prefix + mode + (optimize ? '-opt' : ''); } + var testKeys = makeKey('single-'); + it(testKeys, function (done) { + var nodes = [{name: 'a'}]; + var a = makeBaton(testKeys, nodes, nodes[0], debug, mode).claim(function (key) { + console.log('claimed a'); + assert.equal(testKeys, key); + a.unload(); + done(); + }, noRelease); + }); + var testKeydp = makeKey('dual-parallel-'); + it(testKeydp, function (done) { + this.timeout(10000); + var nodes = [{name: 'ap'}, {name: 'bp'}]; + var a = makeBaton(testKeydp, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydp, nodes, nodes[1], debug, mode, optimize); + function accepted(key) { // Under some circumstances of network timing, either a or b can win. + console.log('claimed ap'); + assert.equal(testKeydp, key); + done(); + } + a.claim(accepted, noRelease); + b.claim(accepted, noRelease); + }); + var testKeyds = makeKey('dual-serial-'); + it(testKeyds, function (done) { + var nodes = [{name: 'as'}, {name: 'bs'}], + gotA = false, + gotB = false; + makeBaton(testKeyds, nodes, nodes[0], debug, mode, optimize).claim(function (key) { + console.log('claimed as', key); + assert.ok(!gotA, "should not get A after B"); + gotA = true; + done(); + }, noRelease); + setTimeout(function () { + makeBaton(testKeyds, nodes, nodes[1], debug, mode, optimize).claim(function (key) { + console.log('claimed bs', key); + assert.ok(!gotB, "should not get B after A"); + gotB = true; + done(); + }, noRelease); + }, 500); + }); + var testKeydsl = makeKey('dual-serial-long-'); + it(testKeydsl, function (done) { + this.timeout(5000); + var nodes = [{name: 'al'}, {name: 'bl'}], + gotA = false, + gotB = false, + releaseA = false; + makeBaton(testKeydsl, nodes, nodes[0], debug, mode, optimize).claim(function (key) { + console.log('claimed al', key); + assert.ok(!gotB, "should not get A after B"); + gotA = true; + if (!gotB) { done(); } + }, function () { + assert.ok(gotA, "Should claim it first"); + releaseA = true; + if (gotB) { done(); } + }); + setTimeout(function () { + makeBaton(testKeydsl, nodes, nodes[1], debug, mode, optimize).claim(function (key) { + console.log('claimed bl', key); + gotB = true; + if (releaseA) { done(); } + }, noRelease); + }, 3000); + }); + var testKeydsr = makeKey('dual-serial-with-release-'); + it(testKeydsr, function (done) { + this.timeout(5000); + var nodes = [{name: 'asr'}, {name: 'bsr'}], + gotClaimA = false, + gotReleaseA = false, + a = makeBaton(testKeydsr, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydsr, nodes, nodes[1], debug, mode, optimize); + a.claim(function (key) { + console.log('claimed asr'); + assert.equal(testKeydsr, key); + gotClaimA = true; + b.claim(function (key) { + console.log('claimed bsr'); + assert.equal(testKeydsr, key); + assert.ok(gotReleaseA); + done(); + }, noRelease); + a.release(); + }, function (key) { + console.log('released asr'); + assert.equal(testKeydsr, key); + assert.ok(gotClaimA); + gotReleaseA = true; + }); + }); + var testKeydpr = makeKey('dual-parallel-with-release-'); + it(testKeydpr, function (done) { + this.timeout(5000); + var nodes = [{name: 'ar'}, {name: 'br'}]; + var a = makeBaton(testKeydpr, nodes, nodes[0], debug, mode, optimize), + b = makeBaton(testKeydpr, nodes, nodes[1], debug, mode, optimize), + gotClaimA = false, + gotReleaseA = false, + gotClaimB = false; + a.claim(function (key) { + console.log('claimed ar'); + assert.equal(testKeydpr, key); + gotClaimA = true; + assert.ok(!gotClaimB, "if b claimed, should not get a"); + a.release(); + }, function (key) { + console.log('released ar'); + assert.equal(testKeydpr, key); + assert.ok(gotClaimA); + gotReleaseA = true; + }); + b.claim(function (key) { + console.log('claimed br', gotClaimA ? 'with' : 'without', 'ar first'); + assert.equal(testKeydpr, key); + gotClaimB = true; + assert.ok(!gotClaimA || gotReleaseA); + done(); + }, noRelease); + }); + } + function defineAllModeTests(optimize) { + defineABunch('delayed', optimize); + defineABunch('immediate2Me', optimize); + defineABunch('immediate', optimize); + } + defineAllModeTests(true); + defineAllModeTests(false); + after(function () { + console.log(messageCount, 'messages sent over', (Date.now() - testStart), 'ms.'); + }); +}); From 61e521bc63b2ebfc4ad24bcf328e1cbf223d21bc Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 8 Feb 2016 09:33:32 -0800 Subject: [PATCH 26/73] Fix build (and remove unneeded test file). --- examples/tests/testBaton.js | 35 ----------------------------------- tests/CMakeLists.txt | 2 +- 2 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 examples/tests/testBaton.js diff --git a/examples/tests/testBaton.js b/examples/tests/testBaton.js deleted file mode 100644 index 74fa0d39e4..0000000000 --- a/examples/tests/testBaton.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -/*jslint nomen: true, plusplus: true, vars: true*/ -var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; -// -// Created by Howard Stearns -// 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 -// -// test libraries/virtualBaton.js -// All participants should run the test script. - - -Script.include("../libraries/virtualBaton.31.js"); -var TICKER_INTERVAL = 1000; // ms -var baton = virtualBaton({key: 'io.highfidelity.testBaton', debugSend: true, debugFlow: true, debugReceive: true}); -var ticker, countDown; - -// Tick every TICKER_INTERVAL. -function gotBaton(key) { - print("gotBaton", key); - countDown = 20; - ticker = Script.setInterval(function () { - print("tick"); - }, 1000); -} -// If we've lost the baton (e.g., to network problems), stop ticking -// but ask for the baton back (waiting indefinitely to get it). -function lostBaton(key) { - print("lostBaton", key); - Script.clearInterval(ticker); - baton.claim(gotBaton, lostBaton); -} -baton.claim(gotBaton, lostBaton); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ffdf4b7602..a8b0727e3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,7 @@ enable_testing() # add the test directories file(GLOB TEST_SUBDIRS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/*") -list(REMOVE_ITEM TEST_SUBDIRS "CMakeFiles") +list(REMOVE_ITEM TEST_SUBDIRS "CMakeFiles" "mocha") foreach(DIR ${TEST_SUBDIRS}) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}") set(TEST_PROJ_NAME ${DIR}) From 9670cf3fe32baf9da0e992deafc48a98aa94b2af Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 8 Feb 2016 09:37:56 -0800 Subject: [PATCH 27/73] Reference correct filename. --- examples/entityScripts/tribble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/entityScripts/tribble.js b/examples/entityScripts/tribble.js index a5aaf4feb6..e34213cfa3 100644 --- a/examples/entityScripts/tribble.js +++ b/examples/entityScripts/tribble.js @@ -1,6 +1,6 @@ (function () { // See tests/performance/tribbles.js - Script.include("../libraries/virtualBaton.42.js"); + Script.include("../libraries/virtualBaton.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, From a80641a2c46022d26343201fab63889ab74a7e2d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Feb 2016 13:15:49 -0800 Subject: [PATCH 28/73] changes --- .../CellScience/Scripts/navigationButton.js | 2 +- .../Scripts/playBackgroundAudio.js | 2 +- .../CellScience/Scripts/showIdentification.js | 4 +- .../CellScience/backgroundMusicAC.js | 97 +++++++++++++++++++ .../CellScience/importCellScience.js | 23 +++-- 5 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js index 2798b58807..ee8e3d0753 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js @@ -7,7 +7,7 @@ (function() { - var version = 10; + var version = 11; var added = false; this.frame = 0; var utilsScript = Script.resolvePath('utils.js'); diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js index 6edccc211c..87302dd743 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js @@ -8,7 +8,7 @@ (function() { var self = this; var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; - var version = 10; + var version = 11; this.preload = function(entityId) { self.soundPlaying = false; self.entityId = entityId; diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js index 7c019865e9..d8b32ab176 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showIdentification.js @@ -10,7 +10,7 @@ var self = this; var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; - var version = 1; + var version = 2; this.preload = function(entityId) { this.soundPlaying = null; this.entityId = entityId; @@ -21,7 +21,7 @@ } this.initialize = function(entityId) { - print(' should initialize' + entityId) + //print(' should initialize' + entityId) var properties = Entities.getEntityProperties(entityId); if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { self.initTimeout = Script.setTimeout(function() { diff --git a/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js b/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js new file mode 100644 index 0000000000..b38390ed3c --- /dev/null +++ b/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js @@ -0,0 +1,97 @@ +var soundMap = [{ + name: 'Cells', + url: "http://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/Cells.wav", + audioOptions: { + position: { + x: 15850, + y: 15850, + z: 15850 + }, + volume: 0.4, + loop: true + } + }, { + name: 'Cell Layout', + url: "http://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/CellLayout.wav", + audioOptions: { + position: { + x: 15950, + y: 15950, + z: 15950 + }, + volume: 0.4, + loop: true + } + }, { + name: 'Ribsome', + url: "http://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/Ribosome.wav", + audioOptions: { + position: { + x: 15650, + y: 15650, + z: 15650 + }, + volume: 0.4, + loop: true + } + }, { + name: 'Hexokinase', + url: "http://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Audio/Hexokinase.wav", + audioOptions: { + position: { + x: 15750, + y: 15750, + z: 15750 + }, + volume: 0.4, + loop: true + } + } + +]; + + +function loadSounds() { + soundMap.forEach(function(soundData) { + soundData.sound = SoundCache.getSound(soundData.url); + }); +} + +function playSound(soundData) { + if (soundData.injector) { + // try/catch in case the injector QObject has been deleted already + try { + soundData.injector.stop(); + } catch (e) { + print('error playing sound' + e) + } + } + soundData.injector = Audio.playSound(soundData.sound, soundData.audioOptions); +} + +function checkDownloaded(soundData) { + if (soundData.sound.downloaded) { + + Script.clearInterval(soundData.downloadTimer); + + if (soundData.hasOwnProperty('playAtInterval')) { + soundData.playingInterval = Script.setInterval(function() { + playSound(soundData) + }, soundData.playAtInterval); + } else { + playSound(soundData); + } + + } +} + +function startCheckDownloadedTimers() { + soundMap.forEach(function(soundData) { + soundData.downloadTimer = Script.setInterval(function() { + checkDownloaded(soundData); + }, 1000); + }); +} + +loadSounds(); +startCheckDownloadedTimers(); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index ad18c2db3c..fe4a3389e1 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1015; +var version = 1016; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; @@ -115,8 +115,7 @@ var scenes = [{ radius: 500, number: 10, userData: JSON.stringify({ - target: locations.cellLayout[1], - location: locations.cellLayout[0], + location: locations.cellLayout[1], baseURL: baseLocation }), script: "zoom.js?" + version, @@ -597,7 +596,7 @@ function ImportScene(scene) { CreateInstances(scene); CreateBoundary(scene); - CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); + // CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); // print("done " + scene.name); @@ -654,9 +653,9 @@ function CreateNavigationButton(scene, number) { blue: 0 }, dimensions: { - x: 10, - y: 10, - z: 10 + x: 16000, + y: 16000, + z: 16000 }, visible: false, userData: JSON.stringify({ @@ -669,7 +668,7 @@ function CreateNavigationButton(scene, number) { grabbable: false } }), - // position:{x:3000,y:13500,z:3000}, + position:{x:0,y:0,z:0}, script: baseLocation + "Scripts/navigationButton.js?" + version, collisionless: true, @@ -853,10 +852,10 @@ function CreateBackgroundAudio(name, position) { Entities.addEntity({ type: "Sphere", name: "Location " + name + " background audio", - dimensions: { - x: 4000, - y: 4000, - z: 4000 + dimensions: { + x: 16000, + y: 16000, + z: 16000 }, position: position, visible: false, From 325c7525c7cd5dbb7f3ee597f5e4a3d89ba8c3f9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Feb 2016 13:43:41 -0800 Subject: [PATCH 29/73] work --- .../DomainContent/CellScience/Scripts/zoom.js | 39 ++++++++++--------- .../CellScience/backgroundMusicAC.js | 27 +++++++++++-- .../CellScience/importCellScience.js | 1 + 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js index 8b720b7fec..5e5a2d0e55 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js @@ -61,29 +61,32 @@ } // this.lookAt(data.target, data.location); - + if(data.hasOwnProperty('entryPoint')&&data.hasOwnProperty('target')){ + this.lookAtTarget(data.entryPoint,data.target); + } } } - // this.lookAt = function(targetPosition, avatarPosition) { - // print('GOING TO') - // var direction = Vec3.normalize(Vec3.subtract(MyAvatar.position, targetPosition)); + this.lookAtTarget = function(entryPoint,target) { - // var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { - // x: 1, - // y: 0, - // z: 0 - // }); - // var yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * 180.0 / Math.PI, { - // x: 0, - // y: 1, - // z: 0 - // }); - // print('JBP ZOOM DEBUG YO') - // MyAvatar.goToLocation(avatarPosition, true, yaw); - // MyAvatar.headYaw = 0; - // } + var direction = Vec3.normalize(Vec3.subtract(entryPoint, target)); + var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { + x: 1, + y: 0, + z: 0 + }); + var yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * 180.0 / Math.PI, { + x: 0, + y: 1, + z: 0 + }); + + MyAvatar.goToLocation(entryPoint, true, yaw); + + MyAvatar.headYaw = 0; + + } diff --git a/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js b/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js index b38390ed3c..0ee5b3bf32 100644 --- a/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js +++ b/unpublishedScripts/DomainContent/CellScience/backgroundMusicAC.js @@ -7,7 +7,7 @@ var soundMap = [{ y: 15850, z: 15850 }, - volume: 0.4, + volume: 0.1, loop: true } }, { @@ -19,7 +19,7 @@ var soundMap = [{ y: 15950, z: 15950 }, - volume: 0.4, + volume: 0.1, loop: true } }, { @@ -31,7 +31,7 @@ var soundMap = [{ y: 15650, z: 15650 }, - volume: 0.4, + volume: 0.1, loop: true } }, { @@ -43,7 +43,7 @@ var soundMap = [{ y: 15750, z: 15750 }, - volume: 0.4, + volume: 0.1, loop: true } } @@ -93,5 +93,24 @@ function startCheckDownloadedTimers() { }); } +Script.scriptEnding.connect(function() { + soundMap.forEach(function(soundData) { + + if (soundData.hasOwnProperty("injector")) { + soundData.injector.stop(); + } + + if (soundData.hasOwnProperty("downloadTimer")) { + Script.clearInterval(soundData.downloadTimer); + } + + if (soundData.hasOwnProperty("playingInterval")) { + Script.clearInterval(soundData.playingInterval); + } + + }); + +}); + loadSounds(); startCheckDownloadedTimers(); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index fe4a3389e1..6be5311f9d 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -115,6 +115,7 @@ var scenes = [{ radius: 500, number: 10, userData: JSON.stringify({ + entryPoint:locations.cellLayout[1], location: locations.cellLayout[1], baseURL: baseLocation }), From 0368bfc4ee20c5b963dd0050b199706b6b07ef2e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Feb 2016 13:53:12 -0800 Subject: [PATCH 30/73] sound on the server --- .../CellScience/importCellScience.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index 6be5311f9d..bf0e41bb62 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -115,8 +115,8 @@ var scenes = [{ radius: 500, number: 10, userData: JSON.stringify({ - entryPoint:locations.cellLayout[1], - location: locations.cellLayout[1], + target: locations.cellLayout[1], + location: locations.cellLayout[0], baseURL: baseLocation }), script: "zoom.js?" + version, @@ -597,7 +597,7 @@ function ImportScene(scene) { CreateInstances(scene); CreateBoundary(scene); - // CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); + CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); // print("done " + scene.name); @@ -654,9 +654,9 @@ function CreateNavigationButton(scene, number) { blue: 0 }, dimensions: { - x: 16000, - y: 16000, - z: 16000 + x: 10, + y: 10, + z: 10 }, visible: false, userData: JSON.stringify({ @@ -669,7 +669,7 @@ function CreateNavigationButton(scene, number) { grabbable: false } }), - position:{x:0,y:0,z:0}, + // position:{x:3000,y:13500,z:3000}, script: baseLocation + "Scripts/navigationButton.js?" + version, collisionless: true, @@ -853,10 +853,10 @@ function CreateBackgroundAudio(name, position) { Entities.addEntity({ type: "Sphere", name: "Location " + name + " background audio", - dimensions: { - x: 16000, - y: 16000, - z: 16000 + dimensions: { + x: 4000, + y: 4000, + z: 4000 }, position: position, visible: false, From 937ba3b79665e55c6e33d9cad65d45b52a363efb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Feb 2016 16:07:54 -0800 Subject: [PATCH 31/73] zoom differently o avoid bug --- .../DomainContent/CellScience/Scripts/zoom.js | 26 ++++++++----------- .../CellScience/importCellScience.js | 7 ++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js index 5e5a2d0e55..f29d5023c9 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js @@ -45,31 +45,27 @@ } this.enterEntity = function(entityID) { - // print(' ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) + print('JBP ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) var data = JSON.parse(Entities.getEntityProperties(this.entityId).userData); - // print(' DATA IS::' + data) + print('JBP DATA IS::' + data) if (data != null) { - // print("Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); - if (self.teleportSound.downloaded) { - //print("play sound"); - MyAvatar.position = data.location; - Audio.playSound(self.teleportSound, self.soundOptions); + print("JBP Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); - } else { - //print("not downloaded"); - } + MyAvatar.position = data.location; + + // if (data.hasOwnProperty('entryPoint') && data.hasOwnProperty('target')) { + // this.lookAtTarget(data.entryPoint, data.target); + // } + // else{ - // this.lookAt(data.target, data.location); - if(data.hasOwnProperty('entryPoint')&&data.hasOwnProperty('target')){ - this.lookAtTarget(data.entryPoint,data.target); - } + // } } } this.lookAtTarget = function(entryPoint,target) { - + print('JBP SHOULD LOOK AT TARGET') var direction = Vec3.normalize(Vec3.subtract(entryPoint, target)); var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { x: 1, diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index bf0e41bb62..acc68da932 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1016; +var version = 1021; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; @@ -115,8 +115,9 @@ var scenes = [{ radius: 500, number: 10, userData: JSON.stringify({ + entryPoint: locations.cellLayout[1], target: locations.cellLayout[1], - location: locations.cellLayout[0], + location: locations.cellLayout[1], baseURL: baseLocation }), script: "zoom.js?" + version, @@ -597,7 +598,7 @@ function ImportScene(scene) { CreateInstances(scene); CreateBoundary(scene); - CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); + // CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); // print("done " + scene.name); From d174e863472e3241dce57a092b1ef25840846a77 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 8 Feb 2016 16:45:25 -0800 Subject: [PATCH 32/73] fix batonName for entity script case --- examples/entityScripts/tribble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/entityScripts/tribble.js b/examples/entityScripts/tribble.js index e34213cfa3..3afdcc43e4 100644 --- a/examples/entityScripts/tribble.js +++ b/examples/entityScripts/tribble.js @@ -65,7 +65,7 @@ oldColor = properties.color; dimensions = Vec3.multiply(scale, properties.dimensions); baton = virtualBaton({ - batonName: 'io.highfidelity.tribble', + batonName: 'io.highfidelity.tribble:' + entityID, // One winner for each entity debugFlow: debug.flow, debugSend: debug.send, debugReceive: debug.receive From cecf2fb611c474df895d413ab5a7505200c419f4 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 8 Feb 2016 17:00:29 -0800 Subject: [PATCH 33/73] Up the number. --- examples/tests/performance/tribbles.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/tests/performance/tribbles.js b/examples/tests/performance/tribbles.js index a48ded730d..f4eef2ff1a 100644 --- a/examples/tests/performance/tribbles.js +++ b/examples/tests/performance/tribbles.js @@ -14,8 +14,8 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print; // The _TIMEOUT parameters can be 0 for no activity, and -1 to be active indefinitely. // -var NUMBER_TO_CREATE = 1; // FIXME 200; -var LIFETIME = 30; // FIXME 60; // seconds +var NUMBER_TO_CREATE = 100; +var LIFETIME = 120; // seconds var EDIT_RATE = 60; // hz var EDIT_TIMEOUT = -1; var MOVE_RATE = 1; // hz @@ -69,7 +69,7 @@ Script.setInterval(function () { moveRate: MOVE_RATE, editTimeout: EDIT_TIMEOUT, editRate: EDIT_RATE, - debug: {flow: true, send: true} + debug: {flow: false, send: false, receive: false} }); for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) { Entities.addEntity({ From 251810edc547979b01351e03e77ac1002b8b509e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 9 Feb 2016 15:03:36 -0800 Subject: [PATCH 34/73] only suppress location edits when they arrive over the network -- script can still update --- libraries/entities/src/EntityItem.cpp | 48 ++++++++++++++++++--------- libraries/entities/src/EntityItem.h | 4 +++ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index e56f2c267a..8f8485236c 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -659,10 +659,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // but since we're using macros below we have to temporarily modify overwriteLocalData. bool oldOverwrite = overwriteLocalData; overwriteLocalData = overwriteLocalData && !weOwnSimulation; - READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePosition); - READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, updateRotation); - READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocity); - READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocity); + READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePositionFromNetwork); + READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, updateRotationFromNetwork); + READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocityFromNetwork); + READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocityFromNetwork); READ_ENTITY_PROPERTY(PROP_ACCELERATION, glm::vec3, setAcceleration); overwriteLocalData = oldOverwrite; } @@ -1344,9 +1344,6 @@ void EntityItem::computeShapeInfo(ShapeInfo& info) { } void EntityItem::updatePosition(const glm::vec3& value) { - if (shouldSuppressLocationEdits()) { - return; - } if (getLocalPosition() != value) { setLocalPosition(value); _dirtyFlags |= Simulation::DIRTY_POSITION; @@ -1359,6 +1356,13 @@ void EntityItem::updatePosition(const glm::vec3& value) { } } +void EntityItem::updatePositionFromNetwork(const glm::vec3& value) { + if (shouldSuppressLocationEdits()) { + return; + } + updatePosition(value); +} + void EntityItem::updateDimensions(const glm::vec3& value) { if (getDimensions() != value) { setDimensions(value); @@ -1367,9 +1371,6 @@ void EntityItem::updateDimensions(const glm::vec3& value) { } void EntityItem::updateRotation(const glm::quat& rotation) { - if (shouldSuppressLocationEdits()) { - return; - } if (getLocalOrientation() != rotation) { setLocalOrientation(rotation); _dirtyFlags |= Simulation::DIRTY_ROTATION; @@ -1383,6 +1384,13 @@ void EntityItem::updateRotation(const glm::quat& rotation) { } } +void EntityItem::updateRotationFromNetwork(const glm::quat& rotation) { + if (shouldSuppressLocationEdits()) { + return; + } + updateRotation(rotation); +} + void EntityItem::updateMass(float mass) { // Setting the mass actually changes the _density (at fixed volume), however // we must protect the density range to help maintain stability of physics simulation @@ -1407,9 +1415,6 @@ void EntityItem::updateMass(float mass) { } void EntityItem::updateVelocity(const glm::vec3& value) { - if (shouldSuppressLocationEdits()) { - return; - } glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { const float MIN_LINEAR_SPEED = 0.001f; @@ -1423,6 +1428,13 @@ void EntityItem::updateVelocity(const glm::vec3& value) { } } +void EntityItem::updateVelocityFromNetwork(const glm::vec3& value) { + if (shouldSuppressLocationEdits()) { + return; + } + updateVelocity(value); +} + void EntityItem::updateDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); if (_damping != clampedDamping) { @@ -1439,9 +1451,6 @@ void EntityItem::updateGravity(const glm::vec3& value) { } void EntityItem::updateAngularVelocity(const glm::vec3& value) { - if (shouldSuppressLocationEdits()) { - return; - } glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { const float MIN_ANGULAR_SPEED = 0.0002f; @@ -1455,6 +1464,13 @@ void EntityItem::updateAngularVelocity(const glm::vec3& value) { } } +void EntityItem::updateAngularVelocityFromNetwork(const glm::vec3& value) { + if (shouldSuppressLocationEdits()) { + return; + } + return updateAngularVelocity(value); +} + void EntityItem::updateAngularDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); if (_angularDamping != clampedDamping) { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index c8b54bca87..e6f2e44930 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -317,16 +317,20 @@ public: // updateFoo() methods to be used when changes need to be accumulated in the _dirtyFlags void updatePosition(const glm::vec3& value); + void updatePositionFromNetwork(const glm::vec3& value); void updateDimensions(const glm::vec3& value); void updateRotation(const glm::quat& rotation); + void updateRotationFromNetwork(const glm::quat& rotation); void updateDensity(float value); void updateMass(float value); void updateVelocity(const glm::vec3& value); + void updateVelocityFromNetwork(const glm::vec3& value); void updateDamping(float value); void updateRestitution(float value); void updateFriction(float value); void updateGravity(const glm::vec3& value); void updateAngularVelocity(const glm::vec3& value); + void updateAngularVelocityFromNetwork(const glm::vec3& value); void updateAngularDamping(float value); void updateCollisionless(bool value); void updateCollisionMask(uint8_t value); From c2cbca88cbc0dda409abc2f120523f96b1b29ed2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 9 Feb 2016 16:54:32 -0800 Subject: [PATCH 35/73] allow grabbing with one hand and then equipping with other --- examples/attachedEntitiesManager.js | 14 +++++++++----- examples/controllers/handControllerGrab.js | 11 +++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/attachedEntitiesManager.js b/examples/attachedEntitiesManager.js index 8cd159ec14..b5d023c70c 100644 --- a/examples/attachedEntitiesManager.js +++ b/examples/attachedEntitiesManager.js @@ -148,7 +148,7 @@ function AttachedEntitiesManager() { this.checkIfWearable = function(grabbedEntity, releasedFromJoint) { var allowedJoints = getEntityCustomData('wearable', grabbedEntity, DEFAULT_WEARABLE_DATA).joints; - var props = Entities.getEntityProperties(grabbedEntity, ["position", "parentID"]); + var props = Entities.getEntityProperties(grabbedEntity, ["position", "parentID", "parentJointIndex"]); if (props.parentID === NULL_UUID || props.parentID === MyAvatar.sessionUUID) { var bestJointName = ""; var bestJointIndex = -1; @@ -192,10 +192,14 @@ function AttachedEntitiesManager() { } Entities.editEntity(grabbedEntity, wearProps); } else if (props.parentID != NULL_UUID) { - // drop the entity with no parent (not on the avatar) - Entities.editEntity(grabbedEntity, { - parentID: NULL_UUID - }); + // drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand. + if (props.parentID === MyAvatar.sessionUUID && + (props.parentJointIndex == MyAvatar.getJointIndex("RightHand") || + props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) { + // this is equipped on a hand -- don't clear the parent. + } else { + Entities.editEntity(grabbedEntity, { parentID: NULL_UUID }); + } } } } diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 61c57fd86f..51814ad5d7 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -946,13 +946,15 @@ function MyController(hand) { return; } // near grab or equip with action - if (near && (grabbableData.refCount < 1 || entityHasActions(this.grabbedEntity))) { + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); + var refCount = ("refCount" in grabData) ? grabData.refCount : 0; + if (near && (refCount < 1 || entityHasActions(this.grabbedEntity))) { this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP); return; } // far grab or equip with action if ((isPhysical || this.state == STATE_EQUIP_SEARCHING) && !near) { - if (entityIsGrabbedByOther(intersection.entityID)) { + if (entityIsGrabbedByOther(this.grabbedEntity)) { // don't distance grab something that is already grabbed. if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': already grabbed by another."); @@ -1449,11 +1451,12 @@ function MyController(hand) { return; } - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID"]); + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position"]); if (props.parentID == MyAvatar.sessionUUID && Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. - print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand."); + print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + + props.parentID + " " + vec3toStr(props.position)); this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed(this.state == STATE_NEAR_GRABBING ? "releaseGrab" : "releaseEquip", [JSON.stringify(this.hand)]); From 727e289ecda21e02434a413b770e2a9aceabc251 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 Feb 2016 17:58:54 -0800 Subject: [PATCH 36/73] SpatiallyNestable: bug fixes for kinematic entities * Kinematic entities were being simulated with position in the local frame but velocity in the global frame. * Also, because getVelocity() was returning non-zero values when the parent was moving, kinematic objects were being simulated even if they should have been at rest. * Bug fix for calculating velocity direction was incorrect, if the parent frame was rotated. --- libraries/entities/src/EntityItem.cpp | 66 +++++++++++++-------- libraries/entities/src/EntityItem.h | 3 + libraries/entities/src/EntitySimulation.cpp | 2 +- libraries/shared/src/SpatiallyNestable.cpp | 4 +- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index e56f2c267a..15107269d2 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -885,83 +885,93 @@ void EntityItem::simulateKinematicMotion(float timeElapsed, bool setFlags) { return; } - if (hasAngularVelocity()) { - glm::vec3 angularVelocity = getAngularVelocity(); + if (hasLocalAngularVelocity()) { + glm::vec3 localAngularVelocity = getLocalAngularVelocity(); // angular damping if (_angularDamping > 0.0f) { - angularVelocity *= powf(1.0f - _angularDamping, timeElapsed); + localAngularVelocity *= powf(1.0f - _angularDamping, timeElapsed); #ifdef WANT_DEBUG qCDebug(entities) << " angularDamping :" << _angularDamping; - qCDebug(entities) << " newAngularVelocity:" << angularVelocity; + qCDebug(entities) << " newAngularVelocity:" << localAngularVelocity; #endif } - float angularSpeed = glm::length(angularVelocity); + float angularSpeed = glm::length(localAngularVelocity); const float EPSILON_ANGULAR_VELOCITY_LENGTH = 0.0017453f; // 0.0017453 rad/sec = 0.1f degrees/sec if (angularSpeed < EPSILON_ANGULAR_VELOCITY_LENGTH) { if (setFlags && angularSpeed > 0.0f) { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } - angularVelocity = ENTITY_ITEM_ZERO_VEC3; + localAngularVelocity = ENTITY_ITEM_ZERO_VEC3; } else { // for improved agreement with the way Bullet integrates rotations we use an approximation // and break the integration into bullet-sized substeps glm::quat rotation = getRotation(); float dt = timeElapsed; while (dt > PHYSICS_ENGINE_FIXED_SUBSTEP) { - glm::quat dQ = computeBulletRotationStep(angularVelocity, PHYSICS_ENGINE_FIXED_SUBSTEP); + glm::quat dQ = computeBulletRotationStep(localAngularVelocity, PHYSICS_ENGINE_FIXED_SUBSTEP); rotation = glm::normalize(dQ * rotation); dt -= PHYSICS_ENGINE_FIXED_SUBSTEP; } // NOTE: this final partial substep can drift away from a real Bullet simulation however // it only becomes significant for rapidly rotating objects // (e.g. around PI/4 radians per substep, or 7.5 rotations/sec at 60 substeps/sec). - glm::quat dQ = computeBulletRotationStep(angularVelocity, dt); + glm::quat dQ = computeBulletRotationStep(localAngularVelocity, dt); rotation = glm::normalize(dQ * rotation); setRotation(rotation); } - setAngularVelocity(angularVelocity); + setLocalAngularVelocity(localAngularVelocity); } - if (hasVelocity()) { + if (hasLocalVelocity()) { + + // acceleration is in the global frame, so transform it into the local frame. + // TODO: Move this into SpatiallyNestable. + bool success; + Transform transform = getParentTransform(success); + glm::vec3 localAcceleration(glm::vec3::_null); + if (success) { + localAcceleration = glm::inverse(transform.getRotation()) * getAcceleration(); + } else { + localAcceleration = getAcceleration(); + } + // linear damping - glm::vec3 velocity = getVelocity(); + glm::vec3 localVelocity = getLocalVelocity(); if (_damping > 0.0f) { - velocity *= powf(1.0f - _damping, timeElapsed); + localVelocity *= powf(1.0f - _damping, timeElapsed); #ifdef WANT_DEBUG qCDebug(entities) << " damping:" << _damping; - qCDebug(entities) << " velocity AFTER dampingResistance:" << velocity; - qCDebug(entities) << " glm::length(velocity):" << glm::length(velocity); + qCDebug(entities) << " velocity AFTER dampingResistance:" << localVelocity; + qCDebug(entities) << " glm::length(velocity):" << glm::length(localVelocity); #endif } // integrate position forward - glm::vec3 position = getPosition(); - glm::vec3 newPosition = position + (velocity * timeElapsed); + glm::vec3 localPosition = getLocalPosition(); + glm::vec3 newLocalPosition = localPosition + (localVelocity * timeElapsed) + 0.5f * localAcceleration * timeElapsed * timeElapsed; #ifdef WANT_DEBUG qCDebug(entities) << " EntityItem::simulate()...."; qCDebug(entities) << " timeElapsed:" << timeElapsed; qCDebug(entities) << " old AACube:" << getMaximumAACube(); - qCDebug(entities) << " old position:" << position; - qCDebug(entities) << " old velocity:" << velocity; + qCDebug(entities) << " old position:" << localPosition; + qCDebug(entities) << " old velocity:" << localVelocity; qCDebug(entities) << " old getAABox:" << getAABox(); qCDebug(entities) << " newPosition:" << newPosition; - qCDebug(entities) << " glm::distance(newPosition, position):" << glm::distance(newPosition, position); + qCDebug(entities) << " glm::distance(newPosition, position):" << glm::distance(newLocalPosition, localPosition); #endif - position = newPosition; + localPosition = newLocalPosition; // apply effective acceleration, which will be the same as gravity if the Entity isn't at rest. - if (hasAcceleration()) { - velocity += getAcceleration() * timeElapsed; - } + localVelocity += localAcceleration * timeElapsed; - float speed = glm::length(velocity); + float speed = glm::length(localVelocity); const float EPSILON_LINEAR_VELOCITY_LENGTH = 0.001f; // 1mm/sec if (speed < EPSILON_LINEAR_VELOCITY_LENGTH) { setVelocity(ENTITY_ITEM_ZERO_VEC3); @@ -969,8 +979,8 @@ void EntityItem::simulateKinematicMotion(float timeElapsed, bool setFlags) { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } } else { - setPosition(position); - setVelocity(velocity); + setLocalPosition(localPosition); + setLocalVelocity(localVelocity); } #ifdef WANT_DEBUG @@ -986,6 +996,10 @@ bool EntityItem::isMoving() const { return hasVelocity() || hasAngularVelocity(); } +bool EntityItem::isMovingRelativeToParent() const { + return hasLocalVelocity() || hasLocalAngularVelocity(); +} + EntityTreePointer EntityItem::getTree() const { EntityTreeElementPointer containingElement = getElement(); EntityTreePointer tree = containingElement ? containingElement->getTree() : nullptr; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index c8b54bca87..4cdacdbbf5 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -194,6 +194,7 @@ public: float getDensity() const { return _density; } bool hasVelocity() const { return getVelocity() != ENTITY_ITEM_ZERO_VEC3; } + bool hasLocalVelocity() const { return getLocalVelocity() != ENTITY_ITEM_ZERO_VEC3; } const glm::vec3& getGravity() const { return _gravity; } /// get gravity in meters void setGravity(const glm::vec3& value) { _gravity = value; } /// gravity in meters @@ -254,6 +255,7 @@ public: { _registrationPoint = glm::clamp(value, 0.0f, 1.0f); requiresRecalcBoxes(); } bool hasAngularVelocity() const { return getAngularVelocity() != ENTITY_ITEM_ZERO_VEC3; } + bool hasLocalAngularVelocity() const { return getLocalAngularVelocity() != ENTITY_ITEM_ZERO_VEC3; } float getAngularDamping() const { return _angularDamping; } void setAngularDamping(float value) { _angularDamping = value; } @@ -339,6 +341,7 @@ public: void clearDirtyFlags(uint32_t mask = 0xffffffff) { _dirtyFlags &= ~mask; } bool isMoving() const; + bool isMovingRelativeToParent() const; bool isSimulated() const { return _simulated; } diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 23e5b99337..a1f55a92a3 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -254,7 +254,7 @@ void EntitySimulation::moveSimpleKinematics(const quint64& now) { SetOfEntities::iterator itemItr = _simpleKinematicEntities.begin(); while (itemItr != _simpleKinematicEntities.end()) { EntityItemPointer entity = *itemItr; - if (entity->isMoving() && !entity->getPhysicsInfo()) { + if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { entity->simulate(now); _entitiesToSort.insert(entity); ++itemItr; diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index d3048194b4..95e8f62d21 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -416,7 +416,7 @@ void SpatiallyNestable::setVelocity(const glm::vec3& velocity, bool& success) { Transform parentTransform = getParentTransform(success); _velocityLock.withWriteLock([&] { // TODO: take parent angularVelocity into account. - _velocity = glm::inverse(parentTransform.getRotation()) * velocity - parentVelocity; + _velocity = glm::inverse(parentTransform.getRotation()) * (velocity - parentVelocity); }); } @@ -460,7 +460,7 @@ void SpatiallyNestable::setAngularVelocity(const glm::vec3& angularVelocity, boo glm::vec3 parentAngularVelocity = getParentAngularVelocity(success); Transform parentTransform = getParentTransform(success); _angularVelocityLock.withWriteLock([&] { - _angularVelocity = glm::inverse(parentTransform.getRotation()) * angularVelocity - parentAngularVelocity; + _angularVelocity = glm::inverse(parentTransform.getRotation()) * (angularVelocity - parentAngularVelocity); }); } From d920a3d7214f581e33a85fd32679ca96c4a8a79c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 9 Feb 2016 18:05:14 -0800 Subject: [PATCH 37/73] updates --- .../CellScience/Scripts/deleteAll.js | 17 ---- .../CellScience/Scripts/findScripts.js | 21 ----- .../CellScience/Scripts/navigationButton.js | 18 ++-- .../Scripts/playBackgroundAudio.js | 86 ------------------- .../DomainContent/CellScience/Scripts/zoom.js | 9 +- .../CellScience/importCellScience.js | 74 +++++++--------- 6 files changed, 42 insertions(+), 183 deletions(-) delete mode 100644 unpublishedScripts/DomainContent/CellScience/Scripts/deleteAll.js delete mode 100644 unpublishedScripts/DomainContent/CellScience/Scripts/findScripts.js delete mode 100644 unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/deleteAll.js b/unpublishedScripts/DomainContent/CellScience/Scripts/deleteAll.js deleted file mode 100644 index 716a5a8424..0000000000 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/deleteAll.js +++ /dev/null @@ -1,17 +0,0 @@ -// 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 -// - -function deleteAllInRadius(r) { - var n = 0; - var arrayFound = Entities.findEntities(MyAvatar.position, r); - for (var i = 0; i < arrayFound.length; i++) { - Entities.deleteEntity(arrayFound[i]); - } - print("deleted " + arrayFound.length + " entities"); -} - -deleteAllInRadius(100000); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/findScripts.js b/unpublishedScripts/DomainContent/CellScience/Scripts/findScripts.js deleted file mode 100644 index bdf6278f19..0000000000 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/findScripts.js +++ /dev/null @@ -1,21 +0,0 @@ -// 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 -// - -var scriptName = "Controller"; - -function findScriptsInRadius(r) { - var n = 0; - var arrayFound = Entities.findEntities(MyAvatar.position, r); - for (var i = 0; i < arrayFound.length; i++) { - if (Entities.getEntityProperties(arrayFound[i]).script.indexOf(scriptName) != -1) { - n++; - } - } - print("found " + n + " copies of " + scriptName); -} - -findScriptsInRadius(100000); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js index ee8e3d0753..4136f1f81b 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/navigationButton.js @@ -23,32 +23,32 @@ } this.initialize = function(entityId) { - // print(' should initialize' + entityId) + print('JBP nav button should initialize' + entityId) var properties = Entities.getEntityProperties(entityId); if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { self.initTimeout = Script.setTimeout(function() { - // print(' no user data yet, try again in one second') + print('JBP no user data yet, try again in one second') self.initialize(entityId); }, 1000) } else { - // print(' userdata before parse attempt' + properties.userData) + print('JBP userdata before parse attempt' + properties.userData) self.userData = null; try { self.userData = JSON.parse(properties.userData); } catch (err) { - // print(' error parsing json'); - // print(' properties are:' + properties.userData); + print('JBP error parsing json'); + print('JBP properties are:' + properties.userData); return; } - + var mySavedSettings = Settings.getValue(entityId); if (mySavedSettings.buttons !== undefined) { - //print(' preload buttons' + mySavedSettings.buttons) + print('JBP preload buttons' + mySavedSettings.buttons) mySavedSettings.buttons.forEach(function(b) { - //print(' deleting button' + b) + print('JBP deleting button' + b) Overlays.deleteOverlay(b); }) Settings.setValue(entityId, '') @@ -56,7 +56,7 @@ self.buttonImageURL = baseURL + "GUI/GUI_" + self.userData.name + ".png?" + version; - //print('BUTTON IMAGE URL:' + self.buttonImageURL) + print('JBP BUTTON IMAGE URL:' + self.buttonImageURL) if (self.button === undefined) { // print('NAV NO BUTTON ADDING ONE!!') self.button = true; diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js b/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js deleted file mode 100644 index 87302dd743..0000000000 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/playBackgroundAudio.js +++ /dev/null @@ -1,86 +0,0 @@ -// 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 -// - -(function() { - var self = this; - var baseURL = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; - var version = 11; - this.preload = function(entityId) { - self.soundPlaying = false; - self.entityId = entityId; - this.initTimeout = null; - this.initialize(entityId); - } - - this.initialize = function(entityID) { - //print(' should initialize' + entityID) - var properties = Entities.getEntityProperties(entityID); - if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { - self.initTimeout = Script.setTimeout(function() { - //print(' no user data yet, try again in one second') - self.initialize(entityID); - }, 1000) - - } else { - //print(' userdata before parse attempt' + properties.userData) - self.userData = null; - try { - self.userData = JSON.parse(properties.userData); - } catch (err) { - //print(' error parsing json'); - //print(' properties are:' + properties.userData); - return; - } - - - //print(' USERDATA NAME ' + self.userData.name) - self.soundURL = baseURL + "Audio/" + self.userData.name + ".wav?" + version; - //print(" creating WAV name location is " + baseURL + "Audio/" + self.userData.name + ".wav"); - //print(' self soundURL' + self.soundURL) - - self.soundOptions = { - stereo: true, - loop: true, - localOnly: true, - volume: 0.035 - }; - - self.sound = SoundCache.getSound(self.soundURL); - } - } - - this.enterEntity = function(entityID) { - //print("entering audio zone"); - if (self.sound.downloaded) { - //print("playing background audio named " + self.userData.name + "which has been downloaded"); - this.soundPlaying = Audio.playSound(self.sound, self.soundOptions); - - } else { - //print("sound is not downloaded"); - } - } - - this.leaveEntity = function(entityID) { - //print("leaving audio area " + self.userData.name); - if (self.soundPlaying !== false) { - //print("not null"); - //print("Stopped sound " + self.userData.name); - self.soundPlaying.stop(); - } else { - //print("Sound not playing"); - } - } - - this.unload = function() { - if (this.initTimeout !== null) { - Script.clearTimeout(this.initTimeout); - } - } - - - -}); \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js index f29d5023c9..da41ec64ba 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/zoom.js @@ -45,12 +45,11 @@ } this.enterEntity = function(entityID) { - print('JBP ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) - + //print('ENTERED A BOUNDARY ENTITY, SHOULD ZOOM', entityID) var data = JSON.parse(Entities.getEntityProperties(this.entityId).userData); - print('JBP DATA IS::' + data) + //print('DATA IS::' + data) if (data != null) { - print("JBP Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); + print("Teleporting to (" + data.location.x + ", " + data.location.y + ", " + data.location.z + ")"); MyAvatar.position = data.location; @@ -65,7 +64,7 @@ } this.lookAtTarget = function(entryPoint,target) { - print('JBP SHOULD LOOK AT TARGET') + //print('SHOULD LOOK AT TARGET') var direction = Vec3.normalize(Vec3.subtract(entryPoint, target)); var pitch = Quat.angleAxis(Math.asin(-direction.y) * 180.0 / Math.PI, { x: 1, diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index acc68da932..3885a29b89 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1021; +var version = 1029; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; @@ -163,7 +163,7 @@ var scenes = [{ z: 0 }, radius: 300, - number: 15, + number: 7, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -188,7 +188,7 @@ var scenes = [{ z: 0 }, radius: 1000, - number: 45, + number: 22, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -210,7 +210,7 @@ var scenes = [{ z: 976 }, radius: 140, - number: 20, + number: 10, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -232,7 +232,7 @@ var scenes = [{ z: 976 }, radius: 115, - number: 15, + number: 7, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -254,7 +254,7 @@ var scenes = [{ z: 0 }, radius: 600, - number: 30, + number: 15, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -276,7 +276,7 @@ var scenes = [{ z: 0 }, radius: 1600, - number: 45, + number: 22, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -298,7 +298,7 @@ var scenes = [{ z: 0 }, radius: 1400, - number: 45, + number: 22, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -320,7 +320,7 @@ var scenes = [{ z: 0 }, radius: 1800, - number: 45, + number: 22, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -365,7 +365,7 @@ var scenes = [{ z: 771 }, radius: 80, - number: 15, + number: 7, userData: JSON.stringify({ grabbableKey: { grabbable: false @@ -390,13 +390,15 @@ var scenes = [{ z: 771 }, radius: 60, - number: 15, + number: 7, userData: JSON.stringify({ grabbableKey: { grabbable: false - } + }, + target: locations.hexokinase[1], + location: locations.hexokinase[0], }), - script: "", + script: "zoom.js?" + version, visible: true }, { model: "glucose_isomerase", @@ -412,13 +414,15 @@ var scenes = [{ z: 771 }, radius: 70, - number: 15, + number: 7, userData: JSON.stringify({ grabbableKey: { grabbable: false - } + }, + target: locations.hexokinase[1], + location: locations.hexokinase[0], }), - script: "", + script: "zoom.js?" + version, visible: true } // { @@ -598,8 +602,6 @@ function ImportScene(scene) { CreateInstances(scene); CreateBoundary(scene); - // CreateBackgroundAudio(scene.name, scene.location, scene.dimensions); - // print("done " + scene.name); } @@ -647,7 +649,7 @@ function CreateNavigationButton(scene, number) { Entities.addEntity({ - type: "Sphere", + type: "Box", name: scene.name + " navigation button", color: { red: 200, @@ -655,9 +657,9 @@ function CreateNavigationButton(scene, number) { blue: 0 }, dimensions: { - x: 10, - y: 10, - z: 10 + x: 16000, + y: 16000, + z: 16000 }, visible: false, userData: JSON.stringify({ @@ -670,10 +672,13 @@ function CreateNavigationButton(scene, number) { grabbable: false } }), - // position:{x:3000,y:13500,z:3000}, + position: { + x: 0, + y: 0, + z: 0 + }, script: baseLocation + "Scripts/navigationButton.js?" + version, collisionless: true, - }); } @@ -850,27 +855,6 @@ function CreateIdentification(name, position, rotation, dimensions, showDistance } -function CreateBackgroundAudio(name, position) { - Entities.addEntity({ - type: "Sphere", - name: "Location " + name + " background audio", - dimensions: { - x: 4000, - y: 4000, - z: 4000 - }, - position: position, - visible: false, - userData: JSON.stringify({ - name: name, - baseURL: baseLocation - }), - script: baseLocation + "Scripts/playBackgroundAudio.js?" + version, - collisionless: true, - - }); - -} function getPointOnSphereOfRadius(radius, number, totalNumber) { From b9a16cec254532f2067b58997b4c7dce19f8b76d Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 9 Feb 2016 18:49:18 -0800 Subject: [PATCH 38/73] Re-enable OpenVR plugin * Enabled preview window * rendering is only mono for some-reason. --- cmake/externals/openvr/CMakeLists.txt | 4 ++-- plugins/openvr/CMakeLists.txt | 11 ++++----- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 24 ++++++++++++++++++++ plugins/openvr/src/OpenVrDisplayPlugin.h | 2 ++ plugins/openvr/src/OpenVrHelpers.cpp | 2 ++ plugins/openvr/src/ViveControllerManager.cpp | 8 ++++++- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/cmake/externals/openvr/CMakeLists.txt b/cmake/externals/openvr/CMakeLists.txt index 9ef95b17da..3fe7df44d0 100644 --- a/cmake/externals/openvr/CMakeLists.txt +++ b/cmake/externals/openvr/CMakeLists.txt @@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://github.com/ValveSoftware/openvr/archive/v0.9.12.zip - URL_MD5 c08dced68ce4e341e1467e6814ae419d + URL https://github.com/ValveSoftware/openvr/archive/v0.9.15.zip + URL_MD5 0ff8560b49b6da1150fcc47360e8ceca CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index c3c4771276..878df3bfa5 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -6,20 +6,17 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -# OpenVR is disabled until a) it works with threaded present and -# b) it doesn't interfere with Oculus SDK 0.8 -if (FALSE) -#if (WIN32) +if (WIN32) # we're using static GLEW, so define GLEW_STATIC add_definitions(-DGLEW_STATIC) set(TARGET_NAME openvr) setup_hifi_plugin(OpenGL Script Qml Widgets) - link_hifi_libraries(shared gl networking controllers - plugins display-plugins input-plugins script-engine + link_hifi_libraries(shared gl networking controllers + plugins display-plugins input-plugins script-engine render-utils model gpu render model-networking fbx) include_hifi_library_headers(octree) - + add_dependency_external_projects(OpenVR) find_package(OpenVR REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 7fb70180c4..96e1d1448e 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -115,6 +115,10 @@ void OpenVrDisplayPlugin::customizeContext() { glGetError(); }); WindowOpenGLDisplayPlugin::customizeContext(); + + enableVsync(false); + // Only enable mirroring if we know vsync is disabled + _enablePreview = !isVsyncEnabled(); } uvec2 OpenVrDisplayPlugin::getRecommendedRenderSize() const { @@ -156,6 +160,22 @@ void OpenVrDisplayPlugin::internalPresent() { // Flip y-axis since GL UV coords are backwards. static vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; + + // screen preview mirroring + if (_enablePreview) { + auto windowSize = toGlm(_window->size()); + if (_monoPreview) { + glViewport(0, 0, windowSize.x * 2, windowSize.y); + glScissor(0, windowSize.y, windowSize.x, windowSize.y); + } else { + glViewport(0, 0, windowSize.x, windowSize.y); + } + glBindTexture(GL_TEXTURE_2D, _currentSceneTexture); + GLenum err = glGetError(); + Q_ASSERT(0 == err); + drawUnitQuad(); + } + vr::Texture_t texture{ (void*)_currentSceneTexture, vr::API_OpenGL, vr::ColorSpace_Auto }; { Lock lock(_mutex); @@ -174,5 +194,9 @@ void OpenVrDisplayPlugin::internalPresent() { }); } + if (_enablePreview) { + swapBuffers(); + } + //WindowOpenGLDisplayPlugin::internalPresent(); } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index e290368de0..1dbf8c2b71 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -45,5 +45,7 @@ protected: private: vr::IVRSystem* _hmd { nullptr }; static const QString NAME; + bool _enablePreview { false }; + bool _monoPreview { true }; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 7020fcb40d..249643c73e 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -40,6 +40,8 @@ vr::IVRSystem* acquireOpenVrSystem() { qCDebug(displayplugins) << "openvr: incrementing refcount"; ++refCount; } + } else { + qCDebug(displayplugins) << "openvr: no hmd present"; } return activeHmd; } diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 0e3a0be72b..e1bda007bc 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -66,10 +66,12 @@ void ViveControllerManager::activate() { } Q_ASSERT(_hmd); + // OpenVR provides 3d mesh representations of the controllers + // Disabled controller rendering code + /* auto renderModels = vr::VRRenderModels(); vr::RenderModel_t model; - /* if (!_hmd->LoadRenderModel(CONTROLLER_MODEL_STRING, &model)) { qDebug() << QString("Unable to load render model %1\n").arg(CONTROLLER_MODEL_STRING); } else { @@ -145,6 +147,7 @@ void ViveControllerManager::deactivate() { void ViveControllerManager::updateRendering(RenderArgs* args, render::ScenePointer scene, render::PendingChanges pendingChanges) { PerformanceTimer perfTimer("ViveControllerManager::updateRendering"); + /* if (_modelLoaded) { //auto controllerPayload = new render::Payload(this); //auto controllerPayloadPointer = ViveControllerManager::PayloadPointer(controllerPayload); @@ -175,9 +178,11 @@ void ViveControllerManager::updateRendering(RenderArgs* args, render::ScenePoint } }); } + */ } void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& batch, int sign) { + /* auto userInputMapper = DependencyManager::get(); Transform transform(userInputMapper->getSensorToWorldMat()); transform.postTranslate(pose.getTranslation() + pose.getRotation() * glm::vec3(0, 0, CONTROLLER_LENGTH_OFFSET)); @@ -199,6 +204,7 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& // mesh->getVertexBuffer()._stride); batch.setIndexBuffer(gpu::UINT16, mesh->getIndexBuffer()._buffer, 0); batch.drawIndexed(gpu::TRIANGLES, mesh->getNumIndices(), 0); + */ } From b3b053ce18a8ed0ef31079045a36ab2a9d21ff51 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 09:52:29 -0800 Subject: [PATCH 39/73] if delta-time is more than 1/30th of a second, clamp it to avoid crazy ik induced orbits when main thread is blocked --- libraries/animation/src/AnimInverseKinematics.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 92d8240510..354b637fa2 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -18,6 +18,8 @@ #include "SwingTwistConstraint.h" #include "AnimationLogging.h" +float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay + AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { } @@ -367,6 +369,10 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + if (dt > MAX_OVERLAY_DT) { + dt = MAX_OVERLAY_DT; + } + if (_relativePoses.size() != underPoses.size()) { loadPoses(underPoses); } else { From 4d1c24abb79916f9d3c5e539051572c1d70d6f3c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 10 Feb 2016 10:02:20 -0800 Subject: [PATCH 40/73] Fix OverlayWindow loading issues --- interface/resources/qml/QmlWindow.qml | 44 ++++++++++++++++++---- interface/resources/qml/windows/Window.qml | 4 +- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 63efd0bc2e..d0f6edf9d1 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -19,12 +19,40 @@ Windows.Window { property var channel; // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false - property alias source: pageLoader.source - - Loader { - id: pageLoader - objectName: "Loader" - focus: true - property var dialog: root + property var source; + property var component; + property var dynamicContent; + onSourceChanged: { + if (dynamicContent) { + dynamicContent.destroy(); + dynamicContent = null; + } + component = Qt.createComponent(source); + console.log("Created component " + component + " from source " + source); } -} // dialog + + onComponentChanged: { + console.log("Component changed to " + component) + populate(); + } + + function populate() { + console.log("Populate called: dynamicContent " + dynamicContent + " component " + component); + if (!dynamicContent && component) { + if (component.status == Component.Error) { + console.log("Error loading component:", component.errorString()); + } else if (component.status == Component.Ready) { + console.log("Building dynamic content"); + dynamicContent = component.createObject(contentHolder); + } else { + console.log("Component not yet ready, connecting to status change"); + component.statusChanged.connect(populate); + } + } + } + + Item { + id: contentHolder + anchors.fill: parent + } +} diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index d7891da2ea..6088a7a0aa 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -17,8 +17,8 @@ Fadable { HifiConstants { id: hifi } // The Window size is the size of the content, while the frame // decorations can extend outside it. - implicitHeight: content.height - implicitWidth: content.width + implicitHeight: content ? content.height : 0 + implicitWidth: content ? content.width : 0 x: -1; y: -1 enabled: visible From 22c53fc395ef708300a6094587316a0b0bfd51d6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 10 Feb 2016 10:05:26 -0800 Subject: [PATCH 41/73] full voiceover vol --- .../DomainContent/CellScience/Scripts/showButtonToPlaySound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js index 429fd0b902..6651e435b4 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/showButtonToPlaySound.js @@ -47,7 +47,7 @@ stereo: true, loop: false, localOnly: true, - volume: 0.75 + volume: 1 }; self.sound = SoundCache.getSound(this.soundURL); From 9b467e94da0791663b53f75e4fbf3ff1ead77285 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 10 Feb 2016 10:30:06 -0800 Subject: [PATCH 42/73] constify and explicit override --- libraries/physics/src/ThreadSafeDynamicsWorld.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index de37554f56..e9708149da 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -40,14 +40,14 @@ public: int stepSimulationWithSubstepCallback(btScalar timeStep, int maxSubSteps = 1, btScalar fixedTimeStep = btScalar(1.)/btScalar(60.), SubStepCallback onSubStep = []() { }); - void synchronizeMotionStates(); + virtual void synchronizeMotionStates() override; // btDiscreteDynamicsWorld::m_localTime is the portion of real-time that has not yet been simulated // but is used for MotionState::setWorldTransform() extrapolation (a feature that Bullet uses to provide // smoother rendering of objects when the physics simulation loop is ansynchronous to the render loop). float getLocalTimeAccumulation() const { return m_localTime; } - VectorOfMotionStates& getChangedMotionStates() { return _changedMotionStates; } + const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() From 4bcb7b1ba90d1940a41bb0009d4fd6ab576375b9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 10 Feb 2016 10:30:50 -0800 Subject: [PATCH 43/73] more correct profiling --- libraries/physics/src/ThreadSafeDynamicsWorld.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index d06a9b8e07..b6f3487f1a 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -115,8 +115,8 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { } void ThreadSafeDynamicsWorld::synchronizeMotionStates() { - _changedMotionStates.clear(); BT_PROFILE("synchronizeMotionStates"); + _changedMotionStates.clear(); if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;i Date: Wed, 10 Feb 2016 10:31:38 -0800 Subject: [PATCH 44/73] use const reference for std:vector --- interface/src/Application.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index efc482a3b4..e0a6fd967c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3158,8 +3158,9 @@ void Application::update(float deltaTime) { PerformanceTimer perfTimer("havestChanges"); if (_physicsEngine->hasOutgoingChanges()) { getEntities()->getTree()->withWriteLock([&] { - _entitySimulation.handleOutgoingChanges(_physicsEngine->getOutgoingChanges(), Physics::getSessionUUID()); - avatarManager->handleOutgoingChanges(_physicsEngine->getOutgoingChanges()); + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); + _entitySimulation.handleOutgoingChanges(outgoingChanges, Physics::getSessionUUID()); + avatarManager->handleOutgoingChanges(outgoingChanges); }); auto collisionEvents = _physicsEngine->getCollisionEvents(); From 84fb983da763d237e1b01323d5935b8597df240d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 10 Feb 2016 10:35:33 -0800 Subject: [PATCH 45/73] don't forget to remove from _outgoingChanges --- libraries/physics/src/PhysicalEntitySimulation.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 71c78b8b86..aaa706b370 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -169,6 +169,7 @@ void PhysicalEntitySimulation::getObjectsToRemoveFromPhysics(VectorOfMotionState EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); assert(motionState); _pendingChanges.remove(motionState); + _outgoingChanges.remove(motionState); _physicalObjects.remove(motionState); result.push_back(motionState); _entitiesToRelease.insert(entity); From a9fc69b4ac6174150f4de197a3b2e6820549b29b Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 10 Feb 2016 10:49:13 -0800 Subject: [PATCH 46/73] cleanup percentage calculation for AM mix stats --- assignment-client/src/audio/AudioMixer.cpp | 14 +++++++++----- assignment-client/src/audio/AudioMixer.h | 2 ++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index ac78298573..47f9204d36 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -489,6 +489,10 @@ void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { } } +float AudioMixer::percentageForMixStats(int counter) { + return (_totalMixes > 0) ? (float(counter) / _totalMixes) * 100.0f : 0; +} + void AudioMixer::sendStatsPacket() { static QJsonObject statsObject; @@ -499,11 +503,11 @@ void AudioMixer::sendStatsPacket() { statsObject["avg_listeners_per_frame"] = (float) _sumListeners / (float) _numStatFrames; QJsonObject mixStats; - mixStats["%_hrtf_mixes"] = (_totalMixes > 0) ? (_hrtfRenders / _totalMixes) * 100.0f : 0; - mixStats["%_hrtf_silent_mixes"] = (_totalMixes > 0) ? (_hrtfSilentRenders / _totalMixes) * 100.0f : 0; - mixStats["%_hrtf_struggle_mixes"] = (_totalMixes > 0) ? (_hrtfStruggleRenders / _totalMixes) * 100.0f : 0; - mixStats["%_manual_stereo_mixes"] = (_totalMixes > 0) ? (_manualStereoMixes / _totalMixes) * 100.0f : 0; - mixStats["%_manual_echo_mixes"] = (_totalMixes > 0) ? (_manualEchoMixes / _totalMixes) * 100.0f : 0; + mixStats["%_hrtf_mixes"] = percentageForMixStats(_hrtfRenders); + mixStats["%_hrtf_silent_mixes"] = percentageForMixStats(_hrtfSilentRenders); + mixStats["%_hrtf_struggle_mixes"] = percentageForMixStats(_hrtfStruggleRenders); + mixStats["%_manual_stereo_mixes"] = percentageForMixStats(_manualStereoMixes); + mixStats["%_manual_echo_mixes"] = percentageForMixStats(_manualEchoMixes); mixStats["total_mixes"] = _totalMixes; mixStats["avg_mixes_per_block"] = _totalMixes / _numStatFrames; diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 1098840180..ee9e5b4eb9 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -71,6 +71,8 @@ private: void perSecondActions(); + float percentageForMixStats(int counter); + bool shouldMute(float quietestFrame); void parseSettingsObject(const QJsonObject& settingsObject); From 90f0821c2b1a06fc1aa2e00b30d4e87756cb574b Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 10 Feb 2016 11:14:26 -0800 Subject: [PATCH 47/73] OpenVR: reduce thread contention, fix sensor reset --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 58 +++++++++++++--------- plugins/openvr/src/OpenVrDisplayPlugin.h | 1 + 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 96e1d1448e..3f159eb916 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -32,12 +32,15 @@ const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here static vr::IVRCompositor* _compositor{ nullptr }; +static vr::TrackedDevicePose_t _presentThreadTrackedDevicePose[vr::k_unMaxTrackedDeviceCount]; vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; static mat4 _sensorResetMat; static uvec2 _windowSize; static uvec2 _renderTargetSize; + + struct PerEyeData { //uvec2 _viewportOrigin; //uvec2 _viewportSize; @@ -87,11 +90,16 @@ void OpenVrDisplayPlugin::activate() { // Recommended render target size is per-eye, so double the X size for // left + right eyes _renderTargetSize.x *= 2; - openvr_for_each_eye([&](vr::Hmd_Eye eye) { - PerEyeData& eyeData = _eyesData[eye]; - eyeData._projectionMatrix = toGlm(_hmd->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); - eyeData._eyeOffset = toGlm(_hmd->GetEyeToHeadTransform(eye)); - }); + + { + Lock lock(_poseMutex); + openvr_for_each_eye([&](vr::Hmd_Eye eye) { + PerEyeData& eyeData = _eyesData[eye]; + eyeData._projectionMatrix = toGlm(_hmd->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); + eyeData._eyeOffset = toGlm(_hmd->GetEyeToHeadTransform(eye)); + }); + } + _compositor = vr::VRCompositor(); Q_ASSERT(_compositor); WindowOpenGLDisplayPlugin::activate(); @@ -130,25 +138,24 @@ mat4 OpenVrDisplayPlugin::getProjection(Eye eye, const mat4& baseProjection) con if (eye == Mono) { eye = Left; } + Lock lock(_poseMutex); return _eyesData[eye]._projectionMatrix; } void OpenVrDisplayPlugin::resetSensors() { - _sensorResetMat = glm::inverse(cancelOutRollAndPitch(_trackedDevicePoseMat4[0])); + Lock lock(_poseMutex); + glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); } glm::mat4 OpenVrDisplayPlugin::getEyeToHeadTransform(Eye eye) const { + Lock lock(_poseMutex); return _eyesData[eye]._eyeOffset; } glm::mat4 OpenVrDisplayPlugin::getHeadPose(uint32_t frameIndex) const { - glm::mat4 result; - { - Lock lock(_mutex); - result = _trackedDevicePoseMat4[0]; - - } - return result; + Lock lock(_poseMutex); + return _trackedDevicePoseMat4[0]; } @@ -177,16 +184,23 @@ void OpenVrDisplayPlugin::internalPresent() { } vr::Texture_t texture{ (void*)_currentSceneTexture, vr::API_OpenGL, vr::ColorSpace_Auto }; - { - Lock lock(_mutex); - _compositor->Submit(vr::Eye_Left, &texture, &leftBounds); - _compositor->Submit(vr::Eye_Right, &texture, &rightBounds); - } + + _compositor->Submit(vr::Eye_Left, &texture, &leftBounds); + _compositor->Submit(vr::Eye_Right, &texture, &rightBounds); + glFinish(); + + if (_enablePreview) { + swapBuffers(); + } + + _compositor->WaitGetPoses(_presentThreadTrackedDevicePose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + { - Lock lock(_mutex); - _compositor->WaitGetPoses(_trackedDevicePose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + // copy and process _presentThreadTrackedDevicePoses + Lock lock(_poseMutex); for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + _trackedDevicePose[i] = _presentThreadTrackedDevicePose[i]; _trackedDevicePoseMat4[i] = _sensorResetMat * toGlm(_trackedDevicePose[i].mDeviceToAbsoluteTracking); } openvr_for_each_eye([&](vr::Hmd_Eye eye) { @@ -194,9 +208,5 @@ void OpenVrDisplayPlugin::internalPresent() { }); } - if (_enablePreview) { - swapBuffers(); - } - //WindowOpenGLDisplayPlugin::internalPresent(); } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 1dbf8c2b71..ac72b0908b 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -47,5 +47,6 @@ private: static const QString NAME; bool _enablePreview { false }; bool _monoPreview { true }; + mutable Mutex _poseMutex; }; From 6fea0b9396762256dca07b69de02b6f22ea74a2b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 11:20:56 -0800 Subject: [PATCH 48/73] also make sure tau is >= 1.0 --- libraries/animation/src/AnimInverseKinematics.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 354b637fa2..79bf5d9a19 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -18,8 +18,6 @@ #include "SwingTwistConstraint.h" #include "AnimationLogging.h" -float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay - AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { } @@ -369,6 +367,7 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + const float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay if (dt > MAX_OVERLAY_DT) { dt = MAX_OVERLAY_DT; } @@ -469,7 +468,8 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars // smooth transitions by relaxing _hipsOffset toward the new value const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f; - _hipsOffset += (newHipsOffset - _hipsOffset) * (dt / HIPS_OFFSET_SLAVE_TIMESCALE); + float tau = dt > HIPS_OFFSET_SLAVE_TIMESCALE ? 1.0 : dt / HIPS_OFFSET_SLAVE_TIMESCALE; + _hipsOffset += (newHipsOffset - _hipsOffset) * tau; } } return _relativePoses; From a4f2fc58fdc46185556032059e62c7c45b8cbc57 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 10 Feb 2016 11:25:49 -0800 Subject: [PATCH 49/73] Ensure the QML content gets destroyed on deletion of a QML surface --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 3a72d98402..42eea08057 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -313,7 +313,7 @@ OffscreenQmlSurface::OffscreenQmlSurface() { OffscreenQmlSurface::~OffscreenQmlSurface() { _renderer->stop(); - + delete _rootItem; delete _renderer; delete _qmlComponent; delete _qmlEngine; From 8598360f2ea7dbcc105783a78e5987f720d8bb31 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 10 Feb 2016 11:50:23 -0800 Subject: [PATCH 50/73] allow string values to be graphable in DS stats --- assignment-client/src/audio/AudioMixer.cpp | 9 +++++++-- assignment-client/src/audio/AudioMixer.h | 2 +- domain-server/resources/web/css/style.css | 6 ++++++ domain-server/resources/web/header.html | 3 ++- domain-server/resources/web/stats/js/stats.js | 16 +++++++++++++--- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 47f9204d36..242b0b0d7a 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -489,8 +489,13 @@ void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { } } -float AudioMixer::percentageForMixStats(int counter) { - return (_totalMixes > 0) ? (float(counter) / _totalMixes) * 100.0f : 0; +QString AudioMixer::percentageForMixStats(int counter) { + if (_totalMixes > 0) { + float mixPercentage = (float(counter) / _totalMixes) * 100.0f; + return QString::number(mixPercentage, 'f', 2); + } else { + return QString("0.0"); + } } void AudioMixer::sendStatsPacket() { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index ee9e5b4eb9..0407c2860c 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -71,7 +71,7 @@ private: void perSecondActions(); - float percentageForMixStats(int counter); + QString percentageForMixStats(int counter); bool shouldMute(float quietestFrame); diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 1ed2594c11..efb9e907c5 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -92,6 +92,12 @@ tr.new-row { background-color: #dff0d8; } +.graphable-stat { + text-align: center; + color: #5286BC; + cursor: pointer; +} + .highchart-modal .modal-dialog { width: 650px; } diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index f3cacf4b21..b4eee406f2 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -5,10 +5,11 @@ + - + diff --git a/domain-server/resources/web/stats/js/stats.js b/domain-server/resources/web/stats/js/stats.js index 3fed1ad29f..6ea233a6e6 100644 --- a/domain-server/resources/web/stats/js/stats.js +++ b/domain-server/resources/web/stats/js/stats.js @@ -1,5 +1,5 @@ $(document).ready(function(){ - + var currentHighchart; // setup a function to grab the nodeStats @@ -17,12 +17,22 @@ $(document).ready(function(){ var stats = JsonHuman.format(json); $('#stats-container').html(stats); + + // add the clickable class to anything that looks like a number + $('.jh-value span').each(function(val){ + console.log(val); + if (!isNaN($(this).text())) { + // this looks like a number - give it the clickable class so we can get graphs for it + $(this).addClass('graphable-stat'); + } + }); + if (currentHighchart) { // get the current time to set with the point var x = (new Date()).getTime(); // get the last value using underscore-keypath - var y = _(json).valueForKeyPath(graphKeypath); + var y = Number(_(json).valueForKeyPath(graphKeypath)); // start shifting the chart once we hit 20 data points var shift = currentHighchart.series[0].data.length > 20; @@ -91,7 +101,7 @@ $(document).ready(function(){ } // handle clicks on numerical values - this lets the user show a line graph in a modal - $('#stats-container').on('click', '.jh-type-number', function(){ + $('#stats-container').on('click', '.graphable-stat', function(){ graphKeypath = $(this).data('keypath'); // setup the new graph modal From 4241f4b30537dc48893298fdbee1ce7fdabe756a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 10 Feb 2016 12:03:07 -0800 Subject: [PATCH 51/73] Convert all simple one-liners containing ') { ' to instead take up three lines. --- examples/entityScripts/tribble.js | 20 +++++++--- examples/libraries/virtualBaton.js | 40 +++++++++++++++----- tests/mocha/test/testVirtualBaton.js | 56 +++++++++++++++++++++------- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/examples/entityScripts/tribble.js b/examples/entityScripts/tribble.js index 3afdcc43e4..22990af1d1 100644 --- a/examples/entityScripts/tribble.js +++ b/examples/entityScripts/tribble.js @@ -29,13 +29,19 @@ accumulated = 0; } } - function randomCentered() { return Math.random() - 0.5; } - function randomVector() { return {x: randomCentered() * dimensions.x, y: randomCentered() * dimensions.y, z: randomCentered() * dimensions.z}; } + function randomCentered() { + return Math.random() - 0.5; + } + function randomVector() { + return {x: randomCentered() * dimensions.x, y: randomCentered() * dimensions.y, z: randomCentered() * dimensions.z}; + } function move() { var newData = {velocity: Vec3.sum({x: 0, y: 1, z: 0}, randomVector()), angularVelocity: Vec3.multiply(Math.PI, randomVector())}; var nextChange = Math.ceil(Math.random() * 2000 / moveRate); Entities.editEntity(entityID, newData); - if (!shutdown) { Script.setTimeout(move, nextChange); } + if (!shutdown) { + Script.setTimeout(move, nextChange); + } } function startUpdate() { print('startUpdate', entityID); @@ -44,7 +50,9 @@ } function stopUpdate() { print('stopUpdate', entityID, hasUpdate); - if (!hasUpdate) { return; } + if (!hasUpdate) { + return; + } hasUpdate = false; Script.update.disconnect(update); } @@ -79,7 +87,9 @@ if (moveTimeout) { Script.setTimeout(move, 1000); if (moveTimeout > 0) { - Script.setTimeout(function () { shutdown = true; }, moveTimeout * 1000); + Script.setTimeout(function () { + shutdown = true; + }, moveTimeout * 1000); } } }; diff --git a/examples/libraries/virtualBaton.js b/examples/libraries/virtualBaton.js index b1f813f764..dad31a7b1e 100644 --- a/examples/libraries/virtualBaton.js +++ b/examples/libraries/virtualBaton.js @@ -60,7 +60,9 @@ function virtualBatonf(options) { // order) of both.) connectionTest = options.connectionTest || function connectionTest(id) { var idLength = 38; - if (id.length === idLength) { return exports.validId(id); } + if (id.length === idLength) { + return exports.validId(id); + } return (id.length === 2 * idLength) && exports.validId(id.slice(0, idLength)) && exports.validId(id.slice(idLength)); }; @@ -70,7 +72,9 @@ function virtualBatonf(options) { // Truthy if id exists as either a connected avatar or valid entity. exports.validId = function validId(id) { var avatar = avatarList.getAvatar(id); - if (avatar && (avatar.sessionUUID === id)) { return true; } + if (avatar && (avatar.sessionUUID === id)) { + return true; + } var properties = entities.getEntityProperties(id, ['type']); return properties && properties.type; }; @@ -80,13 +84,19 @@ function virtualBatonf(options) { log.apply(null, [].map.call(arguments, JSON.stringify)); } function debugFlow() { - if (options.debugFlow) { debug.apply(null, arguments); } + if (options.debugFlow) { + debug.apply(null, arguments); + } } function debugSend(destination, operation, data) { - if (options.debugSend) { debug('baton:', batonName, instanceId, 's=>', destination, operation, data); } + if (options.debugSend) { + debug('baton:', batonName, instanceId, 's=>', destination, operation, data); + } } function debugReceive(senderID, operation, data) { // senderID is client sessionUUID -- not necessarily instanceID! - if (options.debugReceive) { debug('baton:', batonName, senderID, '=>r', instanceId, operation, data); } + if (options.debugReceive) { + debug('baton:', batonName, senderID, '=>r', instanceId, operation, data); + } } // Messages: Just synactic sugar for hooking things up to Messages system. @@ -95,7 +105,9 @@ function virtualBatonf(options) { var channelKey = "io.highfidelity.virtualBaton:" + batonName, subchannelHandlers = {}, // Message channel string => {receiver, op} subchannelKeys = {}; // operation => Message channel string - function subchannelKey(operation) { return channelKey + ':' + operation; } + function subchannelKey(operation) { + return channelKey + ':' + operation; + } function receive(operation, handler) { // Record a handler for an operation on our channelKey var subKey = subchannelKey(operation); subchannelHandlers[subKey] = {receiver: handler, op: operation}; @@ -116,7 +128,9 @@ function virtualBatonf(options) { } function messageHandler(channel, messageString, senderID) { var handler = subchannelHandlers[channel]; - if (!handler) { return; } + if (!handler) { + return; + } var data = JSON.parse(messageString); debugReceive(senderID, handler.op, data); handler.receiver(data); @@ -215,7 +229,9 @@ function virtualBatonf(options) { }); // Paxos Acceptor behavior var bestProposal = {number: 0}, accepted = {}; - function acceptedId() { return accepted && accepted.winner; } + function acceptedId() { + return accepted && accepted.winner; + } receive('prepare!', function (data) { var response = {proposalNumber: data.number, proposerId: data.proposerId}; if (betterNumber(data, bestProposal)) { @@ -256,7 +272,9 @@ function virtualBatonf(options) { function localRelease() { var callback = releaseCallback; debugFlow('baton:', batonName, 'localRelease', 'callback:', !!releaseCallback); - if (!releaseCallback) { return; } // Already released, but we might still receive a stale message. That's ok. + if (!releaseCallback) { + return; + } // Already released, but we might still receive a stale message. That's ok. releaseCallback = undefined; callback(batonName); // Pass batonName so that clients may use the same handler for different batons. } @@ -340,7 +358,9 @@ function virtualBatonf(options) { exports.unload = function unload() { // Disconnect from everything. messages.messageReceived.disconnect(messageHandler); timers.clearInterval(exports.recheckWatchdog); - if (electionWatchdog) { timers.clearTimeout(electionWatchdog); } + if (electionWatchdog) { + timers.clearTimeout(electionWatchdog); + } electionWatchdog = claimCallback = releaseCallback = null; Object.keys(subchannelHandlers).forEach(messages.unsubscribe); debugFlow('baton:', batonName, instanceId, 'unload'); diff --git a/tests/mocha/test/testVirtualBaton.js b/tests/mocha/test/testVirtualBaton.js index c661bfeffc..2a4edb4d5d 100644 --- a/tests/mocha/test/testVirtualBaton.js +++ b/tests/mocha/test/testVirtualBaton.js @@ -7,11 +7,16 @@ var virtualBaton = require('../../../examples/libraries/virtualBaton.js'); describe('temp', function () { var messageCount = 0, testStart = Date.now(); function makeMessager(nodes, me, mode) { // shim for High Fidelity Message system - function noopSend(channel, string, source) { } - function hasChannel(node, channel) { return -1 !== node.subscribed.indexOf(channel); } + function noopSend(channel, string, source) { + } + function hasChannel(node, channel) { + return -1 !== node.subscribed.indexOf(channel); + } function sendSync(channel, message, nodes, skip) { nodes.forEach(function (node) { - if (!hasChannel(node, channel) || (node === skip)) { return; } + if (!hasChannel(node, channel) || (node === skip)) { + return; + } node.sender(channel, message, me.name); }); } @@ -22,7 +27,11 @@ describe('temp', function () { return { subscriberCount: function () { var c = 0; - nodes.forEach(function (n) { if (n.subscribed.length) { c++; } }); + nodes.forEach(function (n) { + if (n.subscribed.length) { + c++; + } + }); return c; }, subscribe: function (channel) { @@ -32,7 +41,9 @@ describe('temp', function () { me.subscribed.splice(me.subscribed.indexOf(channel), 1); }, sendMessage: function (channel, message) { - if ((mode === 'immediate2Me') && hasChannel(me, channel)) { me.sender(channel, message, me.name); } + if ((mode === 'immediate2Me') && hasChannel(me, channel)) { + me.sender(channel, message, me.name); + } if (mode === 'immediate') { sendSync(channel, message, nodes, null); } else { @@ -43,7 +54,9 @@ describe('temp', function () { }, messageReceived: { connect: function (f) { - me.sender = function (c, m, i) { messageCount++; f(c, m, i); }; + me.sender = function (c, m, i) { + messageCount++; f(c, m, i); + }; }, disconnect: function () { me.sender = noopSend; @@ -60,7 +73,9 @@ describe('temp', function () { debugReceive: debug.receive, debugFlow: debug.flow, useOptimizations: optimize, - connectionTest: function (id) { return baton.validId(id); }, + connectionTest: function (id) { + return baton.validId(id); + }, globals: { Messages: makeMessager(nodes, node, mode), MyAvatar: {sessionUUID: node.name}, @@ -71,17 +86,24 @@ describe('temp', function () { clearInterval: clearInterval }, AvatarList: { - getAvatar: function (id) { return {sessionUUID: id}; } + getAvatar: function (id) { + return {sessionUUID: id}; + } }, - Entities: {getEntityProperties: function () { }}, + Entities: {getEntityProperties: function () { + }}, print: console.log } }); return baton; } - function noRelease(batonName) { assert.ok(!batonName, "should not release"); } + function noRelease(batonName) { + assert.ok(!batonName, "should not release"); + } function defineABunch(mode, optimize) { - function makeKey(prefix) { return prefix + mode + (optimize ? '-opt' : ''); } + function makeKey(prefix) { + return prefix + mode + (optimize ? '-opt' : ''); + } var testKeys = makeKey('single-'); it(testKeys, function (done) { var nodes = [{name: 'a'}]; @@ -137,17 +159,23 @@ describe('temp', function () { console.log('claimed al', key); assert.ok(!gotB, "should not get A after B"); gotA = true; - if (!gotB) { done(); } + if (!gotB) { + done(); + } }, function () { assert.ok(gotA, "Should claim it first"); releaseA = true; - if (gotB) { done(); } + if (gotB) { + done(); + } }); setTimeout(function () { makeBaton(testKeydsl, nodes, nodes[1], debug, mode, optimize).claim(function (key) { console.log('claimed bl', key); gotB = true; - if (releaseA) { done(); } + if (releaseA) { + done(); + } }, noRelease); }, 3000); }); From af6bb50f74e4667a0e7e35209544a02097ebf4a1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 12:09:28 -0800 Subject: [PATCH 52/73] fix warning --- libraries/animation/src/AnimInverseKinematics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 79bf5d9a19..e3271037e0 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -468,7 +468,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars // smooth transitions by relaxing _hipsOffset toward the new value const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f; - float tau = dt > HIPS_OFFSET_SLAVE_TIMESCALE ? 1.0 : dt / HIPS_OFFSET_SLAVE_TIMESCALE; + float tau = dt > HIPS_OFFSET_SLAVE_TIMESCALE ? 1.0f : dt / HIPS_OFFSET_SLAVE_TIMESCALE; _hipsOffset += (newHipsOffset - _hipsOffset) * tau; } } From 31d72ffb8d86ba6b5f2971e3c121529d4b9a2fce Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 10 Feb 2016 12:17:58 -0800 Subject: [PATCH 53/73] 2 is magic. --- examples/libraries/virtualBaton.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/libraries/virtualBaton.js b/examples/libraries/virtualBaton.js index dad31a7b1e..13d9abe9e7 100644 --- a/examples/libraries/virtualBaton.js +++ b/examples/libraries/virtualBaton.js @@ -29,7 +29,10 @@ function virtualBatonf(options) { // Answer averages (number +/- variability). Avoids having everyone act in lockstep. function randomize(number, variability) { var allowedDeviation = number * variability; - return number - allowedDeviation + (Math.random() * 2 * allowedDeviation); + var theNumberThatIsTwice = 2; + // random() is (0, 1], averages 0.5. The average of twice that is 1. + var randomDeviation = Math.random() * theNumberThatIsTwice * allowedDeviation; + return number - allowedDeviation + randomDeviation; } // Allow testing outside in a harness outside of High Fidelity. // See sourceCodeSandbox/tests/mocha/test/testVirtualBaton.js From 9a34b68bffc678c08fc327bcc2f47fd771f62025 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 10 Feb 2016 12:50:11 -0800 Subject: [PATCH 54/73] add ownership hack to own and handoff movement --- .../CellScience/Scripts/moveRandomly.js | 176 ++++++++++++++---- .../CellScience/importCellScience.js | 2 +- 2 files changed, 139 insertions(+), 39 deletions(-) diff --git a/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js b/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js index 7c880a44b6..a975f74733 100644 --- a/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js +++ b/unpublishedScripts/DomainContent/CellScience/Scripts/moveRandomly.js @@ -9,7 +9,8 @@ var self = this; this.preload = function(entityId) { - + //print('preload move randomly') + this.isConnected = false; this.entityId = entityId; this.updateInterval = 100; this.posFrame = 0; @@ -21,62 +22,161 @@ this.minAngularVelocity = 0.01; this.maxAngularVelocity = 0.03; + this.initialize(entityId); + this.initTimeout = null; + + + var userData = { + ownershipKey: { + owner: MyAvatar.sessionUUID + }, + grabbableKey: { + grabbable: false + } + }; + + Entities.editEntity(entityId, { + userData: JSON.stringify(userData) + }) + } + + this.initialize = function(entityId) { + //print('move randomly should initialize' + entityId) + var properties = Entities.getEntityProperties(entityId); + if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { + self.initTimeout = Script.setTimeout(function() { + //print('no user data yet, try again in one second') + self.initialize(entityId); + }, 1000) + + } else { + //print('userdata before parse attempt' + properties.userData) + self.userData = null; + try { + self.userData = JSON.parse(properties.userData); + } catch (err) { + //print('error parsing json'); + //print('properties are:' + properties.userData); + return; + } + Script.update.connect(self.update); + this.isConnected = true; + } } this.update = function(deltaTime) { + // print('jbp in update') + var data = Entities.getEntityProperties(self.entityId, 'userData').userData; + var userData; + try { + userData = JSON.parse(data) + } catch (e) { + //print('error parsing json' + data) + return; + }; - self.posFrame++; - self.rotFrame++; - - if (self.posFrame > self.posInterval) { - - self.posInterval = 100 * Math.random() + 300; - self.posFrame = 0; - - var magnitudeV = self.maxVelocity; - var directionV = { - x: Math.random() - 0.5, - y: Math.random() - 0.5, - z: Math.random() - 0.5 - }; - - //print("POS magnitude is " + magnitudeV + " and direction is " + directionV.x); - Entities.editEntity(self.entityId, { - velocity: Vec3.multiply(magnitudeV, Vec3.normalize(directionV)) - - }); - + // print('userdata is' + data) + //if the entity doesnt have an owner set yet + if (userData.hasOwnProperty('ownershipKey') !== true) { + //print('no movement owner yet') + return; } - if (self.rotFrame > self.rotInterval) { + //print('owner is:::' + userData.ownershipKey.owner) + //get all the avatars to see if the owner is around + var avatars = AvatarList.getAvatarIdentifiers(); + var ownerIsAround = false; - self.rotInterval = 100 * Math.random() + 250; - self.rotFrame = 0; + //if the current owner is not me... + if (userData.ownershipKey.owner !== MyAvatar.sessionUUID) { - var magnitudeAV = self.maxAngularVelocity; + //look to see if the current owner is around anymore + for (var i = 0; i < avatars.length; i++) { + if (avatars[i] === userData.ownershipKey.owner) { + ownerIsAround = true + //the owner is around + return; + }; + } - var directionAV = { - x: Math.random() - 0.5, - y: Math.random() - 0.5, - z: Math.random() - 0.5 - }; - //print("ROT magnitude is " + magnitudeAV + " and direction is " + directionAV.x); - Entities.editEntity(self.entityId, { - angularVelocity: Vec3.multiply(magnitudeAV, Vec3.normalize(directionAV)) + //if the owner is not around, then take ownership + if (ownerIsAround === false) { + //print('taking ownership') - }); + var userData = { + ownershipKey: { + owner: MyAvatar.sessionUUID + }, + grabbableKey: { + grabbable: false + } + }; + Entities.editEntity(self.entityId, { + userData: JSON.stringify(data) + }) + } + } + //but if the current owner IS me, then move it + else { + //print('jbp im the owner so move it') + self.posFrame++; + self.rotFrame++; + + if (self.posFrame > self.posInterval) { + + self.posInterval = 100 * Math.random() + 300; + self.posFrame = 0; + + var magnitudeV = self.maxVelocity; + var directionV = { + x: Math.random() - 0.5, + y: Math.random() - 0.5, + z: Math.random() - 0.5 + }; + + //print("POS magnitude is " + magnitudeV + " and direction is " + directionV.x); + Entities.editEntity(self.entityId, { + velocity: Vec3.multiply(magnitudeV, Vec3.normalize(directionV)) + + }); + + } + + if (self.rotFrame > self.rotInterval) { + + self.rotInterval = 100 * Math.random() + 250; + self.rotFrame = 0; + + var magnitudeAV = self.maxAngularVelocity; + + var directionAV = { + x: Math.random() - 0.5, + y: Math.random() - 0.5, + z: Math.random() - 0.5 + }; + //print("ROT magnitude is " + magnitudeAV + " and direction is " + directionAV.x); + Entities.editEntity(self.entityId, { + angularVelocity: Vec3.multiply(magnitudeAV, Vec3.normalize(directionAV)) + + }); + + } } - } this.unload = function() { + if (this.initTimeout !== null) { + Script.clearTimeout(this.initTimeout); + } - Script.update.disconnect(this.update); + if (this.isConnected === true) { + Script.update.disconnect(this.update); + } } - Script.update.connect(this.update); + }) \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/CellScience/importCellScience.js b/unpublishedScripts/DomainContent/CellScience/importCellScience.js index 3885a29b89..ca0eb21f21 100644 --- a/unpublishedScripts/DomainContent/CellScience/importCellScience.js +++ b/unpublishedScripts/DomainContent/CellScience/importCellScience.js @@ -5,7 +5,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var version = 1029; +var version = 1035; var cellLayout; var baseLocation = "https://hifi-content.s3.amazonaws.com/DomainContent/CellScience/"; From 989192e9fd1c4bd9d0875968b0e34408a4297d4c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 10 Feb 2016 12:53:05 -0800 Subject: [PATCH 55/73] Make randomize clearer. --- examples/libraries/virtualBaton.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/libraries/virtualBaton.js b/examples/libraries/virtualBaton.js index 13d9abe9e7..63f96a5c1e 100644 --- a/examples/libraries/virtualBaton.js +++ b/examples/libraries/virtualBaton.js @@ -28,11 +28,11 @@ function virtualBatonf(options) { // Answer averages (number +/- variability). Avoids having everyone act in lockstep. function randomize(number, variability) { - var allowedDeviation = number * variability; - var theNumberThatIsTwice = 2; - // random() is (0, 1], averages 0.5. The average of twice that is 1. - var randomDeviation = Math.random() * theNumberThatIsTwice * allowedDeviation; - return number - allowedDeviation + randomDeviation; + var allowedDeviation = number * variability; // one side of the deviation range + var allowedDeviationRange = allowedDeviation * 2; // total range for +/- deviation + var randomDeviation = Math.random() * allowedDeviationRange; + var result = number - allowedDeviation + randomDeviation; + return result; } // Allow testing outside in a harness outside of High Fidelity. // See sourceCodeSandbox/tests/mocha/test/testVirtualBaton.js From 49f2724608370d86c62ba6415079c4ad80e450cd Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 13:49:03 -0800 Subject: [PATCH 56/73] don't return void --- libraries/entities/src/EntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64d33ceac5..f496eaced4 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1482,7 +1482,7 @@ void EntityItem::updateAngularVelocityFromNetwork(const glm::vec3& value) { if (shouldSuppressLocationEdits()) { return; } - return updateAngularVelocity(value); + updateAngularVelocity(value); } void EntityItem::updateAngularDamping(float value) { From 11d296a4777e4fe9ffba354ddbf2af67d2888437 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 14:08:35 -0800 Subject: [PATCH 57/73] if we are told to clear the octree, disable physics until we have (re)loaded entities --- interface/src/Application.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index efc482a3b4..83f1d78dda 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3877,6 +3877,8 @@ void Application::clearDomainOctreeDetails() { qCDebug(interfaceapp) << "Clearing domain octree details..."; // reset the environment so that we don't erroneously end up with multiple + _physicsEnabled = false; + // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { _entityServerJurisdictions.clear(); From 97cd65cf80b26361bbef7380d945710ad76d4fbb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 10 Feb 2016 15:07:45 -0800 Subject: [PATCH 58/73] don't use usecTimestampNow for per second actions --- assignment-client/src/audio/AudioMixer.cpp | 8 ++++---- assignment-client/src/audio/AudioMixer.h | 2 -- libraries/audio/src/AudioConstants.h | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 242b0b0d7a..51bcece15f 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -82,7 +82,6 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : _performanceThrottlingRatio(0.0f), _attenuationPerDoublingInDistance(DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE), _noiseMutingThreshold(DEFAULT_NOISE_MUTING_THRESHOLD), - _lastPerSecondCallbackTime(usecTimestampNow()), _sendAudioStreamStats(false), _datagramsReadPerCallStats(0, READ_DATAGRAMS_STATS_WINDOW_SECONDS), _timeSpentPerCallStats(0, READ_DATAGRAMS_STATS_WINDOW_SECONDS), @@ -689,10 +688,11 @@ void AudioMixer::broadcastMixes() { ++framesSinceCutoffEvent; } - quint64 now = usecTimestampNow(); - if (now - _lastPerSecondCallbackTime > USECS_PER_SECOND) { + static const int FRAMES_PER_SECOND = int(ceilf(1.0f / AudioConstants::NETWORK_FRAME_SECS)); + + // check if it has been approximately one second since our last call to perSecondActions + if (nextFrame % FRAMES_PER_SECOND == 0) { perSecondActions(); - _lastPerSecondCallbackTime = now; } nodeList->eachNode([&](const SharedNodePointer& node) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 0407c2860c..3a8dfad348 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -113,8 +113,6 @@ private: static bool _printStreamStats; static bool _enableFilter; - quint64 _lastPerSecondCallbackTime; - bool _sendAudioStreamStats; // stats diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index d5fd3f15e7..67a3f8c6ff 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -27,8 +27,8 @@ namespace AudioConstants { const int NETWORK_FRAME_SAMPLES_STEREO = NETWORK_FRAME_BYTES_STEREO / sizeof(AudioSample); const int NETWORK_FRAME_BYTES_PER_CHANNEL = 512; const int NETWORK_FRAME_SAMPLES_PER_CHANNEL = NETWORK_FRAME_BYTES_PER_CHANNEL / sizeof(AudioSample); - const float NETWORK_FRAME_MSECS = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL - / (float)AudioConstants::SAMPLE_RATE) * 1000.0f; + const float NETWORK_FRAME_SECS = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL / float(AudioConstants::SAMPLE_RATE)); + const float NETWORK_FRAME_MSECS = NETWORK_FRAME_SECS * 1000.0f; // be careful with overflows when using this constant const int NETWORK_FRAME_USECS = static_cast(NETWORK_FRAME_MSECS * 1000.0f); From 811859e46fb1f83f4c5b4d10b7b749a837b3f6c4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 10 Feb 2016 15:26:01 -0800 Subject: [PATCH 59/73] Update domain server to store temporary AC scripts in memory --- domain-server/src/DomainServer.cpp | 32 ++++++++++-------------------- domain-server/src/DomainServer.h | 2 ++ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 64b4f123f5..d1cb9d4e4a 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -55,6 +55,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _oauthProviderURL(), _oauthClientID(), _hostname(), + _ephemeralACScripts(), _webAuthenticationStateSet(), _cookieSessionHash(), _automaticNetworkingSetting(), @@ -1211,13 +1212,14 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it // via correct URL for the script so the client can download - QFile scriptFile(pathForAssignmentScript(matchingAssignment->getUUID())); + const auto it = _ephemeralACScripts.find(matchingAssignment->getUUID()); - if (scriptFile.exists() && scriptFile.open(QIODevice::ReadOnly)) { - connection->respond(HTTPConnection::StatusCode200, scriptFile.readAll(), "application/javascript"); + if (it != _ephemeralACScripts.end()) { + connection->respond(HTTPConnection::StatusCode200, it->second, "application/javascript"); } else { connection->respond(HTTPConnection::StatusCode404, "Resource not found."); } + return true; } @@ -1387,29 +1389,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url for (int i = 0; i < numInstances; i++) { - // create an assignment for this saved script Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); - QString newPath = pathForAssignmentScript(scriptAssignment->getUUID()); + _ephemeralACScripts[scriptAssignment->getUUID()] = formData[0].second; - // create a file with the GUID of the assignment in the script host location - QFile scriptFile(newPath); - if (scriptFile.open(QIODevice::WriteOnly)) { - scriptFile.write(formData[0].second); - - qDebug() << qPrintable(QString("Saved a script for assignment at %1%2") - .arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool)); - - // add the script assigment to the assignment queue - SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); - _unfulfilledAssignments.enqueue(sharedScriptedAssignment); - _allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment); - } else { - // unable to save script for assignment - we shouldn't be here but debug it out - qDebug() << "Unable to save a script for assignment at" << newPath; - qDebug() << "Script will not be added to queue"; - } + // add the script assigment to the assignment queue + SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); + _unfulfilledAssignments.enqueue(sharedScriptedAssignment); + _allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment); } // respond with a 200 code for successful upload diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index a4892b446b..afdd7bd26e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -142,6 +142,8 @@ private: QString _oauthClientSecret; QString _hostname; + std::unordered_map _ephemeralACScripts; + QSet _webAuthenticationStateSet; QHash _cookieSessionHash; From c8e6aee3e795eed0c07bd26d757662fddcdccb80 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 10 Feb 2016 16:28:03 -0800 Subject: [PATCH 60/73] OpenVr: fix for conflict between openvr and oculus display plugins * Call vr::VR_Shutdown() when ref-count goes to zero. The ref-count is ncessary because the vr::IVRSystem poitner is shared between the the openvr input and display plugins. * OpenVR plugins options will display in the menu if vr::VR_IsHmdPresent() is true. This is faster then initializing all of openvr and less likely to conflict with the oculus display plugin. --- .../oculus/src/OculusBaseDisplayPlugin.cpp | 2 ++ plugins/openvr/src/OpenVrDisplayPlugin.cpp | 5 +---- plugins/openvr/src/OpenVrHelpers.cpp | 21 ++----------------- plugins/openvr/src/ViveControllerManager.cpp | 5 +---- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 0be66bab67..cadb4a2e40 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -39,6 +39,7 @@ glm::mat4 OculusBaseDisplayPlugin::getHeadPose(uint32_t frameIndex) const { bool OculusBaseDisplayPlugin::isSupported() const { if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { + qDebug() << "OculusBaseDisplayPlugin : ovr_Initialize() failed"; return false; } @@ -48,6 +49,7 @@ bool OculusBaseDisplayPlugin::isSupported() const { if (!OVR_SUCCESS(result)) { ovrErrorInfo error; ovr_GetLastErrorInfo(&error); + qDebug() << "OculusBaseDisplayPlugin : ovr_Create() failed" << result << error.Result << error.ErrorString; ovr_Shutdown(); return false; } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 3f159eb916..508c89803d 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -72,10 +72,7 @@ mat4 toGlm(const vr::HmdMatrix34_t& m) { } bool OpenVrDisplayPlugin::isSupported() const { - auto hmd = acquireOpenVrSystem(); - bool success = nullptr != hmd; - releaseOpenVrSystem(); - return success; + return vr::VR_IsHmdPresent(); } void OpenVrDisplayPlugin::activate() { diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 249643c73e..66a886b90e 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -23,11 +23,11 @@ using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; -static bool hmdPresent = vr::VR_IsHmdPresent(); static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; vr::IVRSystem* acquireOpenVrSystem() { + bool hmdPresent = vr::VR_IsHmdPresent(); if (hmdPresent) { Lock lock(mutex); if (!activeHmd) { @@ -53,24 +53,7 @@ void releaseOpenVrSystem() { --refCount; if (0 == refCount) { qCDebug(displayplugins) << "openvr: zero refcount, deallocate VR system"; - // Avoid spamming the VR system with activate/deactivate calls at system startup by - // putting in a delay before we destory the shutdown the VR subsystem - - // FIXME releasing the VR system at all seems to trigger an exception deep inside the Oculus DLL. - // disabling for now. - //QTimer* releaseTimer = new QTimer(); - //releaseTimer->singleShot(RELEASE_OPENVR_HMD_DELAY_MS, [releaseTimer] { - // Lock lock(mutex); - // qDebug() << "Delayed openvr destroy activated"; - // if (0 == refCount && nullptr != activeHmd) { - // qDebug() << "Delayed openvr destroy: releasing resources"; - // activeHmd = nullptr; - // vr::VR_Shutdown(); - // } else { - // qDebug() << "Delayed openvr destroy: HMD still in use"; - // } - // releaseTimer->deleteLater(); - //}); + vr::VR_Shutdown(); } } } diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index e1bda007bc..522c7f9c3d 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -48,10 +48,7 @@ static const QString RENDER_CONTROLLERS = "Render Hand Controllers"; const QString ViveControllerManager::NAME = "OpenVR"; bool ViveControllerManager::isSupported() const { - auto hmd = acquireOpenVrSystem(); - bool success = hmd != nullptr; - releaseOpenVrSystem(); - return success; + return vr::VR_IsHmdPresent(); } void ViveControllerManager::activate() { From 6707535ced2c08823dc24255324ea3c374647d4a Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 10 Feb 2016 17:33:56 -0800 Subject: [PATCH 61/73] remove unused and incorrect stats from DS stats --- assignment-client/src/audio/AudioMixer.cpp | 123 +----------------- assignment-client/src/audio/AudioMixer.h | 10 -- .../resources/describe-settings.json | 8 -- 3 files changed, 5 insertions(+), 136 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 51bcece15f..5cc6b339f7 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -67,8 +67,6 @@ const QString AUDIO_BUFFER_GROUP_KEY = "audio_buffer"; InboundAudioStream::Settings AudioMixer::_streamSettings; -bool AudioMixer::_printStreamStats = false; - bool AudioMixer::_enableFilter = true; bool AudioMixer::shouldMute(float quietestFrame) { @@ -81,12 +79,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : _minAudibilityThreshold(LOUDNESS_TO_DISTANCE_RATIO / 2.0f), _performanceThrottlingRatio(0.0f), _attenuationPerDoublingInDistance(DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE), - _noiseMutingThreshold(DEFAULT_NOISE_MUTING_THRESHOLD), - _sendAudioStreamStats(false), - _datagramsReadPerCallStats(0, READ_DATAGRAMS_STATS_WINDOW_SECONDS), - _timeSpentPerCallStats(0, READ_DATAGRAMS_STATS_WINDOW_SECONDS), - _timeSpentPerHashMatchCallStats(0, READ_DATAGRAMS_STATS_WINDOW_SECONDS), - _readPendingCallsPerSecondStats(1, READ_DATAGRAMS_STATS_WINDOW_SECONDS) + _noiseMutingThreshold(DEFAULT_NOISE_MUTING_THRESHOLD) { auto nodeList = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); @@ -527,42 +520,6 @@ void AudioMixer::sendStatsPacket() { _totalMixes = 0; _numStatFrames = 0; - QJsonObject readPendingDatagramStats; - - QJsonObject rpdCallsStats; - rpdCallsStats["calls_per_sec_avg_30s"] = _readPendingCallsPerSecondStats.getWindowAverage(); - rpdCallsStats["calls_last_sec"] = _readPendingCallsPerSecondStats.getLastCompleteIntervalStats().getSum() + 0.5; - - readPendingDatagramStats["calls"] = rpdCallsStats; - - QJsonObject packetsPerCallStats; - packetsPerCallStats["avg_30s"] = _datagramsReadPerCallStats.getWindowAverage(); - packetsPerCallStats["avg_1s"] = _datagramsReadPerCallStats.getLastCompleteIntervalStats().getAverage(); - - readPendingDatagramStats["packets_per_call"] = packetsPerCallStats; - - QJsonObject packetsTimePerCallStats; - packetsTimePerCallStats["usecs_per_call_avg_30s"] = _timeSpentPerCallStats.getWindowAverage(); - packetsTimePerCallStats["usecs_per_call_avg_1s"] = _timeSpentPerCallStats.getLastCompleteIntervalStats().getAverage(); - packetsTimePerCallStats["prct_time_in_call_30s"] = - _timeSpentPerCallStats.getWindowSum() / (READ_DATAGRAMS_STATS_WINDOW_SECONDS * USECS_PER_SECOND) * 100.0; - packetsTimePerCallStats["prct_time_in_call_1s"] = - _timeSpentPerCallStats.getLastCompleteIntervalStats().getSum() / USECS_PER_SECOND * 100.0; - - readPendingDatagramStats["packets_time_per_call"] = packetsTimePerCallStats; - - QJsonObject hashMatchTimePerCallStats; - hashMatchTimePerCallStats["usecs_per_hashmatch_avg_30s"] = _timeSpentPerHashMatchCallStats.getWindowAverage(); - hashMatchTimePerCallStats["usecs_per_hashmatch_avg_1s"] - = _timeSpentPerHashMatchCallStats.getLastCompleteIntervalStats().getAverage(); - hashMatchTimePerCallStats["prct_time_in_hashmatch_30s"] - = _timeSpentPerHashMatchCallStats.getWindowSum() / (READ_DATAGRAMS_STATS_WINDOW_SECONDS*USECS_PER_SECOND) * 100.0; - hashMatchTimePerCallStats["prct_time_in_hashmatch_1s"] - = _timeSpentPerHashMatchCallStats.getLastCompleteIntervalStats().getSum() / USECS_PER_SECOND * 100.0; - readPendingDatagramStats["hashmatch_time_per_call"] = hashMatchTimePerCallStats; - - statsObject["read_pending_datagrams"] = readPendingDatagramStats; - // add stats for each listerner auto nodeList = DependencyManager::get(); QJsonObject listenerStats; @@ -688,13 +645,6 @@ void AudioMixer::broadcastMixes() { ++framesSinceCutoffEvent; } - static const int FRAMES_PER_SECOND = int(ceilf(1.0f / AudioConstants::NETWORK_FRAME_SECS)); - - // check if it has been approximately one second since our last call to perSecondActions - if (nextFrame % FRAMES_PER_SECOND == 0) { - perSecondActions(); - } - nodeList->eachNode([&](const SharedNodePointer& node) { if (node->getLinkedData()) { @@ -750,10 +700,11 @@ void AudioMixer::broadcastMixes() { nodeList->sendPacket(std::move(mixPacket), *node); nodeData->incrementOutgoingMixedAudioSequenceNumber(); - // send an audio stream stats packet if it's time - if (_sendAudioStreamStats) { + static const int FRAMES_PER_SECOND = int(ceilf(1.0f / AudioConstants::NETWORK_FRAME_SECS)); + + // send an audio stream stats packet to the client approximately every second + if (nextFrame % FRAMES_PER_SECOND == 0) { nodeData->sendAudioStreamStatsPackets(node); - _sendAudioStreamStats = false; } ++_sumListeners; @@ -781,64 +732,6 @@ void AudioMixer::broadcastMixes() { } } -void AudioMixer::perSecondActions() { - _sendAudioStreamStats = true; - - int callsLastSecond = _datagramsReadPerCallStats.getCurrentIntervalSamples(); - _readPendingCallsPerSecondStats.update(callsLastSecond); - - if (_printStreamStats) { - - printf("\n================================================================================\n\n"); - - printf(" readPendingDatagram() calls per second | avg: %.2f, avg_30s: %.2f, last_second: %d\n", - _readPendingCallsPerSecondStats.getAverage(), - _readPendingCallsPerSecondStats.getWindowAverage(), - callsLastSecond); - - printf(" Datagrams read per call | avg: %.2f, avg_30s: %.2f, last_second: %.2f\n", - _datagramsReadPerCallStats.getAverage(), - _datagramsReadPerCallStats.getWindowAverage(), - _datagramsReadPerCallStats.getCurrentIntervalAverage()); - - printf(" Usecs spent per readPendingDatagram() call | avg: %.2f, avg_30s: %.2f, last_second: %.2f\n", - _timeSpentPerCallStats.getAverage(), - _timeSpentPerCallStats.getWindowAverage(), - _timeSpentPerCallStats.getCurrentIntervalAverage()); - - printf(" Usecs spent per packetVersionAndHashMatch() call | avg: %.2f, avg_30s: %.2f, last_second: %.2f\n", - _timeSpentPerHashMatchCallStats.getAverage(), - _timeSpentPerHashMatchCallStats.getWindowAverage(), - _timeSpentPerHashMatchCallStats.getCurrentIntervalAverage()); - - double WINDOW_LENGTH_USECS = READ_DATAGRAMS_STATS_WINDOW_SECONDS * USECS_PER_SECOND; - - printf(" %% time spent in readPendingDatagram() calls | avg_30s: %.6f%%, last_second: %.6f%%\n", - _timeSpentPerCallStats.getWindowSum() / WINDOW_LENGTH_USECS * 100.0, - _timeSpentPerCallStats.getCurrentIntervalSum() / USECS_PER_SECOND * 100.0); - - printf("%% time spent in packetVersionAndHashMatch() calls: | avg_30s: %.6f%%, last_second: %.6f%%\n", - _timeSpentPerHashMatchCallStats.getWindowSum() / WINDOW_LENGTH_USECS * 100.0, - _timeSpentPerHashMatchCallStats.getCurrentIntervalSum() / USECS_PER_SECOND * 100.0); - - DependencyManager::get()->eachNode([](const SharedNodePointer& node) { - if (node->getLinkedData()) { - AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); - - if (node->getType() == NodeType::Agent && node->getActiveSocket()) { - printf("\nStats for agent %s --------------------------------\n", - node->getUUID().toString().toLatin1().data()); - nodeData->printUpstreamDownstreamStats(); - } - } - }); - } - - _datagramsReadPerCallStats.currentIntervalComplete(); - _timeSpentPerCallStats.currentIntervalComplete(); - _timeSpentPerHashMatchCallStats.currentIntervalComplete(); -} - void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { if (settingsObject.contains(AUDIO_BUFFER_GROUP_KEY)) { QJsonObject audioBufferGroupObject = settingsObject[AUDIO_BUFFER_GROUP_KEY].toObject(); @@ -903,12 +796,6 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { } else { qDebug() << "Repetition with fade disabled"; } - - const QString PRINT_STREAM_STATS_JSON_KEY = "print_stream_stats"; - _printStreamStats = audioBufferGroupObject[PRINT_STREAM_STATS_JSON_KEY].toBool(); - if (_printStreamStats) { - qDebug() << "Stream stats will be printed to stdout"; - } } if (settingsObject.contains(AUDIO_ENV_GROUP_KEY)) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 3a8dfad348..24b4b39704 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -110,17 +110,7 @@ private: static InboundAudioStream::Settings _streamSettings; - static bool _printStreamStats; static bool _enableFilter; - - bool _sendAudioStreamStats; - - // stats - MovingMinMaxAvg _datagramsReadPerCallStats; // update with # of datagrams read for each readPendingDatagrams call - MovingMinMaxAvg _timeSpentPerCallStats; // update with usecs spent inside each readPendingDatagrams call - MovingMinMaxAvg _timeSpentPerHashMatchCallStats; // update with usecs spent inside each packetVersionAndHashMatch call - - MovingMinMaxAvg _readPendingCallsPerSecondStats; // update with # of readPendingDatagrams calls in the last second }; #endif // hifi_AudioMixer_h diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 870573ef6c..891c586a6a 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -366,14 +366,6 @@ "help": "Dropped frames and mixing during starves repeat the last frame, eventually fading to silence", "default": false, "advanced": true - }, - { - "name": "print_stream_stats", - "type": "checkbox", - "label": "Print Stream Stats", - "help": "Audio upstream and downstream stats of each agent printed to audio-mixer stdout", - "default": false, - "advanced": true } ] }, From edfb65acf615895dfc2d826c3af8715f7167a0e3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 10 Feb 2016 17:38:56 -0800 Subject: [PATCH 62/73] more stats printing removal from audio-mixer --- .../src/audio/AudioMixerClientData.cpp | 45 ------------------- .../src/audio/AudioMixerClientData.h | 5 --- 2 files changed, 50 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 9a77be9285..fa706eb0f6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -311,48 +311,3 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { return result; } - -void AudioMixerClientData::printUpstreamDownstreamStats() { - auto streamsCopy = getAudioStreams(); - - // print the upstream (mic stream) stats if the mic stream exists - auto it = streamsCopy.find(QUuid()); - if (it != streamsCopy.end()) { - printf("Upstream:\n"); - printAudioStreamStats(it->second->getAudioStreamStats()); - } - // print the downstream stats if they contain valid info - if (_downstreamAudioStreamStats._packetStreamStats._received > 0) { - printf("Downstream:\n"); - printAudioStreamStats(_downstreamAudioStreamStats); - } -} - -void AudioMixerClientData::printAudioStreamStats(const AudioStreamStats& streamStats) const { - printf(" Packet loss | overall: %5.2f%% (%d lost), last_30s: %5.2f%% (%d lost)\n", - (double)(streamStats._packetStreamStats.getLostRate() * 100.0f), - streamStats._packetStreamStats._lost, - (double)(streamStats._packetStreamWindowStats.getLostRate() * 100.0f), - streamStats._packetStreamWindowStats._lost); - - printf(" Ringbuffer frames | desired: %u, avg_available(10s): %u, available: %u\n", - streamStats._desiredJitterBufferFrames, - streamStats._framesAvailableAverage, - streamStats._framesAvailable); - - printf(" Ringbuffer stats | starves: %u, prev_starve_lasted: %u, frames_dropped: %u, overflows: %u\n", - streamStats._starveCount, - streamStats._consecutiveNotMixedCount, - streamStats._framesDropped, - streamStats._overflowCount); - - printf(" Inter-packet timegaps (overall) | min: %9s, max: %9s, avg: %9s\n", - formatUsecTime(streamStats._timeGapMin).toLatin1().data(), - formatUsecTime(streamStats._timeGapMax).toLatin1().data(), - formatUsecTime(streamStats._timeGapAverage).toLatin1().data()); - - printf(" Inter-packet timegaps (last 30s) | min: %9s, max: %9s, avg: %9s\n", - formatUsecTime(streamStats._timeGapWindowMin).toLatin1().data(), - formatUsecTime(streamStats._timeGapWindowMax).toLatin1().data(), - formatUsecTime(streamStats._timeGapWindowAverage).toLatin1().data()); -} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 7e21a37a37..3627a247c0 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -58,14 +58,9 @@ public: void incrementOutgoingMixedAudioSequenceNumber() { _outgoingMixedAudioSequenceNumber++; } quint16 getOutgoingSequenceNumber() const { return _outgoingMixedAudioSequenceNumber; } - void printUpstreamDownstreamStats(); - signals: void injectorStreamFinished(const QUuid& streamIdentifier); -private: - void printAudioStreamStats(const AudioStreamStats& streamStats) const; - private: QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID From 532f0683e8ab206d332fa0db31f75b4c4a3721b3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 9 Feb 2016 11:32:58 -0800 Subject: [PATCH 63/73] remove all contact info before removing objects --- libraries/physics/src/PhysicsEngine.cpp | 42 +++++++++++++++---------- libraries/physics/src/PhysicsEngine.h | 7 ++--- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 0a7ef606ba..f950d0c017 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -136,23 +136,19 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { motionState->clearIncomingDirtyFlags(); } -// private -void PhysicsEngine::removeObjectFromDynamicsWorld(ObjectMotionState* object) { - // wake up anything touching this object - bump(object); - removeContacts(object); - - btRigidBody* body = object->getRigidBody(); - assert(body); - _dynamicsWorld->removeRigidBody(body); -} - void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { + // first bump and prune contacts for all objects in the list for (auto object : objects) { - removeObjectFromDynamicsWorld(object); + bumpAndPruneContacts(object); + } + + // then remove them + for (auto object : objects) { + btRigidBody* body = object->getRigidBody(); + assert(body); + _dynamicsWorld->removeRigidBody(body); // NOTE: setRigidBody() modifies body->m_userPointer so we should clear the MotionState's body BEFORE deleting it. - btRigidBody* body = object->getRigidBody(); object->setRigidBody(nullptr); body->setMotionState(nullptr); delete body; @@ -163,7 +159,8 @@ void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { void PhysicsEngine::removeObjects(const SetOfMotionStates& objects) { for (auto object : objects) { btRigidBody* body = object->getRigidBody(); - removeObjectFromDynamicsWorld(object); + assert(body); + _dynamicsWorld->removeRigidBody(body); // NOTE: setRigidBody() modifies body->m_userPointer so we should clear the MotionState's body BEFORE deleting it. object->setRigidBody(nullptr); @@ -200,7 +197,13 @@ VectorOfMotionStates PhysicsEngine::changeObjects(const VectorOfMotionStates& ob } void PhysicsEngine::reinsertObject(ObjectMotionState* object) { - removeObjectFromDynamicsWorld(object); + // remove object from DynamicsWorld + bumpAndPruneContacts(object); + btRigidBody* body = object->getRigidBody(); + assert(body); + _dynamicsWorld->removeRigidBody(body); + + // add it back addObjectToDynamicsWorld(object); } @@ -402,13 +405,14 @@ void PhysicsEngine::dumpStatsIfNecessary() { // CF_DISABLE_VISUALIZE_OBJECT = 32, //disable debug drawing // CF_DISABLE_SPU_COLLISION_PROCESSING = 64//disable parallel/SPU processing -void PhysicsEngine::bump(ObjectMotionState* motionState) { +void PhysicsEngine::bumpAndPruneContacts(ObjectMotionState* motionState) { // Find all objects that touch the object corresponding to motionState and flag the other objects // for simulation ownership by the local simulation. assert(motionState); btCollisionObject* object = motionState->getRigidBody(); + std::vector staleManifolds; int numManifolds = _collisionDispatcher->getNumManifolds(); for (int i = 0; i < numManifolds; ++i) { btPersistentManifold* contactManifold = _collisionDispatcher->getManifoldByIndexInternal(i); @@ -423,6 +427,7 @@ void PhysicsEngine::bump(ObjectMotionState* motionState) { objectA->setActivationState(ACTIVE_TAG); } } + staleManifolds.push_back(contactManifold); } else if (objectA == object) { if (!objectB->isStaticOrKinematicObject()) { ObjectMotionState* motionStateB = static_cast(objectB->getUserPointer()); @@ -431,9 +436,14 @@ void PhysicsEngine::bump(ObjectMotionState* motionState) { objectB->setActivationState(ACTIVE_TAG); } } + staleManifolds.push_back(contactManifold); } } } + for (auto manifold : staleManifolds) { + _collisionDispatcher->releaseManifold(manifold); + } + removeContacts(motionState); } void PhysicsEngine::setCharacterController(CharacterController* character) { diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 16c8456e55..f644d6f5b2 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -78,9 +78,6 @@ public: /// \return position of simulation origin in domain-frame const glm::vec3& getOriginOffset() const { return _originOffset; } - /// \brief call bump on any objects that touch the object corresponding to motionState - void bump(ObjectMotionState* motionState); - void setCharacterController(CharacterController* character); void dumpNextStats() { _dumpNextStats = true; } @@ -94,7 +91,9 @@ public: private: void addObjectToDynamicsWorld(ObjectMotionState* motionState); - void removeObjectFromDynamicsWorld(ObjectMotionState* motionState); + + /// \brief bump any objects that touch this one, then remove contact info + void bumpAndPruneContacts(ObjectMotionState* motionState); void removeContacts(ObjectMotionState* motionState); From fed03edde3fdebff34c39c404877c813e64bc686 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 10 Feb 2016 12:06:44 -0800 Subject: [PATCH 64/73] manually removing contact manifolds was a bad idea --- libraries/physics/src/PhysicsEngine.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index f950d0c017..d8108a8aed 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -412,7 +412,6 @@ void PhysicsEngine::bumpAndPruneContacts(ObjectMotionState* motionState) { assert(motionState); btCollisionObject* object = motionState->getRigidBody(); - std::vector staleManifolds; int numManifolds = _collisionDispatcher->getNumManifolds(); for (int i = 0; i < numManifolds; ++i) { btPersistentManifold* contactManifold = _collisionDispatcher->getManifoldByIndexInternal(i); @@ -427,7 +426,6 @@ void PhysicsEngine::bumpAndPruneContacts(ObjectMotionState* motionState) { objectA->setActivationState(ACTIVE_TAG); } } - staleManifolds.push_back(contactManifold); } else if (objectA == object) { if (!objectB->isStaticOrKinematicObject()) { ObjectMotionState* motionStateB = static_cast(objectB->getUserPointer()); @@ -436,13 +434,9 @@ void PhysicsEngine::bumpAndPruneContacts(ObjectMotionState* motionState) { objectB->setActivationState(ACTIVE_TAG); } } - staleManifolds.push_back(contactManifold); } } } - for (auto manifold : staleManifolds) { - _collisionDispatcher->releaseManifold(manifold); - } removeContacts(motionState); } From ac60917b546e987e320452a52b742d96db93280e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 10 Feb 2016 17:57:49 -0800 Subject: [PATCH 65/73] sigh... protect againt null MotionStates --- libraries/physics/src/PhysicalEntitySimulation.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index aaa706b370..629c04ae19 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -167,12 +167,13 @@ void PhysicalEntitySimulation::getObjectsToRemoveFromPhysics(VectorOfMotionState _entitiesToAddToPhysics.remove(entity); EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); - assert(motionState); - _pendingChanges.remove(motionState); - _outgoingChanges.remove(motionState); - _physicalObjects.remove(motionState); - result.push_back(motionState); - _entitiesToRelease.insert(entity); + if (motionState) { + _pendingChanges.remove(motionState); + _outgoingChanges.remove(motionState); + _physicalObjects.remove(motionState); + result.push_back(motionState); + _entitiesToRelease.insert(entity); + } if (entity->isSimulated() && entity->isDead()) { _entitiesToDelete.insert(entity); From 3a2b082a7f2fd72dd54a639ff417923353157e9f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 10 Feb 2016 20:47:22 -0800 Subject: [PATCH 66/73] remove incorrect assert --- libraries/entities/src/EntityEditPacketSender.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index e28dc1e4fa..91f21659ec 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -41,8 +41,6 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityItemI return; // bail early } - assert(properties.getLastEdited() > 0); - QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0); if (EntityItemProperties::encodeEntityEditPacket(type, modelID, properties, bufferOut)) { From 5f395654f09eee78afc002328ebe438a3dfe8082 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 8 Feb 2016 12:29:56 -0800 Subject: [PATCH 67/73] Integrate JobConfig with JSON/qml --- libraries/render/src/render/Task.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index d090938b64..75bc9d8643 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -74,7 +74,7 @@ public: Q_INVOKABLE QString toJSON() { return QJsonDocument(toJsonValue(*this).toObject()).toJson(QJsonDocument::Compact); } public slots: - void load(const QJsonValue& json) { qObjectFromJsonValue(json, *this); } + Q_INVOKABLE void fromJSON(const QJsonValue& json) { qObjectFromJsonValue(json, *this); } }; class TaskConfig : public JobConfig { @@ -85,6 +85,9 @@ public: void init(Task* task) { _task = task; } + // getter for qml integration, prefer the templated getter + Q_INVOKABLE QObject* getConfig(const QString& name) { return QObject::findChild(name); } + // getter for cpp (strictly typed), prefer this getter template typename T::Config* getConfig(std::string job = "") const { QString name = job.empty() ? QString() : QString(job.c_str()); // an empty string is not a null string return findChild(name); From 991d6328ef9b4b371dd6526fdb563f29d89712ab Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 8 Feb 2016 13:02:35 -0800 Subject: [PATCH 68/73] Fix JSON integration for JobConfig --- libraries/render/src/render/Task.h | 4 +++- libraries/shared/src/shared/JSONHelpers.cpp | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 75bc9d8643..a47f8e39fe 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -71,10 +71,12 @@ public: bool alwaysEnabled{ true }; bool enabled{ true }; + // This must be named toJSON to integrate with the global scripting JSON object Q_INVOKABLE QString toJSON() { return QJsonDocument(toJsonValue(*this).toObject()).toJson(QJsonDocument::Compact); } + Q_INVOKABLE void load(const QVariantMap& map) { qObjectFromJsonValue(QJsonObject::fromVariantMap(map), *this); } public slots: - Q_INVOKABLE void fromJSON(const QJsonValue& json) { qObjectFromJsonValue(json, *this); } + void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); } }; class TaskConfig : public JobConfig { diff --git a/libraries/shared/src/shared/JSONHelpers.cpp b/libraries/shared/src/shared/JSONHelpers.cpp index 3aab117370..e717050055 100644 --- a/libraries/shared/src/shared/JSONHelpers.cpp +++ b/libraries/shared/src/shared/JSONHelpers.cpp @@ -111,10 +111,9 @@ void qObjectFromJsonValue(const QJsonValue& j, QObject& o) { for (auto it = object.begin(); it != object.end(); it++) { std::string key = it.key().toStdString(); if (it.value().isObject()) { - QVariant child = o.property(key.c_str()); - if (child.isValid()) { - QObject* object = child.value(); - qObjectFromJsonValue(it.value(), *object); + QObject* child = o.findChild(key.c_str(), Qt::FindChildOption::FindDirectChildrenOnly); + if (child) { + qObjectFromJsonValue(it.value(), *child); } } else { o.setProperty(key.c_str(), it.value()); From dbc59ccc2b86e158855badccc00cad4ec04b2636 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 8 Feb 2016 12:29:06 -0800 Subject: [PATCH 69/73] Revamp render engine debug JS script --- examples/utilities/tools/render/AO.qml | 101 ++++++++ examples/utilities/tools/render/Buffer.qml | 99 ++++++++ .../utilities/tools/render/ConfigSlider.qml | 58 +++++ .../utilities/tools/render/ItemsSlider.qml | 59 +++++ examples/utilities/tools/render/Tone.qml | 49 ++++ examples/utilities/tools/render/debug.js | 38 +++ examples/utilities/tools/render/main.qml | 64 ++++++ examples/utilities/tools/renderEngineDebug.js | 216 ------------------ 8 files changed, 468 insertions(+), 216 deletions(-) create mode 100644 examples/utilities/tools/render/AO.qml create mode 100644 examples/utilities/tools/render/Buffer.qml create mode 100644 examples/utilities/tools/render/ConfigSlider.qml create mode 100644 examples/utilities/tools/render/ItemsSlider.qml create mode 100644 examples/utilities/tools/render/Tone.qml create mode 100644 examples/utilities/tools/render/debug.js create mode 100644 examples/utilities/tools/render/main.qml delete mode 100755 examples/utilities/tools/renderEngineDebug.js diff --git a/examples/utilities/tools/render/AO.qml b/examples/utilities/tools/render/AO.qml new file mode 100644 index 0000000000..529177010d --- /dev/null +++ b/examples/utilities/tools/render/AO.qml @@ -0,0 +1,101 @@ +// +// AO.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + width: 200 + height: 200 + property QtObject config: Render.getConfig("AmbientOcclusion") + + Timer { + interval: 500 + running: true + repeat: true + onTriggered: { parent.timer.text = config.gpuTime.toFixed(2) } + } + + Label { text: qsTr("Ambient Occlusion") } + Label { id: timer; x: 140 } + + CheckBox { + y: 1 * 25 + text: qsTr("Dithering") + partiallyCheckedEnabled: false + checked: parent.config.ditheringEnabled + onCheckedChanged: { parent.config.ditheringEnabled = checked } + } + + ConfigSlider { + y: 2 * 25 + config: parent.config + prop: "resolutionLevel" + label: qsTr("Resolution Level") + min: 0; max: 4 + } + ConfigSlider { + y: 3 * 25 + config: parent.config + prop: "obscuranceLevel" + label: qsTr("Obscurance Level") + min: 0; max: 1 + } + ConfigSlider { + y: 4 * 25 + config: parent.config + prop: "radius" + label: qsTr("Radius") + min: 0; max: 2 + } + ConfigSlider { + y: 5 * 25 + config: parent.config + prop: "numSamples" + label: qsTr("Samples") + min: 0; max: 32 + } + ConfigSlider { + y: 6 * 25 + config: parent.config + prop: "numSpiralTurns" + label: qsTr("Spiral Turns") + min: 0; max: 30 + } + ConfigSlider { + y: 7 * 25 + config: parent.config + prop: "falloffBias" + label: qsTr("Falloff Bias") + min: 0; max: 0.2 + } + ConfigSlider { + y: 8 * 25 + config: parent.config + prop: "edgeSharpness" + label: qsTr("Edge Sharpness") + min: 0; max: 1 + } + ConfigSlider { + y: 9 * 25 + config: parent.config + prop: "blurRadius" + label: qsTr("Blur Radius") + min: 0; max: 6 + } + ConfigSlider { + y: 10 * 25 + config: parent.config + prop: "blurDeviation" + label: qsTr("Blur Deviation") + min: 0; max: 3 + } +} + diff --git a/examples/utilities/tools/render/Buffer.qml b/examples/utilities/tools/render/Buffer.qml new file mode 100644 index 0000000000..bc4b654257 --- /dev/null +++ b/examples/utilities/tools/render/Buffer.qml @@ -0,0 +1,99 @@ +// +// Buffer.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + width: 200 + height: 270 + + Label { + text: qsTr("Debug Buffer") + } + + ExclusiveGroup { id: buffer } + + function setDebugMode(mode) { + var debug = Render.getConfig("DebugDeferredBuffer"); + console.log(mode); + debug.enabled = (mode != 0); + debug.mode = mode; + } + + RadioButton { + x: 8; y: 19 + 0 * 23 + text: qsTr("Off") + exclusiveGroup: buffer + checked: true + onCheckedChanged: { if (checked) { setDebugMode(0) } } + } + RadioButton { + x: 8; y: 19 + 1 * 23 + text: qsTr("Diffuse") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(1) } } + } + RadioButton { + x: 8; y: 19 + 2 * 23 + text: qsTr("Metallic") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(2) } } + } + RadioButton { + x: 8; y: 19 + 3 * 23 + text: qsTr("Roughness") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(3) } } + } + RadioButton { + x: 8; y: 19 + 4 * 23 + text: qsTr("Normal") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(4) } } + } + RadioButton { + x: 8; y: 19 + 5 * 23 + text: qsTr("Depth") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(5) } } + } + RadioButton { + x: 8; y: 19 + 6 * 23 + text: qsTr("Lighting") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(6) } } + } + RadioButton { + x: 8; y: 19 + 7 * 23 + text: qsTr("Shadow") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(7) } } + } + RadioButton { + x: 8; y: 19 + 8 * 23 + text: qsTr("Pyramid Depth") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(8) } } + } + RadioButton { + x: 8; y: 19 + 9 * 23 + text: qsTr("Ambient Occlusion") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(9) } } + } + RadioButton { + x: 8; y: 19 + 10 * 23 + text: qsTr("Custom Shader") + exclusiveGroup: buffer + onCheckedChanged: { if (checked) { setDebugMode(10) } } + } +} + diff --git a/examples/utilities/tools/render/ConfigSlider.qml b/examples/utilities/tools/render/ConfigSlider.qml new file mode 100644 index 0000000000..7c6498a7d2 --- /dev/null +++ b/examples/utilities/tools/render/ConfigSlider.qml @@ -0,0 +1,58 @@ +// +// ConfigSlider.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + width: 400 + height: 24 + property string label + property QtObject config + property string prop + property real min: 0.0 + property real max: 1.0 + + function init() { + stat.text = config[prop].toFixed(2); + slider.value = (config[prop] - min) / (max - min); + } + Component.onCompleted: init() + + function update() { + var val = min + (max - min) * slider.value; + stat.text = val.toFixed(2); + config[prop] = val; + } + + Label { + text: parent.label + y: 7 + anchors.left: parent.left + anchors.leftMargin: 8 + } + + Label { + id: stat + y: 7 + anchors.left: parent.left + anchors.leftMargin: 140 + } + + Slider { + id: slider + y: 3 + width: 192 + height: 20 + onValueChanged: parent.update() + anchors.right: parent.right + anchors.rightMargin: 8 + } +} diff --git a/examples/utilities/tools/render/ItemsSlider.qml b/examples/utilities/tools/render/ItemsSlider.qml new file mode 100644 index 0000000000..1623284579 --- /dev/null +++ b/examples/utilities/tools/render/ItemsSlider.qml @@ -0,0 +1,59 @@ +// +// ItemsSlider.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + width: 400 + height: 24 + property string label + property QtObject config + + function update() { + var val = slider.value; + var max = config.numDrawn; + var drawn = Math.round(val * max); + stat.text = drawn + " / " + max; + config.maxDrawn = (val == 1.0 ? -1 : drawn); + } + + Timer { + interval: 500 + running: true + repeat: true + onTriggered: parent.update() + } + + Label { + text: parent.label + y: 7 + anchors.left: parent.left + anchors.leftMargin: 8 + } + + Label { + id: stat + y: 7 + anchors.left: parent.left + anchors.leftMargin: 108 + } + + Slider { + id: slider + y: 3 + width: 192 + height: 20 + value: 1.0 + onValueChanged: update() + anchors.right: parent.right + anchors.rightMargin: 8 + } +} diff --git a/examples/utilities/tools/render/Tone.qml b/examples/utilities/tools/render/Tone.qml new file mode 100644 index 0000000000..a0eb4b1e00 --- /dev/null +++ b/examples/utilities/tools/render/Tone.qml @@ -0,0 +1,49 @@ +// +// Tone.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + width: 400 + height: 24 + property string label: qsTr("Tone Mapping Exposure") + + function update() { + var val = (slider.value - 0.5) * 20; + stat.text = val.toFixed(2); + Render.getConfig("ToneMapping").exposure = val; + } + + Label { + text: parent.label + y: 7 + anchors.left: parent.left + anchors.leftMargin: 8 + } + + Label { + id: stat + y: 7 + anchors.left: parent.left + anchors.leftMargin: 150 + } + + Slider { + id: slider + y: 3 + width: 192 + height: 20 + value: Render.getConfig("ToneMapping").exposure + onValueChanged: parent.update() + anchors.right: parent.right + anchors.rightMargin: 8 + } +} diff --git a/examples/utilities/tools/render/debug.js b/examples/utilities/tools/render/debug.js new file mode 100644 index 0000000000..896d2ac179 --- /dev/null +++ b/examples/utilities/tools/render/debug.js @@ -0,0 +1,38 @@ +// +// debug.js +// examples/utilities/tools/render +// +// Zach Pomerantz, created on 1/27/2016. +// 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 +// + +oldConfig = Render.toJSON(); +Render.RenderShadowTask.enabled = true; +Render.RenderDeferredTask.AmbientOcclusion.enabled = true; +Render.RenderDeferredTask.DebugDeferredBuffer.enabled = false; + +// Set up the qml ui +var qml = Script.resolvePath('main.qml'); +var window = new OverlayWindow({ + title: 'Render Engine Configuration', + source: qml, + width: 400, height: 900, +}); +window.setPosition(25, 50); +window.closed.connect(function() { Script.stop(); }); + +// Debug buffer sizing +var resizing = false; +Controller.mousePressEvent.connect(function() { resizing = true; }); +Controller.mouseReleaseEvent.connect(function() { resizing = false; }); +Controller.mouseMoveEvent.connect(function(e) { resizing && setDebugBufferSize(e.x); }); +function setDebugBufferSize(x) { + x = (2.0 * (x / Window.innerWidth) - 1.0); // scale + x = Math.min(Math.max(-1, x), 1); // clamp + Render.RenderDeferredTask.DebugDeferredBuffer.size = {x: x, y: -1, z: 1, w: 1}; +} + +Script.scriptEnding.connect(function() { Render.fromJSON(oldConfig); } ); diff --git a/examples/utilities/tools/render/main.qml b/examples/utilities/tools/render/main.qml new file mode 100644 index 0000000000..729e91c4f8 --- /dev/null +++ b/examples/utilities/tools/render/main.qml @@ -0,0 +1,64 @@ +// +// main.qml +// examples/utilities/tools/render +// +// Created by Zach Pomerantz on 2/8/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + // Items + ItemsSlider { + y: 0 * 25 + label: qsTr("Opaque") + config: Render.getConfig("DrawOpaqueDeferred") + } + ItemsSlider { + y: 1 * 25 + label: qsTr("Transparent") + config: Render.getConfig("DrawTransparentDeferred") + } + ItemsSlider { + y: 2 * 25 + label: qsTr("Overlay3D") + config: Render.getConfig("DrawOverlay3D") + } + + // Draw status + Item { + y: 100 + + CheckBox { + text: qsTr("Display Status") + partiallyCheckedEnabled: false + onCheckedChanged: { Render.getConfig("DrawStatus").showDisplay = checked } + } + CheckBox { + x: 200 + text: qsTr("Network/Physics Status") + partiallyCheckedEnabled: false + onCheckedChanged: { Render.getConfig("DrawStatus").showNetwork = checked } + } + } + + // Tone mapping + ConfigSlider { + y: 125 + config: Render.getConfig("ToneMapping") + prop: "exposure" + label: qsTr("Tone Mapping Exposure") + min: -10; max: 10 + } + + // Ambient occlusion + AO { y: 175 } + + // Debug buffer + Buffer { y: 475 } +} + diff --git a/examples/utilities/tools/renderEngineDebug.js b/examples/utilities/tools/renderEngineDebug.js deleted file mode 100755 index 5d38561eda..0000000000 --- a/examples/utilities/tools/renderEngineDebug.js +++ /dev/null @@ -1,216 +0,0 @@ -// -// renderEngineDebug.js -// examples/utilities/tools -// -// Sam Gateau -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -Script.include("cookies.js"); - -var MENU = "Developer>Render>Debug Deferred Buffer"; -var ACTIONS = ["Off", "Diffuse", "Metallic", "Roughness", "Normal", "Depth", "Lighting", "Shadow", "PyramidDepth", "AmbientOcclusion", "OcclusionBlurred", "Custom"]; -var SETTINGS_KEY = "EngineDebugScript.DebugMode"; - -Number.prototype.clamp = function(min, max) { - return Math.min(Math.max(this, min), max); -}; - -var panel = new Panel(10, 100); - -function CounterWidget(parentPanel, name, counter) { - var subPanel = parentPanel.newSubPanel(name); - var widget = parentPanel.items[name]; - widget.editTitle({ width: 270 }); - - subPanel.newSlider('Max Drawn', -1, 1, - function(value) { counter.maxDrawn = value; }, // setter - function() { return counter.maxDrawn; }, // getter - function(value) { return value; }); - - var slider = subPanel.getWidget('Max Drawn'); - - this.update = function () { - var numDrawn = counter.numDrawn; // avoid double polling - var numMax = Math.max(numDrawn, 1); - var title = [ - ' ' + name, - numDrawn + ' / ' + counter.numFeed - ].join('\t'); - - widget.editTitle({ text: title }); - slider.setMaxValue(numMax); - }; -}; - -var opaquesCounter = new CounterWidget(panel, "Opaques", Render.opaque); -var transparentsCounter = new CounterWidget(panel, "Transparents", Render.transparent); -var overlaysCounter = new CounterWidget(panel, "Overlays", Render.overlay3D); - -var resizing = false; -var previousMode = Settings.getValue(SETTINGS_KEY, -1); -previousMode = 8; -Menu.addActionGroup(MENU, ACTIONS, ACTIONS[previousMode + 1]); -Render.deferredDebugMode = previousMode; -Render.deferredDebugSize = { x: 0.0, y: -1.0, z: 1.0, w: 1.0 }; // Reset to default size - -function setEngineDeferredDebugSize(eventX) { - var scaledX = (2.0 * (eventX / Window.innerWidth) - 1.0).clamp(-1.0, 1.0); - Render.deferredDebugSize = { x: scaledX, y: -1.0, z: 1.0, w: 1.0 }; -} -function shouldStartResizing(eventX) { - var x = Math.abs(eventX - Window.innerWidth * (1.0 + Render.deferredDebugSize.x) / 2.0); - var mode = Render.deferredDebugMode; - return mode !== -1 && x < 20; -} - -function menuItemEvent(menuItem) { - var index = ACTIONS.indexOf(menuItem); - if (index >= 0) { - Render.deferredDebugMode = (index - 1); - } -} - -// see libraries/render/src/render/Engine.h -var showDisplayStatusFlag = 1; -var showNetworkStatusFlag = 2; - -panel.newCheckbox("Display status", - function(value) { Render.displayItemStatus = (value ? - Render.displayItemStatus | showDisplayStatusFlag : - Render.displayItemStatus & ~showDisplayStatusFlag); }, - function() { return (Render.displayItemStatus & showDisplayStatusFlag) > 0; }, - function(value) { return (value & showDisplayStatusFlag) > 0; } -); - -panel.newCheckbox("Network/Physics status", - function(value) { Render.displayItemStatus = (value ? - Render.displayItemStatus | showNetworkStatusFlag : - Render.displayItemStatus & ~showNetworkStatusFlag); }, - function() { return (Render.displayItemStatus & showNetworkStatusFlag) > 0; }, - function(value) { return (value & showNetworkStatusFlag) > 0; } -); - -panel.newSlider("Tone Mapping Exposure", -10, 10, - function (value) { Render.tone.exposure = value; }, - function() { return Render.tone.exposure; }, - function (value) { return (value); }); - -panel.newSlider("Ambient Occlusion Resolution Level", 0.0, 4.0, - function (value) { Render.ambientOcclusion.resolutionLevel = value; }, - function() { return Render.ambientOcclusion.resolutionLevel; }, - function (value) { return (value); }); - -panel.newSlider("Ambient Occlusion Radius", 0.0, 2.0, - function (value) { Render.ambientOcclusion.radius = value; }, - function() { return Render.ambientOcclusion.radius; }, - function (value) { return (value.toFixed(2)); }); - -panel.newSlider("Ambient Occlusion Level", 0.0, 1.0, - function (value) { Render.ambientOcclusion.level = value; }, - function() { return Render.ambientOcclusion.level; }, - function (value) { return (value.toFixed(2)); }); - -panel.newSlider("Ambient Occlusion Num Samples", 1, 32, - function (value) { Render.ambientOcclusion.numSamples = value; }, - function() { return Render.ambientOcclusion.numSamples; }, - function (value) { return (value); }); - -panel.newSlider("Ambient Occlusion Num Spiral Turns", 0.0, 30.0, - function (value) { Render.ambientOcclusion.numSpiralTurns = value; }, - function() { return Render.ambientOcclusion.numSpiralTurns; }, - function (value) { return (value.toFixed(2)); }); - -panel.newCheckbox("Ambient Occlusion Dithering", - function (value) { Render.ambientOcclusion.ditheringEnabled = value; }, - function() { return Render.ambientOcclusion.ditheringEnabled; }, - function (value) { return (value); }); - -panel.newSlider("Ambient Occlusion Falloff Bias", 0.0, 0.2, - function (value) { Render.ambientOcclusion.falloffBias = value; }, - function() { return Render.ambientOcclusion.falloffBias; }, - function (value) { return (value.toFixed(2)); }); - -panel.newSlider("Ambient Occlusion Edge Sharpness", 0.0, 1.0, - function (value) { Render.ambientOcclusion.edgeSharpness = value; }, - function() { return Render.ambientOcclusion.edgeSharpness; }, - function (value) { return (value.toFixed(2)); }); - -panel.newSlider("Ambient Occlusion Blur Radius", 0.0, 6.0, - function (value) { Render.ambientOcclusion.blurRadius = value; }, - function() { return Render.ambientOcclusion.blurRadius; }, - function (value) { return (value); }); - -panel.newSlider("Ambient Occlusion Blur Deviation", 0.0, 3.0, - function (value) { Render.ambientOcclusion.blurDeviation = value; }, - function() { return Render.ambientOcclusion.blurDeviation; }, - function (value) { return (value.toFixed(2)); }); - - -panel.newSlider("Ambient Occlusion GPU time", 0.0, 10.0, - function (value) {}, - function() { return Render.ambientOcclusion.gpuTime; }, - function (value) { return (value.toFixed(2) + " ms"); }); - - -var tickTackPeriod = 500; - -function updateCounters() { - opaquesCounter.update(); - transparentsCounter.update(); - overlaysCounter.update(); - panel.update("Ambient Occlusion GPU time"); -} -Script.setInterval(updateCounters, tickTackPeriod); - -function mouseMoveEvent(event) { - if (resizing) { - setEngineDeferredDebugSize(event.x); - } else { - panel.mouseMoveEvent(event); - } -} - -function mousePressEvent(event) { - if (shouldStartResizing(event.x)) { - resizing = true; - } else { - panel.mousePressEvent(event); - } -} - -function mouseReleaseEvent(event) { - if (resizing) { - resizing = false; - } else { - panel.mouseReleaseEvent(event); - } -} - -Controller.mouseMoveEvent.connect(mouseMoveEvent); -Controller.mousePressEvent.connect(mousePressEvent); -Controller.mouseReleaseEvent.connect(mouseReleaseEvent); - -Menu.menuItemEvent.connect(menuItemEvent); - -function scriptEnding() { - panel.destroy(); - Menu.removeActionGroup(MENU); - // Reset - Settings.setValue(SETTINGS_KEY, Render.deferredDebugMode); - Render.deferredDebugMode = -1; - Render.deferredDebugSize = { x: 0.0, y: -1.0, z: 1.0, w: 1.0 }; - Render.opaque.maxDrawn = -1; - Render.transparent.maxDrawn = -1; - Render.overlay3D.maxDrawn = -1; -} -Script.scriptEnding.connect(scriptEnding); - - -// Collapse items -panel.mousePressEvent({ x: panel.x, y: panel.items["Overlays"].y}); -panel.mousePressEvent({ x: panel.x, y: panel.items["Transparents"].y}); -panel.mousePressEvent({ x: panel.x, y: panel.items["Opaques"].y}); From 86412c18900e9a396f5e2e9e825abd3f45ef884f Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 8 Feb 2016 12:30:16 -0800 Subject: [PATCH 70/73] Fix default debug buffer size --- libraries/render-utils/src/DebugDeferredBuffer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index ecc2012466..b31985d6a3 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -27,7 +27,7 @@ public: void setMode(int newMode); int mode{ 0 }; - glm::vec4 size{ 0.0f, 0.0f, 0.0f, 0.0f }; + glm::vec4 size{ 0.0f, -1.0f, 1.0f, 1.0f }; signals: void dirty(); }; From 9c5881345a9683f26623eda3bb656c8f05e74791 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 8 Feb 2016 12:34:45 -0800 Subject: [PATCH 71/73] Disable the AO gpuTimer pending cpp fix --- examples/utilities/tools/render/AO.qml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/utilities/tools/render/AO.qml b/examples/utilities/tools/render/AO.qml index 529177010d..fb2126befd 100644 --- a/examples/utilities/tools/render/AO.qml +++ b/examples/utilities/tools/render/AO.qml @@ -16,15 +16,16 @@ Item { height: 200 property QtObject config: Render.getConfig("AmbientOcclusion") - Timer { - interval: 500 - running: true - repeat: true - onTriggered: { parent.timer.text = config.gpuTime.toFixed(2) } - } + // TODO: Enable the gpuTimer when it is fixed in cpp + // Timer { + // interval: 500 + // running: true + // repeat: true + // onTriggered: { parent.timer.text = config.gpuTime.toFixed(2) } + // } + // Label { id: timer; x: 140 } Label { text: qsTr("Ambient Occlusion") } - Label { id: timer; x: 140 } CheckBox { y: 1 * 25 From 3bace3cdbb07636bc1d76764337cfbf57bef6a7e Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 9 Feb 2016 18:54:48 -0800 Subject: [PATCH 72/73] Rework the qml for brevity --- examples/utilities/tools/render/AO.qml | 102 ---------------- examples/utilities/tools/render/Buffer.qml | 99 ---------------- .../utilities/tools/render/ConfigSlider.qml | 58 +++++---- .../utilities/tools/render/ItemsSlider.qml | 59 ---------- examples/utilities/tools/render/Tone.qml | 49 -------- examples/utilities/tools/render/debug.js | 8 +- examples/utilities/tools/render/main.qml | 110 +++++++++++++----- .../render-utils/src/RenderDeferredTask.cpp | 6 +- .../render-utils/src/RenderDeferredTask.h | 20 +++- 9 files changed, 136 insertions(+), 375 deletions(-) delete mode 100644 examples/utilities/tools/render/AO.qml delete mode 100644 examples/utilities/tools/render/Buffer.qml delete mode 100644 examples/utilities/tools/render/ItemsSlider.qml delete mode 100644 examples/utilities/tools/render/Tone.qml diff --git a/examples/utilities/tools/render/AO.qml b/examples/utilities/tools/render/AO.qml deleted file mode 100644 index fb2126befd..0000000000 --- a/examples/utilities/tools/render/AO.qml +++ /dev/null @@ -1,102 +0,0 @@ -// -// AO.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -Item { - width: 200 - height: 200 - property QtObject config: Render.getConfig("AmbientOcclusion") - - // TODO: Enable the gpuTimer when it is fixed in cpp - // Timer { - // interval: 500 - // running: true - // repeat: true - // onTriggered: { parent.timer.text = config.gpuTime.toFixed(2) } - // } - // Label { id: timer; x: 140 } - - Label { text: qsTr("Ambient Occlusion") } - - CheckBox { - y: 1 * 25 - text: qsTr("Dithering") - partiallyCheckedEnabled: false - checked: parent.config.ditheringEnabled - onCheckedChanged: { parent.config.ditheringEnabled = checked } - } - - ConfigSlider { - y: 2 * 25 - config: parent.config - prop: "resolutionLevel" - label: qsTr("Resolution Level") - min: 0; max: 4 - } - ConfigSlider { - y: 3 * 25 - config: parent.config - prop: "obscuranceLevel" - label: qsTr("Obscurance Level") - min: 0; max: 1 - } - ConfigSlider { - y: 4 * 25 - config: parent.config - prop: "radius" - label: qsTr("Radius") - min: 0; max: 2 - } - ConfigSlider { - y: 5 * 25 - config: parent.config - prop: "numSamples" - label: qsTr("Samples") - min: 0; max: 32 - } - ConfigSlider { - y: 6 * 25 - config: parent.config - prop: "numSpiralTurns" - label: qsTr("Spiral Turns") - min: 0; max: 30 - } - ConfigSlider { - y: 7 * 25 - config: parent.config - prop: "falloffBias" - label: qsTr("Falloff Bias") - min: 0; max: 0.2 - } - ConfigSlider { - y: 8 * 25 - config: parent.config - prop: "edgeSharpness" - label: qsTr("Edge Sharpness") - min: 0; max: 1 - } - ConfigSlider { - y: 9 * 25 - config: parent.config - prop: "blurRadius" - label: qsTr("Blur Radius") - min: 0; max: 6 - } - ConfigSlider { - y: 10 * 25 - config: parent.config - prop: "blurDeviation" - label: qsTr("Blur Deviation") - min: 0; max: 3 - } -} - diff --git a/examples/utilities/tools/render/Buffer.qml b/examples/utilities/tools/render/Buffer.qml deleted file mode 100644 index bc4b654257..0000000000 --- a/examples/utilities/tools/render/Buffer.qml +++ /dev/null @@ -1,99 +0,0 @@ -// -// Buffer.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -Item { - width: 200 - height: 270 - - Label { - text: qsTr("Debug Buffer") - } - - ExclusiveGroup { id: buffer } - - function setDebugMode(mode) { - var debug = Render.getConfig("DebugDeferredBuffer"); - console.log(mode); - debug.enabled = (mode != 0); - debug.mode = mode; - } - - RadioButton { - x: 8; y: 19 + 0 * 23 - text: qsTr("Off") - exclusiveGroup: buffer - checked: true - onCheckedChanged: { if (checked) { setDebugMode(0) } } - } - RadioButton { - x: 8; y: 19 + 1 * 23 - text: qsTr("Diffuse") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(1) } } - } - RadioButton { - x: 8; y: 19 + 2 * 23 - text: qsTr("Metallic") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(2) } } - } - RadioButton { - x: 8; y: 19 + 3 * 23 - text: qsTr("Roughness") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(3) } } - } - RadioButton { - x: 8; y: 19 + 4 * 23 - text: qsTr("Normal") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(4) } } - } - RadioButton { - x: 8; y: 19 + 5 * 23 - text: qsTr("Depth") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(5) } } - } - RadioButton { - x: 8; y: 19 + 6 * 23 - text: qsTr("Lighting") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(6) } } - } - RadioButton { - x: 8; y: 19 + 7 * 23 - text: qsTr("Shadow") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(7) } } - } - RadioButton { - x: 8; y: 19 + 8 * 23 - text: qsTr("Pyramid Depth") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(8) } } - } - RadioButton { - x: 8; y: 19 + 9 * 23 - text: qsTr("Ambient Occlusion") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(9) } } - } - RadioButton { - x: 8; y: 19 + 10 * 23 - text: qsTr("Custom Shader") - exclusiveGroup: buffer - onCheckedChanged: { if (checked) { setDebugMode(10) } } - } -} - diff --git a/examples/utilities/tools/render/ConfigSlider.qml b/examples/utilities/tools/render/ConfigSlider.qml index 7c6498a7d2..5ee62dfe49 100644 --- a/examples/utilities/tools/render/ConfigSlider.qml +++ b/examples/utilities/tools/render/ConfigSlider.qml @@ -12,47 +12,55 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 Item { + id: root width: 400 height: 24 - property string label - property QtObject config - property string prop - property real min: 0.0 - property real max: 1.0 + property bool integral: false + property var config + property string property + property alias label: labelControl.text + property alias min: sliderControl.minimumValue + property alias max: sliderControl.maximumValue - function init() { - stat.text = config[prop].toFixed(2); - slider.value = (config[prop] - min) / (max - min); - } - Component.onCompleted: init() - - function update() { - var val = min + (max - min) * slider.value; - stat.text = val.toFixed(2); - config[prop] = val; + Component.onCompleted: { + // Binding favors qml value, so set it first + sliderControl.value = root.config[root.property]; + bindingControl.when = true; } Label { - text: parent.label - y: 7 - anchors.left: parent.left + id: labelControl + text: root.label + anchors.left: root.left anchors.leftMargin: 8 + anchors.top: root.top + anchors.topMargin: 7 } Label { - id: stat - y: 7 - anchors.left: parent.left + text: sliderControl.value.toFixed(root.integral ? 0 : 2) + anchors.left: root.left anchors.leftMargin: 140 + anchors.top: root.top + anchors.topMargin: 7 + } + + Binding { + id: bindingControl + target: root.config + property: root.property + value: sliderControl.value + when: false } Slider { - id: slider - y: 3 + id: sliderControl + stepSize: root.integral ? 1.0 : 0.0 width: 192 height: 20 - onValueChanged: parent.update() - anchors.right: parent.right + anchors.right: root.right anchors.rightMargin: 8 + anchors.top: root.top + anchors.topMargin: 3 } } diff --git a/examples/utilities/tools/render/ItemsSlider.qml b/examples/utilities/tools/render/ItemsSlider.qml deleted file mode 100644 index 1623284579..0000000000 --- a/examples/utilities/tools/render/ItemsSlider.qml +++ /dev/null @@ -1,59 +0,0 @@ -// -// ItemsSlider.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -Item { - width: 400 - height: 24 - property string label - property QtObject config - - function update() { - var val = slider.value; - var max = config.numDrawn; - var drawn = Math.round(val * max); - stat.text = drawn + " / " + max; - config.maxDrawn = (val == 1.0 ? -1 : drawn); - } - - Timer { - interval: 500 - running: true - repeat: true - onTriggered: parent.update() - } - - Label { - text: parent.label - y: 7 - anchors.left: parent.left - anchors.leftMargin: 8 - } - - Label { - id: stat - y: 7 - anchors.left: parent.left - anchors.leftMargin: 108 - } - - Slider { - id: slider - y: 3 - width: 192 - height: 20 - value: 1.0 - onValueChanged: update() - anchors.right: parent.right - anchors.rightMargin: 8 - } -} diff --git a/examples/utilities/tools/render/Tone.qml b/examples/utilities/tools/render/Tone.qml deleted file mode 100644 index a0eb4b1e00..0000000000 --- a/examples/utilities/tools/render/Tone.qml +++ /dev/null @@ -1,49 +0,0 @@ -// -// Tone.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -Item { - width: 400 - height: 24 - property string label: qsTr("Tone Mapping Exposure") - - function update() { - var val = (slider.value - 0.5) * 20; - stat.text = val.toFixed(2); - Render.getConfig("ToneMapping").exposure = val; - } - - Label { - text: parent.label - y: 7 - anchors.left: parent.left - anchors.leftMargin: 8 - } - - Label { - id: stat - y: 7 - anchors.left: parent.left - anchors.leftMargin: 150 - } - - Slider { - id: slider - y: 3 - width: 192 - height: 20 - value: Render.getConfig("ToneMapping").exposure - onValueChanged: parent.update() - anchors.right: parent.right - anchors.rightMargin: 8 - } -} diff --git a/examples/utilities/tools/render/debug.js b/examples/utilities/tools/render/debug.js index 896d2ac179..f195c607a4 100644 --- a/examples/utilities/tools/render/debug.js +++ b/examples/utilities/tools/render/debug.js @@ -11,8 +11,12 @@ oldConfig = Render.toJSON(); Render.RenderShadowTask.enabled = true; -Render.RenderDeferredTask.AmbientOcclusion.enabled = true; -Render.RenderDeferredTask.DebugDeferredBuffer.enabled = false; +var RDT = Render.RenderDeferredTask; +RDT.AmbientOcclusion.enabled = true; +RDT.DebugDeferredBuffer.enabled = false; +["DrawOpaqueDeferred", "DrawTransparentDeferred", "DrawOverlay3DOpaque", "DrawOverlay3DTransparent"] + .map(function(name) { return RDT[name]; }) + .forEach(function(job) { job.maxDrawn = job.numDrawn; }); // Set up the qml ui var qml = Script.resolvePath('main.qml'); diff --git a/examples/utilities/tools/render/main.qml b/examples/utilities/tools/render/main.qml index 729e91c4f8..9e825ad4df 100644 --- a/examples/utilities/tools/render/main.qml +++ b/examples/utilities/tools/render/main.qml @@ -11,54 +11,100 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -Item { - // Items - ItemsSlider { - y: 0 * 25 - label: qsTr("Opaque") - config: Render.getConfig("DrawOpaqueDeferred") - } - ItemsSlider { - y: 1 * 25 - label: qsTr("Transparent") - config: Render.getConfig("DrawTransparentDeferred") - } - ItemsSlider { - y: 2 * 25 - label: qsTr("Overlay3D") - config: Render.getConfig("DrawOverlay3D") +Column { + spacing: 8 + + Repeater { + model: [ "Opaque:DrawOpaqueDeferred", "Transparent:DrawTransparentDeferred", + "Opaque Overlays:DrawOverlay3DOpaque", "Transparent Overlays:DrawOverlay3DTransparent" ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: true + config: Render.getConfig(modelData.split(":")[1]) + property: "maxDrawn" + max: config.numDrawn + } } - // Draw status - Item { - y: 100 - + Row { CheckBox { text: qsTr("Display Status") - partiallyCheckedEnabled: false onCheckedChanged: { Render.getConfig("DrawStatus").showDisplay = checked } } CheckBox { - x: 200 text: qsTr("Network/Physics Status") - partiallyCheckedEnabled: false onCheckedChanged: { Render.getConfig("DrawStatus").showNetwork = checked } } } - // Tone mapping ConfigSlider { - y: 125 - config: Render.getConfig("ToneMapping") - prop: "exposure" label: qsTr("Tone Mapping Exposure") + config: Render.getConfig("ToneMapping") + property: "exposure" min: -10; max: 10 } - // Ambient occlusion - AO { y: 175 } + Column { + id: ambientOcclusion + property var config: Render.getConfig("AmbientOcclusion") - // Debug buffer - Buffer { y: 475 } + Label { text: qsTr("Ambient Occlusion") } + // TODO: Add gpuTimer + CheckBox { text: qsTr("Dithering"); checked: ambientOcclusion.config.ditheringEnabled } + Repeater { + model: [ + "Resolution Level:resolutionLevel:4", + "Obscurance Level:obscuranceLevel:1", + "Radius:radius:2", + "Falloff Bias:falloffBias:0.2", + "Edge Sharpness:edgeSharpness:1", + "Blur Radius:blurRadius:6", + "Blur Deviation:blurDeviation:3" + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + config: ambientOcclusion.config + property: modelData.split(":")[1] + max: modelData.split(":")[2] + } + } + Repeater { + model: [ + "Samples:numSamples:32", + "Spiral Turns:numSpiralTurns:30:" + ] + ConfigSlider { + label: qsTr(modelData.split(":")[0]) + integral: true + config: ambientOcclusion.config + property: modelData.split(":")[1] + max: modelData.split(":")[2] + } + } + } + + Column { + id: debug + property var config: Render.getConfig("DebugDeferredBuffer") + + function setDebugMode(mode) { + debug.config.enabled = (mode != 0); + debug.config.mode = mode; + } + + Label { text: qsTr("Debug Buffer") } + ExclusiveGroup { id: bufferGroup } + Repeater { + model: [ + "Off", "Diffuse", "Metallic", "Roughness", "Normal", "Depth", + "Lighting", "Shadow", "Pyramid Depth", "Ambient Occlusion", "Custom Shader" + ] + RadioButton { + text: qsTr(modelData) + exclusiveGroup: bufferGroup + checked: index == 0 + onCheckedChanged: if (checked) debug.setDebugMode(index); + } + } + } } - diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d3aeb23a50..30a49f93d5 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -142,7 +142,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont batch.setStateScissorRect(args->_viewport); args->_batch = &batch; - config->numDrawn = (int)inItems.size(); + config->setNumDrawn((int)inItems.size()); + emit config->numDrawnChanged(); glm::mat4 projMat; Transform viewMat; @@ -179,7 +180,8 @@ void DrawOverlay3D::run(const SceneContextPointer& sceneContext, const RenderCon inItems.emplace_back(id); } } - config->numItems = (int)inItems.size(); + config->setNumDrawn((int)inItems.size()); + emit config->numDrawnChanged(); if (!inItems.empty()) { RenderArgs* args = renderContext->args; diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 5d97de3905..3d072bce7b 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -40,15 +40,20 @@ public: class DrawConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(int numDrawn READ getNumDrawn) + Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY numDrawnChanged) Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) public: int getNumDrawn() { return numDrawn; } + void setNumDrawn(int num) { numDrawn = num; emit numDrawnChanged(); } - int numDrawn{ 0 }; int maxDrawn{ -1 }; + signals: + void numDrawnChanged(); void dirty(); + +protected: + int numDrawn{ 0 }; }; class DrawDeferred { @@ -86,15 +91,20 @@ public: class DrawOverlay3DConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(int numItems READ getNumItems) + Q_PROPERTY(int numDrawn READ getNumDrawn NOTIFY numDrawnChanged) Q_PROPERTY(int maxDrawn MEMBER maxDrawn NOTIFY dirty) public: - int getNumItems() { return numItems; } + int getNumDrawn() { return numDrawn; } + void setNumDrawn(int num) { numDrawn = num; emit numDrawnChanged(); } - int numItems{ 0 }; int maxDrawn{ -1 }; + signals: + void numDrawnChanged(); void dirty(); + +protected: + int numDrawn{ 0 }; }; class DrawOverlay3D { From 52cd4ae1a5876951699fd32c42f76e45cc18a5eb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 11 Feb 2016 09:34:21 -0800 Subject: [PATCH 73/73] make sure the AudioMixerClientData exists before using it --- assignment-client/src/audio/AudioMixer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 242b0b0d7a..51bc6cf8fd 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -484,7 +484,9 @@ void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { nodeList->eachNode([injectorClientData, &streamID](const SharedNodePointer& node){ auto listenerClientData = dynamic_cast(node->getLinkedData()); - listenerClientData->removeHRTFForStream(injectorClientData->getNodeID(), streamID); + if (listenerClientData) { + listenerClientData->removeHRTFForStream(injectorClientData->getNodeID(), streamID); + } }); } }