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

This commit is contained in:
Brad Hefta-Gaub 2016-10-08 11:50:10 -07:00
commit 3aa1c36f53
209 changed files with 7220 additions and 4333 deletions

2
.gitignore vendored
View file

@ -45,7 +45,7 @@ gvr-interface/libs/*
# ignore files for various dev environments
TAGS
*.swp
*.sw[po]
# ignore node files for the console
node_modules

View file

@ -48,8 +48,7 @@ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
Agent::Agent(ReceivedMessage& message) :
ThreadedAssignment(message),
_entityEditSender(),
_receivedAudioStream(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO,
RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES) {
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES) {
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
ResourceManager::init();
@ -62,6 +61,7 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::set<recording::Deck>();
DependencyManager::set<recording::Recorder>();
DependencyManager::set<RecordingScriptingInterface>();
DependencyManager::set<ScriptCache>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
@ -351,6 +351,21 @@ void Agent::setIsAvatar(bool isAvatar) {
_avatarIdentityTimer->stop();
delete _avatarIdentityTimer;
_avatarIdentityTimer = nullptr;
// The avatar mixer never times out a connection (e.g., based on identity or data packets)
// but rather keeps avatars in its list as long as "connected". As a result, clients timeout
// when we stop sending identity, but then get woken up again by the mixer itself, which sends
// identity packets to everyone. Here we explicitly tell the mixer to kill the entry for us.
auto nodeList = DependencyManager::get<NodeList>();
auto packetList = NLPacketList::create(PacketType::KillAvatar, QByteArray(), true, true);
packetList->write(getSessionUUID().toRfc4122());
nodeList->eachMatchingNode(
[&](const SharedNodePointer& node)->bool {
return node->getType() == NodeType::AvatarMixer && node->getActiveSocket();
},
[&](const SharedNodePointer& node) {
nodeList->sendPacketList(std::move(packetList), *node);
});
}
}
}

View file

@ -1,21 +0,0 @@
set(EXTERNAL_NAME oglplus)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/oglplus-0.63.0.zip
URL_MD5 de984ab245b185b45c87415c0e052135
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
# Hide this external target (for ide users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include ${SOURCE_DIR}/implement CACHE TYPE INTERNAL)

View file

@ -6,8 +6,8 @@ if (WIN32)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi2.zip
URL_MD5 272b27bd6c211c45c0c23d4701b63b5e
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip
URL_MD5 1a2433f80a788a54c70f505ff4f43ac1
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""

View file

@ -44,13 +44,25 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT)
set(QTAUDIO_PATH $<TARGET_FILE_DIR:${TARGET_NAME}>/audio)
# if present, replace qtaudio_windows.dll with qtaudio_wasapi.dll
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windows.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windowsd.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.pdb ${QTAUDIO_PATH} )
)
if (DEPLOY_PACKAGE)
# copy qtaudio_wasapi.dll alongside qtaudio_windows.dll, and let the installer resolve
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.pdb ${QTAUDIO_PATH} )
)
elseif (${CMAKE_SYSTEM_VERSION} VERSION_LESS 6.2)
# continue using qtaudio_windows.dll on Windows 7
else ()
# replace qtaudio_windows.dll with qtaudio_wasapi.dll on Windows 8/8.1/10
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windows.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windowsd.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.pdb ${QTAUDIO_PATH} )
)
endif ()
endif ()
endmacro()

View file

@ -1,21 +0,0 @@
#
# Copyright 2015 High Fidelity, Inc.
# Created by Bradley Austin Davis on 2015/10/10
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(TARGET_OGLPLUS)
# our OGL plus setup requires glew
target_glew()
# our OGL plus setup requires boostconfig
add_dependency_external_projects(boostconfig)
find_package(BoostConfig REQUIRED)
target_include_directories(${TARGET_NAME} PUBLIC ${BOOSTCONFIG_INCLUDE_DIRS})
add_dependency_external_projects(oglplus)
find_package(OGLPLUS REQUIRED)
target_include_directories(${TARGET_NAME} PUBLIC ${OGLPLUS_INCLUDE_DIRS})
endmacro()

View file

@ -6,6 +6,7 @@
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(TARGET_OPENGL)
add_definitions(-DGLEW_STATIC)
if (APPLE)
# link in required OS X frameworks and include the right GL headers
find_library(OpenGL OpenGL)

View file

@ -1,24 +0,0 @@
#
# Try to find OGLPLUS include path.
# Once done this will define
#
# OGLPLUS_INCLUDE_DIRS
#
# Created by Bradley Austin Davis on 2015/05/22
# 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
#
# setup hints for OGLPLUS search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("oglplus")
# locate header
find_path(OGLPLUS_INCLUDE_DIRS "oglplus/fwd.hpp" HINTS ${OGLPLUS_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(OGLPLUS DEFAULT_MSG OGLPLUS_INCLUDE_DIRS)
mark_as_advanced(OGLPLUS_INCLUDE_DIRS OGLPLUS_SEARCH_DIRS)

View file

@ -23,6 +23,11 @@
;Default installation folder
InstallDir "@CPACK_NSIS_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_DIRECTORY@"
;--------------------------------
;Include WinVer to get Windows version
!include "WinVer.nsh"
;--------------------------------
;General
; leverage the UAC NSIS plugin to promote uninstaller to elevated privileges
@ -600,8 +605,16 @@ Section "-Core installation"
Delete "$INSTDIR\version"
Delete "$INSTDIR\xinput1_3.dll"
;Delete old Qt files
Delete "$INSTDIR\audio\qtaudio_windows.dll"
; The installer includes two different Qt audio plugins.
; On Windows 8 and above, only qtaudio_wasapi.dll should be installed.
; On Windows 7 and below, only qtaudio_windows.dll should be installed.
${If} ${AtLeastWin8}
Delete "$INSTDIR\audio\qtaudio_windows.dll"
Delete "$INSTDIR\audio\qtaudio_windows.pdb"
${Else}
Delete "$INSTDIR\audio\qtaudio_wasapi.dll"
Delete "$INSTDIR\audio\qtaudio_wasapi.pdb"
${EndIf}
; Delete old desktop shortcuts before they were renamed during Sandbox rename
Delete "$DESKTOP\@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@.lnk"

View file

@ -0,0 +1,71 @@
<!-- Copyright 2016 High Fidelity, Inc. -->
<html>
<head>
<meta charset="utf-8"/>
<input type="hidden" id="version" value="1"/>
<title>Welcome to Interface</title>
<style>
body {
background: black;
width: 100%;
overflow-x: hidden;
margin: 0;
padding: 0;
}
#kbm_button {
position: absolute;
left: 70;
top: 118;
width: 297;
height: 80;
}
#hand_controllers_button {
position: absolute;
left: 367;
top: 118;
width: 267;
height: 80;
}
#game_controller_button {
position: absolute;
left: 634;
top: 118;
width: 297;
height: 80;
}
#image_area {
width: 1024;
height: 720;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
}
</style>
<script>
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-keyboard.png");
}
function showHandControllers() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-vive.png");
}
function showGameController() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-gamepad.png");
}
</script>
</head>
<body>
<div id="image_area">
<img id="main_image" src="img/controls-help-keyboard.png" width="1024px" height="720px"></img>
<a href="#" id="kbm_button" onmousedown="showKbm()"></a>
<a href="#" id="hand_controllers_button" onmousedown="showHandControllers()"></a>
<a href="#" id="game_controller_button" onmousedown="showGameController()"></a>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -11,9 +11,12 @@
var POLL_FREQUENCY = 500; // ms
var MAX_WARNINGS = 3;
var numWarnings = 0;
var isKeyboardRaised = false;
var isNumericKeyboard = false;
var KEYBOARD_HEIGHT = 200;
function shouldRaiseKeyboard() {
if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") {
if (document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA") {
return true;
} else {
// check for contenteditable attribute
@ -27,15 +30,39 @@
}
};
function shouldSetNumeric() {
return document.activeElement.type === "number";
};
setInterval(function () {
var event = shouldRaiseKeyboard() ? "_RAISE_KEYBOARD" : "_LOWER_KEYBOARD";
if (typeof EventBridge != "undefined") {
EventBridge.emitWebEvent(event);
} else {
if (numWarnings < MAX_WARNINGS) {
console.log("WARNING: no global EventBridge object found");
numWarnings++;
var keyboardRaised = shouldRaiseKeyboard();
var numericKeyboard = shouldSetNumeric();
if (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard) {
if (typeof EventBridge !== "undefined") {
EventBridge.emitWebEvent(
keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "")) : "_LOWER_KEYBOARD"
);
} else {
if (numWarnings < MAX_WARNINGS) {
console.log("WARNING: no global EventBridge object found");
numWarnings++;
}
}
if (!isKeyboardRaised) {
var delta = document.activeElement.getBoundingClientRect().bottom + 10
- (document.body.clientHeight - KEYBOARD_HEIGHT);
if (delta > 0) {
setTimeout(function () {
document.body.scrollTop += delta;
}, 500); // Allow time for keyboard to be raised in QML.
}
}
isKeyboardRaised = keyboardRaised;
isNumericKeyboard = numericKeyboard;
}
}, POLL_FREQUENCY);
})();

View file

@ -15,6 +15,7 @@ import "styles"
import "windows"
import "hifi"
import "hifi/toolbars"
import "styles-uit" as HifiStyles
import "controls-uit" as HifiControls
Window {
@ -58,21 +59,31 @@ Window {
}
addressLine.text = targetString;
toggleOrGo(true);
clearAddressLineTimer.start();
}
property var allStories: [];
property int cardWidth: 200;
property int cardHeight: 152;
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property bool isCursorVisible: false // Override default cursor visibility.
AddressBarDialog {
id: addressBarDialog
property bool keyboardRaised: false
property bool punctuationMode: false
implicitWidth: backgroundImage.width
implicitHeight: backgroundImage.height
implicitHeight: backgroundImage.height + (keyboardRaised ? 200 : 0)
// The buttons have their button state changed on hover, so we have to manually fix them up here
onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0;
onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0;
onReceivedHifiSchemeURL: resetAfterTeleport();
// Update location after using back and forward buttons.
onMetaverseServerUrlChanged: updateLocationTextTimer.start();
ListModel { id: suggestions }
ListView {
@ -168,7 +179,24 @@ Window {
}
}
// FIXME replace with TextField
HifiStyles.RalewayLight {
id: notice;
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.50;
anchors {
top: parent.top
topMargin: parent.inputAreaStep + 12
left: addressLine.left
right: addressLine.right
}
}
HifiStyles.FiraSansRegular {
id: location;
font.pixelSize: addressLine.font.pixelSize;
color: "gray";
clip: true;
anchors.fill: addressLine;
visible: addressLine.text.length === 0
}
TextInput {
id: addressLine
focus: true
@ -179,20 +207,57 @@ Window {
right: parent.right
leftMargin: forwardArrow.width
rightMargin: forwardArrow.width / 2
topMargin: parent.inputAreaStep + hifi.layout.spacing
bottomMargin: parent.inputAreaStep + hifi.layout.spacing
topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing)
bottomMargin: parent.inputAreaStep
}
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75
helperText: "Go to: place, @user, /path, network address"
helperPixelSize: font.pixelSize * 0.75
helperItalic: true
onTextChanged: filterChoicesByText()
cursorVisible: false
onTextChanged: {
filterChoicesByText();
updateLocationText(text.length > 0);
if (!isCursorVisible && text.length > 0) {
isCursorVisible = true;
cursorVisible = true;
}
}
onActiveFocusChanged: {
cursorVisible = isCursorVisible;
}
MouseArea {
// If user clicks in address bar show cursor to indicate ability to enter address.
anchors.fill: parent
onClicked: {
isCursorVisible = true;
parent.cursorVisible = true;
}
}
}
}
Timer {
// Delay updating location text a bit to avoid flicker of content and so that connection status is valid.
id: updateLocationTextTimer
running: false
interval: 500 // ms
repeat: false
onTriggered: updateLocationText(false);
}
Timer {
// Delay clearing address line so as to avoid flicker of "not connected" being displayed after entering an address.
id: clearAddressLineTimer
running: false
interval: 100 // ms
repeat: false
onTriggered: {
addressLine.text = "";
isCursorVisible = false;
}
}
Window {
width: 938;
height: 625;
width: 938
height: 625
scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25)
HifiControls.WebView {
anchors.fill: parent;
@ -209,6 +274,35 @@ Window {
horizontalCenter: scroll.horizontalCenter;
}
}
// virtual keyboard, letters
HifiControls.Keyboard {
id: keyboard1
y: parent.keyboardRaised ? parent.height : 0
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && !parent.punctuationMode
enabled: parent.keyboardRaised && !parent.punctuationMode
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
}
HifiControls.KeyboardPunctuation {
id: keyboard2
y: parent.keyboardRaised ? parent.height : 0
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && parent.punctuationMode
enabled: parent.keyboardRaised && parent.punctuationMode
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
}
}
function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects.
@ -344,12 +438,23 @@ Window {
});
}
onVisibleChanged: {
if (visible) {
addressLine.forceActiveFocus()
fillDestinations();
function updateLocationText(enteringAddress) {
if (enteringAddress) {
notice.text = "Go to a place, @user, path or network address";
notice.color = "gray";
} else {
addressLine.text = ""
notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected";
notice.color = AddressManager.isConnected ? "gray" : "crimson";
// Display hostname, which includes ip address, localhost, and other non-placenames.
location.text = (AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '');
}
}
onVisibleChanged: {
updateLocationText(false);
if (visible) {
addressLine.forceActiveFocus();
fillDestinations();
}
}
@ -365,11 +470,13 @@ Window {
case Qt.Key_Escape:
case Qt.Key_Back:
root.shown = false
clearAddressLineTimer.start();
event.accepted = true
break
case Qt.Key_Enter:
case Qt.Key_Return:
toggleOrGo()
clearAddressLineTimer.start();
event.accepted = true
break
}

View file

@ -1,5 +1,6 @@
import QtQuick 2.5
import QtQuick.Controls 1.2
import QtWebChannel 1.0
import QtWebEngine 1.2
import "controls-uit"
@ -19,6 +20,9 @@ ScrollingWindow {
property variant permissionsBar: {'securityOrigin':'none','feature':'none'}
property alias url: webview.url
property alias webView: webview
property alias eventBridge: eventBridgeWrapper.eventBridge
x: 100
y: 100
@ -130,10 +134,11 @@ ScrollingWindow {
case Qt.Key_Return:
event.accepted = true
if (text.indexOf("http") != 0) {
text = "http://" + text
text = "http://" + text;
}
root.hidePermissionsBar();
webview.url = text
root.keyboardRaised = false;
webview.url = text;
break;
}
}
@ -197,32 +202,60 @@ ScrollingWindow {
}
}
WebEngineView {
WebView {
id: webview
url: "https://highfidelity.com"
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
webChannel.registeredObjects: [eventBridgeWrapper]
// Create a global EventBridge object for raiseAndLowerKeyboard.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.DocumentCreation
worldId: WebEngineScript.MainWorld
}
// Detect when may want to raise and lower keyboard.
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
anchors.top: buttons.bottom
anchors.topMargin: 8
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
onFeaturePermissionRequested: {
permissionsBar.securityOrigin = securityOrigin;
permissionsBar.feature = feature;
root.showPermissionsBar();
}
onLoadingChanged: {
if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
addressBar.text = loadRequest.url
}
}
onIconChanged: {
console.log("New icon: " + icon)
}
onNewViewRequested: {
var component = Qt.createComponent("Browser.qml");
var newWindow = component.createObject(desktop);
request.openIn(newWindow.webView)
}
onWindowCloseRequested: {
root.destroy();
}
@ -230,8 +263,6 @@ ScrollingWindow {
Component.onCompleted: {
desktop.initWebviewProfileHandlers(webview.profile)
}
profile: desktop.browserProfile
}
} // item

View file

@ -66,6 +66,24 @@ Windows.ScrollingWindow {
anchors.fill: parent
focus: true
webChannel.registeredObjects: [eventBridgeWrapper]
// Create a global EventBridge object for raiseAndLowerKeyboard.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.DocumentCreation
worldId: WebEngineScript.MainWorld
}
// Detect when may want to raise and lower keyboard.
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
}
}
}

View file

@ -19,6 +19,7 @@ import "windows"
import "controls-uit"
import "styles-uit"
ScrollingWindow {
id: toolWindow
resizable: true
@ -47,92 +48,101 @@ ScrollingWindow {
property alias y: toolWindow.y
}
TabView {
id: tabView;
Item {
id: toolWindowTabViewItem
height: pane.scrollHeight
width: pane.contentWidth
height: pane.scrollHeight // Pane height so that don't use Window's scrollbars otherwise tabs may be scrolled out of view.
property int tabCount: 0
anchors.left: parent.left
anchors.top: parent.top
Repeater {
model: 4
Tab {
// Force loading of the content even if the tab is not visible
// (required for letting the C++ code access the webview)
active: true
enabled: false
property string originalUrl: "";
TabView {
id: tabView
width: pane.contentWidth
// Pane height so that don't use Window's scrollbars otherwise tabs may be scrolled out of view.
height: pane.scrollHeight
property int tabCount: 0
WebView {
id: webView;
anchors.fill: parent
Repeater {
model: 4
Tab {
// Force loading of the content even if the tab is not visible
// (required for letting the C++ code access the webview)
active: true
enabled: false
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
property string originalUrl: ""
WebView {
id: webView
anchors.fill: parent
enabled: false
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge
}
webChannel.registeredObjects: [eventBridgeWrapper]
onEnabledChanged: toolWindow.updateVisiblity()
}
}
}
style: TabViewStyle {
frame: Rectangle { // Background shown before content loads.
anchors.fill: parent
color: hifi.colors.baseGray
}
frameOverlap: 0
tab: Rectangle {
implicitWidth: text.width
implicitHeight: 3 * text.height
color: styleData.selected ? hifi.colors.black : hifi.colors.tabBackgroundDark
RalewayRegular {
id: text
text: styleData.title
font.capitalization: Font.AllUppercase
size: hifi.fontSizes.tabName
width: tabView.tabCount > 1 ? styleData.availableWidth / tabView.tabCount : implicitWidth + 2 * hifi.dimensions.contentSpacing.x
elide: Text.ElideRight
color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.lightGrayText
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
webChannel.registeredObjects: [eventBridgeWrapper]
onEnabledChanged: toolWindow.updateVisiblity();
}
}
}
style: TabViewStyle {
frame: Rectangle { // Background shown before content loads.
anchors.fill: parent
color: hifi.colors.baseGray
}
frameOverlap: 0
tab: Rectangle {
implicitWidth: text.width
implicitHeight: 3 * text.height
color: styleData.selected ? hifi.colors.black : hifi.colors.tabBackgroundDark
RalewayRegular {
id: text
text: styleData.title
font.capitalization: Font.AllUppercase
size: hifi.fontSizes.tabName
width: tabView.tabCount > 1 ? styleData.availableWidth / tabView.tabCount : implicitWidth + 2 * hifi.dimensions.contentSpacing.x
elide: Text.ElideRight
color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.lightGrayText
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
Rectangle { // Separator.
width: 1
height: parent.height
color: hifi.colors.black
anchors.left: parent.left
anchors.top: parent.top
visible: styleData.index > 0
Rectangle {
Rectangle { // Separator.
width: 1
height: 1
color: hifi.colors.baseGray
height: parent.height
color: hifi.colors.black
anchors.left: parent.left
anchors.top: parent.top
visible: styleData.index > 0
Rectangle {
width: 1
height: 1
color: hifi.colors.baseGray
anchors.left: parent.left
anchors.bottom: parent.bottom
}
}
Rectangle { // Active underline.
width: parent.width - (styleData.index > 0 ? 1 : 0)
height: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.baseGray
}
}
Rectangle { // Active underline.
width: parent.width - (styleData.index > 0 ? 1 : 0)
height: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.baseGray
}
tabOverlap: 0
}
tabOverlap: 0
}
}
@ -224,7 +234,6 @@ ScrollingWindow {
return;
}
if (properties.width) {
tabView.width = Math.min(Math.max(tabView.width, properties.width), toolWindow.maxSize.x);
}

View file

@ -9,7 +9,7 @@
//
import QtQuick 2.5
import QtWebEngine 1.1
import QtWebEngine 1.2
WebEngineView {
id: root

View file

@ -109,8 +109,13 @@ Column {
}
MouseArea {
// Events are propogated so that any active control is defocused.
anchors.fill: parent
onClicked: toggleCollapsed()
propagateComposedEvents: true
onPressed: {
toggleCollapsed();
mouse.accepted = false;
}
}
}

View file

@ -304,13 +304,13 @@ Item {
Key {
id: key31
width: 43
glyph: ","
glyph: "_"
}
Key {
id: key33
width: 43
glyph: "."
glyph: "?"
}
Key {

View file

@ -208,49 +208,49 @@ Item {
Key {
id: key22
width: 43
glyph: "_"
glyph: ","
}
Key {
id: key23
width: 43
glyph: ";"
glyph: "."
}
Key {
id: key24
width: 43
glyph: ":"
glyph: ";"
}
Key {
id: key25
width: 43
glyph: "'"
glyph: ":"
}
Key {
id: key26
width: 43
glyph: "\""
glyph: "'"
}
Key {
id: key31
width: 43
glyph: "<"
glyph: "\""
}
Key {
id: key33
width: 43
glyph: ">"
glyph: "<"
}
Key {
id: key36
width: 43
glyph: "?"
glyph: ">"
}
}

View file

@ -1,6 +1,7 @@
import QtQuick 2.5
import QtWebEngine 1.1
import QtWebChannel 1.0
import "../controls-uit" as HiFiControls
Item {
property alias url: root.url
@ -105,7 +106,7 @@ Item {
}
// virtual keyboard, letters
Keyboard {
HiFiControls.Keyboard {
id: keyboard1
y: keyboardRaised ? parent.height : 0
height: keyboardRaised ? 200 : 0
@ -119,7 +120,7 @@ Item {
anchors.bottomMargin: 0
}
KeyboardPunctuation {
HiFiControls.KeyboardPunctuation {
id: keyboard2
y: keyboardRaised ? parent.height : 0
height: keyboardRaised ? 200 : 0

View file

@ -22,6 +22,7 @@ ModalWindow {
implicitWidth: 640;
implicitHeight: 320;
visible: true;
keyboardEnabled: false // Disable ModalWindow's keyboard.
signal selected(var result);
signal canceled();
@ -50,6 +51,10 @@ ModalWindow {
}
}
property bool keyboardRaised: false
property bool punctuationMode: false
onKeyboardRaisedChanged: d.resize();
property var warning: "";
property var result;
@ -110,7 +115,9 @@ ModalWindow {
var targetWidth = Math.max(titleWidth, pane.width);
var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) +
(extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) +
(buttons.height + 3 * hifi.dimensions.contentSpacing.y);
(buttons.height + 3 * hifi.dimensions.contentSpacing.y) +
(root.keyboardRaised ? (200 + hifi.dimensions.contentSpacing.y) : 0);
root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth);
root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ?
d.maxHeight : targetHeight);
@ -130,7 +137,6 @@ ModalWindow {
left: parent.left;
right: parent.right;
margins: 0;
bottomMargin: hifi.dimensions.contentSpacing.y;
}
// FIXME make a text field type that can be bound to a history for autocompletion
@ -142,7 +148,43 @@ ModalWindow {
anchors {
left: parent.left;
right: parent.right;
bottom: parent.bottom;
bottom: keyboard.top;
bottomMargin: hifi.dimensions.contentSpacing.y;
}
}
Item {
id: keyboard
height: keyboardRaised ? 200 : 0
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
bottomMargin: keyboardRaised ? hifi.dimensions.contentSpacing.y : 0
}
Keyboard {
id: keyboard1
visible: keyboardRaised && !punctuationMode
enabled: keyboardRaised && !punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
KeyboardPunctuation {
id: keyboard2
visible: keyboardRaised && punctuationMode
enabled: keyboardRaised && punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
}
}

View file

@ -27,7 +27,7 @@ ModalWindow {
id: root
resizable: true
implicitWidth: 480
implicitHeight: 360
implicitHeight: 360 + (fileDialogItem.keyboardRaised ? 200 + hifi.dimensions.contentSpacing.y : 0)
minSize: Qt.vector2d(360, 240)
draggable: true
@ -100,16 +100,23 @@ ModalWindow {
}
Item {
id: fileDialogItem
clip: true
width: pane.width
height: pane.height
anchors.margins: 0
property bool keyboardRaised: false
property bool punctuationMode: false
MouseArea {
// Clear selection when click on internal unused area.
anchors.fill: parent
drag.target: root
onClicked: d.clearSelection()
onClicked: {
d.clearSelection();
frame.forceActiveFocus(); // Defocus text field so that the keyboard gets hidden.
}
}
Row {
@ -619,7 +626,7 @@ ModalWindow {
left: parent.left
right: selectionType.visible ? selectionType.left: parent.right
rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0
bottom: buttonRow.top
bottom: keyboard1.top
bottomMargin: hifi.dimensions.contentSpacing.y
}
readOnly: !root.saveDialog
@ -640,6 +647,28 @@ ModalWindow {
KeyNavigation.right: openButton
}
Keyboard {
id: keyboard1
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && !parent.punctuationMode
enabled: visible
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: buttonRow.top
anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0
}
KeyboardPunctuation {
id: keyboard2
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && parent.punctuationMode
enabled: visible
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: buttonRow.top
anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0
}
Row {
id: buttonRow
anchors {

View file

@ -97,9 +97,9 @@ ScrollingWindow {
footer: Row {
anchors {
right: parent.right;
top: parent.top
right: parent.right
rightMargin: hifi.dimensions.contentMargin.x
verticalCenter: parent.verticalCenter
}
spacing: hifi.dimensions.contentSpacing.x

View file

@ -53,11 +53,17 @@ ModalWindow {
}
Item {
id: modalWindowItem
clip: true
width: pane.width
height: pane.height
anchors.margins: 0
property bool keyboardRaised: false
property bool punctuationMode: false
onKeyboardRaisedChanged: d.resize();
QtObject {
id: d
readonly property int minWidth: 480
@ -69,14 +75,14 @@ ModalWindow {
var targetWidth = Math.max(titleWidth, pane.width)
var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height
root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth)
root.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)
root.height = ((targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + (modalWindowItem.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0)
}
}
Item {
anchors {
top: parent.top
bottom: buttons.top;
bottom: keyboard1.top;
left: parent.left;
right: parent.right;
margins: 0
@ -110,6 +116,35 @@ ModalWindow {
}
}
// virtual keyboard, letters
Keyboard {
id: keyboard1
y: parent.keyboardRaised ? parent.height : 0
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && !parent.punctuationMode
enabled: parent.keyboardRaised && !parent.punctuationMode
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.bottom: buttons.top
anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
}
KeyboardPunctuation {
id: keyboard2
y: parent.keyboardRaised ? parent.height : 0
height: parent.keyboardRaised ? 200 : 0
visible: parent.keyboardRaised && parent.punctuationMode
enabled: parent.keyboardRaised && parent.punctuationMode
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.bottom: buttons.top
anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
}
Flow {
id: buttons
focus: true

View file

@ -45,7 +45,7 @@ ScrollingWindow {
Rectangle {
width: parent.width
height: root.height
height: root.height - (keyboardRaised ? 200 : 0)
radius: 4
color: hifi.colors.baseGray
@ -128,6 +128,10 @@ ScrollingWindow {
}
onCountChanged: MyAvatar.setAttachmentsVariant(attachments);
}
function scrollBy(delta) {
flickableItem.contentY += delta;
}
}
}
@ -204,5 +208,22 @@ ScrollingWindow {
}
}
}
onKeyboardRaisedChanged: {
if (keyboardRaised) {
// Scroll to item with focus if necessary.
var footerHeight = newAttachmentButton.height + buttonRow.height + 3 * hifi.dimensions.contentSpacing.y;
var delta = activator.mouseY
- (activator.height + activator.y - 200 - footerHeight - hifi.dimensions.controlLineHeight);
if (delta > 0) {
scrollView.scrollBy(delta);
} else {
// HACK: Work around for case where are 100% scrolled; stops window from erroneously scrolling to 100% when show keyboard.
scrollView.scrollBy(-1);
scrollView.scrollBy(1);
}
}
}
}

View file

@ -30,7 +30,7 @@ ScrollingWindow {
Rectangle {
width: parent.width
height: root.height
height: root.height - (keyboardRaised ? 200 : 0)
radius: 4
color: hifi.colors.baseGray

View file

@ -1,3 +1,4 @@
//
// Window.qml
//
@ -15,6 +16,7 @@ import QtGraphicalEffects 1.0
import "."
import "../styles-uit"
import "../controls-uit" as HiFiControls
// FIXME how do I set the initial position of a window without
// overriding places where the a individual client of the window
@ -23,12 +25,18 @@ import "../styles-uit"
// FIXME how to I enable dragging without allowing the window to lay outside
// of the desktop? How do I ensure when the desktop resizes all the windows
// are still at least partially visible?
Window {
id: window
HifiConstants { id: hifi }
children: [ swallower, frame, pane, activator ]
children: [ swallower, frame, defocuser, pane, activator ]
property var footer: Item { } // Optional static footer at the bottom of the dialog.
readonly property var footerContentHeight: footer.height > 0 ? (footer.height + 2 * hifi.dimensions.contentSpacing.y + 3) : 0
property bool keyboardEnabled: true // Set false if derived control implements its own keyboard.
property bool keyboardRaised: false
property bool punctuationMode: false
// Scrollable window content.
// FIXME this should not define any visual content in this type. The base window
@ -73,7 +81,7 @@ Window {
verticalScrollBarPolicy: Qt.ScrollBarAsNeeded
anchors.fill: parent
anchors.rightMargin: parent.isScrolling ? 1 : 0
anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0
anchors.bottomMargin: footerPane.height
style: ScrollViewStyle {
@ -116,21 +124,36 @@ Window {
}
}
function scrollBy(delta) {
scrollView.flickableItem.contentY += delta;
}
Rectangle {
// Optional non-scrolling footer.
id: footerPane
property alias keyboardEnabled: window.keyboardEnabled
property alias keyboardRaised: window.keyboardRaised
property alias punctuationMode: window.punctuationMode
anchors {
left: parent.left
bottom: parent.bottom
}
width: parent.contentWidth
height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3
height: footerContentHeight + (keyboardEnabled && keyboardRaised ? 200 : 0)
color: hifi.colors.baseGray
visible: footer.height > 0
visible: footer.height > 0 || keyboardEnabled && keyboardRaised
Item {
// Horizontal rule.
anchors.fill: parent
anchors {
top: parent.top
left: parent.left
right: parent.right
}
visible: footer.height > 0
Rectangle {
width: parent.width
@ -148,10 +171,53 @@ Window {
}
Item {
anchors.fill: parent
anchors.topMargin: 3 // Horizontal rule.
anchors {
left: parent.left
right: parent.right
top: parent.top
topMargin: hifi.dimensions.contentSpacing.y + 3
}
children: [ footer ]
}
HiFiControls.Keyboard {
id: keyboard1
height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0
visible: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode
enabled: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
HiFiControls.KeyboardPunctuation {
id: keyboard2
height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0
visible: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode
enabled: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
}
}
onKeyboardRaisedChanged: {
if (keyboardEnabled && keyboardRaised) {
var delta = activator.mouseY
- (activator.height + activator.y - 200 - footerContentHeight - hifi.dimensions.controlLineHeight);
if (delta > 0) {
pane.scrollBy(delta);
} else {
// HACK: Work around for case where are 100% scrolled; stops window from erroneously scrolling to 100% when show keyboard.
pane.scrollBy(-1);
pane.scrollBy(1);
}
}
}
}

View file

@ -44,7 +44,7 @@ Fadable {
implicitHeight: content ? content.height : 0
implicitWidth: content ? content.width : 0
x: desktop.invalid_position; y: desktop.invalid_position;
children: [ swallower, frame, content, activator ]
children: [ swallower, frame, defocuser, content, activator ]
//
// Custom properties
@ -122,6 +122,21 @@ Fadable {
}
}
// This mouse area defocuses the current control so that the HMD keyboard gets hidden.
property var defocuser: MouseArea {
width: frame.decoration ? frame.decoration.width : window.width
height: frame.decoration ? frame.decoration.height : window.height
x: frame.decoration ? frame.decoration.anchors.leftMargin : 0
y: frame.decoration ? frame.decoration.anchors.topMargin : 0
propagateComposedEvents: true
acceptedButtons: Qt.AllButtons
enabled: window.visible
onPressed: {
frame.forceActiveFocus();
mouse.accepted = false;
}
}
// This mouse area serves to swallow mouse events while the mouse is over the window
// to prevent things like mouse wheel events from reaching the application and changing
// the camera if the user is scrolling through a list and gets to the end.

View file

@ -1,78 +0,0 @@
//
// Created by Bradley Austin Davis on 2016/07/11
// Copyright 2013-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
//
#version 410 core
uniform sampler2D sampler;
uniform mat3 reprojection = mat3(1);
uniform mat4 inverseProjections[2];
uniform mat4 projections[2];
in vec2 vTexCoord;
in vec3 vPosition;
out vec4 FragColor;
void main() {
vec2 uv = vTexCoord;
mat4 eyeInverseProjection;
mat4 eyeProjection;
float xoffset = 1.0;
vec2 uvmin = vec2(0.0);
vec2 uvmax = vec2(1.0);
// determine the correct projection and inverse projection to use.
if (vTexCoord.x < 0.5) {
uvmax.x = 0.5;
eyeInverseProjection = inverseProjections[0];
eyeProjection = projections[0];
} else {
xoffset = -1.0;
uvmin.x = 0.5;
uvmax.x = 1.0;
eyeInverseProjection = inverseProjections[1];
eyeProjection = projections[1];
}
// Account for stereo in calculating the per-eye NDC coordinates
vec4 ndcSpace = vec4(vPosition, 1.0);
ndcSpace.x *= 2.0;
ndcSpace.x += xoffset;
// Convert from NDC to eyespace
vec4 eyeSpace = eyeInverseProjection * ndcSpace;
eyeSpace /= eyeSpace.w;
// Convert to a noramlized ray
vec3 ray = eyeSpace.xyz;
ray = normalize(ray);
// Adjust the ray by the rotation
ray = reprojection * ray;
// Project back on to the texture plane
ray *= eyeSpace.z / ray.z;
// Update the eyespace vector
eyeSpace.xyz = ray;
// Reproject back into NDC
ndcSpace = eyeProjection * eyeSpace;
ndcSpace /= ndcSpace.w;
ndcSpace.x -= xoffset;
ndcSpace.x /= 2.0;
// Calculate the new UV coordinates
uv = (ndcSpace.xy / 2.0) + 0.5;
if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) {
FragColor = vec4(0.0, 0.0, 0.0, 1.0);
} else {
FragColor = texture(sampler, uv);
}
}

View file

@ -1,20 +0,0 @@
//
// Created by Bradley Austin Davis on 2016/07/11
// Copyright 2013-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
//
#version 410 core
in vec3 Position;
in vec2 TexCoord;
out vec3 vPosition;
out vec2 vTexCoord;
void main() {
gl_Position = vec4(Position, 1);
vTexCoord = TexCoord;
vPosition = Position;
}

View file

@ -87,6 +87,7 @@
#include <PhysicsEngine.h>
#include <PhysicsHelpers.h>
#include <plugins/PluginManager.h>
#include <plugins/PluginUtils.h>
#include <plugins/CodecPlugin.h>
#include <RecordingScriptingInterface.h>
#include <RenderableWebEntityItem.h>
@ -198,8 +199,9 @@ static const float MIRROR_FIELD_OF_VIEW = 30.0f;
static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND;
static const QString INFO_HELP_PATH = "html/interface-welcome.html";
static const QString INFO_WELCOME_PATH = "html/interface-welcome.html";
static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html";
static const QString INFO_HELP_PATH = "html/help.html";
static const unsigned int THROTTLED_SIM_FRAMERATE = 15;
static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE;
@ -486,7 +488,7 @@ bool setupEssentials(int& argc, char** argv) {
// FIXME move to header, or better yet, design some kind of UI manager
// to take care of highlighting keyboard focused items, rather than
// continuing to overburden Application.cpp
Cube3DOverlay* _keyboardFocusHighlight{ nullptr };
std::shared_ptr<Cube3DOverlay> _keyboardFocusHighlight{ nullptr };
int _keyboardFocusHighlightID{ -1 };
@ -682,10 +684,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
// send a location update immediately
discoverabilityManager->updateLocation();
auto myAvatar = getMyAvatar();
connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded);
connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled);
connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated);
connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID);
connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID);
connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID);
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
@ -715,7 +719,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount);
// Save avatar location immediately after a teleport.
connect(getMyAvatar(), &MyAvatar::positionGoneTo,
connect(myAvatar.get(), &MyAvatar::positionGoneTo,
DependencyManager::get<AddressManager>().data(), &AddressManager::storeCurrentAddress);
auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
@ -752,7 +756,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent);
// send the identity packet for our avatar each second to our avatar mixer
connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket);
connect(&identityPacketTimer, &QTimer::timeout, myAvatar.get(), &MyAvatar::sendIdentityPacket);
identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
const char** constArgv = const_cast<const char**>(argv);
@ -815,7 +819,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
{ "gl_version", glContextData["version"] },
{ "gl_vender", glContextData["vendor"] },
{ "gl_sl_version", glContextData["slVersion"] },
{ "gl_renderer", glContextData["renderer"] }
{ "gl_renderer", glContextData["renderer"] },
{ "ideal_thread_count", QThread::idealThreadCount() }
};
auto macVersion = QSysInfo::macVersion();
if (macVersion != QSysInfo::MV_None) {
@ -825,13 +830,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
if (windowsVersion != QSysInfo::WV_None) {
properties["os_win_version"] = QSysInfo::windowsVersion();
}
ProcessorInfo procInfo;
if (getProcessorInfo(procInfo)) {
properties["processor_core_count"] = procInfo.numProcessorCores;
properties["logical_processor_count"] = procInfo.numLogicalProcessors;
properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1;
properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2;
properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3;
}
UserActivityLogger::getInstance().logAction("launch", properties);
_connectionMonitor.init();
// Tell our entity edit sender about our known jurisdictions
_entityEditSender.setServerJurisdictions(&_entityServerJurisdictions);
_entityEditSender.setMyAvatar(getMyAvatar());
_entityEditSender.setMyAvatar(myAvatar.get());
// For now we're going to set the PPS for outbound packets to be super high, this is
// probably not the right long term solution. But for now, we're going to do this to
@ -852,7 +867,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData);
// FIXME -- I'm a little concerned about this.
connect(getMyAvatar()->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded,
connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded,
this, &Application::checkSkeleton, Qt::QueuedConnection);
// Setup the userInputMapper with the actions
@ -1067,7 +1082,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
applicationUpdater->checkForUpdate();
// Now that menu is initialized we can sync myAvatar with it's state.
getMyAvatar()->updateMotionBehaviorFromMenu();
myAvatar->updateMotionBehaviorFromMenu();
// FIXME spacemouse code still needs cleanup
#if 0
@ -1102,10 +1117,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
static int SEND_STATS_INTERVAL_MS = 10000;
static int NEARBY_AVATAR_RADIUS_METERS = 10;
static glm::vec3 lastAvatarPosition = getMyAvatar()->getPosition();
static glm::vec3 lastAvatarPosition = myAvatar->getPosition();
static glm::mat4 lastHMDHeadPose = getHMDSensorPose();
static controller::Pose lastLeftHandPose = getMyAvatar()->getLeftHandPose();
static controller::Pose lastRightHandPose = getMyAvatar()->getRightHandPose();
static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose();
static controller::Pose lastRightHandPose = myAvatar->getRightHandPose();
// Periodically send fps as a user activity event
QTimer* sendStatsTimer = new QTimer(this);
@ -1123,6 +1138,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
auto displayPlugin = qApp->getActiveDisplayPlugin();
properties["fps"] = _frameCounter.rate();
properties["target_frame_rate"] = getTargetFrameRate();
properties["present_rate"] = displayPlugin->presentRate();
properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate();
properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate();
@ -1153,7 +1169,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false;
glm::vec3 avatarPosition = getMyAvatar()->getPosition();
auto myAvatar = getMyAvatar();
glm::vec3 avatarPosition = myAvatar->getPosition();
properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition;
lastAvatarPosition = avatarPosition;
@ -1164,12 +1181,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount;
properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount;
properties["active_display_plugin"] = getActiveDisplayPlugin()->getName();
properties["using_hmd"] = isHMDMode();
auto hmdHeadPose = getHMDSensorPose();
properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose);
lastHMDHeadPose = hmdHeadPose;
auto leftHandPose = getMyAvatar()->getLeftHandPose();
auto rightHandPose = getMyAvatar()->getRightHandPose();
auto leftHandPose = myAvatar->getLeftHandPose();
auto rightHandPose = myAvatar->getRightHandPose();
// controller::Pose considers two poses to be different if either are invalid. In our case, we actually
// want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first.
properties["hand_pose_changed"] =
@ -1216,7 +1236,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender();
EntityEditPacketSender* entityPacketSender = static_cast<EntityEditPacketSender*>(packetSender);
entityPacketSender->setMyAvatar(getMyAvatar());
entityPacketSender->setMyAvatar(myAvatar.get());
connect(this, &Application::applicationStateChanged, this, &Application::activeChanged);
qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0);
@ -1236,8 +1256,87 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
return entityServerNode && !isPhysicsEnabled();
});
// Get sandbox content set version, if available
auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
auto contentVersionPath = acDirPath + "content-version.txt";
qDebug() << "Checking " << contentVersionPath << " for content version";
auto contentVersion = 0;
QFile contentVersionFile(contentVersionPath);
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString line = contentVersionFile.readAll();
// toInt() returns 0 if the conversion fails, so we don't need to specifically check for failure
contentVersion = line.toInt();
}
qDebug() << "Server content version: " << contentVersion;
bool hasTutorialContent = contentVersion >= 1;
Setting::Handle<bool> firstRun { Settings::firstRun, true };
bool hasHMDAndHandControllers = PluginUtils::isHMDAvailable("OpenVR (Vive)") && PluginUtils::isHandControllerAvailable();
Setting::Handle<bool> tutorialComplete { "tutorialComplete", false };
bool shouldGoToTutorial = hasHMDAndHandControllers && hasTutorialContent && !tutorialComplete.get();
qDebug() << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName();
qDebug() << "Has tutorial content: " << hasTutorialContent;
qDebug() << "Tutorial complete: " << tutorialComplete.get();
qDebug() << "Should go to tutorial: " << shouldGoToTutorial;
// when --url in command line, teleport to location
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY);
QString addressLookupString;
if (urlIndex != -1) {
addressLookupString = arguments().value(urlIndex + 1);
}
const QString TUTORIAL_PATH = "/tutorial_begin";
if (shouldGoToTutorial) {
DependencyManager::get<AddressManager>()->ifLocalSandboxRunningElse([=]() {
qDebug() << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox(TUTORIAL_PATH);
}, [=]() {
qDebug() << "Home sandbox does not appear to be running, going to Entry.";
if (firstRun.get()) {
showHelp();
}
if (addressLookupString.isEmpty()) {
DependencyManager::get<AddressManager>()->goToEntry();
} else {
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
});
} else {
bool isFirstRun = firstRun.get();
if (isFirstRun) {
showHelp();
}
// If this is a first run we short-circuit the address passed in
if (isFirstRun) {
if (hasHMDAndHandControllers) {
DependencyManager::get<AddressManager>()->ifLocalSandboxRunningElse([=]() {
qDebug() << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
}, [=]() {
qDebug() << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
});
} else {
DependencyManager::get<AddressManager>()->goToEntry();
}
} else {
qDebug() << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
}
// After all of the constructor is completed, then set firstRun to false.
Setting::Handle<bool> firstRun{ Settings::firstRun, true };
firstRun.set(false);
}
@ -1600,7 +1699,7 @@ void Application::initializeUi() {
FileScriptingInterface* fileDownload = new FileScriptingInterface(engine);
rootContext->setContextProperty("File", fileDownload);
connect(fileDownload, &FileScriptingInterface::unzipSuccess, this, &Application::showAssetServerWidget);
rootContext->setContextProperty("MyAvatar", getMyAvatar());
rootContext->setContextProperty("MyAvatar", getMyAvatar().get());
rootContext->setContextProperty("Messages", DependencyManager::get<MessagesClient>().data());
rootContext->setContextProperty("Recording", DependencyManager::get<RecordingScriptingInterface>().data());
rootContext->setContextProperty("Preferences", DependencyManager::get<Preferences>().data());
@ -1981,11 +2080,11 @@ void Application::setFieldOfView(float fov) {
}
void Application::aboutApp() {
InfoView::show(INFO_HELP_PATH);
InfoView::show(INFO_WELCOME_PATH);
}
void Application::showHelp() {
InfoView::show(INFO_EDIT_ENTITIES_PATH);
InfoView::show(INFO_HELP_PATH);
}
void Application::resizeEvent(QResizeEvent* event) {
@ -2170,7 +2269,7 @@ bool Application::event(QEvent* event) {
// handle custom URL
if (event->type() == QEvent::FileOpen) {
QFileOpenEvent* fileEvent = static_cast<QFileOpenEvent*>(event);
QFileOpenEvent* fileEvent = static_cast<QFileOpenEvent*>(event);
QUrl url = fileEvent->url();
@ -3260,15 +3359,6 @@ void Application::init() {
_timerStart.start();
_lastTimeUpdated.start();
// when --url in command line, teleport to location
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY);
QString addressLookupString;
if (urlIndex != -1) {
addressLookupString = arguments().value(urlIndex + 1);
}
// when +connect_lobby in command line, join steam lobby
const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby";
int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY);
@ -3277,21 +3367,6 @@ void Application::init() {
SteamClient::joinLobby(lobbyId);
}
Setting::Handle<bool> firstRun { Settings::firstRun, true };
if (addressLookupString.isEmpty() && firstRun.get()) {
qCDebug(interfaceapp) << "First run and no URL passed... attempting to go to Home or Entry...";
DependencyManager::get<AddressManager>()->ifLocalSandboxRunningElse([](){
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
},
[](){
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
});
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
}
qCDebug(interfaceapp) << "Loaded settings";
@ -3350,7 +3425,7 @@ void Application::init() {
entity->setCollisionSound(sound);
}
}, Qt::QueuedConnection);
connect(getMyAvatar(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) {
connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) {
if (auto avatar = getMyAvatar()) {
auto sound = DependencyManager::get<SoundCache>()->getSound(newURL);
avatar->setCollisionSound(sound);
@ -3406,7 +3481,7 @@ void Application::updateMyAvatarLookAtPosition() {
}
} else {
AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock();
if (lookingAt && myAvatar != lookingAt.get()) {
if (lookingAt && myAvatar.get() != lookingAt.get()) {
// If I am looking at someone else, look directly at one of their eyes
isLookingAtSomeone = true;
auto lookingAtHead = static_pointer_cast<Avatar>(lookingAt)->getHead();
@ -3607,7 +3682,7 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
_keyboardFocusedItem.set(entityItemID);
_lastAcceptedKeyPress = usecTimestampNow();
if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) {
_keyboardFocusHighlight = new Cube3DOverlay();
_keyboardFocusHighlight = std::make_shared<Cube3DOverlay>();
_keyboardFocusHighlight->setAlpha(1.0f);
_keyboardFocusHighlight->setBorderSize(1.0f);
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });
@ -4255,7 +4330,7 @@ PickRay Application::computePickRay(float x, float y) const {
return result;
}
MyAvatar* Application::getMyAvatar() const {
std::shared_ptr<MyAvatar> Application::getMyAvatar() const {
return DependencyManager::get<AvatarManager>()->getMyAvatar();
}
@ -4361,8 +4436,13 @@ namespace render {
auto scene = DependencyManager::get<SceneScriptingInterface>()->getStage();
auto sceneKeyLight = scene->getKeyLight();
auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture();
sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance());
sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture);
if (defaultSkyboxAmbientTexture) {
sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance());
sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture);
} else {
static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(
"Failed to get a valid Default Skybox Ambient Texture ? probably because it couldn't be find during initialization step");
}
// fall through: render defaults skybox
} else {
break;
@ -4843,7 +4923,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine
scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar());
scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get());
qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue);
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());

View file

@ -401,7 +401,7 @@ private:
int sendNackPackets();
MyAvatar* getMyAvatar() const;
std::shared_ptr<MyAvatar> getMyAvatar() const;
void checkSkeleton() const;

View file

@ -13,6 +13,7 @@
#include <EntityTree.h>
#include <EntityTreeRenderer.h>
#include <avatar/AvatarManager.h>
#include <AvatarData.h>
#include "InterfaceParentFinder.h"
@ -45,6 +46,11 @@ SpatiallyNestableWeakPointer InterfaceParentFinder::find(QUuid parentID, bool& s
return parent;
}
if (parentID == AVATAR_SELF_ID) {
success = true;
return avatarManager->getMyAvatar();
}
success = false;
return parent;
}

View file

@ -166,7 +166,7 @@ Menu::Menu() {
// Avatar menu ----------------------------------
MenuWrapper* avatarMenu = addMenu("Avatar");
auto avatarManager = DependencyManager::get<AvatarManager>();
QObject* avatar = avatarManager->getMyAvatar();
auto avatar = avatarManager->getMyAvatar();
// Avatar > Attachments...
auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments);
@ -182,19 +182,19 @@ Menu::Menu() {
addActionToQMenuAndActionHash(avatarSizeMenu,
MenuOption::IncreaseAvatarSize,
0, // QML Qt::Key_Plus,
avatar, SLOT(increaseSize()));
avatar.get(), SLOT(increaseSize()));
// Avatar > Size > Decrease
addActionToQMenuAndActionHash(avatarSizeMenu,
MenuOption::DecreaseAvatarSize,
0, // QML Qt::Key_Minus,
avatar, SLOT(decreaseSize()));
avatar.get(), SLOT(decreaseSize()));
// Avatar > Size > Reset
addActionToQMenuAndActionHash(avatarSizeMenu,
MenuOption::ResetAvatarSize,
0, // QML Qt::Key_Equal,
avatar, SLOT(resetSize()));
avatar.get(), SLOT(resetSize()));
// Avatar > Reset Sensors
addActionToQMenuAndActionHash(avatarMenu,
@ -517,38 +517,38 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderOtherLookAtVectors, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawDefaultPose, 0, false,
avatar, SLOT(setEnableDebugDrawDefaultPose(bool)));
avatar.get(), SLOT(setEnableDebugDrawDefaultPose(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false,
avatar, SLOT(setEnableDebugDrawAnimPose(bool)));
avatar.get(), SLOT(setEnableDebugDrawAnimPose(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawPosition, 0, false,
avatar, SLOT(setEnableDebugDrawPosition(bool)));
avatar.get(), SLOT(setEnableDebugDrawPosition(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true,
avatar, SLOT(setEnableMeshVisible(bool)));
avatar.get(), SLOT(setEnableMeshVisible(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::TurnWithHead, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::UseAnimPreAndPostRotations, 0, true,
avatar, SLOT(setUseAnimPreAndPostRotations(bool)));
avatar.get(), SLOT(setUseAnimPreAndPostRotations(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableInverseKinematics, 0, true,
avatar, SLOT(setEnableInverseKinematics(bool)));
avatar.get(), SLOT(setEnableInverseKinematics(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderSensorToWorldMatrix, 0, false,
avatar, SLOT(setEnableDebugDrawSensorToWorldMatrix(bool)));
avatar.get(), SLOT(setEnableDebugDrawSensorToWorldMatrix(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ActionMotorControl,
Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar, SLOT(updateMotionBehaviorFromMenu()),
Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ScriptedMotorControl, 0, true,
avatar, SLOT(updateMotionBehaviorFromMenu()),
avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableCharacterController, 0, true,
avatar, SLOT(updateMotionBehaviorFromMenu()),
avatar.get(), SLOT(updateMotionBehaviorFromMenu()),
UNSPECIFIED_POSITION, "Developer");
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,
avatar, SLOT(setEnableDebugDrawHandControllers(bool)));
avatar.get(), SLOT(setEnableDebugDrawHandControllers(bool)));
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::LowVelocityFilter, 0, true,
qApp, SLOT(setLowVelocityFilter(bool)));

View file

@ -250,8 +250,6 @@ int AudioScope::addSilenceToScope(QByteArray* byteArray, int frameOffset, int si
}
const int STEREO_FACTOR = 2;
void AudioScope::addStereoSilenceToScope(int silentSamplesPerChannel) {
if (!_isEnabled || _isPaused) {
return;
@ -265,10 +263,10 @@ void AudioScope::addStereoSamplesToScope(const QByteArray& samples) {
return;
}
const int16_t* samplesData = reinterpret_cast<const int16_t*>(samples.data());
int samplesPerChannel = samples.size() / sizeof(int16_t) / STEREO_FACTOR;
int samplesPerChannel = samples.size() / sizeof(int16_t) / AudioConstants::STEREO;
addBufferToScope(_scopeOutputLeft, _scopeOutputOffset, samplesData, samplesPerChannel, 0, STEREO_FACTOR);
_scopeOutputOffset = addBufferToScope(_scopeOutputRight, _scopeOutputOffset, samplesData, samplesPerChannel, 1, STEREO_FACTOR);
addBufferToScope(_scopeOutputLeft, _scopeOutputOffset, samplesData, samplesPerChannel, 0, AudioConstants::STEREO);
_scopeOutputOffset = addBufferToScope(_scopeOutputRight, _scopeOutputOffset, samplesData, samplesPerChannel, 1, AudioConstants::STEREO);
_scopeLastFrame = samples.right(AudioConstants::NETWORK_FRAME_BYTES_STEREO);
}
@ -282,9 +280,9 @@ void AudioScope::addLastFrameRepeatedWithFadeToScope(int samplesPerChannel) {
int samplesToWriteThisIteration = std::min(samplesRemaining, (int) AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
float fade = calculateRepeatedFrameFadeFactor(indexOfRepeat);
addBufferToScope(_scopeOutputLeft, _scopeOutputOffset, lastFrameData,
samplesToWriteThisIteration, 0, STEREO_FACTOR, fade);
samplesToWriteThisIteration, 0, AudioConstants::STEREO, fade);
_scopeOutputOffset = addBufferToScope(_scopeOutputRight, _scopeOutputOffset,
lastFrameData, samplesToWriteThisIteration, 1, STEREO_FACTOR, fade);
lastFrameData, samplesToWriteThisIteration, 1, AudioConstants::STEREO, fade);
samplesRemaining -= samplesToWriteThisIteration;
indexOfRepeat++;

View file

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

View file

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

View file

@ -31,6 +31,7 @@
#include <SettingHandle.h>
#include <UsersScriptingInterface.h>
#include <UUID.h>
#include <AvatarData.h>
#include "Application.h"
#include "Avatar.h"
@ -323,7 +324,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
// an id of null. Thus this code handles any collision in which one of the participating objects is
// my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.)
if (collision.idA.isNull() || collision.idB.isNull()) {
MyAvatar* myAvatar = getMyAvatar();
auto myAvatar = getMyAvatar();
auto collisionSound = myAvatar->getCollisionSound();
if (collisionSound) {
const auto characterController = myAvatar->getCharacterController();
@ -399,7 +400,7 @@ void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) {
AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) {
if (sessionID == _myAvatar->getSessionUUID()) {
if (sessionID == AVATAR_SELF_ID || sessionID == _myAvatar->getSessionUUID()) {
return _myAvatar;
}

View file

@ -39,7 +39,7 @@ public:
void init();
MyAvatar* getMyAvatar() { return _myAvatar.get(); }
std::shared_ptr<MyAvatar> getMyAvatar() { return _myAvatar; }
AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) override;
void updateMyAvatar(float deltaTime);

View file

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

View file

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

View file

@ -29,6 +29,7 @@
#include "InterfaceLogging.h"
#include "UserActivityLogger.h"
#include "MainWindow.h"
#include <QtCore/QProcess>
#ifdef HAS_BUGSPLAT
#include <BuildInfo.h>
@ -121,6 +122,29 @@ int main(int argc, const char* argv[]) {
}
}
QCommandLineParser parser;
QCommandLineOption runServerOption("runServer", "Whether to run the server");
QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath");
parser.addOption(runServerOption);
parser.addOption(serverContentPathOption);
parser.parse(arguments);
if (parser.isSet(runServerOption)) {
QString applicationDirPath = QFileInfo(arguments[0]).path();
QString serverPath = applicationDirPath + "/server-console/server-console.exe";
qDebug() << "Application dir path is: " << applicationDirPath;
qDebug() << "Server path is: " << serverPath;
QStringList args;
if (parser.isSet(serverContentPathOption)) {
QString serverContentPath = QFileInfo(arguments[0]).path() + "/" + parser.value(serverContentPathOption);
args << "--" << "--contentPath" << serverContentPath;
}
qDebug() << QFileInfo(arguments[0]).path();
qDebug() << QProcess::startDetached(serverPath, args);
// Sleep a short amount of time to give the server a chance to start
usleep(2000000);
}
QElapsedTimer startupTime;
startupTime.start();

View file

@ -13,10 +13,12 @@
#include <QtScript/QScriptContext>
#include <avatar/AvatarManager.h>
#include <display-plugins/DisplayPlugin.h>
#include <display-plugins/CompositorHelper.h>
#include <OffscreenUi.h>
#include <avatar/AvatarManager.h>
#include <plugins/PluginUtils.h>
#include "Application.h"
HMDScriptingInterface::HMDScriptingInterface() {
@ -47,6 +49,14 @@ glm::vec2 HMDScriptingInterface::overlayToSpherical(const glm::vec2 & position)
return qApp->getApplicationCompositor().overlayToSpherical(position);
}
bool HMDScriptingInterface::isHMDAvailable() {
return PluginUtils::isHMDAvailable();
}
bool HMDScriptingInterface::isHandControllerAvailable() {
return PluginUtils::isHandControllerAvailable();
}
QScriptValue HMDScriptingInterface::getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine) {
glm::vec3 hudIntersection;
auto instance = DependencyManager::get<HMDScriptingInterface>();
@ -79,7 +89,7 @@ bool HMDScriptingInterface::getHUDLookAtPosition3D(glm::vec3& result) const {
}
glm::mat4 HMDScriptingInterface::getWorldHMDMatrix() const {
MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
return myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix();
}

View file

@ -38,6 +38,9 @@ public:
Q_INVOKABLE QString preferredAudioInput() const;
Q_INVOKABLE QString preferredAudioOutput() const;
Q_INVOKABLE bool isHMDAvailable();
Q_INVOKABLE bool isHandControllerAvailable();
Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const;
Q_INVOKABLE void disableHandLasers(int hands) const;

View file

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

View file

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

View file

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

View file

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

View file

@ -38,6 +38,7 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare
});
_backEnabled = !(DependencyManager::get<AddressManager>()->getBackStack().isEmpty());
_forwardEnabled = !(DependencyManager::get<AddressManager>()->getForwardStack().isEmpty());
connect(addressManager.data(), &AddressManager::hostChanged, this, &AddressBarDialog::metaverseServerUrlChanged);
connect(DependencyManager::get<DialogsManager>().data(), &DialogsManager::setUseFeed, this, &AddressBarDialog::setUseFeed);
connect(qApp, &Application::receivedHifiSchemeURL, this, &AddressBarDialog::receivedHifiSchemeURL);
}

View file

@ -37,7 +37,7 @@ signals:
void forwardEnabledChanged();
void useFeedChanged();
void receivedHifiSchemeURL(const QString& url);
void metaverseServerUrlChanged(); // While it is a constant, qml will complain about not seeing a change signal.
void metaverseServerUrlChanged();
protected:
void displayAddressOfflineMessage();

View file

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

View file

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

View file

@ -43,7 +43,7 @@ bool OverlayConductor::headOutsideOverlay() const {
bool OverlayConductor::updateAvatarIsAtRest() {
MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s
const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms
@ -69,7 +69,7 @@ bool OverlayConductor::updateAvatarIsAtRest() {
}
bool OverlayConductor::updateAvatarHasDriveInput() {
MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms
const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s
@ -103,7 +103,7 @@ void OverlayConductor::update(float dt) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
bool currentVisible = !offscreenUi->getDesktop()->property("pinned").toBool();
MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
// centerUI when hmd mode is first enabled and mounted
if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible()) {
if (!_hmdMode) {

View file

@ -32,7 +32,7 @@
void setupPreferences() {
auto preferences = DependencyManager::get<Preferences>();
MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
static const QString AVATAR_BASICS { "Avatar Basics" };
{
auto getter = [=]()->QString { return myAvatar->getDisplayName(); };

View file

@ -170,7 +170,7 @@ void Stats::updateStats(bool force) {
STAT_UPDATE(entitiesPing, octreeServerCount ? totalPingOctree / octreeServerCount : -1);
// Third column, avatar stats
MyAvatar* myAvatar = avatarManager->getMyAvatar();
auto myAvatar = avatarManager->getMyAvatar();
glm::vec3 avatarPos = myAvatar->getPosition();
STAT_UPDATE(position, QVector3D(avatarPos.x, avatarPos.y, avatarPos.z));
STAT_UPDATE_FLOAT(speed, glm::length(myAvatar->getVelocity()), 0.01f);

View file

@ -124,6 +124,12 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) {
}
properties.remove("start"); // so that Base3DOverlay doesn't respond to it
auto localStart = properties["localStart"];
if (localStart.isValid()) {
_start = vec3FromVariant(localStart);
}
properties.remove("localStart"); // so that Base3DOverlay doesn't respond to it
auto end = properties["end"];
// if "end" property was not there, check to see if they included aliases: endPoint
if (!end.isValid()) {
@ -133,6 +139,12 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) {
setEnd(vec3FromVariant(end));
}
auto localEnd = properties["localEnd"];
if (localEnd.isValid()) {
_end = vec3FromVariant(localEnd);
}
properties.remove("localEnd"); // so that Base3DOverlay doesn't respond to it
auto glow = properties["glow"];
if (glow.isValid()) {
setGlow(glow.toFloat());

View file

@ -79,11 +79,10 @@ void ModelOverlay::render(RenderArgs* args) {
_model->removeFromScene(scene, pendingChanges);
_model->addToScene(scene, pendingChanges);
}
scene->enqueuePendingChanges(pendingChanges);
if (!_visible) {
return;
}
_model->setVisibleInScene(_visible, scene);
scene->enqueuePendingChanges(pendingChanges);
}
void ModelOverlay::setProperties(const QVariantMap& properties) {
@ -123,17 +122,8 @@ void ModelOverlay::setProperties(const QVariantMap& properties) {
auto texturesValue = properties["textures"];
if (texturesValue.isValid() && texturesValue.canConvert(QVariant::Map)) {
QVariantMap textureMap = texturesValue.toMap();
foreach(const QString& key, textureMap.keys()) {
QUrl newTextureURL = textureMap[key].toUrl();
qDebug() << "Updating texture named" << key << "to texture at URL" << newTextureURL;
QMetaObject::invokeMethod(_model.get(), "setTextureWithNameToURL", Qt::AutoConnection,
Q_ARG(const QString&, key),
Q_ARG(const QUrl&, newTextureURL));
_modelTextures[key] = newTextureURL; // Keep local track of textures for getProperty()
}
QMetaObject::invokeMethod(_model.get(), "setTextures", Qt::AutoConnection,
Q_ARG(const QVariantMap&, textureMap));
}
}

View file

@ -64,7 +64,7 @@ namespace render {
if (args) {
if (overlay->getAnchor() == Overlay::MY_AVATAR) {
auto batch = args->_batch;
MyAvatar* avatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto avatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
glm::quat myAvatarRotation = avatar->getOrientation();
glm::vec3 myAvatarPosition = avatar->getPosition();
float angle = glm::degrees(glm::angle(myAvatarRotation));

View file

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

View file

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

View file

@ -115,7 +115,7 @@ AudioClient::AudioClient() :
_loopbackAudioOutput(NULL),
_loopbackOutputDevice(NULL),
_inputRingBuffer(0),
_receivedAudioStream(0, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
_isStereoInput(false),
_outputStarveDetectionStartTimeMsec(0),
_outputStarveDetectionCount(0),
@ -817,6 +817,13 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
return;
}
// NOTE: we assume the inputFormat and the outputFormat are the same, since on any modern
// multimedia OS they should be. If there is a device that this is not true for, we can
// add back support to do resampling.
if (_inputFormat.sampleRate() != _outputFormat.sampleRate()) {
return;
}
// if this person wants local loopback add that to the locally injected audio
// if there is reverb apply it to local audio and substract the origin samples
@ -833,11 +840,6 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
}
}
// NOTE: we assume the inputFormat and the outputFormat are the same, since on any modern
// multimedia OS they should be. If there is a device that this is not true for, we can
// add back support to do resampling.
Q_ASSERT(_inputFormat.sampleRate() == _outputFormat.sampleRate());
static QByteArray loopBackByteArray;
int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE;
@ -1150,9 +1152,9 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) {
}
void AudioClient::outputFormatChanged() {
int outputFormatChannelCountTimesSampleRate = _outputFormat.channelCount() * _outputFormat.sampleRate();
_outputFrameSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * outputFormatChannelCountTimesSampleRate / _desiredOutputFormat.sampleRate();
_receivedAudioStream.outputFormatChanged(outputFormatChannelCountTimesSampleRate);
_outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount() * _outputFormat.sampleRate()) /
_desiredOutputFormat.sampleRate();
_receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), _outputFormat.channelCount());
}
bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) {
@ -1362,7 +1364,7 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) {
// proportional to the accelerator ratio.
#ifdef Q_OS_WIN
const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 1.0f;
const float AudioClient::CALLBACK_ACCELERATOR_RATIO = IsWindows8OrGreater() ? 1.0f : 0.25f;
#endif
#ifdef Q_OS_MAC

View file

@ -46,10 +46,11 @@ static const int STATS_FOR_STATS_PACKET_WINDOW_SECONDS = 30;
// _currentJitterBufferFrames is updated with the time-weighted avg and the running time-weighted avg is reset.
static const quint64 FRAMES_AVAILABLE_STAT_WINDOW_USECS = 10 * USECS_PER_SECOND;
InboundAudioStream::InboundAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames) :
_ringBuffer(numFrameSamples, numFramesCapacity),
_dynamicJitterBufferEnabled(numStaticJitterFrames == -1),
_staticJitterBufferFrames(std::max(numStaticJitterFrames, DEFAULT_STATIC_JITTER_FRAMES)),
InboundAudioStream::InboundAudioStream(int numChannels, int numFrames, int numBlocks, int numStaticJitterBlocks) :
_ringBuffer(numChannels * numFrames, numBlocks),
_numChannels(numChannels),
_dynamicJitterBufferEnabled(numStaticJitterBlocks == -1),
_staticJitterBufferFrames(std::max(numStaticJitterBlocks, DEFAULT_STATIC_JITTER_FRAMES)),
_desiredJitterBufferFrames(_dynamicJitterBufferEnabled ? 1 : _staticJitterBufferFrames),
_incomingSequenceNumberStats(STATS_FOR_STATS_PACKET_WINDOW_SECONDS),
_starveHistory(STARVE_HISTORY_CAPACITY),
@ -121,11 +122,11 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
packetReceivedUpdateTimingStats();
int networkSamples;
int networkFrames;
// parse the info after the seq number and before the audio data (the stream properties)
int prePropertyPosition = message.getPosition();
int propertyBytes = parseStreamProperties(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkSamples);
int propertyBytes = parseStreamProperties(message.getType(), message.readWithoutCopy(message.getBytesLeftToRead()), networkFrames);
message.seek(prePropertyPosition + propertyBytes);
// handle this packet based on its arrival status.
@ -135,7 +136,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
// NOTE: we assume that each dropped packet contains the same number of samples
// as the packet we just received.
int packetsDropped = arrivalInfo._seqDiffFromExpected;
writeSamplesForDroppedPackets(packetsDropped * networkSamples);
writeFramesForDroppedPackets(packetsDropped * networkFrames);
// fall through to OnTime case
}
@ -143,7 +144,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
// Packet is on time; parse its data to the ringbuffer
if (message.getType() == PacketType::SilentAudioFrame) {
// FIXME - Some codecs need to know about these silent frames... and can produce better output
writeDroppableSilentSamples(networkSamples);
writeDroppableSilentFrames(networkFrames);
} else {
// note: PCM and no codec are identical
bool selectedPCM = _selectedCodecName == "pcm" || _selectedCodecName == "";
@ -153,7 +154,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) {
parseAudioData(message.getType(), afterProperties);
} else {
qDebug() << "Codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence";
writeDroppableSilentSamples(networkSamples);
writeDroppableSilentFrames(networkFrames);
// inform others of the mismatch
auto sendingNode = DependencyManager::get<NodeList>()->nodeWithUUID(message.getSourceID());
emit mismatchedAudioCodec(sendingNode, _selectedCodecName, codecInPacket);
@ -218,12 +219,13 @@ int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packet
return _ringBuffer.writeData(decodedBuffer.data(), actualSize);
}
int InboundAudioStream::writeDroppableSilentSamples(int silentSamples) {
int InboundAudioStream::writeDroppableSilentFrames(int silentFrames) {
if (_decoder) {
_decoder->trackLostFrames(silentSamples);
_decoder->trackLostFrames(silentFrames);
}
// calculate how many silent frames we should drop.
int silentSamples = silentFrames * _numChannels;
int samplesPerFrame = _ringBuffer.getNumFrameSamples();
int desiredJitterBufferFramesPlusPadding = _desiredJitterBufferFrames + DESIRED_JITTER_BUFFER_FRAMES_PADDING;
int numSilentFramesToDrop = 0;
@ -414,14 +416,14 @@ void InboundAudioStream::packetReceivedUpdateTimingStats() {
_lastPacketReceivedTime = now;
}
int InboundAudioStream::writeSamplesForDroppedPackets(int networkSamples) {
return writeLastFrameRepeatedWithFade(networkSamples);
int InboundAudioStream::writeFramesForDroppedPackets(int networkFrames) {
return writeLastFrameRepeatedWithFade(networkFrames);
}
int InboundAudioStream::writeLastFrameRepeatedWithFade(int samples) {
int InboundAudioStream::writeLastFrameRepeatedWithFade(int frames) {
AudioRingBuffer::ConstIterator frameToRepeat = _ringBuffer.lastFrameWritten();
int frameSize = _ringBuffer.getNumFrameSamples();
int samplesToWrite = samples;
int samplesToWrite = frames * _numChannels;
int indexOfRepeat = 0;
do {
int samplesToWriteThisIteration = std::min(samplesToWrite, frameSize);
@ -434,7 +436,7 @@ int InboundAudioStream::writeLastFrameRepeatedWithFade(int samples) {
indexOfRepeat++;
} while (samplesToWrite > 0);
return samples;
return frames;
}
AudioStreamStats InboundAudioStream::getAudioStreamStats() const {

View file

@ -47,7 +47,7 @@ public:
static const bool REPETITION_WITH_FADE;
InboundAudioStream() = delete;
InboundAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames = -1);
InboundAudioStream(int numChannels, int numFrames, int numBlocks, int numStaticJitterBlocks);
~InboundAudioStream();
void reset();
@ -115,7 +115,7 @@ public slots:
private:
void packetReceivedUpdateTimingStats();
int writeSamplesForDroppedPackets(int networkSamples);
int writeFramesForDroppedPackets(int networkFrames);
void popSamplesNoCheck(int samples);
void framesAvailableChanged();
@ -134,16 +134,17 @@ protected:
/// default implementation assumes packet contains raw audio samples after stream properties
virtual int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties);
/// writes silent samples to the buffer that may be dropped to reduce latency caused by the buffer
virtual int writeDroppableSilentSamples(int silentSamples);
/// writes silent frames to the buffer that may be dropped to reduce latency caused by the buffer
virtual int writeDroppableSilentFrames(int silentFrames);
/// writes the last written frame repeatedly, gradually fading to silence.
/// used for writing samples for dropped packets.
virtual int writeLastFrameRepeatedWithFade(int samples);
virtual int writeLastFrameRepeatedWithFade(int frames);
protected:
AudioRingBuffer _ringBuffer;
int _numChannels;
bool _lastPopSucceeded { false };
AudioRingBuffer::ConstIterator _lastPopOutput;

View file

@ -11,5 +11,8 @@
#include "MixedAudioStream.h"
MixedAudioStream::MixedAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames) :
InboundAudioStream(numFrameSamples, numFramesCapacity, numStaticJitterFrames) {}
#include "AudioConstants.h"
MixedAudioStream::MixedAudioStream(int numFramesCapacity, int numStaticJitterFrames) :
InboundAudioStream(AudioConstants::STEREO, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL,
numFramesCapacity, numStaticJitterFrames) {}

View file

@ -16,7 +16,7 @@
class MixedAudioStream : public InboundAudioStream {
public:
MixedAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames = -1);
MixedAudioStream(int numFramesCapacity, int numStaticJitterFrames = -1);
float getNextOutputFrameLoudness() const { return _ringBuffer.getNextOutputFrameLoudness(); }
};

View file

@ -12,33 +12,30 @@
#include "MixedProcessedAudioStream.h"
#include "AudioLogging.h"
static const int STEREO_FACTOR = 2;
MixedProcessedAudioStream::MixedProcessedAudioStream(int numFramesCapacity, int numStaticJitterFrames)
: InboundAudioStream(AudioConstants::STEREO, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL,
numFramesCapacity, numStaticJitterFrames) {}
MixedProcessedAudioStream::MixedProcessedAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames)
: InboundAudioStream(numFrameSamples, numFramesCapacity, numStaticJitterFrames) {}
void MixedProcessedAudioStream::outputFormatChanged(int outputFormatChannelCountTimesSampleRate) {
_outputFormatChannelsTimesSampleRate = outputFormatChannelCountTimesSampleRate;
int deviceOutputFrameSize = networkToDeviceSamples(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
_ringBuffer.resizeForFrameSize(deviceOutputFrameSize);
void MixedProcessedAudioStream::outputFormatChanged(int sampleRate, int channelCount) {
_outputSampleRate = sampleRate;
_outputChannelCount = channelCount;
int deviceOutputFrameFrames = networkToDeviceFrames(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO / AudioConstants::STEREO);
int deviceOutputFrameSamples = deviceOutputFrameFrames * AudioConstants::STEREO;
_ringBuffer.resizeForFrameSize(deviceOutputFrameSamples);
}
int MixedProcessedAudioStream::writeDroppableSilentSamples(int silentSamples) {
int deviceSilentSamplesWritten = InboundAudioStream::writeDroppableSilentSamples(networkToDeviceSamples(silentSamples));
emit addedSilence(deviceToNetworkSamples(deviceSilentSamplesWritten) / STEREO_FACTOR);
return deviceSilentSamplesWritten;
int MixedProcessedAudioStream::writeDroppableSilentFrames(int silentFrames) {
int deviceSilentFrames = networkToDeviceFrames(silentFrames);
int deviceSilentFramesWritten = InboundAudioStream::writeDroppableSilentFrames(deviceSilentFrames);
emit addedSilence(deviceToNetworkFrames(deviceSilentFramesWritten));
return deviceSilentFramesWritten;
}
int MixedProcessedAudioStream::writeLastFrameRepeatedWithFade(int samples) {
int deviceSamplesWritten = InboundAudioStream::writeLastFrameRepeatedWithFade(networkToDeviceSamples(samples));
emit addedLastFrameRepeatedWithFade(deviceToNetworkSamples(deviceSamplesWritten) / STEREO_FACTOR);
return deviceSamplesWritten;
int MixedProcessedAudioStream::writeLastFrameRepeatedWithFade(int frames) {
int deviceFrames = networkToDeviceFrames(frames);
int deviceFramesWritten = InboundAudioStream::writeLastFrameRepeatedWithFade(deviceFrames);
emit addedLastFrameRepeatedWithFade(deviceToNetworkFrames(deviceFramesWritten));
return deviceFramesWritten;
}
int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) {
@ -56,16 +53,16 @@ int MixedProcessedAudioStream::parseAudioData(PacketType type, const QByteArray&
_ringBuffer.writeData(outputBuffer.data(), outputBuffer.size());
qCDebug(audiostream, "Wrote %d samples to buffer (%d available)", outputBuffer.size() / (int)sizeof(int16_t), getSamplesAvailable());
return packetAfterStreamProperties.size();
}
int MixedProcessedAudioStream::networkToDeviceSamples(int networkSamples) {
return (quint64)networkSamples * (quint64)_outputFormatChannelsTimesSampleRate / (quint64)(STEREO_FACTOR
* AudioConstants::SAMPLE_RATE);
int MixedProcessedAudioStream::networkToDeviceFrames(int networkFrames) {
return ((quint64)networkFrames * _outputChannelCount * _outputSampleRate) /
(quint64)(AudioConstants::STEREO * AudioConstants::SAMPLE_RATE);
}
int MixedProcessedAudioStream::deviceToNetworkSamples(int deviceSamples) {
return (quint64)deviceSamples * (quint64)(STEREO_FACTOR * AudioConstants::SAMPLE_RATE)
/ (quint64)_outputFormatChannelsTimesSampleRate;
int MixedProcessedAudioStream::deviceToNetworkFrames(int deviceFrames) {
return (quint64)deviceFrames * (quint64)(AudioConstants::STEREO * AudioConstants::SAMPLE_RATE) /
(_outputSampleRate * _outputChannelCount);
}

View file

@ -19,7 +19,7 @@ class AudioClient;
class MixedProcessedAudioStream : public InboundAudioStream {
Q_OBJECT
public:
MixedProcessedAudioStream(int numFrameSamples, int numFramesCapacity, int numStaticJitterFrames = -1);
MixedProcessedAudioStream(int numFramesCapacity, int numStaticJitterFrames = -1);
signals:
@ -30,19 +30,20 @@ signals:
void processSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer);
public:
void outputFormatChanged(int outputFormatChannelCountTimesSampleRate);
void outputFormatChanged(int sampleRate, int channelCount);
protected:
int writeDroppableSilentSamples(int silentSamples) override;
int writeLastFrameRepeatedWithFade(int samples) override;
int writeDroppableSilentFrames(int silentFrames) override;
int writeLastFrameRepeatedWithFade(int frames) override;
int parseAudioData(PacketType type, const QByteArray& packetAfterStreamProperties) override;
private:
int networkToDeviceSamples(int networkSamples);
int deviceToNetworkSamples(int deviceSamples);
int networkToDeviceFrames(int networkFrames);
int deviceToNetworkFrames(int deviceFrames);
private:
int _outputFormatChannelsTimesSampleRate;
quint64 _outputSampleRate;
quint64 _outputChannelCount;
};
#endif // hifi_MixedProcessedAudioStream_h

View file

@ -22,10 +22,10 @@
#include <UUID.h>
PositionalAudioStream::PositionalAudioStream(PositionalAudioStream::Type type, bool isStereo, int numStaticJitterFrames) :
InboundAudioStream(isStereo
? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO
: AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL,
AUDIOMIXER_INBOUND_RING_BUFFER_FRAME_CAPACITY, numStaticJitterFrames),
InboundAudioStream(isStereo ? AudioConstants::STEREO : AudioConstants::MONO,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL,
AUDIOMIXER_INBOUND_RING_BUFFER_FRAME_CAPACITY,
numStaticJitterFrames),
_type(type),
_position(0.0f, 0.0f, 0.0f),
_orientation(0.0f, 0.0f, 0.0f, 0.0f),

View file

@ -976,10 +976,16 @@ void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& ide
packetStream >> identityOut.uuid >> identityOut.skeletonModelURL >> identityOut.attachmentData >> identityOut.displayName >> identityOut.avatarEntityData;
}
static const QUrl emptyURL("");
const QUrl& AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) {
// We don't put file urls on the wire, but instead convert to empty.
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
}
bool AvatarData::processAvatarIdentity(const Identity& identity) {
bool hasIdentityChanged = false;
if (_firstSkeletonCheck || (identity.skeletonModelURL != _skeletonModelURL)) {
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
setSkeletonModelURL(identity.skeletonModelURL);
hasIdentityChanged = true;
_firstSkeletonCheck = false;
@ -1010,8 +1016,7 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) {
QByteArray AvatarData::identityByteArray() {
QByteArray identityData;
QDataStream identityStream(&identityData, QIODevice::Append);
QUrl emptyURL("");
const QUrl& urlToSend = _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL);
_avatarEntitiesLock.withReadLock([&] {
identityStream << getSessionUUID() << urlToSend << _attachmentData << _displayName << _avatarEntityData;

View file

@ -55,6 +55,7 @@ typedef unsigned long long quint64;
#include <NumericalConstants.h>
#include <Packed.h>
#include <ThreadSafeValueCache.h>
#include <SharedUtil.h>
#include "AABox.h"
#include "HeadData.h"
@ -138,10 +139,6 @@ class AttachmentData;
class Transform;
using TransformPointer = std::shared_ptr<Transform>;
// When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows
// the value to be reset when the sessionID changes.
const QUuid AVATAR_SELF_ID = QUuid("{00000000-0000-0000-0000-000000000001}");
class AvatarData : public QObject, public SpatiallyNestable {
Q_OBJECT
@ -402,6 +399,7 @@ protected:
QUrl _skeletonFBXURL;
QVector<AttachmentData> _attachmentData;
QString _displayName;
const QUrl& cannonicalSkeletonModelURL(const QUrl& empty);
float _displayNameTargetAlpha;
float _displayNameAlpha;

View file

@ -6,7 +6,3 @@ link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui render-utils)
target_opengl()
GroupSources("src/display-plugins")
if (NOT ANDROID)
target_oglplus()
endif ()

View file

@ -591,17 +591,17 @@ void EntityTreeRenderer::deleteReleasedModels() {
RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType,
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard) {
const QVector<EntityItemID>& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) {
RayToEntityIntersectionResult result;
if (_tree) {
EntityTreePointer entityTree = std::static_pointer_cast<EntityTree>(_tree);
OctreeElementPointer element;
EntityItemPointer intersectedEntity = NULL;
result.intersects = entityTree->findRayIntersection(ray.origin, ray.direction, element, result.distance,
result.face, result.surfaceNormal, entityIdsToInclude, entityIdsToDiscard,
(void**)&intersectedEntity, lockType, &result.accurate,
precisionPicking);
result.intersects = entityTree->findRayIntersection(ray.origin, ray.direction,
entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking,
element, result.distance, result.face, result.surfaceNormal,
(void**)&intersectedEntity, lockType, &result.accurate);
if (result.intersects && intersectedEntity) {
result.entityID = intersectedEntity->getEntityItemID();
result.properties = intersectedEntity->getProperties();

View file

@ -149,7 +149,8 @@ private:
QList<ModelPointer> _releasedModels;
RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType,
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude = QVector<EntityItemID>(),
const QVector<EntityItemID>& entityIdsToDiscard = QVector<EntityItemID>());
const QVector<EntityItemID>& entityIdsToDiscard = QVector<EntityItemID>(), bool visibleOnly=false,
bool collidableOnly = false);
EntityItemID _currentHoverOverEntityID;
EntityItemID _currentClickingOnEntityID;

View file

@ -38,39 +38,6 @@ static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND;
static int MAX_WINDOW_SIZE = 4096;
static float OPAQUE_ALPHA_THRESHOLD = 0.99f;
void WebEntityAPIHelper::synthesizeKeyPress(QString key) {
if (_renderableWebEntityItem) {
_renderableWebEntityItem->synthesizeKeyPress(key);
}
}
void WebEntityAPIHelper::emitScriptEvent(const QVariant& message) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
} else {
emit scriptEventReceived(message);
}
}
void WebEntityAPIHelper::emitWebEvent(const QVariant& message) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
} else {
// special case to handle raising and lowering the virtual keyboard
if (message.type() == QVariant::String && message.toString() == "_RAISE_KEYBOARD" && _renderableWebEntityItem) {
if (_renderableWebEntityItem) {
_renderableWebEntityItem->setKeyboardRaised(true);
}
} else if (message.type() == QVariant::String && message.toString() == "_LOWER_KEYBOARD" && _renderableWebEntityItem) {
if (_renderableWebEntityItem) {
_renderableWebEntityItem->setKeyboardRaised(false);
}
} else {
emit webEventReceived(message);
}
}
}
EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer entity{ new RenderableWebEntityItem(entityID) };
entity->setProperties(properties);
@ -85,21 +52,9 @@ RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemI
_touchDevice.setType(QTouchDevice::TouchScreen);
_touchDevice.setName("RenderableWebEntityItemTouchDevice");
_touchDevice.setMaximumTouchPoints(4);
_webEntityAPIHelper = new WebEntityAPIHelper;
_webEntityAPIHelper->setRenderableWebEntityItem(this);
_webEntityAPIHelper->moveToThread(qApp->thread());
// forward web events to EntityScriptingInterface
auto entities = DependencyManager::get<EntityScriptingInterface>();
QObject::connect(_webEntityAPIHelper, &WebEntityAPIHelper::webEventReceived, [=](const QVariant& message) {
emit entities->webEventReceived(entityItemID, message);
});
}
RenderableWebEntityItem::~RenderableWebEntityItem() {
_webEntityAPIHelper->setRenderableWebEntityItem(nullptr);
_webEntityAPIHelper->deleteLater();
destroyWebSurface();
qDebug() << "Destroyed web entity " << getID();
}
@ -129,20 +84,35 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) {
// Save the original GL context, because creating a QML surface will create a new context
QOpenGLContext * currentContext = QOpenGLContext::currentContext();
QSurface * currentSurface = currentContext->surface();
_webSurface = new OffscreenQmlSurface();
auto deleter = [](OffscreenQmlSurface* webSurface) {
AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
webSurface->deleteLater();
});
};
_webSurface = QSharedPointer<OffscreenQmlSurface>(new OffscreenQmlSurface(), deleter);
// The lifetime of the QML surface MUST be managed by the main thread
// Additionally, we MUST use local variables copied by value, rather than
// member variables, since they would implicitly refer to a this that
// is no longer valid
_webSurface->create(currentContext);
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/"));
_webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) {
context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject));
});
_webSurface->resume();
_webSurface->getRootItem()->setProperty("eventBridge", QVariant::fromValue(_webEntityAPIHelper));
_webSurface->getRootItem()->setProperty("url", _sourceUrl);
_webSurface->getRootContext()->setContextProperty("desktop", QVariant());
_webSurface->getRootContext()->setContextProperty("webEntity", _webEntityAPIHelper);
_connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) {
_texture = textureId;
// forward web events to EntityScriptingInterface
auto entities = DependencyManager::get<EntityScriptingInterface>();
const EntityItemID entityItemID = getID();
QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, [=](const QVariant& message) {
emit entities->webEventReceived(entityItemID, message);
});
// Restore the original GL context
currentContext->makeCurrent(currentSurface);
@ -217,20 +187,33 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
// without worrying about excessive overhead.
_webSurface->resize(QSize(windowSize.x, windowSize.y));
if (!_texture) {
auto webSurface = _webSurface;
auto recycler = [webSurface] (uint32_t recycleTexture, void* recycleFence) {
webSurface->releaseTexture({ recycleTexture, recycleFence });
};
_texture = gpu::TexturePointer(gpu::Texture::createExternal2D(recycler));
_texture->setSource(__FUNCTION__);
}
OffscreenQmlSurface::TextureAndFence newTextureAndFence;
bool newTextureAvailable = _webSurface->fetchTexture(newTextureAndFence);
if (newTextureAvailable) {
_texture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second);
}
PerformanceTimer perfTimer("RenderableWebEntityItem::render");
Q_ASSERT(getType() == EntityTypes::Web);
static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f);
Q_ASSERT(args->_batch);
gpu::Batch& batch = *args->_batch;
bool success;
batch.setModelTransform(getTransformToCenter(success));
if (!success) {
return;
}
if (_texture) {
batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture);
}
batch.setResourceTexture(0, _texture);
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
@ -344,20 +327,10 @@ void RenderableWebEntityItem::destroyWebSurface() {
_mouseMoveConnection = QMetaObject::Connection();
QObject::disconnect(_hoverLeaveConnection);
_hoverLeaveConnection = QMetaObject::Connection();
// The lifetime of the QML surface MUST be managed by the main thread
// Additionally, we MUST use local variables copied by value, rather than
// member variables, since they would implicitly refer to a this that
// is no longer valid
auto webSurface = _webSurface;
AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
webSurface->deleteLater();
});
_webSurface = nullptr;
_webSurface.reset();
}
}
void RenderableWebEntityItem::update(const quint64& now) {
auto interval = now - _lastRenderTime;
if (interval > MAX_NO_RENDER_INTERVAL) {
@ -365,78 +338,13 @@ void RenderableWebEntityItem::update(const quint64& now) {
}
}
bool RenderableWebEntityItem::isTransparent() {
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
return fadeRatio < OPAQUE_ALPHA_THRESHOLD;
}
// UTF-8 encoded symbols
static const uint8_t UPWARDS_WHITE_ARROW_FROM_BAR[] = { 0xE2, 0x87, 0xAA, 0x00 }; // shift
static const uint8_t LEFT_ARROW[] = { 0xE2, 0x86, 0x90, 0x00 }; // backspace
static const uint8_t LEFTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA6, 0x00 }; // left arrow
static const uint8_t RIGHTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA8, 0x00 }; // right arrow
static const uint8_t ASTERISIM[] = { 0xE2, 0x81, 0x82, 0x00 }; // symbols
static const uint8_t RETURN_SYMBOL[] = { 0xE2, 0x8F, 0x8E, 0x00 }; // return
static const char PUNCTUATION_STRING[] = "&123";
static const char ALPHABET_STRING[] = "abc";
static bool equals(const QByteArray& byteArray, const uint8_t* ptr) {
int i;
for (i = 0; i < byteArray.size(); i++) {
if ((char)ptr[i] != byteArray[i]) {
return false;
}
}
return ptr[i] == 0x00;
}
void RenderableWebEntityItem::synthesizeKeyPress(QString key) {
auto eventHandler = getEventHandler();
if (eventHandler) {
auto utf8Key = key.toUtf8();
int scanCode = (int)utf8Key[0];
QString keyString = key;
if (equals(utf8Key, UPWARDS_WHITE_ARROW_FROM_BAR) || equals(utf8Key, ASTERISIM) ||
equals(utf8Key, (uint8_t*)PUNCTUATION_STRING) || equals(utf8Key, (uint8_t*)ALPHABET_STRING)) {
return; // ignore
} else if (equals(utf8Key, LEFT_ARROW)) {
scanCode = Qt::Key_Backspace;
keyString = "\x08";
} else if (equals(utf8Key, RETURN_SYMBOL)) {
scanCode = Qt::Key_Return;
keyString = "\x0d";
} else if (equals(utf8Key, LEFTWARD_WHITE_ARROW)) {
scanCode = Qt::Key_Left;
keyString = "";
} else if (equals(utf8Key, RIGHTWARD_WHITE_ARROW)) {
scanCode = Qt::Key_Right;
keyString = "";
}
QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString);
QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString);
QCoreApplication::postEvent(eventHandler, pressEvent);
QCoreApplication::postEvent(eventHandler, releaseEvent);
}
}
void RenderableWebEntityItem::emitScriptEvent(const QVariant& message) {
if (_webEntityAPIHelper) {
_webEntityAPIHelper->emitScriptEvent(message);
}
}
void RenderableWebEntityItem::setKeyboardRaised(bool raised) {
// raise the keyboard only while in HMD mode and it's being requested.
bool value = AbstractViewStateInterface::instance()->isHMDMode() && raised;
if (_webSurface) {
auto rootItem = _webSurface->getRootItem();
if (rootItem) {
rootItem->setProperty("keyboardRaised", QVariant(value));
}
_webSurface->emitScriptEvent(message);
}
}

View file

@ -13,36 +13,18 @@
#include <QMouseEvent>
#include <QTouchEvent>
#include <PointerEvent.h>
#include <gl/OffscreenQmlSurface.h>
#include <WebEntityItem.h>
#include "RenderableEntityItem.h"
class OffscreenQmlSurface;
class QWindow;
class QObject;
class EntityTreeRenderer;
class RenderableWebEntityItem;
class WebEntityAPIHelper : public QObject {
Q_OBJECT
public:
void setRenderableWebEntityItem(RenderableWebEntityItem* renderableWebEntityItem) {
_renderableWebEntityItem = renderableWebEntityItem;
}
Q_INVOKABLE void synthesizeKeyPress(QString key);
// event bridge
public slots:
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
signals:
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
protected:
RenderableWebEntityItem* _renderableWebEntityItem{ nullptr };
};
class RenderableWebEntityItem : public WebEntityItem {
public:
@ -64,29 +46,24 @@ public:
bool needsToCallUpdate() const override { return _webSurface != nullptr; }
virtual void emitScriptEvent(const QVariant& message) override;
void setKeyboardRaised(bool raised);
SIMPLE_RENDERABLE();
virtual bool isTransparent() override;
public:
void synthesizeKeyPress(QString key);
private:
bool buildWebSurface(EntityTreeRenderer* renderer);
void destroyWebSurface();
glm::vec2 getWindowSize() const;
OffscreenQmlSurface* _webSurface{ nullptr };
QSharedPointer<OffscreenQmlSurface> _webSurface;
QMetaObject::Connection _connection;
uint32_t _texture{ 0 };
ivec2 _lastPress{ INT_MIN };
gpu::TexturePointer _texture;
ivec2 _lastPress { INT_MIN };
bool _pressed{ false };
QTouchEvent _lastTouchEvent { QEvent::TouchUpdate };
uint64_t _lastRenderTime{ 0 };
QTouchDevice _touchDevice;
WebEntityAPIHelper* _webEntityAPIHelper;
QMetaObject::Connection _mousePressConnection;
QMetaObject::Connection _mouseReleaseConnection;

View file

@ -12,6 +12,7 @@
#include "EntityItemID.h"
#include <VariantMapToScriptValue.h>
#include <SharedUtil.h>
#include <SpatialParentFinder.h>
#include "EntitiesLogging.h"
@ -181,6 +182,11 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
propertiesWithSimID.setOwningAvatarID(myNodeID);
}
if (propertiesWithSimID.getParentID() == AVATAR_SELF_ID) {
qDebug() << "ERROR: Cannot set entity parent ID to the local-only MyAvatar ID";
propertiesWithSimID.setParentID(QUuid());
}
auto dimensions = propertiesWithSimID.getDimensions();
float volume = dimensions.x * dimensions.y * dimensions.z;
auto density = propertiesWithSimID.getDensity();
@ -357,6 +363,9 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
if (!scriptSideProperties.parentIDChanged()) {
properties.setParentID(entity->getParentID());
} else if (scriptSideProperties.getParentID() == AVATAR_SELF_ID) {
qDebug() << "ERROR: Cannot set entity parent ID to the local-only MyAvatar ID";
properties.setParentID(QUuid());
}
if (!scriptSideProperties.parentJointIndexChanged()) {
properties.setParentJointIndex(entity->getParentJointIndex());
@ -386,9 +395,14 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
}
});
if (!updatedEntity) {
return QUuid();
}
// FIXME: We need to figure out a better way to handle this. Allowing these edits to go through potentially
// breaks avatar energy and entities that are parented.
//
// To handle cases where a script needs to edit an entity with a _known_ entity id but doesn't exist
// in the local entity tree, we need to allow those edits to go through to the server.
// if (!updatedEntity) {
// return QUuid();
// }
_entityTree->withReadLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
@ -610,11 +624,11 @@ QVector<QUuid> EntityScriptingInterface::findEntitiesInFrustum(QVariantMap frust
}
RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking,
const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard) {
const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) {
QVector<EntityItemID> entitiesToInclude = qVectorEntityItemIDFromScriptValue(entityIdsToInclude);
QVector<EntityItemID> entitiesToDiscard = qVectorEntityItemIDFromScriptValue(entityIdsToDiscard);
return findRayIntersectionWorker(ray, Octree::Lock, precisionPicking, entitiesToInclude, entitiesToDiscard);
return findRayIntersectionWorker(ray, Octree::Lock, precisionPicking, entitiesToInclude, entitiesToDiscard, visibleOnly, collidableOnly);
}
// FIXME - we should remove this API and encourage all users to use findRayIntersection() instead. We've changed
@ -629,17 +643,18 @@ RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionBlock
}
RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionWorker(const PickRay& ray,
Octree::lockType lockType,
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard) {
Octree::lockType lockType, bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) {
RayToEntityIntersectionResult result;
if (_entityTree) {
OctreeElementPointer element;
EntityItemPointer intersectedEntity = NULL;
result.intersects = _entityTree->findRayIntersection(ray.origin, ray.direction, element, result.distance, result.face,
result.surfaceNormal, entityIdsToInclude, entityIdsToDiscard, (void**)&intersectedEntity, lockType, &result.accurate,
precisionPicking);
result.intersects = _entityTree->findRayIntersection(ray.origin, ray.direction,
entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking,
element, result.distance, result.face, result.surfaceNormal,
(void**)&intersectedEntity, lockType, &result.accurate);
if (result.intersects && intersectedEntity) {
result.entityID = intersectedEntity->getEntityItemID();
result.properties = intersectedEntity->getProperties();

View file

@ -143,7 +143,9 @@ public slots:
/// If the scripting context has visible entities, this will determine a ray intersection, the results
/// may be inaccurate if the engine is unable to access the visible entities, in which case result.accurate
/// will be false.
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersection(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersection(const PickRay& ray, bool precisionPicking = false,
const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue(),
bool visibleOnly = false, bool collidableOnly = false);
/// If the scripting context has visible entities, this will determine a ray intersection, and will block in
/// order to return an accurate result
@ -257,7 +259,8 @@ private:
/// actually does the work of finding the ray intersection, can be called in locking mode or tryLock mode
RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType,
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard);
bool precisionPicking, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard,
bool visibleOnly = false, bool collidableOnly = false);
EntityTreePointer _entityTree;

View file

@ -29,6 +29,28 @@ static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC *
const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour
// combines the ray cast arguments into a single object
class RayArgs {
public:
// Inputs
glm::vec3 origin;
glm::vec3 direction;
const QVector<EntityItemID>& entityIdsToInclude;
const QVector<EntityItemID>& entityIdsToDiscard;
bool visibleOnly;
bool collidableOnly;
bool precisionPicking;
// Outputs
OctreeElementPointer& element;
float& distance;
BoxFace& face;
glm::vec3& surfaceNormal;
void** intersectedObject;
bool found;
};
EntityTree::EntityTree(bool shouldReaverage) :
Octree(shouldReaverage),
_fbxService(NULL),
@ -538,40 +560,28 @@ bool EntityTree::findNearPointOperation(OctreeElementPointer element, void* extr
// if this element doesn't contain the point, then none of its children can contain the point, so stop searching
return false;
}
// combines the ray cast arguments into a single object
class RayArgs {
public:
glm::vec3 origin;
glm::vec3 direction;
OctreeElementPointer& element;
float& distance;
BoxFace& face;
glm::vec3& surfaceNormal;
const QVector<EntityItemID>& entityIdsToInclude;
const QVector<EntityItemID>& entityIdsToDiscard;
void** intersectedObject;
bool found;
bool precisionPicking;
};
bool findRayIntersectionOp(OctreeElementPointer element, void* extraData) {
RayArgs* args = static_cast<RayArgs*>(extraData);
bool keepSearching = true;
EntityTreeElementPointer entityTreeElementPointer = std::dynamic_pointer_cast<EntityTreeElement>(element);
if (entityTreeElementPointer ->findRayIntersection(args->origin, args->direction, keepSearching,
if (entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching,
args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude,
args->entityIdsToDiscard, args->intersectedObject, args->precisionPicking)) {
args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->intersectedObject, args->precisionPicking)) {
args->found = true;
}
return keepSearching;
}
bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard, void** intersectedObject,
Octree::lockType lockType, bool* accurateResult, bool precisionPicking) {
RayArgs args = { origin, direction, element, distance, face, surfaceNormal, entityIdsToInclude, entityIdsToDiscard, intersectedObject, false, precisionPicking };
QVector<EntityItemID> entityIdsToInclude, QVector<EntityItemID> entityIdsToDiscard,
bool visibleOnly, bool collidableOnly, bool precisionPicking,
OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject,
Octree::lockType lockType, bool* accurateResult) {
RayArgs args = { origin, direction, entityIdsToInclude, entityIdsToDiscard,
visibleOnly, collidableOnly, precisionPicking,
element, distance, face, surfaceNormal, intersectedObject, false };
distance = FLT_MAX;
bool requireLock = lockType == Octree::Lock;

View file

@ -31,6 +31,7 @@ using ModelWeakPointer = std::weak_ptr<Model>;
class EntitySimulation;
class NewlyCreatedEntityHook {
public:
virtual void entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) = 0;
@ -89,13 +90,11 @@ public:
const SharedNodePointer& senderNode) override;
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
OctreeElementPointer& node, float& distance, BoxFace& face, glm::vec3& surfaceNormal,
const QVector<EntityItemID>& entityIdsToInclude = QVector<EntityItemID>(),
const QVector<EntityItemID>& entityIdsToDiscard = QVector<EntityItemID>(),
void** intersectedObject = NULL,
Octree::lockType lockType = Octree::TryLock,
bool* accurateResult = NULL,
bool precisionPicking = false);
QVector<EntityItemID> entityIdsToInclude, QVector<EntityItemID> entityIdsToDiscard,
bool visibleOnly, bool collidableOnly, bool precisionPicking,
OctreeElementPointer& node, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject = NULL,
Octree::lockType lockType = Octree::TryLock, bool* accurateResult = NULL);
virtual bool rootElementHasData() const override { return true; }

View file

@ -534,7 +534,8 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3
bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
bool& keepSearching, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard, void** intersectedObject, bool precisionPicking) {
const QVector<EntityItemID>& entityIdsToDiscard, bool visibleOnly, bool collidableOnly,
void** intersectedObject, bool precisionPicking) {
keepSearching = true; // assume that we will continue searching after this.
@ -559,7 +560,8 @@ bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::
if (_cube.contains(origin) || distanceToElementCube < distance) {
if (findDetailedRayIntersection(origin, direction, keepSearching, element, distanceToElementDetails,
face, localSurfaceNormal, entityIdsToInclude, entityIdsToDiscard, intersectedObject, precisionPicking, distanceToElementCube)) {
face, localSurfaceNormal, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly,
intersectedObject, precisionPicking, distanceToElementCube)) {
if (distanceToElementDetails < distance) {
distance = distanceToElementDetails;
@ -574,13 +576,16 @@ bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::
bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching,
OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal,
const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIDsToDiscard, void** intersectedObject, bool precisionPicking, float distanceToElementCube) {
const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIDsToDiscard,
bool visibleOnly, bool collidableOnly, void** intersectedObject, bool precisionPicking, float distanceToElementCube) {
// only called if we do intersect our bounding cube, but find if we actually intersect with entities...
int entityNumber = 0;
bool somethingIntersected = false;
forEachEntity([&](EntityItemPointer entity) {
if ( (entityIdsToInclude.size() > 0 && !entityIdsToInclude.contains(entity->getID())) || (entityIDsToDiscard.size() > 0 && entityIDsToDiscard.contains(entity->getID())) ) {
if ( (visibleOnly && !entity->isVisible()) || (collidableOnly && (entity->getCollisionless() || entity->getShapeType() == SHAPE_TYPE_NONE))
|| (entityIdsToInclude.size() > 0 && !entityIdsToInclude.contains(entity->getID()))
|| (entityIDsToDiscard.size() > 0 && entityIDsToDiscard.contains(entity->getID())) ) {
return;
}
@ -638,7 +643,7 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con
if (localDistance < distance && entity->getType() != EntityTypes::ParticleEffect) {
distance = localDistance;
face = localFace;
surfaceNormal = localSurfaceNormal;
surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 1.0f));
*intersectedObject = (void*)entity.get();
somethingIntersected = true;
}

View file

@ -147,12 +147,12 @@ public:
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
bool& keepSearching, OctreeElementPointer& node, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard,
const QVector<EntityItemID>& entityIdsToDiscard, bool visibleOnly = false, bool collidableOnly = false,
void** intersectedObject = NULL, bool precisionPicking = false);
virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
bool& keepSearching, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
const QVector<EntityItemID>& entityIdsToDiscard,
const QVector<EntityItemID>& entityIdsToDiscard, bool visibleOnly, bool collidableOnly,
void** intersectedObject, bool precisionPicking, float distanceToElementCube);
virtual bool findSpherePenetration(const glm::vec3& center, float radius,
glm::vec3& penetration, void** penetratedObject) const override;

View file

@ -6,5 +6,4 @@ target_opengl()
if (NOT ANDROID)
target_glew()
target_oglplus()
endif ()

View file

@ -0,0 +1,189 @@
#include "GLShaders.h"
#include "GLLogging.h"
namespace gl {
#ifdef SEPARATE_PROGRAM
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject) {
#else
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject) {
#endif
if (shaderSource.empty()) {
qCDebug(glLogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create";
return false;
}
// Create the shader object
GLuint glshader = glCreateShader(shaderDomain);
if (!glshader) {
qCDebug(glLogging) << "GLShader::compileShader - failed to create the gl shader object";
return false;
}
// Assign the source
const int NUM_SOURCE_STRINGS = 2;
const GLchar* srcstr[] = { defines.c_str(), shaderSource.c_str() };
glShaderSource(glshader, NUM_SOURCE_STRINGS, srcstr, NULL);
// Compile !
glCompileShader(glshader);
// check if shader compiled
GLint compiled = 0;
glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled);
// if compilation fails
if (!compiled) {
// save the source code to a temp file so we can debug easily
/*
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << srcstr[0];
filestream << srcstr[1];
filestream.close();
}
*/
GLint infoLength = 0;
glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetShaderInfoLog(glshader, infoLength, NULL, temp);
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << std::string(temp);
filestream.close();
}
*/
qCWarning(glLogging) << "GLShader::compileShader - failed to compile the gl shader object:";
for (auto s : srcstr) {
qCWarning(glLogging) << s;
}
qCWarning(glLogging) << "GLShader::compileShader - errors:";
qCWarning(glLogging) << temp;
delete[] temp;
glDeleteShader(glshader);
return false;
}
#ifdef SEPARATE_PROGRAM
GLuint glprogram = 0;
// so far so good, program is almost done, need to link:
GLuint glprogram = glCreateProgram();
if (!glprogram) {
qCDebug(glLogging) << "GLShader::compileShader - failed to create the gl shader & gl program object";
return false;
}
glProgramParameteri(glprogram, GL_PROGRAM_SEPARABLE, GL_TRUE);
glAttachShader(glprogram, glshader);
glLinkProgram(glprogram);
GLint linked = 0;
glGetProgramiv(glprogram, GL_LINK_STATUS, &linked);
if (!linked) {
/*
// save the source code to a temp file so we can debug easily
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << shaderSource->source;
filestream.close();
}
*/
GLint infoLength = 0;
glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetProgramInfoLog(glprogram, infoLength, NULL, temp);
qCDebug(glLogging) << "GLShader::compileShader - failed to LINK the gl program object :";
qCDebug(glLogging) << temp;
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << String(temp);
filestream.close();
}
*/
delete[] temp;
glDeleteShader(glshader);
glDeleteProgram(glprogram);
return false;
}
programObject = glprogram;
#endif
shaderObject = glshader;
return true;
}
GLuint compileProgram(const std::vector<GLuint>& glshaders) {
// A brand new program:
GLuint glprogram = glCreateProgram();
if (!glprogram) {
qCDebug(glLogging) << "GLShader::compileProgram - failed to create the gl program object";
return 0;
}
// glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE);
// Create the program from the sub shaders
for (auto so : glshaders) {
glAttachShader(glprogram, so);
}
// Link!
glLinkProgram(glprogram);
GLint linked = 0;
glGetProgramiv(glprogram, GL_LINK_STATUS, &linked);
if (!linked) {
/*
// save the source code to a temp file so we can debug easily
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << shaderSource->source;
filestream.close();
}
*/
GLint infoLength = 0;
glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetProgramInfoLog(glprogram, infoLength, NULL, temp);
qCDebug(glLogging) << "GLShader::compileProgram - failed to LINK the gl program object :";
qCDebug(glLogging) << temp;
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << std::string(temp);
filestream.close();
}
*/
delete[] temp;
glDeleteProgram(glprogram);
return 0;
}
return glprogram;
}
}

View file

@ -0,0 +1,29 @@
//
// Created by Bradley Austin Davis 2016/09/27
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_GLShaders_h
#define hifi_GLShaders_h
#include "Config.h"
#include <vector>
#include <string>
namespace gl {
#ifdef SEPARATE_PROGRAM
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject);
#else
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject);
#endif
GLuint compileProgram(const std::vector<GLuint>& glshaders);
}
#endif

View file

@ -32,24 +32,25 @@
#include <AbstractUriHandler.h>
#include <AccountManager.h>
#include <NetworkAccessManager.h>
#include <GLMHelpers.h>
#include "OffscreenGLCanvas.h"
#include "GLEscrow.h"
#include "GLHelpers.h"
#include "GLLogging.h"
#include "TextureRecycler.h"
#include "Context.h"
QString fixupHifiUrl(const QString& urlString) {
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
QUrl url(urlString);
QUrlQuery query(url);
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
auto accountManager = DependencyManager::get<AccountManager>();
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token);
url.setQuery(query.query());
return url.toString();
}
QUrlQuery query(url);
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
auto accountManager = DependencyManager::get<AccountManager>();
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token);
url.setQuery(query.query());
return url.toString();
}
return urlString;
}
@ -115,250 +116,8 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) {
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1);
static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2);
static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3);
static const QEvent::Type STOP = QEvent::Type(QEvent::User + 4);
class RawTextureRecycler {
public:
using TexturePtr = GLuint;
RawTextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
void setSize(const uvec2& size);
void clear();
TexturePtr getNextTexture();
void recycleTexture(GLuint texture);
private:
struct TexInfo {
TexturePtr _tex { 0 };
uvec2 _size;
bool _active { false };
TexInfo() {}
TexInfo(TexturePtr tex, const uvec2& size) : _tex(tex), _size(size) {}
};
using Map = std::map<GLuint, TexInfo>;
using Queue = std::queue<TexturePtr>;
Map _allTextures;
Queue _readyTextures;
uvec2 _size { 1920, 1080 };
bool _useMipmaps;
};
void RawTextureRecycler::setSize(const uvec2& size) {
if (size == _size) {
return;
}
_size = size;
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
std::set<Map::key_type> toDelete;
std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) {
if (!item.second._active && item.second._size != _size) {
toDelete.insert(item.first);
}
});
std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) {
_allTextures.erase(key);
});
}
void RawTextureRecycler::clear() {
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
_allTextures.clear();
}
RawTextureRecycler::TexturePtr RawTextureRecycler::getNextTexture() {
if (_readyTextures.empty()) {
TexturePtr newTexture;
glGenTextures(1, &newTexture);
glBindTexture(GL_TEXTURE_2D, newTexture);
if (_useMipmaps) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
_allTextures[newTexture] = TexInfo { newTexture, _size };
_readyTextures.push(newTexture);
}
TexturePtr result = _readyTextures.front();
_readyTextures.pop();
auto& item = _allTextures[result];
item._active = true;
return result;
}
void RawTextureRecycler::recycleTexture(GLuint texture) {
Q_ASSERT(_allTextures.count(texture));
auto& item = _allTextures[texture];
Q_ASSERT(item._active);
item._active = false;
if (item._size != _size) {
// Buh-bye
_allTextures.erase(texture);
return;
}
_readyTextures.push(item._tex);
}
class OffscreenQmlRenderThread : public QThread {
public:
OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext);
virtual ~OffscreenQmlRenderThread() = default;
virtual void run() override;
virtual bool event(QEvent *e) override;
protected:
class Queue : private QQueue<QEvent*> {
public:
void add(QEvent::Type type);
QEvent* take();
private:
QMutex _mutex;
QWaitCondition _waitCondition;
bool _isWaiting{ false };
};
friend class OffscreenQmlSurface;
QJsonObject getGLContextData();
Queue _queue;
QMutex _mutex;
QWaitCondition _waitCondition;
std::atomic<bool> _rendering { false };
QJsonObject _glData;
QMutex _glMutex;
QWaitCondition _glWait;
private:
// Event-driven methods
void init();
void render();
void resize();
void cleanup();
// Helper methods
void setupFbo();
bool allowNewFrame(uint8_t fps);
// Rendering members
OffscreenGLCanvas _canvas;
OffscreenQmlSurface* _surface{ nullptr };
QQuickWindow* _quickWindow{ nullptr };
QMyQuickRenderControl* _renderControl{ nullptr };
GLuint _fbo { 0 };
GLuint _depthStencil { 0 };
RawTextureRecycler _textures { true };
GLTextureEscrow _escrow;
uint64_t _lastRenderTime{ 0 };
uvec2 _size{ 1920, 1080 };
QSize _newSize;
bool _quit{ false };
};
void OffscreenQmlRenderThread::Queue::add(QEvent::Type type) {
QMutexLocker locker(&_mutex);
enqueue(new QEvent(type));
if (_isWaiting) {
_waitCondition.wakeOne();
}
}
QEvent* OffscreenQmlRenderThread::Queue::take() {
QMutexLocker locker(&_mutex);
while (isEmpty()) {
_isWaiting = true;
_waitCondition.wait(&_mutex);
_isWaiting = false;
}
QEvent* e = dequeue();
return e;
}
OffscreenQmlRenderThread::OffscreenQmlRenderThread(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) {
_canvas.setObjectName("OffscreenQmlRenderCanvas");
qCDebug(glLogging) << "Building QML Renderer";
if (!_canvas.create(shareContext)) {
qWarning("Failed to create OffscreenGLCanvas");
_quit = true;
return;
};
_renderControl = new QMyQuickRenderControl();
QQuickWindow::setDefaultAlphaBuffer(true);
// Create a QQuickWindow that is associated with our render control.
// This window never gets created or shown, meaning that it will never get an underlying native (platform) window.
// NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events
// NOTE: Must be created on the rendering thread or it will refuse to render,
// so we wait until after its ctor to move object/context to this thread.
_quickWindow = new QQuickWindow(_renderControl);
_quickWindow->setColor(QColor(255, 255, 255, 0));
_quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
// We can prepare, but we must wait to start() the thread until after the ctor
_renderControl->prepareThread(this);
_canvas.getContextObject()->moveToThread(this);
moveToThread(this);
_queue.add(INIT);
}
void OffscreenQmlRenderThread::run() {
qCDebug(glLogging) << "Starting QML Renderer thread";
while (!_quit) {
QEvent* e = _queue.take();
event(e);
delete e;
}
}
bool OffscreenQmlRenderThread::event(QEvent *e) {
switch (int(e->type())) {
case INIT:
init();
return true;
case RENDER:
render();
return true;
case RESIZE:
resize();
return true;
case STOP:
cleanup();
return true;
default:
return QObject::event(e);
}
}
void OffscreenQmlRenderThread::setupFbo() {
void OffscreenQmlSurface::setupFbo() {
_canvas->makeCurrent();
_textures.setSize(_size);
if (_depthStencil) {
glDeleteRenderbuffers(1, &_depthStencil);
@ -376,44 +135,12 @@ void OffscreenQmlRenderThread::setupFbo() {
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
_canvas->doneCurrent();
}
QJsonObject OffscreenQmlRenderThread::getGLContextData() {
_glMutex.lock();
if (_glData.isEmpty()) {
_glWait.wait(&_glMutex);
}
_glMutex.unlock();
return _glData;
}
void OffscreenQmlRenderThread::init() {
qCDebug(glLogging) << "Initializing QML Renderer";
if (!_canvas.makeCurrent()) {
qWarning("Failed to make context current on QML Renderer Thread");
_quit = true;
return;
}
_glMutex.lock();
_glData = ::getGLContextData();
_glMutex.unlock();
_glWait.wakeAll();
connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender);
connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate);
_renderControl->initialize(_canvas.getContext());
setupFbo();
_escrow.setRecycler([this](GLuint texture){
_textures.recycleTexture(texture);
});
}
void OffscreenQmlRenderThread::cleanup() {
void OffscreenQmlSurface::cleanup() {
_canvas->makeCurrent();
_renderControl->invalidate();
if (_depthStencil) {
glDeleteRenderbuffers(1, &_depthStencil);
_depthStencil = 0;
@ -424,88 +151,97 @@ void OffscreenQmlRenderThread::cleanup() {
}
_textures.clear();
_canvas.doneCurrent();
_canvas.getContextObject()->moveToThread(QCoreApplication::instance()->thread());
_quit = true;
_canvas->doneCurrent();
}
void OffscreenQmlRenderThread::resize() {
// Lock _newSize changes
{
QMutexLocker locker(&_mutex);
// Update our members
if (_quickWindow) {
_quickWindow->setGeometry(QRect(QPoint(), _newSize));
_quickWindow->contentItem()->setSize(_newSize);
}
// Qt bug in 5.4 forces this check of pixel ratio,
// even though we're rendering offscreen.
qreal pixelRatio = 1.0;
if (_renderControl && _renderControl->_renderWindow) {
pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
}
uvec2 newOffscreenSize = toGlm(_newSize * pixelRatio);
if (newOffscreenSize == _size) {
return;
}
qCDebug(glLogging) << "Offscreen UI resizing to " << _newSize.width() << "x" << _newSize.height() << " with pixel ratio " << pixelRatio;
_size = newOffscreenSize;
}
_textures.setSize(_size);
setupFbo();
}
void OffscreenQmlRenderThread::render() {
// Ensure we always release the main thread
Finally releaseMainThread([this] {
_waitCondition.wakeOne();
});
if (_surface->_paused) {
void OffscreenQmlSurface::render() {
if (_paused) {
return;
}
_rendering = true;
Finally unmarkRenderingFlag([this] {
_rendering = false;
});
{
QMutexLocker locker(&_mutex);
_renderControl->sync();
releaseMainThread.trigger();
}
_canvas->makeCurrent();
_renderControl->sync();
_quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y));
try {
GLuint texture = _textures.getNextTexture();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
PROFILE_RANGE("qml_render->rendercontrol")
_renderControl->render();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
_quickWindow->resetOpenGLState();
_escrow.submit(texture);
_lastRenderTime = usecTimestampNow();
} catch (std::runtime_error& error) {
qWarning() << "Failed to render QML: " << error.what();
// Clear out any pending textures to be returned
{
std::list<OffscreenQmlSurface::TextureAndFence> returnedTextures;
{
std::unique_lock<std::mutex> lock(_textureMutex);
returnedTextures.swap(_returnedTextures);
}
if (!returnedTextures.empty()) {
for (const auto& textureAndFence : returnedTextures) {
GLsync fence = static_cast<GLsync>(textureAndFence.second);
if (fence) {
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(fence);
}
_textures.recycleTexture(textureAndFence.first);
}
}
}
GLuint texture = _textures.getNextTexture();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
PROFILE_RANGE("qml_render->rendercontrol")
_renderControl->render();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
{
std::unique_lock<std::mutex> lock(_textureMutex);
// If the most recent texture was unused, we can directly recycle it
if (_latestTextureAndFence.first) {
_textures.recycleTexture(_latestTextureAndFence.first);
glDeleteSync(static_cast<GLsync>(_latestTextureAndFence.second));
_latestTextureAndFence = { 0, 0 };
}
_latestTextureAndFence = { texture, glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0) };
// Fence will be used in another thread / context, so a flush is required
glFlush();
}
_quickWindow->resetOpenGLState();
_lastRenderTime = usecTimestampNow();
_canvas->doneCurrent();
}
bool OffscreenQmlRenderThread::allowNewFrame(uint8_t fps) {
bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) {
textureAndFence = { 0, 0 };
std::unique_lock<std::mutex> lock(_textureMutex);
if (0 == _latestTextureAndFence.first) {
return false;
}
// Ensure writes to the latest texture are complete before before returning it for reading
textureAndFence = _latestTextureAndFence;
_latestTextureAndFence = { 0, 0 };
return true;
}
void OffscreenQmlSurface::releaseTexture(const TextureAndFence& textureAndFence) {
std::unique_lock<std::mutex> lock(_textureMutex);
_returnedTextures.push_back(textureAndFence);
}
bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) {
// If we already have a pending texture, don't render another one
// i.e. don't render faster than the consumer context, since it wastes
// GPU cycles on producing output that will never be seen
{
std::unique_lock<std::mutex> lock(_textureMutex);
if (0 != _latestTextureAndFence.first) {
return false;
}
}
auto minRenderInterval = USECS_PER_SECOND / fps;
auto lastInterval = usecTimestampNow() - _lastRenderTime;
return (lastInterval > minRenderInterval);
@ -519,33 +255,46 @@ OffscreenQmlSurface::~OffscreenQmlSurface() {
QObject::disconnect(&_updateTimer);
QObject::disconnect(qApp);
qCDebug(glLogging) << "Stopping QML Renderer Thread " << _renderer->currentThreadId();
_renderer->_queue.add(STOP);
if (!_renderer->wait(MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) {
qWarning() << "Failed to shut down the QML Renderer Thread";
}
delete _rootItem;
delete _renderer;
delete _qmlComponent;
delete _qmlEngine;
cleanup();
_canvas->deleteLater();
_rootItem->deleteLater();
_qmlComponent->deleteLater();
_qmlEngine->deleteLater();
_quickWindow->deleteLater();
}
void OffscreenQmlSurface::onAboutToQuit() {
_paused = true;
QObject::disconnect(&_updateTimer);
}
void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
qCDebug(glLogging) << "Building QML surface";
_renderer = new OffscreenQmlRenderThread(this, shareContext);
_renderer->moveToThread(_renderer);
_renderer->setObjectName("QML Renderer Thread");
_renderer->start();
_renderControl = new QMyQuickRenderControl();
_renderer->_renderControl->_renderWindow = _proxyWindow;
QQuickWindow::setDefaultAlphaBuffer(true);
connect(_renderer->_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged);
// Create a QQuickWindow that is associated with our render control.
// This window never gets created or shown, meaning that it will never get an underlying native (platform) window.
// NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events
// NOTE: Must be created on the rendering thread or it will refuse to render,
// so we wait until after its ctor to move object/context to this thread.
_quickWindow = new QQuickWindow(_renderControl);
_quickWindow->setColor(QColor(255, 255, 255, 0));
_quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
_renderControl->_renderWindow = _proxyWindow;
_canvas = new OffscreenGLCanvas();
if (!_canvas->create(shareContext)) {
qFatal("Failed to create OffscreenGLCanvas");
return;
};
connect(_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged);
// Create a QML engine.
_qmlEngine = new QQmlEngine;
@ -556,13 +305,26 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
importList.insert(importList.begin(), PathUtils::resourcesPath());
_qmlEngine->setImportPathList(importList);
if (!_qmlEngine->incubationController()) {
_qmlEngine->setIncubationController(_renderer->_quickWindow->incubationController());
_qmlEngine->setIncubationController(_quickWindow->incubationController());
}
_qmlEngine->rootContext()->setContextProperty("GL", _renderer->getGLContextData());
// FIXME
_qmlEngine->rootContext()->setContextProperty("GL", _glData);
_qmlEngine->rootContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow()));
_qmlComponent = new QQmlComponent(_qmlEngine);
connect(_renderControl, &QQuickRenderControl::renderRequested, [this] { _render = true; });
connect(_renderControl, &QQuickRenderControl::sceneChanged, [this] { _render = _polish = true; });
if (!_canvas->makeCurrent()) {
qWarning("Failed to make context current for QML Renderer");
return;
}
_glData = ::getGLContextData();
_renderControl->initialize(_canvas->getContext());
setupFbo();
// When Quick says there is a need to render, we will not render immediately. Instead,
// a timer with a small interval is used to get better performance.
QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick);
@ -577,7 +339,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
if (!_renderer || !_renderer->_quickWindow) {
if (!_quickWindow) {
return;
}
@ -593,7 +355,7 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
std::max(static_cast<int>(scale * newSize.height()), 10));
}
QSize currentSize = _renderer->_quickWindow->geometry().size();
QSize currentSize = _quickWindow->geometry().size();
if (newSize == currentSize && !forceResize) {
return;
}
@ -604,12 +366,26 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) {
_rootItem->setSize(newSize);
}
{
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_newSize = newSize;
// Update our members
_quickWindow->setGeometry(QRect(QPoint(), newSize));
_quickWindow->contentItem()->setSize(newSize);
// Qt bug in 5.4 forces this check of pixel ratio,
// even though we're rendering offscreen.
qreal pixelRatio = 1.0;
if (_renderControl && _renderControl->_renderWindow) {
pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
}
_renderer->_queue.add(RESIZE);
uvec2 newOffscreenSize = toGlm(newSize * pixelRatio);
if (newOffscreenSize == _size) {
return;
}
qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio;
_size = newOffscreenSize;
_textures.setSize(_size);
setupFbo();
}
QQuickItem* OffscreenQmlSurface::getRootItem() {
@ -627,13 +403,13 @@ QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function<void(QQm
_qmlComponent->loadUrl(qmlSource, QQmlComponent::PreferSynchronous);
if (_qmlComponent->isLoading()) {
connect(_qmlComponent, &QQmlComponent::statusChanged, this,
connect(_qmlComponent, &QQmlComponent::statusChanged, this,
[this, f](QQmlComponent::Status){
finishQmlLoad(f);
});
return nullptr;
}
return finishQmlLoad(f);
}
@ -641,15 +417,6 @@ void OffscreenQmlSurface::clearCache() {
getRootContext()->engine()->clearComponentCache();
}
void OffscreenQmlSurface::requestUpdate() {
_polish = true;
_render = true;
}
void OffscreenQmlSurface::requestRender() {
_render = true;
}
QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f) {
disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0);
if (_qmlComponent->isError()) {
@ -660,6 +427,19 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
return nullptr;
}
// FIXME: Refactor with similar code in RenderableWebEntityItem
QString javaScriptToInject;
QFile webChannelFile(":qtwebchannel/qwebchannel.js");
QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js");
if (webChannelFile.open(QFile::ReadOnly | QFile::Text) &&
createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) {
QString webChannelStr = QTextStream(&webChannelFile).readAll();
QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll();
javaScriptToInject = webChannelStr + createGlobalEventBridgeStr;
} else {
qWarning() << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
}
QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp);
QObject* newObject = _qmlComponent->beginCreate(newContext);
if (_qmlComponent->isError()) {
@ -672,6 +452,9 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
return nullptr;
}
newObject->setProperty("eventBridge", QVariant::fromValue(this));
newContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject));
f(newContext, newObject);
_qmlComponent->completeCreate();
@ -679,7 +462,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
// All quick items should be focusable
QQuickItem* newItem = qobject_cast<QQuickItem*>(newObject);
if (newItem) {
// Make sure we make items focusable (critical for
// Make sure we make items focusable (critical for
// supporting keyboard shortcuts)
newItem->setFlag(QQuickItem::ItemIsFocusScope, true);
}
@ -701,38 +484,31 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
}
// The root item is ready. Associate it with the window.
_rootItem = newItem;
_rootItem->setParentItem(_renderer->_quickWindow->contentItem());
_rootItem->setSize(_renderer->_quickWindow->renderTargetSize());
_rootItem->setParentItem(_quickWindow->contentItem());
_rootItem->setSize(_quickWindow->renderTargetSize());
return _rootItem;
}
void OffscreenQmlSurface::updateQuick() {
// If we're
// If we're
// a) not set up
// b) already rendering a frame
// c) rendering too fast
// then skip this
if (!_renderer || _renderer->_rendering || !_renderer->allowNewFrame(_maxFps)) {
// then skip this
if (!allowNewFrame(_maxFps)) {
return;
}
if (_polish) {
_renderer->_renderControl->polishItems();
_renderControl->polishItems();
_polish = false;
}
if (_render) {
PROFILE_RANGE(__FUNCTION__);
// Lock the GUI size while syncing
QMutexLocker locker(&(_renderer->_mutex));
_renderer->_queue.add(RENDER);
_renderer->_waitCondition.wait(&(_renderer->_mutex));
render();
_render = false;
}
if (_renderer->_escrow.fetchSignaledAndRelease(_currentTexture)) {
emit textureUpdated(_currentTexture);
}
}
QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) {
@ -744,7 +520,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec
}
vec2 offscreenPosition = toGlm(sourcePosition);
offscreenPosition /= sourceSize;
offscreenPosition *= vec2(toGlm(_renderer->_quickWindow->size()));
offscreenPosition *= vec2(toGlm(_quickWindow->size()));
return QPointF(offscreenPosition.x, offscreenPosition.y);
}
@ -752,14 +528,13 @@ QPointF OffscreenQmlSurface::mapToVirtualScreen(const QPointF& originalPoint, QO
return _mouseTranslator(originalPoint);
}
///////////////////////////////////////////////////////
//
// Event handling customization
//
bool OffscreenQmlSurface::filterEnabled(QObject* originalDestination, QEvent* event) const {
if (_renderer->_quickWindow == originalDestination) {
if (_quickWindow == originalDestination) {
return false;
}
// Only intercept events while we're in an active state
@ -777,12 +552,11 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
// Don't intercept our own events, or we enter an infinite recursion
QObject* recurseTest = originalDestination;
while (recurseTest) {
Q_ASSERT(recurseTest != _rootItem && recurseTest != _renderer->_quickWindow);
Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow);
recurseTest = recurseTest->parent();
}
#endif
switch (event->type()) {
case QEvent::Resize: {
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(event);
@ -796,7 +570,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
case QEvent::KeyPress:
case QEvent::KeyRelease: {
event->ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, event)) {
if (QCoreApplication::sendEvent(_quickWindow, event)) {
return event->isAccepted();
}
break;
@ -810,7 +584,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
wheelEvent->delta(), wheelEvent->buttons(),
wheelEvent->modifiers(), wheelEvent->orientation());
mappedEvent.ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
return mappedEvent.isAccepted();
}
break;
@ -831,7 +605,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
_qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos);
}
mappedEvent.ignore();
if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
return mappedEvent.isAccepted();
}
break;
@ -850,7 +624,10 @@ void OffscreenQmlSurface::pause() {
void OffscreenQmlSurface::resume() {
_paused = false;
requestRender();
_render = true;
getRootItem()->setProperty("eventBridge", QVariant::fromValue(this));
getRootContext()->setContextProperty("webEntity", this);
}
bool OffscreenQmlSurface::isPaused() const {
@ -859,8 +636,8 @@ bool OffscreenQmlSurface::isPaused() const {
void OffscreenQmlSurface::setProxyWindow(QWindow* window) {
_proxyWindow = window;
if (_renderer && _renderer->_renderControl) {
_renderer->_renderControl->_renderWindow = window;
if (_renderControl) {
_renderControl->_renderWindow = window;
}
}
@ -869,11 +646,11 @@ QObject* OffscreenQmlSurface::getEventHandler() {
}
QQuickWindow* OffscreenQmlSurface::getWindow() {
return _renderer->_quickWindow;
return _quickWindow;
}
QSize OffscreenQmlSurface::size() const {
return _renderer->_quickWindow->geometry().size();
return _quickWindow->geometry().size();
}
QQmlContext* OffscreenQmlSurface::getRootContext() {
@ -908,15 +685,37 @@ QVariant OffscreenQmlSurface::returnFromUiThread(std::function<QVariant()> funct
return function();
}
void OffscreenQmlSurface::focusDestroyed(QObject *obj) {
_currentFocusItem = nullptr;
}
void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) {
if (!object) {
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
if (!item) {
setFocusText(false);
_currentFocusItem = nullptr;
return;
}
QInputMethodQueryEvent query(Qt::ImEnabled);
qApp->sendEvent(object, &query);
setFocusText(query.value(Qt::ImEnabled).toBool());
if (_currentFocusItem) {
disconnect(_currentFocusItem, &QObject::destroyed, this, 0);
}
// Raise and lower keyboard for QML text fields.
// HTML text fields are handled in emitWebEvent() methods - testing READ_ONLY_PROPERTY prevents action for HTML files.
const char* READ_ONLY_PROPERTY = "readOnly";
bool raiseKeyboard = item->hasActiveFocus() && item->property(READ_ONLY_PROPERTY) == false;
if (_currentFocusItem && !raiseKeyboard) {
setKeyboardRaised(_currentFocusItem, false);
}
setKeyboardRaised(item, raiseKeyboard); // Always set focus so that alphabetic / numeric setting is updated.
_currentFocusItem = item;
connect(_currentFocusItem, &QObject::destroyed, this, &OffscreenQmlSurface::focusDestroyed);
}
void OffscreenQmlSurface::setFocusText(bool newFocusText) {
@ -926,4 +725,103 @@ void OffscreenQmlSurface::setFocusText(bool newFocusText) {
}
}
// UTF-8 encoded symbols
static const uint8_t UPWARDS_WHITE_ARROW_FROM_BAR[] = { 0xE2, 0x87, 0xAA, 0x00 }; // shift
static const uint8_t LEFT_ARROW[] = { 0xE2, 0x86, 0x90, 0x00 }; // backspace
static const uint8_t LEFTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA6, 0x00 }; // left arrow
static const uint8_t RIGHTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA8, 0x00 }; // right arrow
static const uint8_t ASTERISIM[] = { 0xE2, 0x81, 0x82, 0x00 }; // symbols
static const uint8_t RETURN_SYMBOL[] = { 0xE2, 0x8F, 0x8E, 0x00 }; // return
static const char PUNCTUATION_STRING[] = "&123";
static const char ALPHABET_STRING[] = "abc";
static bool equals(const QByteArray& byteArray, const uint8_t* ptr) {
int i;
for (i = 0; i < byteArray.size(); i++) {
if ((char)ptr[i] != byteArray[i]) {
return false;
}
}
return ptr[i] == 0x00;
}
void OffscreenQmlSurface::synthesizeKeyPress(QString key) {
auto eventHandler = getEventHandler();
if (eventHandler) {
auto utf8Key = key.toUtf8();
int scanCode = (int)utf8Key[0];
QString keyString = key;
if (equals(utf8Key, UPWARDS_WHITE_ARROW_FROM_BAR) || equals(utf8Key, ASTERISIM) ||
equals(utf8Key, (uint8_t*)PUNCTUATION_STRING) || equals(utf8Key, (uint8_t*)ALPHABET_STRING)) {
return; // ignore
} else if (equals(utf8Key, LEFT_ARROW)) {
scanCode = Qt::Key_Backspace;
keyString = "\x08";
} else if (equals(utf8Key, RETURN_SYMBOL)) {
scanCode = Qt::Key_Return;
keyString = "\x0d";
} else if (equals(utf8Key, LEFTWARD_WHITE_ARROW)) {
scanCode = Qt::Key_Left;
keyString = "";
} else if (equals(utf8Key, RIGHTWARD_WHITE_ARROW)) {
scanCode = Qt::Key_Right;
keyString = "";
}
QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString);
QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString);
QCoreApplication::postEvent(eventHandler, pressEvent);
QCoreApplication::postEvent(eventHandler, releaseEvent);
}
}
void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool numeric) {
if (!object) {
return;
}
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
while (item) {
// Numeric value may be set in parameter from HTML UI; for QML UI, detect numeric fields here.
numeric = numeric || QString(item->metaObject()->className()).left(7) == "SpinBox";
if (item->property("keyboardRaised").isValid()) {
if (item->property("punctuationMode").isValid()) {
item->setProperty("punctuationMode", QVariant(numeric));
}
item->setProperty("keyboardRaised", QVariant(raised));
return;
}
item = dynamic_cast<QQuickItem*>(item->parentItem());
}
}
void OffscreenQmlSurface::emitScriptEvent(const QVariant& message) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
} else {
emit scriptEventReceived(message);
}
}
void OffscreenQmlSurface::emitWebEvent(const QVariant& message) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
} else {
// Special case to handle raising and lowering the virtual keyboard.
const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD";
const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC";
const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD";
QString messageString = message.type() == QVariant::String ? message.toString() : "";
if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) {
setKeyboardRaised(_currentFocusItem, true, messageString == RAISE_KEYBOARD_NUMERIC);
} else if (messageString == LOWER_KEYBOARD) {
setKeyboardRaised(_currentFocusItem, false);
} else {
emit webEventReceived(message);
}
}
}
#include "OffscreenQmlSurface.moc"

View file

@ -9,25 +9,27 @@
#ifndef hifi_OffscreenQmlSurface_h
#define hifi_OffscreenQmlSurface_h
#include <QTimer>
#include <QUrl>
#include <atomic>
#include <functional>
#include <QtCore/QJsonObject>
#include <QTimer>
#include <QUrl>
#include <GLMHelpers.h>
#include <ThreadHelpers.h>
#include "TextureRecycler.h"
class QWindow;
class QMyQuickRenderControl;
class OffscreenGLCanvas;
class QOpenGLContext;
class QQmlEngine;
class QQmlContext;
class QQmlComponent;
class QQuickWindow;
class QQuickItem;
class OffscreenQmlRenderThread;
class OffscreenQmlSurface : public QObject {
Q_OBJECT
Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged)
@ -71,15 +73,35 @@ public:
QPointF mapToVirtualScreen(const QPointF& originalPoint, QObject* originalWidget);
bool eventFilter(QObject* originalDestination, QEvent* event) override;
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
Q_INVOKABLE void synthesizeKeyPress(QString key);
using TextureAndFence = std::pair<uint32_t, void*>;
// Checks to see if a new texture is available. If one is, the function returns true and
// textureAndFence will be populated with the texture ID and a fence which will be signalled
// when the texture is safe to read.
// Returns false if no new texture is available
bool fetchTexture(TextureAndFence& textureAndFence);
// Release a previously acquired texture, along with a fence which indicates when reads from the
// texture have completed.
void releaseTexture(const TextureAndFence& textureAndFence);
signals:
void textureUpdated(unsigned int texture);
void focusObjectChanged(QObject* newFocus);
void focusTextChanged(bool focusText);
public slots:
void requestUpdate();
void requestRender();
void onAboutToQuit();
void focusDestroyed(QObject *obj);
// event bridge
public slots:
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
signals:
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
protected:
bool filterEnabled(QObject* originalDestination, QEvent* event) const;
@ -88,26 +110,47 @@ protected:
private:
QObject* finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f);
QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject);
void setupFbo();
bool allowNewFrame(uint8_t fps);
void render();
void cleanup();
QJsonObject getGLContextData();
private slots:
void updateQuick();
void onFocusObjectChanged(QObject* newFocus);
private:
friend class OffscreenQmlRenderThread;
OffscreenQmlRenderThread* _renderer{ nullptr };
QQmlEngine* _qmlEngine{ nullptr };
QQmlComponent* _qmlComponent{ nullptr };
QQuickItem* _rootItem{ nullptr };
QQuickWindow* _quickWindow { nullptr };
QMyQuickRenderControl* _renderControl{ nullptr };
QQmlEngine* _qmlEngine { nullptr };
QQmlComponent* _qmlComponent { nullptr };
QQuickItem* _rootItem { nullptr };
OffscreenGLCanvas* _canvas { nullptr };
QJsonObject _glData;
QTimer _updateTimer;
uint32_t _currentTexture{ 0 };
bool _render{ false };
bool _polish{ true };
bool _paused{ true };
uint32_t _fbo { 0 };
uint32_t _depthStencil { 0 };
uint64_t _lastRenderTime { 0 };
uvec2 _size { 1920, 1080 };
TextureRecycler _textures { true };
// Texture management
std::mutex _textureMutex;
TextureAndFence _latestTextureAndFence { 0, 0 };
std::list<TextureAndFence> _returnedTextures;
bool _render { false };
bool _polish { true };
bool _paused { true };
bool _focusText { false };
uint8_t _maxFps{ 60 };
MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } };
uint8_t _maxFps { 60 };
MouseTranslator _mouseTranslator { [](const QPointF& p) { return p.toPoint(); } };
QWindow* _proxyWindow { nullptr };
QQuickItem* _currentFocusItem { nullptr };
};
#endif

View file

@ -1,563 +0,0 @@
//
// Created by Bradley Austin Davis on 2015/05/29
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "OglplusHelpers.h"
#include <set>
#include <oglplus/shapes/plane.hpp>
#include <oglplus/shapes/sky_box.hpp>
#include "GLLogging.h"
using namespace oglplus;
using namespace oglplus::shapes;
static const char * SIMPLE_TEXTURED_VS = R"VS(#version 410 core
#pragma line __LINE__
uniform mat4 mvp = mat4(1);
in vec3 Position;
in vec2 TexCoord;
out vec3 vPosition;
out vec2 vTexCoord;
void main() {
gl_Position = mvp * vec4(Position, 1);
vTexCoord = TexCoord;
vPosition = Position;
}
)VS";
static const char * SIMPLE_TEXTURED_FS = R"FS(#version 410 core
#pragma line __LINE__
uniform sampler2D sampler;
uniform float alpha = 1.0;
in vec3 vPosition;
in vec2 vTexCoord;
out vec4 FragColor;
void main() {
FragColor = texture(sampler, vTexCoord);
FragColor.a *= alpha;
if (FragColor.a <= 0.0) {
discard;
}
}
)FS";
static const char * SIMPLE_TEXTURED_CUBEMAP_FS = R"FS(#version 410 core
#pragma line __LINE__
uniform samplerCube sampler;
uniform float alpha = 1.0;
in vec3 vPosition;
in vec3 vTexCoord;
out vec4 FragColor;
void main() {
FragColor = texture(sampler, vPosition);
FragColor.a *= alpha;
}
)FS";
ProgramPtr loadDefaultShader() {
ProgramPtr result;
compileProgram(result, SIMPLE_TEXTURED_VS, SIMPLE_TEXTURED_FS);
return result;
}
ProgramPtr loadCubemapShader() {
ProgramPtr result;
compileProgram(result, SIMPLE_TEXTURED_VS, SIMPLE_TEXTURED_CUBEMAP_FS);
return result;
}
void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs) {
using namespace oglplus;
try {
result = std::make_shared<Program>();
// attach the shaders to the program
result->AttachShader(
VertexShader()
.Source(GLSLSource(vs))
.Compile()
);
result->AttachShader(
GeometryShader()
.Source(GLSLSource(gs))
.Compile()
);
result->AttachShader(
FragmentShader()
.Source(GLSLSource(fs))
.Compile()
);
result->Link();
} catch (ProgramBuildError& err) {
Q_UNUSED(err);
qWarning() << err.Log().c_str();
Q_ASSERT_X(false, "compileProgram", "Failed to build shader program");
qFatal("%s", (const char*)err.Message);
result.reset();
}
}
void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs) {
using namespace oglplus;
try {
result = std::make_shared<Program>();
// attach the shaders to the program
result->AttachShader(
VertexShader()
.Source(GLSLSource(vs))
.Compile()
);
result->AttachShader(
FragmentShader()
.Source(GLSLSource(fs))
.Compile()
);
result->Link();
} catch (ProgramBuildError& err) {
Q_UNUSED(err);
qWarning() << err.Log().c_str();
Q_ASSERT_X(false, "compileProgram", "Failed to build shader program");
qFatal("%s", (const char*) err.Message);
result.reset();
}
}
ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect) {
using namespace oglplus;
Vec3f a(1, 0, 0);
Vec3f b(0, 1, 0);
if (aspect > 1) {
b[1] /= aspect;
} else {
a[0] *= aspect;
}
return ShapeWrapperPtr(
new shapes::ShapeWrapper({ "Position", "TexCoord" }, shapes::Plane(a, b), *program)
);
}
ShapeWrapperPtr loadSkybox(ProgramPtr program) {
return ShapeWrapperPtr(new shapes::ShapeWrapper(std::initializer_list<std::string>{ "Position" }, shapes::SkyBox(), *program));
}
// Return a point's cartesian coordinates on a sphere from pitch and yaw
static glm::vec3 getPoint(float yaw, float pitch) {
return glm::vec3(glm::cos(-pitch) * (-glm::sin(yaw)),
glm::sin(-pitch),
glm::cos(-pitch) * (-glm::cos(yaw)));
}
class SphereSection : public DrawingInstructionWriter, public DrawMode {
public:
using IndexArray = std::vector<GLuint>;
using PosArray = std::vector<float>;
using TexArray = std::vector<float>;
/// The type of the index container returned by Indices()
// vertex positions
PosArray _pos_data;
// vertex tex coords
TexArray _tex_data;
IndexArray _idx_data;
unsigned int _prim_count{ 0 };
public:
SphereSection(
const float fov,
const float aspectRatio,
const int slices_,
const int stacks_) {
//UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm
if (fov >= PI) {
qCDebug(glLogging) << "TexturedHemisphere::buildVBO(): FOV greater or equal than Pi will create issues";
}
int gridSize = std::max(slices_, stacks_);
int gridSizeLog2 = 1;
while (1 << gridSizeLog2 < gridSize) {
++gridSizeLog2;
}
gridSize = (1 << gridSizeLog2) + 1;
// Compute number of vertices needed
int vertices = gridSize * gridSize;
_pos_data.resize(vertices * 3);
_tex_data.resize(vertices * 2);
// Compute vertices positions and texture UV coordinate
for (int y = 0; y <= gridSize; ++y) {
for (int x = 0; x <= gridSize; ++x) {
}
}
for (int i = 0; i < gridSize; i++) {
float stacksRatio = (float)i / (float)(gridSize - 1); // First stack is 0.0f, last stack is 1.0f
// abs(theta) <= fov / 2.0f
float pitch = -fov * (stacksRatio - 0.5f);
for (int j = 0; j < gridSize; j++) {
float slicesRatio = (float)j / (float)(gridSize - 1); // First slice is 0.0f, last slice is 1.0f
// abs(phi) <= fov * aspectRatio / 2.0f
float yaw = -fov * aspectRatio * (slicesRatio - 0.5f);
int vertex = i * gridSize + j;
int posOffset = vertex * 3;
int texOffset = vertex * 2;
vec3 pos = getPoint(yaw, pitch);
_pos_data[posOffset] = pos.x;
_pos_data[posOffset + 1] = pos.y;
_pos_data[posOffset + 2] = pos.z;
_tex_data[texOffset] = slicesRatio;
_tex_data[texOffset + 1] = stacksRatio;
}
} // done with vertices
int rowLen = gridSize;
// gridsize now refers to the triangles, not the vertices, so reduce by one
// or die by fencepost error http://en.wikipedia.org/wiki/Off-by-one_error
--gridSize;
int quads = gridSize * gridSize;
for (int t = 0; t < quads; ++t) {
int x =
((t & 0x0001) >> 0) |
((t & 0x0004) >> 1) |
((t & 0x0010) >> 2) |
((t & 0x0040) >> 3) |
((t & 0x0100) >> 4) |
((t & 0x0400) >> 5) |
((t & 0x1000) >> 6) |
((t & 0x4000) >> 7);
int y =
((t & 0x0002) >> 1) |
((t & 0x0008) >> 2) |
((t & 0x0020) >> 3) |
((t & 0x0080) >> 4) |
((t & 0x0200) >> 5) |
((t & 0x0800) >> 6) |
((t & 0x2000) >> 7) |
((t & 0x8000) >> 8);
int i = x * (rowLen) + y;
_idx_data.push_back(i);
_idx_data.push_back(i + 1);
_idx_data.push_back(i + rowLen + 1);
_idx_data.push_back(i + rowLen + 1);
_idx_data.push_back(i + rowLen);
_idx_data.push_back(i);
}
_prim_count = quads * 2;
}
/// Returns the winding direction of faces
FaceOrientation FaceWinding(void) const {
return FaceOrientation::CCW;
}
typedef GLuint(SphereSection::*VertexAttribFunc)(std::vector<GLfloat>&) const;
/// Makes the vertex positions and returns the number of values per vertex
template <typename T>
GLuint Positions(std::vector<T>& dest) const {
dest.clear();
dest.insert(dest.begin(), _pos_data.begin(), _pos_data.end());
return 3;
}
/// Makes the vertex normals and returns the number of values per vertex
template <typename T>
GLuint Normals(std::vector<T>& dest) const {
dest.clear();
return 3;
}
/// Makes the vertex tangents and returns the number of values per vertex
template <typename T>
GLuint Tangents(std::vector<T>& dest) const {
dest.clear();
return 3;
}
/// Makes the vertex bi-tangents and returns the number of values per vertex
template <typename T>
GLuint Bitangents(std::vector<T>& dest) const {
dest.clear();
return 3;
}
/// Makes the texture coordinates returns the number of values per vertex
template <typename T>
GLuint TexCoordinates(std::vector<T>& dest) const {
dest.clear();
dest.insert(dest.begin(), _tex_data.begin(), _tex_data.end());
return 2;
}
typedef VertexAttribsInfo<
SphereSection,
std::tuple<
VertexPositionsTag,
VertexNormalsTag,
VertexTangentsTag,
VertexBitangentsTag,
VertexTexCoordinatesTag
>
> VertexAttribs;
Spheref MakeBoundingSphere(void) const {
GLfloat min_x = _pos_data[3], max_x = _pos_data[3];
GLfloat min_y = _pos_data[4], max_y = _pos_data[4];
GLfloat min_z = _pos_data[5], max_z = _pos_data[5];
for (std::size_t v = 0, vn = _pos_data.size() / 3; v != vn; ++v) {
GLfloat x = _pos_data[v * 3 + 0];
GLfloat y = _pos_data[v * 3 + 1];
GLfloat z = _pos_data[v * 3 + 2];
if (min_x > x) min_x = x;
if (min_y > y) min_y = y;
if (min_z > z) min_z = z;
if (max_x < x) max_x = x;
if (max_y < y) max_y = y;
if (max_z < z) max_z = z;
}
Vec3f c(
(min_x + max_x) * 0.5f,
(min_y + max_y) * 0.5f,
(min_z + max_z) * 0.5f
);
return Spheref(
c.x(), c.y(), c.z(),
Distance(c, Vec3f(min_x, min_y, min_z))
);
}
/// Queries the bounding sphere coordinates and dimensions
template <typename T>
void BoundingSphere(oglplus::Sphere<T>& bounding_sphere) const {
bounding_sphere = oglplus::Sphere<T>(MakeBoundingSphere());
}
/// Returns element indices that are used with the drawing instructions
const IndexArray & Indices(Default = Default()) const {
return _idx_data;
}
/// Returns the instructions for rendering of faces
DrawingInstructions Instructions(PrimitiveType primitive) const {
DrawingInstructions instr = MakeInstructions();
DrawOperation operation;
operation.method = DrawOperation::Method::DrawElements;
operation.mode = primitive;
operation.first = 0;
operation.count = _prim_count * 3;
operation.restart_index = DrawOperation::NoRestartIndex();
operation.phase = 0;
AddInstruction(instr, operation);
return instr;
}
/// Returns the instructions for rendering of faces
DrawingInstructions Instructions(Default = Default()) const {
return Instructions(PrimitiveType::Triangles);
}
};
ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, int slices, int stacks) {
using namespace oglplus;
return ShapeWrapperPtr(
new shapes::ShapeWrapper({ "Position", "TexCoord" }, SphereSection(fov, aspect, slices, stacks), *program)
);
}
namespace oglplus {
namespace shapes {
class Laser : public DrawingInstructionWriter, public DrawMode {
public:
using IndexArray = std::vector<GLuint>;
using PosArray = std::vector<float>;
/// The type of the index container returned by Indices()
// vertex positions
PosArray _pos_data;
IndexArray _idx_data;
unsigned int _prim_count { 0 };
public:
Laser() {
int vertices = 2;
_pos_data.resize(vertices * 3);
_pos_data[0] = 0;
_pos_data[1] = 0;
_pos_data[2] = 0;
_pos_data[3] = 0;
_pos_data[4] = 0;
_pos_data[5] = -1;
_idx_data.push_back(0);
_idx_data.push_back(1);
_prim_count = 1;
}
/// Returns the winding direction of faces
FaceOrientation FaceWinding(void) const {
return FaceOrientation::CCW;
}
/// Queries the bounding sphere coordinates and dimensions
template <typename T>
void BoundingSphere(Sphere<T>& bounding_sphere) const {
bounding_sphere = Sphere<T>(0, 0, -0.5, 0.5);
}
typedef GLuint(Laser::*VertexAttribFunc)(std::vector<GLfloat>&) const;
/// Makes the vertex positions and returns the number of values per vertex
template <typename T>
GLuint Positions(std::vector<T>& dest) const {
dest.clear();
dest.insert(dest.begin(), _pos_data.begin(), _pos_data.end());
return 3;
}
typedef VertexAttribsInfo<
Laser,
std::tuple<VertexPositionsTag>
> VertexAttribs;
/// Returns element indices that are used with the drawing instructions
const IndexArray & Indices(Default = Default()) const {
return _idx_data;
}
/// Returns the instructions for rendering of faces
DrawingInstructions Instructions(PrimitiveType primitive) const {
DrawingInstructions instr = MakeInstructions();
DrawOperation operation;
operation.method = DrawOperation::Method::DrawElements;
operation.mode = primitive;
operation.first = 0;
operation.count = _prim_count * 3;
operation.restart_index = DrawOperation::NoRestartIndex();
operation.phase = 0;
AddInstruction(instr, operation);
return instr;
}
/// Returns the instructions for rendering of faces
DrawingInstructions Instructions(Default = Default()) const {
return Instructions(PrimitiveType::Lines);
}
};
}
}
ShapeWrapperPtr loadLaser(const ProgramPtr& program) {
return std::make_shared<shapes::ShapeWrapper>(shapes::ShapeWrapper("Position", shapes::Laser(), *program));
}
void TextureRecycler::setSize(const uvec2& size) {
if (size == _size) {
return;
}
_size = size;
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
std::set<Map::key_type> toDelete;
std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) {
if (!item.second._active && item.second._size != _size) {
toDelete.insert(item.first);
}
});
std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) {
_allTextures.erase(key);
});
}
void TextureRecycler::clear() {
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
_allTextures.clear();
}
TexturePtr TextureRecycler::getNextTexture() {
using namespace oglplus;
if (_readyTextures.empty()) {
TexturePtr newTexture(new Texture());
if (_useMipmaps) {
Context::Bound(oglplus::Texture::Target::_2D, *newTexture)
.MinFilter(TextureMinFilter::LinearMipmapLinear)
.MagFilter(TextureMagFilter::Linear)
.WrapS(TextureWrap::ClampToEdge)
.WrapT(TextureWrap::ClampToEdge)
.Anisotropy(8.0f)
.LODBias(-0.2f)
.Image2D(0, PixelDataInternalFormat::RGBA8,
_size.x, _size.y,
0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr);
} else {
Context::Bound(oglplus::Texture::Target::_2D, *newTexture)
.MinFilter(TextureMinFilter::Linear)
.MagFilter(TextureMagFilter::Linear)
.WrapS(TextureWrap::ClampToEdge)
.WrapT(TextureWrap::ClampToEdge)
.Image2D(0, PixelDataInternalFormat::RGBA8,
_size.x, _size.y,
0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr);
}
GLuint texId = GetName(*newTexture);
_allTextures[texId] = TexInfo{ newTexture, _size };
_readyTextures.push(newTexture);
}
TexturePtr result = _readyTextures.front();
_readyTextures.pop();
GLuint texId = GetName(*result);
auto& item = _allTextures[texId];
item._active = true;
return result;
}
void TextureRecycler::recycleTexture(GLuint texture) {
Q_ASSERT(_allTextures.count(texture));
auto& item = _allTextures[texture];
Q_ASSERT(item._active);
item._active = false;
if (item._size != _size) {
// Buh-bye
_allTextures.erase(texture);
return;
}
_readyTextures.push(item._tex);
}

View file

@ -1,217 +0,0 @@
//
// Created by Bradley Austin Davis on 2015/05/26
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
// FIXME support oglplus on all platforms
// For now it's a convenient helper for Windows
#include <queue>
#include <map>
#include <QtGlobal>
#include "GLMHelpers.h"
#define OGLPLUS_USE_GLCOREARB_H 0
#define OGLPLUS_USE_GLEW 1
#define OGLPLUS_USE_BOOST_CONFIG 1
#define OGLPLUS_NO_SITE_CONFIG 1
#define OGLPLUS_LOW_PROFILE 1
// NOTE: oglplus does some naked "#pragma GCC" without proper platform wrapping, so we need to disable this warning.
#ifdef _WIN32
#pragma warning(push)
#pragma warning( disable : 4068 )
#elif defined(Q_OS_MAC)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpessimizing-move"
#endif
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
#if __GNUC__ >= 5 && __GNUC_MINOR__ >= 1
#pragma GCC diagnostic ignored "-Wsuggest-override"
#endif
#endif
#include <oglplus/gl.hpp>
#include <oglplus/all.hpp>
#include <oglplus/interop/glm.hpp>
#include <oglplus/bound/texture.hpp>
#include <oglplus/bound/framebuffer.hpp>
#include <oglplus/bound/renderbuffer.hpp>
#include <oglplus/shapes/wrapper.hpp>
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic pop
#endif
#ifdef _WIN32
#pragma warning(pop)
#elif defined(Q_OS_MAC)
#pragma clang diagnostic pop
#endif
#include "NumericalConstants.h"
using FramebufferPtr = std::shared_ptr<oglplus::Framebuffer>;
using RenderbufferPtr = std::shared_ptr<oglplus::Renderbuffer>;
using TexturePtr = std::shared_ptr<oglplus::Texture>;
using ShapeWrapperPtr = std::shared_ptr<oglplus::shapes::ShapeWrapper>;
using BufferPtr = std::shared_ptr<oglplus::Buffer>;
using VertexArrayPtr = std::shared_ptr<oglplus::VertexArray>;
using ProgramPtr = std::shared_ptr<oglplus::Program>;
using Mat4Uniform = oglplus::Uniform<mat4>;
ProgramPtr loadDefaultShader();
ProgramPtr loadCubemapShader();
void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs);
void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs);
ShapeWrapperPtr loadSkybox(ProgramPtr program);
ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f);
ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128);
ShapeWrapperPtr loadLaser(const ProgramPtr& program);
// A basic wrapper for constructing a framebuffer with a renderbuffer
// for the depth attachment and an undefined type for the color attachement
// This allows us to reuse the basic framebuffer code for both the Mirror
// FBO as well as the Oculus swap textures we will use to render the scene
// Though we don't really need depth at all for the mirror FBO, or even an
// FBO, but using one means I can just use a glBlitFramebuffer to get it onto
// the screen.
template <
typename C,
typename D
>
struct FramebufferWrapper {
uvec2 size;
oglplus::Framebuffer fbo;
C color;
D depth;
FramebufferWrapper() {}
virtual ~FramebufferWrapper() {
}
virtual void Init(const uvec2 & size) {
this->size = size;
initColor();
initDepth();
initDone();
}
template <typename F>
void Bound(F f) {
Bound(oglplus::Framebuffer::Target::Draw, f);
}
template <typename F>
void Bound(oglplus::Framebuffer::Target target , F f) {
fbo.Bind(target);
onBind(target);
f();
onUnbind(target);
oglplus::DefaultFramebuffer().Bind(target);
}
void Viewport() {
oglplus::Context::Viewport(size.x, size.y);
}
protected:
virtual void onBind(oglplus::Framebuffer::Target target) {}
virtual void onUnbind(oglplus::Framebuffer::Target target) {}
static GLenum toEnum(oglplus::Framebuffer::Target target) {
switch (target) {
case oglplus::Framebuffer::Target::Draw:
return GL_DRAW_FRAMEBUFFER;
case oglplus::Framebuffer::Target::Read:
return GL_READ_FRAMEBUFFER;
default:
Q_ASSERT(false);
return GL_FRAMEBUFFER;
}
}
virtual void initDepth() {}
virtual void initColor() {}
virtual void initDone() = 0;
};
struct BasicFramebufferWrapper : public FramebufferWrapper <oglplus::Texture, oglplus::Renderbuffer> {
protected:
virtual void initDepth() override {
using namespace oglplus;
Context::Bound(Renderbuffer::Target::Renderbuffer, depth)
.Storage(
PixelDataInternalFormat::DepthComponent,
size.x, size.y);
}
virtual void initColor() override {
using namespace oglplus;
Context::Bound(oglplus::Texture::Target::_2D, color)
.MinFilter(TextureMinFilter::Linear)
.MagFilter(TextureMagFilter::Linear)
.WrapS(TextureWrap::ClampToEdge)
.WrapT(TextureWrap::ClampToEdge)
.Image2D(
0, PixelDataInternalFormat::RGBA8,
size.x, size.y,
0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr
);
}
virtual void initDone() override {
using namespace oglplus;
static const Framebuffer::Target target = Framebuffer::Target::Draw;
Bound(target, [&] {
fbo.AttachTexture(target, FramebufferAttachment::Color, color, 0);
fbo.AttachRenderbuffer(target, FramebufferAttachment::Depth, depth);
fbo.Complete(target);
});
}
};
using BasicFramebufferWrapperPtr = std::shared_ptr<BasicFramebufferWrapper>;
class TextureRecycler {
public:
TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
void setSize(const uvec2& size);
void clear();
TexturePtr getNextTexture();
void recycleTexture(GLuint texture);
private:
struct TexInfo {
TexturePtr _tex;
uvec2 _size;
bool _active{ false };
TexInfo() {}
TexInfo(TexturePtr tex, const uvec2& size) : _tex(tex), _size(size) {}
};
using Map = std::map<GLuint, TexInfo>;
using Queue = std::queue<TexturePtr>;
Map _allTextures;
Queue _readyTextures;
uvec2 _size{ 1920, 1080 };
bool _useMipmaps;
};

View file

@ -0,0 +1,83 @@
//
// Created by Bradley Austin Davis on 2016-10-05
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "TextureRecycler.h"
#include "Config.h"
#include <set>
void TextureRecycler::setSize(const uvec2& size) {
if (size == _size) {
return;
}
_size = size;
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
std::set<Map::key_type> toDelete;
std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) {
if (!item.second._active && item.second._size != _size) {
toDelete.insert(item.first);
}
});
std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) {
_allTextures.erase(key);
});
}
void TextureRecycler::clear() {
while (!_readyTextures.empty()) {
_readyTextures.pop();
}
_allTextures.clear();
}
uint32_t TextureRecycler::getNextTexture() {
if (_readyTextures.empty()) {
uint32_t newTexture;
glGenTextures(1, &newTexture);
glBindTexture(GL_TEXTURE_2D, newTexture);
if (_useMipmaps) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
_allTextures.emplace(std::piecewise_construct, std::forward_as_tuple(newTexture), std::forward_as_tuple(newTexture, _size));
_readyTextures.push(newTexture);
}
uint32_t result = _readyTextures.front();
_readyTextures.pop();
auto& item = _allTextures[result];
item._active = true;
return result;
}
void TextureRecycler::recycleTexture(GLuint texture) {
Q_ASSERT(_allTextures.count(texture));
auto& item = _allTextures[texture];
Q_ASSERT(item._active);
item._active = false;
if (item._size != _size) {
// Buh-bye
_allTextures.erase(texture);
return;
}
_readyTextures.push(item._tex);
}

View file

@ -0,0 +1,47 @@
//
// Created by Bradley Austin Davis on 2015-04-04
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_TextureRecycler_h
#define hifi_TextureRecycler_h
#include <atomic>
#include <queue>
#include <map>
#include <GLMHelpers.h>
class TextureRecycler {
public:
TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {}
void setSize(const uvec2& size);
void clear();
uint32_t getNextTexture();
void recycleTexture(uint32_t texture);
private:
struct TexInfo {
const uint32_t _tex{ 0 };
const uvec2 _size;
bool _active { false };
TexInfo() {}
TexInfo(uint32_t tex, const uvec2& size) : _tex(tex), _size(size) {}
TexInfo(const TexInfo& other) : _tex(other._tex), _size(other._size) {}
};
using Map = std::map<uint32_t, TexInfo>;
using Queue = std::queue<uint32_t>;
Map _allTextures;
Queue _readyTextures;
uvec2 _size{ 1920, 1080 };
bool _useMipmaps;
};
#endif

View file

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

View file

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

View file

@ -6,6 +6,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "GLShader.h"
#include <gl/GLShaders.h>
#include "GLBackend.h"
using namespace gpu;
@ -68,7 +70,11 @@ GLShader* compileBackendShader(GLBackend& backend, const Shader& shader) {
std::string shaderDefines = glslVersion + "\n" + DOMAIN_DEFINES[shader.getType()] + "\n" + VERSION_DEFINES[version];
bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram);
#ifdef SEPARATE_PROGRAM
bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram);
#else
bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader);
#endif
if (!result) {
return nullptr;
}
@ -103,7 +109,7 @@ GLShader* compileBackendProgram(GLBackend& backend, const Shader& program) {
}
}
GLuint glprogram = compileProgram(shaderGLObjects);
GLuint glprogram = ::gl::compileProgram(shaderGLObjects);
if (glprogram == 0) {
return nullptr;
}

View file

@ -692,187 +692,6 @@ int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Sh
return 0; //inputsCount;
}
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject) {
if (shaderSource.empty()) {
qCDebug(gpugllogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create";
return false;
}
// Create the shader object
GLuint glshader = glCreateShader(shaderDomain);
if (!glshader) {
qCDebug(gpugllogging) << "GLShader::compileShader - failed to create the gl shader object";
return false;
}
// Assign the source
const int NUM_SOURCE_STRINGS = 2;
const GLchar* srcstr[] = { defines.c_str(), shaderSource.c_str() };
glShaderSource(glshader, NUM_SOURCE_STRINGS, srcstr, NULL);
// Compile !
glCompileShader(glshader);
// check if shader compiled
GLint compiled = 0;
glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled);
// if compilation fails
if (!compiled) {
// save the source code to a temp file so we can debug easily
/*
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << srcstr[0];
filestream << srcstr[1];
filestream.close();
}
*/
GLint infoLength = 0;
glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetShaderInfoLog(glshader, infoLength, NULL, temp);
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << std::string(temp);
filestream.close();
}
*/
qCWarning(gpugllogging) << "GLShader::compileShader - failed to compile the gl shader object:";
for (auto s : srcstr) {
qCWarning(gpugllogging) << s;
}
qCWarning(gpugllogging) << "GLShader::compileShader - errors:";
qCWarning(gpugllogging) << temp;
delete[] temp;
glDeleteShader(glshader);
return false;
}
GLuint glprogram = 0;
#ifdef SEPARATE_PROGRAM
// so far so good, program is almost done, need to link:
GLuint glprogram = glCreateProgram();
if (!glprogram) {
qCDebug(gpugllogging) << "GLShader::compileShader - failed to create the gl shader & gl program object";
return false;
}
glProgramParameteri(glprogram, GL_PROGRAM_SEPARABLE, GL_TRUE);
glAttachShader(glprogram, glshader);
glLinkProgram(glprogram);
GLint linked = 0;
glGetProgramiv(glprogram, GL_LINK_STATUS, &linked);
if (!linked) {
/*
// save the source code to a temp file so we can debug easily
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << shaderSource->source;
filestream.close();
}
*/
GLint infoLength = 0;
glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetProgramInfoLog(glprogram, infoLength, NULL, temp);
qCDebug(gpugllogging) << "GLShader::compileShader - failed to LINK the gl program object :";
qCDebug(gpugllogging) << temp;
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << String(temp);
filestream.close();
}
*/
delete[] temp;
glDeleteShader(glshader);
glDeleteProgram(glprogram);
return false;
}
#endif
shaderObject = glshader;
programObject = glprogram;
return true;
}
GLuint compileProgram(const std::vector<GLuint>& glshaders) {
// A brand new program:
GLuint glprogram = glCreateProgram();
if (!glprogram) {
qCDebug(gpugllogging) << "GLShader::compileProgram - failed to create the gl program object";
return 0;
}
// glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE);
// Create the program from the sub shaders
for (auto so : glshaders) {
glAttachShader(glprogram, so);
}
// Link!
glLinkProgram(glprogram);
GLint linked = 0;
glGetProgramiv(glprogram, GL_LINK_STATUS, &linked);
if (!linked) {
/*
// save the source code to a temp file so we can debug easily
std::ofstream filestream;
filestream.open("debugshader.glsl");
if (filestream.is_open()) {
filestream << shaderSource->source;
filestream.close();
}
*/
GLint infoLength = 0;
glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength);
char* temp = new char[infoLength];
glGetProgramInfoLog(glprogram, infoLength, NULL, temp);
qCDebug(gpugllogging) << "GLShader::compileProgram - failed to LINK the gl program object :";
qCDebug(gpugllogging) << temp;
/*
filestream.open("debugshader.glsl.info.txt");
if (filestream.is_open()) {
filestream << std::string(temp);
filestream.close();
}
*/
delete[] temp;
glDeleteProgram(glprogram);
return 0;
}
return glprogram;
}
void makeProgramBindings(ShaderObject& shaderObject) {
if (!shaderObject.glprogram) {
return;

View file

@ -44,8 +44,6 @@ int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,
int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers);
int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs);
int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs);
bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject);
GLuint compileProgram(const std::vector<GLuint>& glshaders);
void makeProgramBindings(ShaderObject& shaderObject);
enum GLSyncState {

View file

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

Some files were not shown because too many files have changed in this diff Show more