diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000000..f000a27017
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,39 @@
+Language: Cpp
+Standard: Cpp11
+BasedOnStyle: "Chromium"
+ColumnLimit: 128
+IndentWidth: 4
+UseTab: Never
+
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterEnum: true
+ AfterClass: false
+ AfterControlStatement: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ SplitEmptyFunction: false
+ SplitEmptyNamespace: true
+
+
+AccessModifierOffset: -4
+AllowShortFunctionsOnASingleLine: InlineOnly
+BreakConstructorInitializers: BeforeColon
+BreakConstructorInitializersBeforeComma: true
+IndentCaseLabels: true
+ReflowComments: false
+Cpp11BracedListStyle: false
+ContinuationIndentWidth: 4
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+CompactNamespaces: true
+SortIncludes: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+
+PenaltyReturnTypeOnItsOwnLine: 1000
+PenaltyBreakBeforeFirstCallParameter: 1000
+
diff --git a/.eslintrc.js b/.eslintrc.js
index b4d88777f2..5667a04984 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -54,7 +54,11 @@ module.exports = {
"Window": false,
"XMLHttpRequest": false,
"location": false,
- "print": false
+ "print": false,
+ "RayPick": false,
+ "LaserPointers": false,
+ "ContextOverlay": false,
+ "module": false
},
"rules": {
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
@@ -64,7 +68,7 @@ module.exports = {
"eqeqeq": ["error", "always"],
"indent": ["error", 4, { "SwitchCase": 1 }],
"keyword-spacing": ["error", { "before": true, "after": true }],
- "max-len": ["error", 192, 4],
+ "max-len": ["error", 128, 4],
"new-cap": ["error"],
"no-floating-decimal": ["error"],
//"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }],
diff --git a/.gitattributes b/.gitattributes
index 406780d20a..4a06c4288a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,6 +10,7 @@
*.json text
*.js text
*.qml text
+*.qrc text
*.slf text
*.slh text
*.slv text
diff --git a/.gitignore b/.gitignore
index d6227f1f30..c1eef3817f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,20 @@ ext/
Makefile
*.user
+# Android Studio
+*.iml
+local.properties
+android/libraries
+
+# VSCode
+# List taken from Github Global Ignores master@435c4d92
+# https://github.com/github/gitignore/commits/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
# Xcode
*.xcodeproj
*.xcworkspace
@@ -50,6 +64,10 @@ gvr-interface/libs/*
# ignore files for various dev environments
TAGS
*.sw[po]
+*.qmlc
+
+# ignore QML compilation output
+*.qmlc
# ignore node files for the console
node_modules
diff --git a/BUILD.md b/BUILD.md
index c45b7cb636..feed677828 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -1,34 +1,31 @@
### Dependencies
-* [cmake](https://cmake.org/download/) ~> 3.3.2
-* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2
-* [OpenSSL](https://www.openssl.org/community/binaries.html)
- * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities.
-* [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
+- [cmake](https://cmake.org/download/): 3.9
+- [Qt](https://www.qt.io/download-open-source): 5.9.1
+- [OpenSSL](https://www.openssl.org/): Use the latest available 1.0 version (**NOT** 1.1) of OpenSSL to avoid security vulnerabilities.
+- [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
-#### CMake External Project Dependencies
+### CMake External Project Dependencies
-* [boostconfig](https://github.com/boostorg/config) ~> 1.58
-* [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83
-* [GLEW](http://glew.sourceforge.net/)
-* [glm](https://glm.g-truc.net/0.9.5/index.html) ~> 0.9.5.4
-* [gverb](https://github.com/highfidelity/gverb)
-* [Oculus SDK](https://developer.oculus.com/downloads/) ~> 0.6 (Win32) / 0.5 (Mac / Linux)
-* [oglplus](http://oglplus.org/) ~> 0.63
-* [OpenVR](https://github.com/ValveSoftware/openvr) ~> 0.91 (Win32 only)
-* [Polyvox](http://www.volumesoffun.com/) ~> 0.2.1
-* [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/) ~> 0.7.1
-* [SDL2](https://www.libsdl.org/download-2.0.php) ~> 2.0.3
-* [soxr](https://sourceforge.net/p/soxr/wiki/Home/) ~> 0.1.1
-* [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/) ~> 4.3
-* [Sixense](http://sixense.com/) ~> 071615
-* [zlib](http://www.zlib.net/) ~> 1.28 (Win32 only)
+These dependencies need not be installed manually. They are automatically downloaded on the platforms where they are required.
+- [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases): 2.83
+- [GLEW](http://glew.sourceforge.net/): 1.13
+- [glm](https://glm.g-truc.net/0.9.8/index.html): 0.9.8
+- [Oculus SDK](https://developer.oculus.com/downloads/): 1.11 (Win32) / 0.5 (Mac)
+- [OpenVR](https://github.com/ValveSoftware/openvr): 1.0.6 (Win32 only)
+- [Polyvox](http://www.volumesoffun.com/): 0.2.1
+- [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/): 0.7.3
+- [SDL2](https://www.libsdl.org/download-2.0.php): 2.0.3
+- [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/): 4.3
+- [Sixense](http://sixense.com/): 071615
+- [zlib](http://www.zlib.net/): 1.28 (Win32 only)
+- nVidia Texture Tools: 2.1
The above dependencies will be downloaded, built, linked and included automatically by CMake where we require them. The CMakeLists files that handle grabbing each of the following external dependencies can be found in the [cmake/externals folder](cmake/externals). The resulting downloads, source files and binaries will be placed in the `build/ext` folder in each of the subfolders for each external project.
These are not placed in your normal build tree when doing an out of source build so that they do not need to be re-downloaded and re-compiled every time the CMake build folder is cleared. Should you want to force a re-download and re-compile of a specific external, you can simply remove that directory from the appropriate subfolder in `build/ext`. Should you want to force a re-download and re-compile of all externals, just remove the `build/ext` folder.
-If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE_LOCAL_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project.
+If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE\_LOCAL\_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project.
### OS Specific Build Guides
diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md
index d69d20ee8a..cc51e58b1d 100644
--- a/BUILD_ANDROID.md
+++ b/BUILD_ANDROID.md
@@ -1,19 +1,56 @@
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Android specific instructions are found in this file.
-### Android Dependencies
+# Android Dependencies
You will need the following tools to build our Android targets.
-* [cmake](http://www.cmake.org/download/) ~> 3.5.1
-* [Qt](http://www.qt.io/download-open-source/#) ~> 5.6.2
-* [ant](http://ant.apache.org/bindownload.cgi) ~> 1.9.4
-* [Android NDK](https://developer.android.com/tools/sdk/ndk/index.html) ~> r10d
-* [Android SDK](http://developer.android.com/sdk/installing/index.html) ~> 24.4.1.1
- * Install the latest Platform-tools
- * Install the latest Build-tools
- * Install the SDK Platform for API Level 19
- * Install Sources for Android SDK for API Level 19
- * Install the ARM EABI v7a System Image if you want to run an emulator.
+* [Qt](http://www.qt.io/download-open-source/#) ~> 5.9.1
+* [Android Studio](https://developer.android.com/studio/index.html)
+* [Google VR SDK](https://github.com/googlevr/gvr-android-sdk/releases)
+* [Gradle](https://gradle.org/releases/)
+
+### Qt
+
+Download the Qt online installer. Run the installer and select the android_armv7 binaries. Installing to the default path is recommended
+
+### Android Studio
+
+Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager
+
+From the SDK Platforms tab, select API level 26.
+
+* Install the ARM EABI v7a System Image if you want to run an emulator.
+
+From the SDK Tools tab select the following
+
+* Android SDK Build-Tools
+* GPU Debugging Tools
+* CMake (even if you have a separate CMake installation)
+* LLDB
+* Android SDK Platform-Tools
+* Android SDK Tools
+* Android SDK Tools
+* NDK (even if you have the NDK installed separately)
+
+### Google VR SDK
+
+Download the 1.8 Google VR SDK [release](https://github.com/googlevr/gvr-android-sdk/archive/v1.80.0.zip). Unzip the archive to a location on your drive.
+
+### Gradle
+
+Download [Gradle 4.1](https://services.gradle.org/distributions/gradle-4.1-all.zip) and unzip it on your local drive. You may wish to add the location of the bin directory within the archive to your path
+
+#### Set up machine specific Gradle properties
+
+Create a `gradle.properties` file in ~/.gradle. Edit the file to contain the following
+
+ QT5_ROOT=C\:\\Qt\\5.9.1\\android_armv7
+ GVR_ROOT=C\:\\Android\\gvr-android-sdk
+
+Replace the paths with your local installations of Qt5 and the Google VR SDK
+
+
+# TODO fix the rest
You will also need to cross-compile the dependencies required for all platforms for Android, and help CMake find these compiled libraries on your machine.
diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md
index d40576a75d..038f53154c 100644
--- a/BUILD_LINUX.md
+++ b/BUILD_LINUX.md
@@ -1,7 +1,96 @@
+# Linux build guide
+
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file.
-### Qt5 Dependencies
+## Qt5 Dependencies
Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required:
libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev
+
+## Ubuntu 16.04 specific build guide
+
+### Prepare environment
+
+Install qt:
+```bash
+wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb
+sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb
+```
+
+Install build dependencies:
+```bash
+sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev
+```
+
+To compile interface in a server you must install:
+```bash
+sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1
+```
+
+Install build tools:
+```bash
+sudo apt install cmake
+```
+
+### Get code and checkout the tag you need
+
+Clone this repository:
+```bash
+git clone https://github.com/highfidelity/hifi.git
+```
+
+To compile a RELEASE version checkout the tag you need getting a list of all tags:
+```bash
+git fetch -a
+git tags
+```
+
+Then checkout last tag with:
+```bash
+git checkout tags/RELEASE-6819
+```
+
+Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type:
+```bash
+git checkout tags/RELEASE-6731
+```
+
+### Compiling
+
+Create the build directory:
+```bash
+mkdir -p hifi/build
+cd hifi/build
+```
+
+Prepare makefiles:
+```bash
+cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake ..
+```
+
+Start compilation and get a cup of coffee:
+```bash
+make domain-server assignment-client interface
+```
+
+In a server does not make sense to compile interface
+
+### Running the software
+
+Running domain server:
+```bash
+./domain-server/domain-server
+```
+
+Running assignment client:
+```bash
+./assignment-client/assignment-client -n 6
+```
+
+Running interface:
+```bash
+./interface/interface
+```
+
+Go to localhost in running interface.
diff --git a/BUILD_OSX.md b/BUILD_OSX.md
index 3365627b8c..6b66863534 100644
--- a/BUILD_OSX.md
+++ b/BUILD_OSX.md
@@ -1,29 +1,28 @@
-Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file.
+Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only macOS specific instructions are found in this file.
### Homebrew
-[Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple.
+[Homebrew](https://brew.sh/) is an excellent package manager for macOS. It makes install of some High Fidelity dependencies very simple.
- brew tap homebrew/versions
- brew install cmake openssl
+ brew install cmake openssl qt
### OpenSSL
Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations.
For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR:
- export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2h_1/
+ export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2l
Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change.
### Qt
-Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg).
+Assuming you've installed Qt using the homebrew instructions above, you'll need to set QT_CMAKE_PREFIX_PATH so CMake can find your installations.
+For Qt installed via homebrew, set QT_CMAKE_PREFIX_PATH:
-Keep the default components checked when going through the installer.
+ export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.9.1/lib/cmake
-Once Qt is installed, you need to manually configure the following:
-* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory.
+Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change.
### Xcode
diff --git a/BUILD_WIN.md b/BUILD_WIN.md
index 818a176f75..eea1f85e5b 100644
--- a/BUILD_WIN.md
+++ b/BUILD_WIN.md
@@ -1,46 +1,58 @@
This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit.
## Building High Fidelity
+Note: We are now using Visual Studio 2017 and Qt 5.9.1. If you are upgrading from Visual Studio 2013 and Qt 5.6.2, do a clean uninstall of those versions before going through this guide.
-### Step 1. Installing Visual Studio 2013
+Note: The prerequisites will require about 10 GB of space on your drive. You will also need a system with at least 8GB of main memory.
-If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer.
+### Step 1. Visual Studio 2017
-Note: Newer versions of Visual Studio are not yet compatible.
+If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/).
+
+When selecting components, check "Desktop development with C++." Also check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)" on the Summary toolbar on the right.
### Step 2. Installing CMake
-Download and install the [CMake 3.8.0 win64-x64 Installer](https://cmake.org/files/v3.8/cmake-3.8.0-win64-x64.msi). Make sure "Add CMake to system PATH for all users" is checked when going through the installer.
+Download and install the latest version of CMake 3.9. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). Make sure to check "Add CMake to system PATH for all users" when prompted during installation.
### Step 3. Installing Qt
-Download and install the [Qt 5.6.2 for Windows 64-bit (VS 2013)](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-windows-x86-msvc2013_64-5.6.2.exe).
+Download and install the [Qt Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.9.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)".
-Keep the default components checked when going through the installer.
+Note: Installing the Sources is optional but recommended if you have room for them (~2GB).
### Step 4. Setting Qt Environment Variable
Go to `Control Panel > System > Advanced System Settings > Environment Variables > New...` (or search “Environment Variables” in Start Search).
* Set "Variable name": `QT_CMAKE_PREFIX_PATH`
-* Set "Variable value": `%QT_DIR%\5.6\msvc2013_64\lib\cmake`
+* Set "Variable value": `C:\Qt\5.9.1\msvc2017_64\lib\cmake`
-### Step 5. Installing OpenSSL
+### Step 5. Installing [vcpkg](https://github.com/Microsoft/vcpkg)
-Download and install the [Win64 OpenSSL v1.0.2L Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2L.exe).
+ * Clone the VCPKG [repository](https://github.com/Microsoft/vcpkg)
+ * Follow the instructions in the [readme](https://github.com/Microsoft/vcpkg/blob/master/README.md) to bootstrap vcpkg
+ * Note, you may need to do these in a _Developer Command Prompt_
+ * Set an environment variable VCPKG_ROOT to the location of the cloned repository
+ * Close and re-open any command prompts after setting the environment variable so that they will pick up the change
-### Step 6. Running CMake to Generate Build Files
+### Step 6. Installing OpenSSL via vcpkg
+
+ * In the vcpkg directory, install the 64 bit OpenSSL package with the command `vcpkg install openssl:x64-windows`
+ * Once the build completes you should have a file `ssl.h` in `${VCPKG_ROOT}/installed/x64-windows/include/openssl`
+
+### Step 7. Running CMake to Generate Build Files
Run Command Prompt from Start and run the following commands:
-````
+```
cd "%HIFI_DIR%"
mkdir build
cd build
-cmake .. -G "Visual Studio 12 Win64"
-````
+cmake .. -G "Visual Studio 15 Win64"
+```
Where `%HIFI_DIR%` is the directory for the highfidelity repository.
-### Step 7. Making a Build
+### Step 8. Making a Build
Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio.
@@ -48,7 +60,7 @@ Change the Solution Configuration (next to the green play button) from "Debug" t
Run `Build > Build Solution`.
-### Step 8. Testing Interface
+### Step 9. Testing Interface
Create another environment variable (see Step #4)
* Set "Variable name": `_NO_DEBUG_HEAP`
@@ -62,24 +74,20 @@ Note: You can also run Interface by launching it from command line or File Explo
## Troubleshooting
-For any problems after Step #6, first try this:
+For any problems after Step #7, first try this:
* Delete your locally cloned copy of the highfidelity repository
* Restart your computer
* Redownload the [repository](https://github.com/highfidelity/hifi)
-* Restart directions from Step #6
+* Restart directions from Step #7
#### CMake gives you the same error message repeatedly after the build fails
Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory.
-#### nmake cannot be found
+#### CMake can't find OpenSSL
-Make sure nmake.exe is located at the following path:
-
- C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin
-
-If not, add the directory where nmake is located to the PATH environment variable.
+Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. Verify that your VCPKG_ROOT environment variable is set and pointing to the correct location. Verify that the file `${VCPKG_ROOT}/installed/x64-windows/include/openssl/ssl.h` exists.
#### Qt is throwing an error
-Make sure you have the correct version (5.6.2) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly.
+Make sure you have the correct version (5.9.1) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3c90256134..2c10e714a3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,253 +1,95 @@
-cmake_minimum_required(VERSION 3.2)
-
-if (USE_ANDROID_TOOLCHAIN)
- set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/android/android.toolchain.cmake")
- set(ANDROID_NATIVE_API_LEVEL 19)
- set(ANDROID_TOOLCHAIN_NAME arm-linux-androideabi-clang3.5)
- set(ANDROID_STL c++_shared)
-endif ()
-
-if (WIN32)
- cmake_policy(SET CMP0020 NEW)
-endif (WIN32)
-
-if (POLICY CMP0028)
- cmake_policy(SET CMP0028 OLD)
-endif ()
-
-if (POLICY CMP0043)
- cmake_policy(SET CMP0043 OLD)
-endif ()
-
-if (POLICY CMP0042)
- cmake_policy(SET CMP0042 OLD)
-endif ()
-
-set_property(GLOBAL PROPERTY USE_FOLDERS ON)
-set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMakeTargets")
+# If we're running under the gradle build, HIFI_ANDROID will be set here, but
+# ANDROID will not be set until after the `project` statement. This is the *ONLY*
+# place you need to use `HIFI_ANDROID` instead of `ANDROID`
+if (WIN32 AND NOT HIFI_ANDROID)
+ cmake_minimum_required(VERSION 3.7)
+else()
+ cmake_minimum_required(VERSION 3.2)
+endif()
project(hifi)
-add_definitions(-DGLM_FORCE_RADIANS)
-set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
-find_package( Threads )
+include("cmake/init.cmake")
-if (WIN32)
- add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS)
+include("cmake/compiler.cmake")
- if (NOT WINDOW_SDK_PATH)
- set(DEBUG_DISCOVERED_SDK_PATH TRUE)
- endif()
-
- # sets path for Microsoft SDKs
- # if you get build error about missing 'glu32' this path is likely wrong
- if (MSVC10)
- set(WINDOW_SDK_PATH "C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1 " CACHE PATH "Windows SDK PATH")
- elseif (MSVC12)
- if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
- set(WINDOW_SDK_FOLDER "x64")
- else()
- set(WINDOW_SDK_FOLDER "x86")
- endif()
- set(WINDOW_SDK_PATH "C:\\Program Files (x86)\\Windows Kits\\8.1\\Lib\\winv6.3\\um\\${WINDOW_SDK_FOLDER}" CACHE PATH "Windows SDK PATH")
- endif ()
-
- if (DEBUG_DISCOVERED_SDK_PATH)
- message(STATUS "The discovered Windows SDK path is ${WINDOW_SDK_PATH}")
- endif ()
-
- set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${WINDOW_SDK_PATH})
- # /wd4351 disables warning C4351: new behavior: elements of array will be default initialized
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /wd4351")
- # /LARGEADDRESSAWARE enables 32-bit apps to use more than 2GB of memory.
- # Caveats: http://stackoverflow.com/questions/2288728/drawbacks-of-using-largeaddressaware-for-32-bit-windows-executables
- # TODO: Remove when building 64-bit.
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LARGEADDRESSAWARE")
- # always produce symbols as PDB files
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi")
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DEBUG /OPT:REF /OPT:ICF")
-else ()
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fno-strict-aliasing -Wno-unused-parameter")
- if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -Woverloaded-virtual -Wdouble-promotion")
- if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER "5.1") # gcc 5.1 and on have suggest-override
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override")
- endif ()
- endif ()
-endif(WIN32)
-
-if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
- if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "5.3")
- # GLM 0.9.8 on Ubuntu 14 (gcc 4.4) has issues with the simd declarations
- add_definitions(-DGLM_FORCE_PURE)
- endif()
+if (NOT DEFINED SERVER_ONLY)
+ set(SERVER_ONLY 0)
endif()
-if (NOT ANDROID)
- if ((NOT MSVC12) AND (NOT MSVC14))
- include(CheckCXXCompilerFlag)
- CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
- CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
-
- if (COMPILER_SUPPORTS_CXX11)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
- elseif(COMPILER_SUPPORTS_CXX0X)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
- else()
- message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
- endif()
- endif ()
-else ()
- # assume that the toolchain selected for android has C++11 support
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
-endif ()
-
-if (APPLE)
- set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++0x")
- set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++")
-endif ()
-
-if (NOT ANDROID_LIB_DIR)
- set(ANDROID_LIB_DIR $ENV{ANDROID_LIB_DIR})
-endif ()
-
-if (ANDROID)
- if (NOT ANDROID_QT_CMAKE_PREFIX_PATH)
- set(QT_CMAKE_PREFIX_PATH ${ANDROID_LIB_DIR}/Qt/5.5/android_armv7/lib/cmake)
- else ()
- set(QT_CMAKE_PREFIX_PATH ${ANDROID_QT_CMAKE_PREFIX_PATH})
- endif ()
-
- set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
- set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib)
-
- if (ANDROID_LIB_DIR)
- list(APPEND CMAKE_FIND_ROOT_PATH ${ANDROID_LIB_DIR})
- endif ()
-else ()
- if (NOT QT_CMAKE_PREFIX_PATH)
- set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH})
- endif ()
- if (NOT QT_CMAKE_PREFIX_PATH)
- get_filename_component(QT_CMAKE_PREFIX_PATH "${Qt5_DIR}/.." REALPATH)
- endif ()
-endif ()
-
-set(QT_DIR $ENV{QT_DIR})
-
-if (WIN32)
- if (NOT EXISTS ${QT_CMAKE_PREFIX_PATH})
- message(FATAL_ERROR "Could not determine QT_CMAKE_PREFIX_PATH.")
- endif ()
+if (ANDROID OR UWP)
+ set(MOBILE 1)
+else()
+ set(MOBILE 0)
endif()
-# figure out where the qt dir is
-get_filename_component(QT_DIR "${QT_CMAKE_PREFIX_PATH}/../../" ABSOLUTE)
+if (ANDROID OR UWP)
+ option(BUILD_SERVER "Build server components" OFF)
+ option(BUILD_TOOLS "Build tools" OFF)
+else()
+ option(BUILD_SERVER "Build server components" ON)
+ option(BUILD_TOOLS "Build tools" ON)
+endif()
-set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${QT_CMAKE_PREFIX_PATH})
+if (SERVER_ONLY)
+ option(BUILD_CLIENT "Build client components" OFF)
+ option(BUILD_TESTS "Build tests" OFF)
+else()
+ option(BUILD_CLIENT "Build client components" ON)
+ option(BUILD_TESTS "Build tests" ON)
+endif()
-if (APPLE)
+option(BUILD_INSTALLER "Build installer" ON)
- exec_program(sw_vers ARGS -productVersion OUTPUT_VARIABLE OSX_VERSION)
- string(REGEX MATCH "^[0-9]+\\.[0-9]+" OSX_VERSION ${OSX_VERSION})
- message(STATUS "Detected OS X version = ${OSX_VERSION}")
+MESSAGE(STATUS "Build server: " ${BUILD_SERVER})
+MESSAGE(STATUS "Build client: " ${BUILD_CLIENT})
+MESSAGE(STATUS "Build tests: " ${BUILD_TESTS})
+MESSAGE(STATUS "Build tools: " ${BUILD_TOOLS})
+MESSAGE(STATUS "Build installer: " ${BUILD_INSTALLER})
- set(OSX_SDK "${OSX_VERSION}" CACHE String "OS X SDK version to look for inside Xcode bundle or at OSX_SDK_PATH")
+if (UNIX AND DEFINED ENV{HIFI_MEMORY_DEBUGGING})
+ MESSAGE(STATUS "Memory debugging is enabled")
+endif()
- # set our OS X deployment target
- set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8)
-
- # find the SDK path for the desired SDK
- find_path(
- _OSX_DESIRED_SDK_PATH
- NAME MacOSX${OSX_SDK}.sdk
- HINTS ${OSX_SDK_PATH}
- PATHS /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/
- /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/
- )
-
- if (NOT _OSX_DESIRED_SDK_PATH)
- message(STATUS "Could not find OS X ${OSX_SDK} SDK. Will fall back to default. If you want a specific SDK, please pass OSX_SDK and optionally OSX_SDK_PATH to CMake.")
- else ()
- message(STATUS "Found OS X ${OSX_SDK} SDK at ${_OSX_DESIRED_SDK_PATH}/MacOSX${OSX_SDK}.sdk")
-
- # set that as the SDK to use
- set(CMAKE_OSX_SYSROOT ${_OSX_DESIRED_SDK_PATH}/MacOSX${OSX_SDK}.sdk)
- endif ()
-
-endif ()
-
-# Hide automoc folders (for IDEs)
-set(AUTOGEN_TARGETS_FOLDER "hidden/generated")
-
-# Find includes in corresponding build directories
-set(CMAKE_INCLUDE_CURRENT_DIR ON)
-# Instruct CMake to run moc automatically when needed.
-set(CMAKE_AUTOMOC ON)
-# Instruct CMake to run rcc automatically when needed
-set(CMAKE_AUTORCC ON)
-
-set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries")
-
-# setup for find modules
-set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/")
-
-if (CMAKE_BUILD_TYPE)
- string(TOUPPER ${CMAKE_BUILD_TYPE} UPPER_CMAKE_BUILD_TYPE)
-else ()
- set(UPPER_CMAKE_BUILD_TYPE DEBUG)
-endif ()
-
-set(HF_CMAKE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
-set(MACRO_DIR "${HF_CMAKE_DIR}/macros")
-set(EXTERNAL_PROJECT_DIR "${HF_CMAKE_DIR}/externals")
-
-file(GLOB HIFI_CUSTOM_MACROS "cmake/macros/*.cmake")
-foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS})
- include(${CUSTOM_MACRO})
-endforeach()
+#
+# Helper projects
+#
+file(GLOB_RECURSE CMAKE_SRC cmake/*.cmake cmake/CMakeLists.txt)
+add_custom_target(cmake SOURCES ${CMAKE_SRC})
+GroupSources("cmake")
+unset(CMAKE_SRC)
file(GLOB_RECURSE JS_SRC scripts/*.js unpublishedScripts/*.js)
add_custom_target(js SOURCES ${JS_SRC})
GroupSources("scripts")
GroupSources("unpublishedScripts")
+unset(JS_SRC)
-if (UNIX)
- install(
- DIRECTORY "${CMAKE_SOURCE_DIR}/scripts"
- DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface
- COMPONENT ${CLIENT_COMPONENT}
- )
-endif()
+# Locate the required Qt build on the filesystem
+setup_qt()
+list(APPEND CMAKE_PREFIX_PATH "${QT_CMAKE_PREFIX_PATH}")
-if (ANDROID)
- file(GLOB ANDROID_CUSTOM_MACROS "cmake/android/*.cmake")
- foreach(CUSTOM_MACRO ${ANDROID_CUSTOM_MACROS})
- include(${CUSTOM_MACRO})
- endforeach()
-endif ()
+find_package( Threads )
+
+add_definitions(-DGLM_FORCE_RADIANS)
+set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries")
set(EXTERNAL_PROJECT_PREFIX "project")
set_property(DIRECTORY PROPERTY EP_PREFIX ${EXTERNAL_PROJECT_PREFIX})
setup_externals_binary_dir()
option(USE_NSIGHT "Attempt to find the nSight libraries" 1)
-option(GET_QUAZIP "Get QuaZip library automatically as external project" 1)
-
-
-if (WIN32)
- add_paths_to_fixup_libs("${QT_DIR}/bin")
-endif ()
-
-if (NOT DEFINED SERVER_ONLY)
- set(SERVER_ONLY 0)
-endif()
set_packaging_parameters()
+# FIXME hack to work on the proper Android toolchain
+if (ANDROID)
+ add_subdirectory(android/app)
+ return()
+endif()
+
# add subdirectories for all targets
-if (NOT ANDROID)
+if (BUILD_SERVER)
add_subdirectory(assignment-client)
set_target_properties(assignment-client PROPERTIES FOLDER "Apps")
add_subdirectory(domain-server)
@@ -255,28 +97,35 @@ if (NOT ANDROID)
add_subdirectory(ice-server)
set_target_properties(ice-server PROPERTIES FOLDER "Apps")
add_subdirectory(server-console)
- if (NOT SERVER_ONLY)
- add_subdirectory(interface)
- set_target_properties(interface PROPERTIES FOLDER "Apps")
- add_subdirectory(tests)
- endif()
- add_subdirectory(plugins)
- add_subdirectory(tools)
endif()
-if (ANDROID OR DESKTOP_GVR)
- add_subdirectory(interface)
- add_subdirectory(gvr-interface)
- add_subdirectory(plugins)
-endif ()
+if (BUILD_CLIENT)
+ add_subdirectory(interface)
+ set_target_properties(interface PROPERTIES FOLDER "Apps")
+ if (ANDROID)
+ add_subdirectory(gvr-interface)
+ set_target_properties(gvr-interface PROPERTIES FOLDER "Apps")
+ endif()
+endif()
-if (DEFINED ENV{HIFI_MEMORY_DEBUGGING})
- SET( HIFI_MEMORY_DEBUGGING true )
-endif ()
-if (HIFI_MEMORY_DEBUGGING)
- if (UNIX)
- MESSAGE("-- Memory debugging is enabled")
- endif (UNIX)
-endif ()
+if (BUILD_CLIENT OR BUILD_SERVER)
+ add_subdirectory(plugins)
+endif()
-generate_installers()
+# BUILD_TOOLS option will be handled inside the tools's CMakeLists.txt because 'scribe' tool is required for build anyway
+add_subdirectory(tools)
+
+if (BUILD_TESTS)
+ add_subdirectory(tests)
+endif()
+
+if (BUILD_INSTALLER)
+ if (UNIX)
+ install(
+ DIRECTORY "${CMAKE_SOURCE_DIR}/scripts"
+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface
+ COMPONENT ${CLIENT_COMPONENT}
+ )
+ endif()
+ generate_installers()
+endif()
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a0d867ade9..4654c311cc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,7 +16,7 @@ Contributing
git checkout -b new_branch_name
```
4. Code
- * Follow the [coding standard](https://wiki.highfidelity.com/wiki/Coding_Standards)
+ * Follow the [coding standard](https://docs.highfidelity.com/build-guide/coding-standards)
5. Commit
* Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
6. Update your branch
diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt
new file mode 100644
index 0000000000..2d6df925e9
--- /dev/null
+++ b/android/app/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(TARGET_NAME native-lib)
+setup_hifi_library()
+link_hifi_libraries(shared networking gl gpu gpu-gles render-utils)
+autoscribe_shader_lib(gpu model render render-utils)
+target_opengl()
+target_link_libraries(native-lib android log m)
+target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers")
+target_link_libraries(native-lib "C:/Users/bdavis/Git/hifi/android/libraries/jni/armeabi-v7a/libgvr.so")
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000000..bd1c596bf3
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,57 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 26
+ buildToolsVersion "26.0.1"
+ defaultConfig {
+ applicationId "org.saintandreas.testapp"
+ minSdkVersion 24
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ ndk { abiFilters 'armeabi-v7a' }
+ externalNativeBuild {
+ cmake {
+ arguments '-DHIFI_ANDROID=1',
+ '-DANDROID_PLATFORM=android-24',
+ '-DANDROID_TOOLCHAIN=clang',
+ '-DANDROID_STL=gnustl_shared',
+ '-DGVR_ROOT=' + GVR_ROOT,
+ '-DNATIVE_SCRIBE=c:/bin/scribe.exe',
+ "-DHIFI_ANDROID_PRECOMPILED=${project.rootDir}/libraries/jni/armeabi-v7a"
+ }
+ }
+ jackOptions { enabled true }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs += '../libraries/jni';
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path '../../CMakeLists.txt'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: "${project.rootDir}/libraries/jar", include: 'QtAndroid-bundled.jar')
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile 'com.google.vr:sdk-audio:1.80.0'
+ compile 'com.google.vr:sdk-base:1.80.0'
+}
+
+build.dependsOn(':extractQt5')
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000000..b3c0078513
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Android\SDK/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..05547bd5ae
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/cpp/GoogleVRHelpers.h b/android/app/src/main/cpp/GoogleVRHelpers.h
new file mode 100644
index 0000000000..10c46b036f
--- /dev/null
+++ b/android/app/src/main/cpp/GoogleVRHelpers.h
@@ -0,0 +1,50 @@
+#include
+#include
+#include
+
+namespace googlevr {
+
+ // Convert a GVR matrix to GLM matrix
+ glm::mat4 toGlm(const gvr::Mat4f &matrix) {
+ glm::mat4 result;
+ for (int i = 0; i < 4; ++i) {
+ for (int j = 0; j < 4; ++j) {
+ result[j][i] = matrix.m[i][j];
+ }
+ }
+ return result;
+ }
+
+ // Given a field of view in degrees, compute the corresponding projection
+// matrix.
+ glm::mat4 perspectiveMatrixFromView(const gvr::Rectf& fov, float z_near, float z_far) {
+ const float x_left = -std::tan(fov.left * M_PI / 180.0f) * z_near;
+ const float x_right = std::tan(fov.right * M_PI / 180.0f) * z_near;
+ const float y_bottom = -std::tan(fov.bottom * M_PI / 180.0f) * z_near;
+ const float y_top = std::tan(fov.top * M_PI / 180.0f) * z_near;
+ const float Y = (2 * z_near) / (y_top - y_bottom);
+ const float A = (x_right + x_left) / (x_right - x_left);
+ const float B = (y_top + y_bottom) / (y_top - y_bottom);
+ const float C = (z_near + z_far) / (z_near - z_far);
+ const float D = (2 * z_near * z_far) / (z_near - z_far);
+
+ glm::mat4 result { 0 };
+ result[2][0] = A;
+ result[1][1] = Y;
+ result[2][1] = B;
+ result[2][2] = C;
+ result[3][2] = D;
+ result[2][3] = -1;
+ return result;
+ }
+
+ glm::quat toGlm(const gvr::ControllerQuat& q) {
+ glm::quat result;
+ result.w = q.qw;
+ result.x = q.qx;
+ result.y = q.qy;
+ result.z = q.qz;
+ return result;
+ }
+
+}
diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp
new file mode 100644
index 0000000000..156d43d849
--- /dev/null
+++ b/android/app/src/main/cpp/native-lib.cpp
@@ -0,0 +1,78 @@
+#include
+
+#include
+#include
+
+#include "renderer.h"
+
+int QtMsgTypeToAndroidPriority(QtMsgType type) {
+ int priority = ANDROID_LOG_UNKNOWN;
+ switch (type) {
+ case QtDebugMsg: priority = ANDROID_LOG_DEBUG; break;
+ case QtWarningMsg: priority = ANDROID_LOG_WARN; break;
+ case QtCriticalMsg: priority = ANDROID_LOG_ERROR; break;
+ case QtFatalMsg: priority = ANDROID_LOG_FATAL; break;
+ case QtInfoMsg: priority = ANDROID_LOG_INFO; break;
+ default: break;
+ }
+ return priority;
+}
+
+void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) {
+ __android_log_write(QtMsgTypeToAndroidPriority(type), "Interface", message.toStdString().c_str());
+}
+
+static jlong toJni(NativeRenderer *renderer) {
+ return reinterpret_cast(renderer);
+}
+
+static NativeRenderer *fromJni(jlong renderer) {
+ return reinterpret_cast(renderer);
+}
+
+#define JNI_METHOD(r, name) JNIEXPORT r JNICALL Java_org_saintandreas_testapp_MainActivity_##name
+
+extern "C" {
+
+JNI_METHOD(jlong, nativeCreateRenderer)
+(JNIEnv *env, jclass clazz, jobject class_loader, jobject android_context, jlong native_gvr_api) {
+ qInstallMessageHandler(messageHandler);
+#if defined(GVR)
+ auto gvrContext = reinterpret_cast(native_gvr_api);
+ return toJni(new NativeRenderer(gvrContext));
+#else
+ return toJni(new NativeRenderer(nullptr));
+#endif
+}
+
+JNI_METHOD(void, nativeDestroyRenderer)
+(JNIEnv *env, jclass clazz, jlong renderer) {
+ delete fromJni(renderer);
+}
+
+JNI_METHOD(void, nativeInitializeGl)
+(JNIEnv *env, jobject obj, jlong renderer) {
+ fromJni(renderer)->InitializeGl();
+}
+
+JNI_METHOD(void, nativeDrawFrame)
+(JNIEnv *env, jobject obj, jlong renderer) {
+ fromJni(renderer)->DrawFrame();
+}
+
+JNI_METHOD(void, nativeOnTriggerEvent)
+(JNIEnv *env, jobject obj, jlong renderer) {
+ fromJni(renderer)->OnTriggerEvent();
+}
+
+JNI_METHOD(void, nativeOnPause)
+(JNIEnv *env, jobject obj, jlong renderer) {
+ fromJni(renderer)->OnPause();
+}
+
+JNI_METHOD(void, nativeOnResume)
+(JNIEnv *env, jobject obj, jlong renderer) {
+ fromJni(renderer)->OnResume();
+}
+
+} // extern "C"
diff --git a/android/app/src/main/cpp/renderer.cpp b/android/app/src/main/cpp/renderer.cpp
new file mode 100644
index 0000000000..a877ebd777
--- /dev/null
+++ b/android/app/src/main/cpp/renderer.cpp
@@ -0,0 +1,636 @@
+#include "renderer.h"
+
+#include
+
+#include
+#include
+
+#include "GoogleVRHelpers.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#if 0
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+
+template
+void withFrameBuffer(gvr::Frame& frame, int32_t index, F f) {
+ frame.BindBuffer(index);
+ f();
+ frame.Unbind();
+}
+
+
+static const uint64_t kPredictionTimeWithoutVsyncNanos = 50000000;
+
+// Each shader has two variants: a single-eye ES 2.0 variant, and a multiview
+// ES 3.0 variant. The multiview vertex shaders use transforms defined by
+// arrays of mat4 uniforms, using gl_ViewID_OVR to determine the array index.
+
+#define UNIFORM_LIGHT_POS 20
+#define UNIFORM_M 16
+#define UNIFORM_MV 8
+#define UNIFORM_MVP 0
+
+#if 0
+uniform Transform { // API uses “Transform[2]” to refer to instance 2
+ mat4 u_MVP[2];
+ mat4 u_MVMatrix[2];
+ mat4 u_Model;
+ vec3 u_LightPos[2];
+};
+static const char *kDiffuseLightingVertexShader = R"glsl(
+#version 300 es
+#extension GL_OVR_multiview2 : enable
+
+layout(num_views=2) in;
+
+layout(location = 0) uniform mat4 u_MVP[2];
+layout(location = 8) uniform mat4 u_MVMatrix[2];
+layout(location = 16) uniform mat4 u_Model;
+layout(location = 20) uniform vec3 u_LightPos[2];
+
+layout(location = 0) in vec4 a_Position;
+layout(location = 1) in vec4 a_Color;
+layout(location = 2) in vec3 a_Normal;
+
+out vec4 v_Color;
+out vec3 v_Grid;
+
+void main() {
+ mat4 mvp = u_MVP[gl_ViewID_OVR];
+ mat4 modelview = u_MVMatrix[gl_ViewID_OVR];
+ vec3 lightpos = u_LightPos[gl_ViewID_OVR];
+ v_Grid = vec3(u_Model * a_Position);
+ vec3 modelViewVertex = vec3(modelview * a_Position);
+ vec3 modelViewNormal = vec3(modelview * vec4(a_Normal, 0.0));
+ float distance = length(lightpos - modelViewVertex);
+ vec3 lightVector = normalize(lightpos - modelViewVertex);
+ float diffuse = max(dot(modelViewNormal, lightVector), 0.5);
+ diffuse = diffuse * (1.0 / (1.0 + (0.00001 * distance * distance)));
+ v_Color = vec4(a_Color.rgb * diffuse, a_Color.a);
+ gl_Position = mvp * a_Position;
+}
+)glsl";
+#endif
+
+
+static const char *kSimepleVertexShader = R"glsl(
+#version 300 es
+#extension GL_OVR_multiview2 : enable
+
+layout(num_views=2) in;
+
+layout(location = 0) in vec4 a_Position;
+
+out vec4 v_Color;
+
+void main() {
+ v_Color = vec4(a_Position.xyz, 1.0);
+ gl_Position = vec4(a_Position.xyz, 1.0);
+}
+)glsl";
+
+
+static const char *kPassthroughFragmentShader = R"glsl(
+#version 300 es
+precision mediump float;
+in vec4 v_Color;
+out vec4 FragColor;
+
+void main() { FragColor = v_Color; }
+)glsl";
+
+static void CheckGLError(const char* label) {
+ int gl_error = glGetError();
+ if (gl_error != GL_NO_ERROR) {
+ qWarning("GL error @ %s: %d", label, gl_error);
+ // Crash immediately to make OpenGL errors obvious.
+ abort();
+ }
+}
+
+// Contains vertex, normal and other data.
+namespace cube {
+ const std::array CUBE_COORDS{{
+ // Front face
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+
+ // Right face
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f,
+
+ // Back face
+ 1.0f, 1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+
+ // Left face
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, -1.0f, 1.0f,
+ -1.0f, 1.0f, 1.0f,
+
+ // Top face
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f,
+ -1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f,
+
+ // Bottom face
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, -1.0f
+ }};
+
+ const std::array CUBE_COLORS{{
+ // front, green
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+
+ // right, blue
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+
+ // back, also green
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+ 0.0f, 0.5273f, 0.2656f,
+
+ // left, also blue
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+ 0.0f, 0.3398f, 0.9023f,
+
+ // top, red
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+
+ // bottom, also red
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f,
+ 0.8359375f, 0.17578125f, 0.125f
+ }};
+
+ const std::array CUBE_NORMALS{{
+ // Front face
+ 0.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f,
+
+ // Right face
+ 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f,
+
+ // Back face
+ 0.0f, 0.0f, -1.0f,
+ 0.0f, 0.0f, -1.0f,
+ 0.0f, 0.0f, -1.0f,
+ 0.0f, 0.0f, -1.0f,
+ 0.0f, 0.0f, -1.0f,
+ 0.0f, 0.0f, -1.0f,
+
+ // Left face
+ -1.0f, 0.0f, 0.0f,
+ -1.0f, 0.0f, 0.0f,
+ -1.0f, 0.0f, 0.0f,
+ -1.0f, 0.0f, 0.0f,
+ -1.0f, 0.0f, 0.0f,
+ -1.0f, 0.0f, 0.0f,
+
+ // Top face
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+
+ // Bottom face
+ 0.0f, -1.0f, 0.0f,
+ 0.0f, -1.0f, 0.0f,
+ 0.0f, -1.0f, 0.0f,
+ 0.0f, -1.0f, 0.0f,
+ 0.0f, -1.0f, 0.0f,
+ 0.0f, -1.0f, 0.0f
+ }};
+}
+
+namespace triangle {
+ static std::array TRIANGLE_VERTS {{
+ -0.5f, -0.5f, 0.0f,
+ 0.5f, -0.5f, 0.0f,
+ 0.0f, 0.5f, 0.0f
+ }};
+}
+
+std::array buildViewports(const std::unique_ptr &gvrapi) {
+ return { {gvrapi->CreateBufferViewport(), gvrapi->CreateBufferViewport()} };
+};
+
+const std::string VERTEX_SHADER_DEFINES{ R"GLSL(
+#version 300 es
+#extension GL_EXT_clip_cull_distance : enable
+#define GPU_VERTEX_SHADER
+#define GPU_SSBO_TRANSFORM_OBJECT 1
+#define GPU_TRANSFORM_IS_STEREO
+#define GPU_TRANSFORM_STEREO_CAMERA
+#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED
+#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN
+)GLSL" };
+
+const std::string PIXEL_SHADER_DEFINES{ R"GLSL(
+#version 300 es
+precision mediump float;
+#define GPU_PIXEL_SHADER
+#define GPU_TRANSFORM_IS_STEREO
+#define GPU_TRANSFORM_STEREO_CAMERA
+#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED
+#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN
+)GLSL" };
+
+
+#if defined(GVR)
+NativeRenderer::NativeRenderer(gvr_context *vrContext) :
+ _gvrapi(new gvr::GvrApi(vrContext, false)),
+ _viewports(buildViewports(_gvrapi)),
+ _gvr_viewer_type(_gvrapi->GetViewerType())
+#else
+NativeRenderer::NativeRenderer(void *vrContext)
+#endif
+{
+ start = std::chrono::system_clock::now();
+ qDebug() << "QQQ" << __FUNCTION__;
+}
+
+
+/**
+ * Converts a raw text file, saved as a resource, into an OpenGL ES shader.
+ *
+ * @param type The type of shader we will be creating.
+ * @param resId The resource ID of the raw text file.
+ * @return The shader object handler.
+ */
+int LoadGLShader(int type, const char *shadercode) {
+ GLuint result = 0;
+ std::string shaderError;
+ static const std::string SHADER_DEFINES;
+ if (!gl::compileShader(type, shadercode, SHADER_DEFINES, result, shaderError)) {
+ qWarning() << "QQQ" << __FUNCTION__ << "Shader compile failure" << shaderError.c_str();
+ }
+ return result;
+}
+
+// Computes a texture size that has approximately half as many pixels. This is
+// equivalent to scaling each dimension by approximately sqrt(2)/2.
+static gvr::Sizei HalfPixelCount(const gvr::Sizei &in) {
+ // Scale each dimension by sqrt(2)/2 ~= 7/10ths.
+ gvr::Sizei out;
+ out.width = (7 * in.width) / 10;
+ out.height = (7 * in.height) / 10;
+ return out;
+}
+
+
+#if defined(GVR)
+void NativeRenderer::InitializeVR() {
+ _gvrapi->InitializeGl();
+ bool multiviewEnabled = _gvrapi->IsFeatureSupported(GVR_FEATURE_MULTIVIEW);
+ qWarning() << "QQQ" << __FUNCTION__ << "Multiview enabled " << multiviewEnabled;
+ // Because we are using 2X MSAA, we can render to half as many pixels and
+ // achieve similar quality.
+ _renderSize = HalfPixelCount(_gvrapi->GetMaximumEffectiveRenderTargetSize());
+
+ std::vector specs;
+ specs.push_back(_gvrapi->CreateBufferSpec());
+ specs[0].SetColorFormat(GVR_COLOR_FORMAT_RGBA_8888);
+ specs[0].SetDepthStencilFormat(GVR_DEPTH_STENCIL_FORMAT_DEPTH_16);
+ specs[0].SetSamples(2);
+ gvr::Sizei half_size = {_renderSize.width / 2, _renderSize.height};
+ specs[0].SetMultiviewLayers(2);
+ specs[0].SetSize(half_size);
+
+ _swapchain.reset(new gvr::SwapChain(_gvrapi->CreateSwapChain(specs)));
+ _viewportlist.reset(new gvr::BufferViewportList(_gvrapi->CreateEmptyBufferViewportList()));
+}
+void NativeRenderer::PrepareFramebuffer() {
+ const gvr::Sizei recommended_size = HalfPixelCount(
+ _gvrapi->GetMaximumEffectiveRenderTargetSize());
+ if (_renderSize.width != recommended_size.width ||
+ _renderSize.height != recommended_size.height) {
+ // We need to resize the framebuffer. Note that multiview uses two texture
+ // layers, each with half the render width.
+ gvr::Sizei framebuffer_size = recommended_size;
+ framebuffer_size.width /= 2;
+ _swapchain->ResizeBuffer(0, framebuffer_size);
+ _renderSize = recommended_size;
+ }
+}
+#endif
+
+void testShaderBuild(const char* vs_src, const char * fs_src) {
+ std::string error;
+ GLuint vs, fs;
+ if (!gl::compileShader(GL_VERTEX_SHADER, vs_src, VERTEX_SHADER_DEFINES, vs, error) ||
+ !gl::compileShader(GL_FRAGMENT_SHADER, fs_src, PIXEL_SHADER_DEFINES, fs, error)) {
+ throw std::runtime_error("Failed to compile shader");
+ }
+ auto pr = gl::compileProgram({ vs, fs }, error);
+ if (!pr) {
+ throw std::runtime_error("Failed to link shader");
+ }
+}
+
+void NativeRenderer::InitializeGl() {
+ qDebug() << "QQQ" << __FUNCTION__;
+ //gl::initModuleGl();
+#if defined(GVR)
+ InitializeVR();
+#endif
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_SCISSOR_TEST);
+ glDisable(GL_BLEND);
+
+
+
+ const uint32_t vertShader = LoadGLShader(GL_VERTEX_SHADER, kSimepleVertexShader);
+ //const uint32_t vertShader = LoadGLShader(GL_VERTEX_SHADER, kDiffuseLightingVertexShader);
+ const uint32_t fragShader = LoadGLShader(GL_FRAGMENT_SHADER, kPassthroughFragmentShader);
+ std::string error;
+ _cubeProgram = gl::compileProgram({ vertShader, fragShader }, error);
+ CheckGLError("build program");
+
+ glGenBuffers(1, &_cubeBuffer);
+ glBindBuffer(GL_ARRAY_BUFFER, _cubeBuffer);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 9, triangle::TRIANGLE_VERTS.data(), GL_STATIC_DRAW);
+ /*
+ glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 3, NULL, GL_STATIC_DRAW);
+ glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 0, sizeof(float) * 108, cube::CUBE_COORDS.data());
+ glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 1, sizeof(float) * 108, cube::CUBE_COLORS.data());
+ glBufferSubData(GL_ARRAY_BUFFER, sizeof(float) * 108 * 2, sizeof(float) * 108, cube::CUBE_NORMALS.data());
+ */
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ CheckGLError("upload vertices");
+
+ glGenVertexArrays(1, &_cubeVao);
+ glBindBuffer(GL_ARRAY_BUFFER, _cubeBuffer);
+ glBindVertexArray(_cubeVao);
+
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(0);
+ /*
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, (const void*)(sizeof(float) * 108 * 1) );
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, (const void*)(sizeof(float) * 108 * 2));
+ glEnableVertexAttribArray(2);
+ */
+ glBindVertexArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ CheckGLError("build vao ");
+
+ static std::once_flag once;
+ std::call_once(once, [&]{
+ testShaderBuild(sdf_text3D_vert, sdf_text3D_frag);
+
+ testShaderBuild(DrawTransformUnitQuad_vert, DrawTexture_frag);
+ testShaderBuild(DrawTexcoordRectTransformUnitQuad_vert, DrawTexture_frag);
+ testShaderBuild(DrawViewportQuadTransformTexcoord_vert, DrawTexture_frag);
+ testShaderBuild(DrawTransformUnitQuad_vert, DrawTextureOpaque_frag);
+ testShaderBuild(DrawTransformUnitQuad_vert, DrawColoredTexture_frag);
+
+ testShaderBuild(simple_vert, simple_frag);
+ testShaderBuild(simple_vert, simple_textured_frag);
+ testShaderBuild(simple_vert, simple_textured_unlit_frag);
+ testShaderBuild(deferred_light_vert, directional_ambient_light_frag);
+ testShaderBuild(deferred_light_vert, directional_skybox_light_frag);
+ testShaderBuild(standardTransformPNTC_vert, standardDrawTexture_frag);
+ testShaderBuild(standardTransformPNTC_vert, DrawTextureOpaque_frag);
+
+ testShaderBuild(model_vert, model_frag);
+ testShaderBuild(model_normal_map_vert, model_normal_map_frag);
+ testShaderBuild(model_vert, model_specular_map_frag);
+ testShaderBuild(model_normal_map_vert, model_normal_specular_map_frag);
+ testShaderBuild(model_vert, model_translucent_frag);
+ testShaderBuild(model_normal_map_vert, model_translucent_frag);
+ testShaderBuild(model_lightmap_vert, model_lightmap_frag);
+ testShaderBuild(model_lightmap_normal_map_vert, model_lightmap_normal_map_frag);
+ testShaderBuild(model_lightmap_vert, model_lightmap_specular_map_frag);
+ testShaderBuild(model_lightmap_normal_map_vert, model_lightmap_normal_specular_map_frag);
+
+ testShaderBuild(skin_model_vert, model_frag);
+ testShaderBuild(skin_model_normal_map_vert, model_normal_map_frag);
+ testShaderBuild(skin_model_vert, model_specular_map_frag);
+ testShaderBuild(skin_model_normal_map_vert, model_normal_specular_map_frag);
+ testShaderBuild(skin_model_vert, model_translucent_frag);
+ testShaderBuild(skin_model_normal_map_vert, model_translucent_frag);
+
+ testShaderBuild(model_shadow_vert, model_shadow_frag);
+
+ testShaderBuild(overlay3D_vert, overlay3D_frag);
+
+#if 0
+ testShaderBuild(textured_particle_vert, textured_particle_frag);
+ testShaderBuild(skybox_vert, skybox_frag);
+ testShaderBuild(paintStroke_vert,paintStroke_frag);
+ testShaderBuild(polyvox_vert, polyvox_frag);
+#endif
+
+ });
+
+ qDebug() << "done";
+}
+
+static const float kZNear = 1.0f;
+static const float kZFar = 100.0f;
+static const gvr_rectf fullscreen = {0, 1, 0, 1};
+
+void NativeRenderer::DrawFrame() {
+ auto now = std::chrono::duration_cast(
+ std::chrono::system_clock::now() - start);
+ glm::vec3 v;
+ v.r = (float) (now.count() % 1000) / 1000.0f;
+ v.g = 1.0f - v.r;
+ v.b = 1.0f;
+
+ PrepareFramebuffer();
+
+ // A client app does its rendering here.
+ gvr::ClockTimePoint target_time = gvr::GvrApi::GetTimePointNow();
+ target_time.monotonic_system_time_nanos += kPredictionTimeWithoutVsyncNanos;
+
+ using namespace googlevr;
+ using namespace bilateral;
+ const auto gvrHeadPose = _gvrapi->GetHeadSpaceFromStartSpaceRotation(target_time);
+ _head_view = toGlm(gvrHeadPose);
+ _viewportlist->SetToRecommendedBufferViewports();
+
+ glm::mat4 eye_views[2];
+ for_each_side([&](bilateral::Side side) {
+ int eye = index(side);
+ const gvr::Eye gvr_eye = eye == 0 ? GVR_LEFT_EYE : GVR_RIGHT_EYE;
+ const auto& eyeView = eye_views[eye] = toGlm(_gvrapi->GetEyeFromHeadMatrix(gvr_eye)) * _head_view;
+ auto& viewport = _viewports[eye];
+
+ _viewportlist->GetBufferViewport(eye, &viewport);
+ viewport.SetSourceUv(fullscreen);
+ viewport.SetSourceLayer(eye);
+ _viewportlist->SetBufferViewport(eye, viewport);
+ const auto &mvc = _modelview_cube[eye] = eyeView * _model_cube;
+ const auto &mvf = _modelview_floor[eye] = eyeView * _model_floor;
+ const gvr_rectf fov = viewport.GetSourceFov();
+ const glm::mat4 perspective = perspectiveMatrixFromView(fov, kZNear, kZFar);
+ _modelview_projection_cube[eye] = perspective * mvc;
+ _modelview_projection_floor[eye] = perspective * mvf;
+ _light_pos_eye_space[eye] = glm::vec3(eyeView * _light_pos_world_space);
+ });
+
+
+ gvr::Frame frame = _swapchain->AcquireFrame();
+ withFrameBuffer(frame, 0, [&]{
+ glClearColor(v.r, v.g, v.b, 1);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ glViewport(0, 0, _renderSize.width / 2, _renderSize.height);
+ glUseProgram(_cubeProgram);
+ glBindVertexArray(_cubeVao);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ /*
+ float* fp;
+ fp = (float*)&_light_pos_eye_space[0];
+ glUniform3fv(UNIFORM_LIGHT_POS, 2, fp);
+ fp = (float*)&_modelview_cube[0];
+ glUniformMatrix4fv(UNIFORM_MV, 2, GL_FALSE, fp);
+ fp = (float*)&_modelview_projection_cube[0];
+ glUniformMatrix4fv(UNIFORM_MVP, 2, GL_FALSE, fp);
+ fp = (float*)&_model_cube;
+ glUniformMatrix4fv(UNIFORM_M, 1, GL_FALSE, fp);
+ glDrawArrays(GL_TRIANGLES, 0, 36);
+ */
+ glBindVertexArray(0);
+ });
+
+ frame.Submit(*_viewportlist, gvrHeadPose);
+ CheckGLError("onDrawFrame");
+
+}
+
+void NativeRenderer::OnTriggerEvent() {
+ qDebug() << "QQQ" << __FUNCTION__;
+}
+
+void NativeRenderer::OnPause() {
+ qDebug() << "QQQ" << __FUNCTION__;
+ _gvrapi->PauseTracking();
+}
+
+void NativeRenderer::OnResume() {
+ qDebug() << "QQQ" << __FUNCTION__;
+ _gvrapi->ResumeTracking();
+ _gvrapi->RefreshViewerProfile();
+}
diff --git a/android/app/src/main/cpp/renderer.h b/android/app/src/main/cpp/renderer.h
new file mode 100644
index 0000000000..df7c51cab4
--- /dev/null
+++ b/android/app/src/main/cpp/renderer.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include
+#include
+#include
+
+#define GVR
+
+#if defined(GVR)
+#include
+#endif
+
+class NativeRenderer {
+public:
+
+#if defined(GVR)
+ NativeRenderer(gvr_context* vrContext);
+#else
+ NativeRenderer(void* vrContext);
+#endif
+
+ void InitializeGl();
+ void DrawFrame();
+ void OnTriggerEvent();
+ void OnPause();
+ void OnResume();
+
+private:
+
+
+ std::chrono::time_point start;
+#if defined(GVR)
+ void InitializeVR();
+ void PrepareFramebuffer();
+
+ std::unique_ptr _gvrapi;
+ gvr::ViewerType _gvr_viewer_type;
+ std::unique_ptr _viewportlist;
+ std::unique_ptr _swapchain;
+ std::array _viewports;
+ gvr::Sizei _renderSize;
+#endif
+
+ uint32_t _cubeBuffer { 0 };
+ uint32_t _cubeVao { 0 };
+ uint32_t _cubeProgram { 0 };
+
+ glm::mat4 _head_view;
+ glm::mat4 _model_cube;
+ glm::mat4 _camera;
+ glm::mat4 _view;
+ glm::mat4 _model_floor;
+
+ std::array _modelview_cube;
+ std::array _modelview_floor;
+ std::array _modelview_projection_cube;
+ std::array _modelview_projection_floor;
+ std::array _light_pos_eye_space;
+ const glm::vec4 _light_pos_world_space{ 0, 2, 0, 1};
+};
diff --git a/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java b/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java
new file mode 100644
index 0000000000..7eea14dce9
--- /dev/null
+++ b/android/app/src/main/java/org/saintandreas/testapp/MainActivity.java
@@ -0,0 +1,105 @@
+package org.saintandreas.testapp;
+
+import android.app.Activity;
+import android.content.Context;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import android.view.View;
+
+import com.google.vr.ndk.base.AndroidCompat;
+import com.google.vr.ndk.base.GvrLayout;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+public class MainActivity extends Activity {
+ private final static int IMMERSIVE_STICKY_VIEW_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+ static {
+ System.loadLibrary("gvr");
+ System.loadLibrary("native-lib");
+ }
+
+ private long nativeRenderer;
+ private GvrLayout gvrLayout;
+ private GLSurfaceView surfaceView;
+
+ private native long nativeCreateRenderer(ClassLoader appClassLoader, Context context, long nativeGvrContext);
+ private native void nativeDestroyRenderer(long renderer);
+ private native void nativeInitializeGl(long renderer);
+ private native void nativeDrawFrame(long renderer);
+ private native void nativeOnTriggerEvent(long renderer);
+ private native void nativeOnPause(long renderer);
+ private native void nativeOnResume(long renderer);
+
+ class NativeRenderer implements GLSurfaceView.Renderer {
+ @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { nativeInitializeGl(nativeRenderer); }
+ @Override public void onSurfaceChanged(GL10 gl, int width, int height) { }
+ @Override public void onDrawFrame(GL10 gl) {
+ nativeDrawFrame(nativeRenderer);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setImmersiveSticky();
+ getWindow()
+ .getDecorView()
+ .setOnSystemUiVisibilityChangeListener((int visibility)->{
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { setImmersiveSticky(); }
+ });
+
+ gvrLayout = new GvrLayout(this);
+ nativeRenderer = nativeCreateRenderer(
+ getClass().getClassLoader(),
+ getApplicationContext(),
+ gvrLayout.getGvrApi().getNativeGvrContext());
+
+ surfaceView = new GLSurfaceView(this);
+ surfaceView.setEGLContextClientVersion(3);
+ surfaceView.setEGLConfigChooser(8, 8, 8, 0, 0, 0);
+ surfaceView.setPreserveEGLContextOnPause(true);
+ surfaceView.setRenderer(new NativeRenderer());
+
+ gvrLayout.setPresentationView(surfaceView);
+ setContentView(gvrLayout);
+ if (gvrLayout.setAsyncReprojectionEnabled(true)) {
+ AndroidCompat.setSustainedPerformanceMode(this, true);
+ }
+ AndroidCompat.setVrModeEnabled(this, true);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ gvrLayout.shutdown();
+ nativeDestroyRenderer(nativeRenderer);
+ nativeRenderer = 0;
+ }
+
+ @Override
+ protected void onPause() {
+ surfaceView.queueEvent(()->nativeOnPause(nativeRenderer));
+ surfaceView.onPause();
+ gvrLayout.onPause();
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ gvrLayout.onResume();
+ surfaceView.onResume();
+ surfaceView.queueEvent(()->nativeOnResume(nativeRenderer));
+ }
+
+ private void setImmersiveSticky() {
+ getWindow().getDecorView().setSystemUiVisibility(IMMERSIVE_STICKY_VIEW_FLAGS);
+ }
+}
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..cde69bccce
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..9a078e3e1a
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c133a0cbd3
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..efc028a636
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..bfa42f0e7b
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..3af2608a44
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..324e72cdd7
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..9bec2e6231
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..aee44e1384
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..34947cd6bb
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..344907f039
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #ffffff
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..5d6a4c1b99
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ TestApp
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..033324ac58
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000000..77c3dd498c
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,91 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.3'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+task extractQt5jars(type: Copy) {
+ from fileTree(QT5_ROOT + "/jar")
+ into("${project.rootDir}/libraries/jar")
+ include("*.jar")
+}
+
+task extractQt5so(type: Copy) {
+ from fileTree(QT5_ROOT + "/lib")
+ into("${project.rootDir}/libraries/jni/armeabi-v7a/")
+ include("libQt5AndroidExtras.so")
+ include("libQt5Concurrent.so")
+ include("libQt5Core.so")
+ include("libQt5Gamepad.so")
+ include("libQt5Gui.so")
+ include("libQt5Location.so")
+ include("libQt5Multimedia.so")
+ include("libQt5MultimediaQuick_p.so")
+ include("libQt5Network.so")
+ include("libQt5NetworkAuth.so")
+ include("libQt5OpenGL.so")
+ include("libQt5Positioning.so")
+ include("libQt5Qml.so")
+ include("libQt5Quick.so")
+ include("libQt5QuickControls2.so")
+ include("libQt5QuickParticles.so")
+ include("libQt5QuickTemplates2.so")
+ include("libQt5QuickWidgets.so")
+ include("libQt5Script.so")
+ include("libQt5ScriptTools.so")
+ include("libQt5Sensors.so")
+ include("libQt5Svg.so")
+ include("libQt5WebChannel.so")
+ include("libQt5WebSockets.so")
+ include("libQt5WebView.so")
+ include("libQt5Widgets.so")
+ include("libQt5Xml.so")
+ include("libQt5XmlPatterns.so")
+}
+
+task extractAudioSo(type: Copy) {
+ from zipTree(GVR_ROOT + "/libraries/sdk-audio-1.80.0.aar")
+ into "${project.rootDir}/libraries/"
+ include "jni/armeabi-v7a/libgvr_audio.so"
+}
+
+task extractGvrSo(type: Copy) {
+ from zipTree(GVR_ROOT + "/libraries/sdk-base-1.80.0.aar")
+ into "${project.rootDir}/libraries/"
+ include "jni/armeabi-v7a/libgvr.so"
+}
+
+task extractNdk { }
+extractNdk.dependsOn extractAudioSo
+extractNdk.dependsOn extractGvrSo
+
+task extractQt5 { }
+extractQt5.dependsOn extractQt5so
+extractQt5.dependsOn extractQt5jars
+
+task extractBinaries { }
+extractBinaries.dependsOn extractQt5
+extractBinaries.dependsOn extractNdk
+
+task deleteBinaries(type: Delete) {
+ delete "${project.rootDir}/libraries/jni"
+}
+
+//clean.dependsOn(deleteBinaries)
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000000..aac7c9b461
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000000..e7b4def49c
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt
index 0ef2db3b9d..0421195612 100644
--- a/assignment-client/CMakeLists.txt
+++ b/assignment-client/CMakeLists.txt
@@ -13,7 +13,7 @@ setup_memory_debugger()
link_hifi_libraries(
audio avatars octree gpu model fbx entities
networking animation recording shared script-engine embedded-webserver
- physics plugins
+ controllers physics plugins midi baking image
)
if (WIN32)
diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp
index 8aec5adb1f..4efc3343d1 100644
--- a/assignment-client/src/Agent.cpp
+++ b/assignment-client/src/Agent.cpp
@@ -23,6 +23,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -50,14 +51,14 @@
#include "RecordingScriptingInterface.h"
#include "AbstractAudioInterface.h"
-#include "AvatarAudioTimer.h"
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
Agent::Agent(ReceivedMessage& message) :
ThreadedAssignment(message),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
- _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO)
+ _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO),
+ _avatarAudioTimer(this)
{
_entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT);
DependencyManager::get()->setPacketSender(&_entityEditSender);
@@ -81,6 +82,9 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::set();
DependencyManager::set();
+ // Needed to ensure the creation of the DebugDraw instance on the main thread
+ DebugDraw::getInstance();
+
auto& packetReceiver = DependencyManager::get()->getPacketReceiver();
@@ -92,6 +96,14 @@ Agent::Agent(ReceivedMessage& message) :
this, "handleOctreePacket");
packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket");
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
+
+
+ // 100Hz timer for audio
+ const int TARGET_INTERVAL_MSEC = 10; // 10ms
+ connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio);
+ _avatarAudioTimer.setSingleShot(false);
+ _avatarAudioTimer.setInterval(TARGET_INTERVAL_MSEC);
+ _avatarAudioTimer.setTimerType(Qt::PreciseTimer);
}
void Agent::playAvatarSound(SharedSoundPointer sound) {
@@ -172,6 +184,9 @@ void Agent::run() {
// make sure we hear about connected nodes so we can grab an ATP script if a request is pending
connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &Agent::nodeActivated);
+ // make sure we hear about dissappearing nodes so we can clear the entity tree if an entity server goes away
+ connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &Agent::nodeKilled);
+
nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer
});
@@ -247,6 +262,13 @@ void Agent::nodeActivated(SharedNodePointer activatedNode) {
}
}
+void Agent::nodeKilled(SharedNodePointer killedNode) {
+ if (killedNode->getType() == NodeType::EntityServer) {
+ // an entity server has gone away, ask the headless viewer to clear its tree
+ _entityViewer.clear();
+ }
+}
+
void Agent::negotiateAudioFormat() {
auto nodeList = DependencyManager::get();
auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat);
@@ -332,15 +354,16 @@ void Agent::scriptRequestFinished() {
void Agent::executeScript() {
- _scriptEngine = std::unique_ptr(new ScriptEngine(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload));
+ _scriptEngine = scriptEngineFactory(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload);
_scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do
- DependencyManager::get()->setScriptEngine(_scriptEngine.get());
+ DependencyManager::get()->setScriptEngine(_scriptEngine);
// setup an Avatar for the script to use
auto scriptedAvatar = DependencyManager::get();
- connect(_scriptEngine.get(), SIGNAL(update(float)), scriptedAvatar.data(), SLOT(update(float)), Qt::ConnectionType::QueuedConnection);
+ connect(_scriptEngine.data(), SIGNAL(update(float)),
+ scriptedAvatar.data(), SLOT(update(float)), Qt::ConnectionType::QueuedConnection);
scriptedAvatar->setForceFaceTrackerConnected(true);
// call model URL setters with empty URLs so our avatar, if user, will have the default models
@@ -471,14 +494,7 @@ void Agent::executeScript() {
DependencyManager::set(_entityViewer.getTree());
- // 100Hz timer for audio
- AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer();
- audioTimerWorker->moveToThread(&_avatarAudioTimerThread);
- connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio);
- connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start);
- connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop);
- connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater);
- _avatarAudioTimerThread.start();
+ QMetaObject::invokeMethod(&_avatarAudioTimer, "start");
// Agents should run at 45hz
static const int AVATAR_DATA_HZ = 45;
@@ -557,7 +573,7 @@ void Agent::setIsAvatar(bool isAvatar) {
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets
// tell the avatarAudioTimer to start ticking
- emit startAvatarAudioTimer();
+ QMetaObject::invokeMethod(&_avatarAudioTimer, "start");
}
@@ -586,7 +602,7 @@ void Agent::setIsAvatar(bool isAvatar) {
nodeList->sendPacket(std::move(packet), *node);
});
}
- emit stopAvatarAudioTimer();
+ QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
}
}
@@ -604,6 +620,24 @@ void Agent::processAgentAvatar() {
AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData;
QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail);
+
+ int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber);
+
+ if (avatarByteArray.size() > maximumByteArraySize) {
+ qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data";
+ avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true);
+
+ if (avatarByteArray.size() > maximumByteArraySize) {
+ qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData";
+ avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true);
+
+ if (avatarByteArray.size() > maximumByteArraySize) {
+ qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!";
+ return;
+ }
+ }
+ }
+
scriptedAvatar->doneEncoding(true);
static AvatarDataSequenceNumber sequenceNumber = 0;
@@ -796,8 +830,7 @@ void Agent::aboutToFinish() {
DependencyManager::destroy();
DependencyManager::destroy();
- emit stopAvatarAudioTimer();
- _avatarAudioTimerThread.quit();
+ QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
// cleanup codec & encoder
if (_codec && _encoder) {
diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h
index 549a0858b7..168da185b6 100644
--- a/assignment-client/src/Agent.h
+++ b/assignment-client/src/Agent.h
@@ -77,20 +77,18 @@ private slots:
void handleSelectedAudioFormat(QSharedPointer message);
void nodeActivated(SharedNodePointer activatedNode);
+ void nodeKilled(SharedNodePointer killedNode);
void processAgentAvatar();
void processAgentAvatarAudio();
-signals:
- void startAvatarAudioTimer();
- void stopAvatarAudioTimer();
private:
void negotiateAudioFormat();
void selectAudioFormat(const QString& selectedCodecName);
void encodeFrameOfZeros(QByteArray& encodedZeros);
void computeLoudness(const QByteArray* decodedBuffer, QSharedPointer);
- std::unique_ptr _scriptEngine;
+ ScriptEnginePointer _scriptEngine;
EntityEditPacketSender _entityEditSender;
EntityTreeHeadlessViewer _entityViewer;
@@ -112,13 +110,13 @@ private:
QHash _outgoingScriptAudioSequenceNumbers;
AudioGate _audioGate;
- bool _audioGateOpen { false };
+ bool _audioGateOpen { true };
bool _isNoiseGateEnabled { false };
CodecPluginPointer _codec;
QString _selectedCodecName;
Encoder* _encoder { nullptr };
- QThread _avatarAudioTimerThread;
+ QTimer _avatarAudioTimer;
bool _flushEncoder { false };
};
diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp
index abfc66ac55..efced972a0 100644
--- a/assignment-client/src/AssignmentClient.cpp
+++ b/assignment-client/src/AssignmentClient.cpp
@@ -16,6 +16,7 @@
#include
#include
+#include
#include
#include
#include
@@ -141,7 +142,7 @@ void AssignmentClient::stopAssignmentClient() {
QThread* currentAssignmentThread = _currentAssignment->thread();
// ask the current assignment to stop
- QMetaObject::invokeMethod(_currentAssignment, "stop", Qt::BlockingQueuedConnection);
+ BLOCKING_INVOKE_METHOD(_currentAssignment, "stop");
// ask the current assignment to delete itself on its thread
_currentAssignment->deleteLater();
diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp
index 7e9042e609..dd3050ba4e 100644
--- a/assignment-client/src/AssignmentClientApp.cpp
+++ b/assignment-client/src/AssignmentClientApp.cpp
@@ -9,21 +9,21 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
-#include
-#include
+#include "AssignmentClientApp.h"
+
+#include
+#include
+#include
+#include
#include
-#include
#include
+#include
#include
#include "Assignment.h"
#include "AssignmentClient.h"
#include "AssignmentClientMonitor.h"
-#include "AssignmentClientApp.h"
-#include
-#include
-
AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
QCoreApplication(argc, argv)
@@ -87,6 +87,9 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
const QCommandLineOption logDirectoryOption(ASSIGNMENT_LOG_DIRECTORY, "directory to store logs", "log-directory");
parser.addOption(logDirectoryOption);
+ const QCommandLineOption parentPIDOption(PARENT_PID_OPTION, "PID of the parent process", "parent-pid");
+ parser.addOption(parentPIDOption);
+
if (!parser.parse(QCoreApplication::arguments())) {
qCritical() << parser.errorText() << endl;
parser.showHelp();
@@ -203,6 +206,16 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
}
}
+ if (parser.isSet(parentPIDOption)) {
+ bool ok = false;
+ int parentPID = parser.value(parentPIDOption).toInt(&ok);
+
+ if (ok) {
+ qDebug() << "Parent process PID is" << parentPID;
+ watchParentProcess(parentPID);
+ }
+ }
+
QThread::currentThread()->setObjectName("main thread");
DependencyManager::registerInheritance();
diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp
index 1ee876ceea..1868ccfafe 100644
--- a/assignment-client/src/AssignmentClientMonitor.cpp
+++ b/assignment-client/src/AssignmentClientMonitor.cpp
@@ -28,6 +28,10 @@
const QString ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME = "assignment-client-monitor";
const int WAIT_FOR_CHILD_MSECS = 1000;
+#ifdef Q_OS_WIN
+HANDLE PROCESS_GROUP = createProcessGroup();
+#endif
+
AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmentClientForks,
const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks,
@@ -91,9 +95,22 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) {
}
}
-void AssignmentClientMonitor::childProcessFinished(qint64 pid) {
+void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) {
+ auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + ".";
+
if (_childProcesses.remove(pid)) {
- qDebug() << "Child process" << pid << "has finished. Removed from internal map.";
+ message.append(" Removed from internal map.");
+ } else {
+ message.append(" Could not find process in internal map.");
+ }
+
+ switch (exitStatus) {
+ case QProcess::NormalExit:
+ qDebug() << qPrintable(message.arg("returned"));
+ break;
+ case QProcess::CrashExit:
+ qCritical() << qPrintable(message.arg("crashed"));
+ break;
}
}
@@ -131,7 +148,6 @@ void AssignmentClientMonitor::aboutToQuit() {
void AssignmentClientMonitor::spawnChildClient() {
QProcess* assignmentClient = new QProcess(this);
-
// unparse the parts of the command-line that the child cares about
QStringList _childArguments;
if (_assignmentPool != "") {
@@ -160,6 +176,9 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
_childArguments.append(QString::number(DependencyManager::get()->getLocalSockAddr().getPort()));
+ _childArguments.append("--" + PARENT_PID_OPTION);
+ _childArguments.append(QString::number(QCoreApplication::applicationPid()));
+
QString nowString, stdoutFilenameTemp, stderrFilenameTemp, stdoutPathTemp, stderrPathTemp;
@@ -187,6 +206,10 @@ void AssignmentClientMonitor::spawnChildClient() {
assignmentClient->setProcessChannelMode(QProcess::ForwardedChannels);
assignmentClient->start(QCoreApplication::applicationFilePath(), _childArguments);
+#ifdef Q_OS_WIN
+ addProcessToGroup(PROCESS_GROUP, assignmentClient->processId());
+#endif
+
QString stdoutPath, stderrPath;
if (_wantsChildFileLogging) {
@@ -219,7 +242,9 @@ void AssignmentClientMonitor::spawnChildClient() {
auto pid = assignmentClient->processId();
// make sure we hear that this process has finished when it does
connect(assignmentClient, static_cast(&QProcess::finished),
- this, [this, pid]() { childProcessFinished(pid); });
+ this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) {
+ childProcessFinished(pid, exitCode, exitStatus);
+ });
qDebug() << "Spawned a child client with PID" << assignmentClient->processId();
diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h
index a7f69a559b..8848d503ae 100644
--- a/assignment-client/src/AssignmentClientMonitor.h
+++ b/assignment-client/src/AssignmentClientMonitor.h
@@ -44,7 +44,7 @@ public:
void stopChildProcesses();
private slots:
void checkSpares();
- void childProcessFinished(qint64 pid);
+ void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus);
void handleChildStatusPacket(QSharedPointer message);
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override;
diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp
deleted file mode 100644
index d031b9d9f6..0000000000
--- a/assignment-client/src/AvatarAudioTimer.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-//
-// AvatarAudioTimer.cpp
-// assignment-client/src
-//
-// Created by David Kelly on 10/12/13.
-// Copyright 2016 High Fidelity, Inc.
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-#include
-#include
-#include "AvatarAudioTimer.h"
-
-// this should send a signal every 10ms, with pretty good precision. Hardcoding
-// to 10ms since that's what you'd want for audio.
-void AvatarAudioTimer::start() {
- auto startTime = usecTimestampNow();
- quint64 frameCounter = 0;
- const int TARGET_INTERVAL_USEC = 10000; // 10ms
- while (!_quit) {
- ++frameCounter;
-
- // tick every 10ms from startTime
- quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC;
- quint64 now = usecTimestampNow();
-
- // avoid quint64 underflow
- if (now < targetTime) {
- usleep(targetTime - now);
- }
-
- emit avatarTick();
- }
- qDebug() << "AvatarAudioTimer is finished";
-}
diff --git a/assignment-client/src/AvatarAudioTimer.h b/assignment-client/src/AvatarAudioTimer.h
deleted file mode 100644
index 1f6381b030..0000000000
--- a/assignment-client/src/AvatarAudioTimer.h
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// AvatarAudioTimer.h
-// assignment-client/src
-//
-// Created by David Kelly on 10/12/13.
-// Copyright 2016 High Fidelity, Inc.
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-#ifndef hifi_AvatarAudioTimer_h
-#define hifi_AvatarAudioTimer_h
-
-#include
-
-class AvatarAudioTimer : public QObject {
- Q_OBJECT
-
-signals:
- void avatarTick();
-
-public slots:
- void start();
- void stop() { _quit = true; }
-
-private:
- bool _quit { false };
-};
-
-#endif //hifi_AvatarAudioTimer_h
diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp
index 3886ff8d92..ca0f222e0c 100644
--- a/assignment-client/src/assets/AssetServer.cpp
+++ b/assignment-client/src/assets/AssetServer.cpp
@@ -13,24 +13,33 @@
#include "AssetServer.h"
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#include
#include
-#include "NetworkLogging.h"
-#include "NodeType.h"
+#include "AssetServerLogging.h"
+#include "BakeAssetTask.h"
#include "SendAssetTask.h"
#include "UploadAssetTask.h"
-#include
+
static const uint8_t MIN_CORES_FOR_MULTICORE = 4;
static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2;
@@ -41,6 +50,157 @@ static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000;
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
+static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" };
+static QStringList BAKEABLE_TEXTURE_EXTENSIONS;
+static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = {};
+static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx";
+static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx";
+static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js";
+
+void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) {
+ qDebug() << "Starting bake for: " << assetPath << assetHash;
+ auto it = _pendingBakes.find(assetHash);
+ if (it == _pendingBakes.end()) {
+ auto task = std::make_shared(assetHash, assetPath, filePath);
+ task->setAutoDelete(false);
+ _pendingBakes[assetHash] = task;
+
+ connect(task.get(), &BakeAssetTask::bakeComplete, this, &AssetServer::handleCompletedBake);
+ connect(task.get(), &BakeAssetTask::bakeFailed, this, &AssetServer::handleFailedBake);
+ connect(task.get(), &BakeAssetTask::bakeAborted, this, &AssetServer::handleAbortedBake);
+
+ _bakingTaskPool.start(task.get());
+ } else {
+ qDebug() << "Already in queue";
+ }
+}
+
+QString AssetServer::getPathToAssetHash(const AssetHash& assetHash) {
+ return _filesDirectory.absoluteFilePath(assetHash);
+}
+
+std::pair AssetServer::getAssetStatus(const AssetPath& path, const AssetHash& hash) {
+ auto it = _pendingBakes.find(hash);
+ if (it != _pendingBakes.end()) {
+ return { (*it)->isBaking() ? Baking : Pending, "" };
+ }
+
+ if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
+ return { Baked, "" };
+ }
+
+ auto dotIndex = path.lastIndexOf(".");
+ if (dotIndex == -1) {
+ return { Irrelevant, "" };
+ }
+
+ auto extension = path.mid(dotIndex + 1);
+
+ QString bakedFilename;
+
+ if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_MODEL_SIMPLE_NAME;
+ } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) {
+ bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
+ } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
+ } else {
+ return { Irrelevant, "" };
+ }
+
+ auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename;
+ auto jt = _fileMappings.find(bakedPath);
+ if (jt != _fileMappings.end()) {
+ if (jt->second == hash) {
+ return { NotBaked, "" };
+ } else {
+ return { Baked, "" };
+ }
+ } else {
+ bool loaded;
+ AssetMeta meta;
+
+ std::tie(loaded, meta) = readMetaFile(hash);
+ if (loaded && meta.failedLastBake) {
+ return { Error, meta.lastBakeErrors };
+ }
+ }
+
+ return { Pending, "" };
+}
+
+void AssetServer::bakeAssets() {
+ auto it = _fileMappings.cbegin();
+ for (; it != _fileMappings.cend(); ++it) {
+ auto path = it->first;
+ auto hash = it->second;
+ maybeBake(path, hash);
+ }
+}
+
+void AssetServer::maybeBake(const AssetPath& path, const AssetHash& hash) {
+ if (needsToBeBaked(path, hash)) {
+ qDebug() << "Queuing bake of: " << path;
+ bakeAsset(hash, path, getPathToAssetHash(hash));
+ }
+}
+
+void AssetServer::createEmptyMetaFile(const AssetHash& hash) {
+ QString metaFilePath = "atp:/" + hash + "/meta.json";
+ QFile metaFile { metaFilePath };
+
+ if (!metaFile.exists()) {
+ qDebug() << "Creating metafile for " << hash;
+ if (metaFile.open(QFile::WriteOnly)) {
+ qDebug() << "Created metafile for " << hash;
+ metaFile.write("{}");
+ }
+ }
+}
+
+bool AssetServer::hasMetaFile(const AssetHash& hash) {
+ QString metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json";
+
+ return _fileMappings.find(metaFilePath) != _fileMappings.end();
+}
+
+bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHash) {
+ if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
+ return false;
+ }
+
+ auto dotIndex = path.lastIndexOf(".");
+ if (dotIndex == -1) {
+ return false;
+ }
+
+ auto extension = path.mid(dotIndex + 1);
+
+ QString bakedFilename;
+
+ bool loaded;
+ AssetMeta meta;
+ std::tie(loaded, meta) = readMetaFile(assetHash);
+
+ // TODO: Allow failed bakes that happened on old versions to be re-baked
+ if (loaded && meta.failedLastBake) {
+ return false;
+ }
+
+ if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_MODEL_SIMPLE_NAME;
+ } else if (loaded && BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit())) {
+ bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
+ } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
+ } else {
+ return false;
+ }
+
+ auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
+ return _fileMappings.find(bakedPath) == _fileMappings.end();
+}
+
bool interfaceRunning() {
bool result = false;
@@ -67,20 +227,37 @@ void updateConsumedCores() {
if (isInterfaceRunning) {
coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW;
}
- qDebug() << "Setting max consumed cores to " << coreCount;
+ qCDebug(asset_server) << "Setting max consumed cores to " << coreCount;
setMaxCores(coreCount);
}
AssetServer::AssetServer(ReceivedMessage& message) :
ThreadedAssignment(message),
- _taskPool(this)
+ _transferTaskPool(this),
+ _bakingTaskPool(this),
+ _filesizeLimit(MAX_UPLOAD_SIZE)
{
+ // store the current state of image compression so we can reset it when this assignment is complete
+ _wasColorTextureCompressionEnabled = image::isColorTexturesCompressionEnabled();
+ _wasGrayscaleTextureCompressionEnabled = image::isGrayscaleTexturesCompressionEnabled();
+ _wasNormalTextureCompressionEnabled = image::isNormalTexturesCompressionEnabled();
+ _wasCubeTextureCompressionEnabled = image::isCubeTexturesCompressionEnabled();
+
+ // enable compression in image library
+ image::setColorTexturesCompressionEnabled(true);
+ image::setGrayscaleTexturesCompressionEnabled(true);
+ image::setNormalTexturesCompressionEnabled(true);
+ image::setCubeTexturesCompressionEnabled(true);
+
+ BAKEABLE_TEXTURE_EXTENSIONS = TextureBaker::getSupportedFormats();
+ qDebug() << "Supported baking texture formats:" << BAKEABLE_MODEL_EXTENSIONS;
// Most of the work will be I/O bound, reading from disk and constructing packet objects,
// so the ideal is greater than the number of cores on the system.
static const int TASK_POOL_THREAD_COUNT = 50;
- _taskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT);
+ _transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT);
+ _bakingTaskPool.setMaxThreadCount(1);
auto& packetReceiver = DependencyManager::get()->getPacketReceiver();
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
@@ -103,9 +280,39 @@ AssetServer::AssetServer(ReceivedMessage& message) :
#endif
}
+void AssetServer::aboutToFinish() {
+
+ // remove pending transfer tasks
+ _transferTaskPool.clear();
+
+ // abort each of our still running bake tasks, remove pending bakes that were never put on the thread pool
+ auto it = _pendingBakes.begin();
+ while (it != _pendingBakes.end()) {
+ auto pendingRunnable = _bakingTaskPool.tryTake(it->get());
+
+ if (pendingRunnable) {
+ it = _pendingBakes.erase(it);
+ } else {
+ it.value()->abort();
+ ++it;
+ }
+ }
+
+ // make sure all bakers are finished or aborted
+ while (_pendingBakes.size() > 0) {
+ QCoreApplication::processEvents();
+ }
+
+ // re-set defaults in image library
+ image::setColorTexturesCompressionEnabled(_wasCubeTextureCompressionEnabled);
+ image::setGrayscaleTexturesCompressionEnabled(_wasGrayscaleTextureCompressionEnabled);
+ image::setNormalTexturesCompressionEnabled(_wasNormalTextureCompressionEnabled);
+ image::setCubeTexturesCompressionEnabled(_wasCubeTextureCompressionEnabled);
+}
+
void AssetServer::run() {
- qDebug() << "Waiting for connection to domain to request settings from domain-server.";
+ qCDebug(asset_server) << "Waiting for connection to domain to request settings from domain-server.";
// wait until we have the domain-server settings, otherwise we bail
DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler();
@@ -126,7 +333,7 @@ void AssetServer::completeSetup() {
static const QString ASSET_SERVER_SETTINGS_KEY = "asset_server";
if (!settingsObject.contains(ASSET_SERVER_SETTINGS_KEY)) {
- qCritical() << "Received settings from the domain-server with no asset-server section. Stopping assignment.";
+ qCCritical(asset_server) << "Received settings from the domain-server with no asset-server section. Stopping assignment.";
setFinished(true);
return;
}
@@ -137,11 +344,11 @@ void AssetServer::completeSetup() {
auto maxBandwidthValue = assetServerObject[MAX_BANDWIDTH_OPTION];
auto maxBandwidthFloat = maxBandwidthValue.toDouble(-1);
+ const int BITS_PER_MEGABITS = 1000 * 1000;
if (maxBandwidthFloat > 0.0) {
- const int BITS_PER_MEGABITS = 1000 * 1000;
int maxBandwidth = maxBandwidthFloat * BITS_PER_MEGABITS;
nodeList->setConnectionMaxBandwidth(maxBandwidth);
- qInfo() << "Set maximum bandwith per connection to" << maxBandwidthFloat << "Mb/s."
+ qCInfo(asset_server) << "Set maximum bandwith per connection to" << maxBandwidthFloat << "Mb/s."
" (" << maxBandwidth << "bits/s)";
}
@@ -150,7 +357,7 @@ void AssetServer::completeSetup() {
auto assetsJSONValue = assetServerObject[ASSETS_PATH_OPTION];
if (!assetsJSONValue.isString()) {
- qCritical() << "Received an assets path from the domain-server that could not be parsed. Stopping assignment.";
+ qCCritical(asset_server) << "Received an assets path from the domain-server that could not be parsed. Stopping assignment.";
setFinished(true);
return;
}
@@ -167,19 +374,19 @@ void AssetServer::completeSetup() {
_resourcesDirectory = QDir(absoluteFilePath);
- qDebug() << "Creating resources directory";
+ qCDebug(asset_server) << "Creating resources directory";
_resourcesDirectory.mkpath(".");
_filesDirectory = _resourcesDirectory;
if (!_resourcesDirectory.mkpath(ASSET_FILES_SUBDIR) || !_filesDirectory.cd(ASSET_FILES_SUBDIR)) {
- qCritical() << "Unable to create file directory for asset-server files. Stopping assignment.";
+ qCCritical(asset_server) << "Unable to create file directory for asset-server files. Stopping assignment.";
setFinished(true);
return;
}
// load whatever mappings we currently have from the local file
if (loadMappingsFromFile()) {
- qInfo() << "Serving files from: " << _filesDirectory.path();
+ qCInfo(asset_server) << "Serving files from: " << _filesDirectory.path();
// Check the asset directory to output some information about what we have
auto files = _filesDirectory.entryList(QDir::Files);
@@ -187,18 +394,28 @@ void AssetServer::completeSetup() {
QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING };
auto hashedFiles = files.filter(hashFileRegex);
- qInfo() << "There are" << hashedFiles.size() << "asset files in the asset directory.";
+ qCInfo(asset_server) << "There are" << hashedFiles.size() << "asset files in the asset directory.";
- if (_fileMappings.count() > 0) {
+ if (_fileMappings.size() > 0) {
cleanupUnmappedFiles();
}
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
+
+ bakeAssets();
} else {
- qCritical() << "Asset Server assignment will not continue because mapping file could not be loaded.";
+ qCCritical(asset_server) << "Asset Server assignment will not continue because mapping file could not be loaded.";
setFinished(true);
}
+ // get file size limit for an asset
+ static const QString ASSETS_FILESIZE_LIMIT_OPTION = "assets_filesize_limit";
+ auto assetsFilesizeLimitJSONValue = assetServerObject[ASSETS_FILESIZE_LIMIT_OPTION];
+ auto assetsFilesizeLimit = (uint64_t)assetsFilesizeLimitJSONValue.toInt(MAX_UPLOAD_SIZE);
+
+ if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < MAX_UPLOAD_SIZE) {
+ _filesizeLimit = assetsFilesizeLimit * BITS_PER_MEGABITS;
+ }
}
void AssetServer::cleanupUnmappedFiles() {
@@ -206,21 +423,28 @@ void AssetServer::cleanupUnmappedFiles() {
auto files = _filesDirectory.entryInfoList(QDir::Files);
- // grab the currently mapped hashes
- auto mappedHashes = _fileMappings.values();
-
- qInfo() << "Performing unmapped asset cleanup.";
+ qCInfo(asset_server) << "Performing unmapped asset cleanup.";
for (const auto& fileInfo : files) {
- if (hashFileRegex.exactMatch(fileInfo.fileName())) {
- if (!mappedHashes.contains(fileInfo.fileName())) {
+ auto filename = fileInfo.fileName();
+ if (hashFileRegex.exactMatch(filename)) {
+ bool matched { false };
+ for (auto& pair : _fileMappings) {
+ if (pair.second == filename) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
// remove the unmapped file
QFile removeableFile { fileInfo.absoluteFilePath() };
if (removeableFile.remove()) {
- qDebug() << "\tDeleted" << fileInfo.fileName() << "from asset files directory since it is unmapped.";
+ qCDebug(asset_server) << "\tDeleted" << filename << "from asset files directory since it is unmapped.";
+
+ removeBakedPathsForDeletedAsset(filename);
} else {
- qDebug() << "\tAttempt to delete unmapped file" << fileInfo.fileName() << "failed";
+ qCDebug(asset_server) << "\tAttempt to delete unmapped file" << filename << "failed";
}
}
}
@@ -238,26 +462,24 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer me
replyPacket->writePrimitive(messageID);
switch (operationType) {
- case AssetMappingOperationType::Get: {
+ case AssetMappingOperationType::Get:
handleGetMappingOperation(*message, senderNode, *replyPacket);
break;
- }
- case AssetMappingOperationType::GetAll: {
+ case AssetMappingOperationType::GetAll:
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
break;
- }
- case AssetMappingOperationType::Set: {
+ case AssetMappingOperationType::Set:
handleSetMappingOperation(*message, senderNode, *replyPacket);
break;
- }
- case AssetMappingOperationType::Delete: {
+ case AssetMappingOperationType::Delete:
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
break;
- }
- case AssetMappingOperationType::Rename: {
+ case AssetMappingOperationType::Rename:
handleRenameMappingOperation(*message, senderNode, *replyPacket);
break;
- }
+ case AssetMappingOperationType::SetBakingEnabled:
+ handleSetBakingEnabledOperation(*message, senderNode, *replyPacket);
+ break;
}
auto nodeList = DependencyManager::get();
@@ -267,11 +489,77 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer me
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
QString assetPath = message.readString();
+ QUrl url { assetPath };
+ assetPath = url.path();
+
auto it = _fileMappings.find(assetPath);
if (it != _fileMappings.end()) {
- auto assetHash = it->toString();
+
+ // check if we should re-direct to a baked asset
+
+ // first, figure out from the mapping extension what type of file this is
+ auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower();
+ QString bakedRootFile;
+
+ if (BAKEABLE_MODEL_EXTENSIONS.contains(assetPathExtension)) {
+ bakedRootFile = BAKED_MODEL_SIMPLE_NAME;
+ } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) {
+ bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME;
+ } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(assetPathExtension)) {
+ bakedRootFile = BAKED_SCRIPT_SIMPLE_NAME;
+ }
+
+ auto originalAssetHash = it->second;
+ QString redirectedAssetHash;
+ QString bakedAssetPath;
+ quint8 wasRedirected = false;
+ bool bakingDisabled = false;
+
+ if (!bakedRootFile.isEmpty()) {
+ // we ran into an asset for which we could have a baked version, let's check if it's ready
+ bakedAssetPath = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile;
+ auto bakedIt = _fileMappings.find(bakedAssetPath);
+
+ if (bakedIt != _fileMappings.end()) {
+ if (bakedIt->second != originalAssetHash) {
+ qDebug() << "Did find baked version for: " << originalAssetHash << assetPath;
+ // we found a baked version of the requested asset to serve, redirect to that
+ redirectedAssetHash = bakedIt->second;
+ wasRedirected = true;
+ } else {
+ qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)";
+ bakingDisabled = true;
+ }
+ } else {
+ qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath;
+ }
+ }
+
replyPacket.writePrimitive(AssetServerError::NoError);
- replyPacket.write(QByteArray::fromHex(assetHash.toUtf8()));
+
+ if (wasRedirected) {
+ qDebug() << "Writing re-directed hash for" << originalAssetHash << "to" << redirectedAssetHash;
+ replyPacket.write(QByteArray::fromHex(redirectedAssetHash.toUtf8()));
+
+ // add a flag saying that this mapping request was redirect
+ replyPacket.writePrimitive(wasRedirected);
+
+ // include the re-directed path in case the caller needs to make relative path requests for the baked asset
+ replyPacket.writeString(bakedAssetPath);
+
+ } else {
+ replyPacket.write(QByteArray::fromHex(originalAssetHash.toUtf8()));
+ replyPacket.writePrimitive(wasRedirected);
+
+ auto query = QUrlQuery(url.query());
+ bool isSkybox = query.hasQueryItem("skybox");
+ if (isSkybox) {
+ writeMetaFile(originalAssetHash);
+ if (!bakingDisabled) {
+ maybeBake(assetPath, originalAssetHash);
+ }
+ }
+ }
} else {
replyPacket.writePrimitive(AssetServerError::AssetNotFound);
}
@@ -280,13 +568,23 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
replyPacket.writePrimitive(AssetServerError::NoError);
- auto count = _fileMappings.size();
+ uint32_t count = (uint32_t)_fileMappings.size();
replyPacket.writePrimitive(count);
for (auto it = _fileMappings.cbegin(); it != _fileMappings.cend(); ++ it) {
- replyPacket.writeString(it.key());
- replyPacket.write(QByteArray::fromHex(it.value().toString().toUtf8()));
+ auto mapping = it->first;
+ auto hash = it->second;
+ replyPacket.writeString(mapping);
+ replyPacket.write(QByteArray::fromHex(hash.toUtf8()));
+
+ BakingStatus status;
+ QString lastBakeErrors;
+ std::tie(status, lastBakeErrors) = getAssetStatus(mapping, hash);
+ replyPacket.writePrimitive(status);
+ if (status == Error) {
+ replyPacket.writeString(lastBakeErrors);
+ }
}
}
@@ -296,11 +594,18 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
- if (setMapping(assetPath, assetHash)) {
- replyPacket.writePrimitive(AssetServerError::NoError);
+ // don't process a set mapping operation that is inside the hidden baked folder
+ if (assetPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
+ qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << HIDDEN_BAKED_CONTENT_FOLDER;
+ replyPacket.writePrimitive(AssetServerError::PermissionDenied);
} else {
- replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
+ if (setMapping(assetPath, assetHash)) {
+ replyPacket.writePrimitive(AssetServerError::NoError);
+ } else {
+ replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
+ }
}
+
} else {
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
}
@@ -314,7 +619,14 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
QStringList mappingsToDelete;
for (int i = 0; i < numberOfDeletedMappings; ++i) {
- mappingsToDelete << message.readString();
+ auto mapping = message.readString();
+
+ if (!mapping.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
+ mappingsToDelete << mapping;
+ } else {
+ qCDebug(asset_server) << "Refusing to delete mapping" << mapping
+ << "that is inside" << HIDDEN_BAKED_CONTENT_FOLDER;
+ }
}
if (deleteMappings(mappingsToDelete)) {
@@ -332,7 +644,38 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN
QString oldPath = message.readString();
QString newPath = message.readString();
- if (renameMapping(oldPath, newPath)) {
+ if (oldPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
+ qCDebug(asset_server) << "Cannot rename" << oldPath << "to" << newPath
+ << "since one of the paths is inside" << HIDDEN_BAKED_CONTENT_FOLDER;
+ replyPacket.writePrimitive(AssetServerError::PermissionDenied);
+ } else {
+ if (renameMapping(oldPath, newPath)) {
+ replyPacket.writePrimitive(AssetServerError::NoError);
+ } else {
+ replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
+ }
+ }
+
+ } else {
+ replyPacket.writePrimitive(AssetServerError::PermissionDenied);
+ }
+}
+
+void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
+ if (senderNode->getCanWriteToAssetServer()) {
+ bool enabled { true };
+ message.readPrimitive(&enabled);
+
+ int numberOfMappings{ 0 };
+ message.readPrimitive(&numberOfMappings);
+
+ QStringList mappings;
+
+ for (int i = 0; i < numberOfMappings; ++i) {
+ mappings << message.readString();
+ }
+
+ if (setBakingEnabled(mappings, enabled)) {
replyPacket.writePrimitive(AssetServerError::NoError);
} else {
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
@@ -347,7 +690,7 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh
MessageID messageID;
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) {
- qDebug() << "ERROR bad file request";
+ qCDebug(asset_server) << "ERROR bad file request";
return;
}
@@ -366,11 +709,11 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh
QFileInfo fileInfo { _filesDirectory.filePath(fileName) };
if (fileInfo.exists() && fileInfo.isReadable()) {
- qDebug() << "Opening file: " << fileInfo.filePath();
+ qCDebug(asset_server) << "Opening file: " << fileInfo.filePath();
replyPacket->writePrimitive(AssetServerError::NoError);
replyPacket->writePrimitive(fileInfo.size());
} else {
- qDebug() << "Asset not found: " << QString(hexHash);
+ qCDebug(asset_server) << "Asset not found: " << QString(hexHash);
replyPacket->writePrimitive(AssetServerError::AssetNotFound);
}
@@ -383,22 +726,22 @@ void AssetServer::handleAssetGet(QSharedPointer message, Shared
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset));
if (message->getSize() < minSize) {
- qDebug() << "ERROR bad file request";
+ qCDebug(asset_server) << "ERROR bad file request";
return;
}
// Queue task
auto task = new SendAssetTask(message, senderNode, _filesDirectory);
- _taskPool.start(task);
+ _transferTaskPool.start(task);
}
void AssetServer::handleAssetUpload(QSharedPointer message, SharedNodePointer senderNode) {
if (senderNode->getCanWriteToAssetServer()) {
- qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
+ qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
- auto task = new UploadAssetTask(message, senderNode, _filesDirectory);
- _taskPool.start(task);
+ auto task = new UploadAssetTask(message, senderNode, _filesDirectory, _filesizeLimit);
+ _transferTaskPool.start(task);
} else {
// this is a node the domain told us is not allowed to rez entities
// for now this also means it isn't allowed to add assets
@@ -502,39 +845,46 @@ bool AssetServer::loadMappingsFromFile() {
auto jsonDocument = QJsonDocument::fromJson(mapFile.readAll(), &error);
if (error.error == QJsonParseError::NoError) {
- _fileMappings = jsonDocument.object().toVariantHash();
-
- // remove any mappings that don't match the expected format
- auto it = _fileMappings.begin();
- while (it != _fileMappings.end()) {
- bool shouldDrop = false;
-
- if (!isValidFilePath(it.key())) {
- qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path.";
- shouldDrop = true;
- }
-
- if (!isValidHash(it.value().toString())) {
- qWarning() << "Will not keep mapping for" << it.key() << "since it does not have a valid hash.";
- shouldDrop = true;
- }
-
- if (shouldDrop) {
- it = _fileMappings.erase(it);
- } else {
- ++it;
- }
+ if (!jsonDocument.isObject()) {
+ qCWarning(asset_server) << "Failed to read mapping file, root value in" << mapFilePath << "is not an object";
+ return false;
}
- qInfo() << "Loaded" << _fileMappings.count() << "mappings from map file at" << mapFilePath;
+ //_fileMappings = jsonDocument.object().toVariantHash();
+ auto root = jsonDocument.object();
+ for (auto it = root.begin(); it != root.end(); ++it) {
+ auto key = it.key();
+ auto value = it.value();
+
+ if (!value.isString()) {
+ qCWarning(asset_server) << "Skipping" << key << ":" << value << "because it is not a string";
+ continue;
+ }
+
+ if (!isValidFilePath(key)) {
+ qCWarning(asset_server) << "Will not keep mapping for" << key << "since it is not a valid path.";
+ continue;
+ }
+
+ if (!isValidHash(value.toString())) {
+ qCWarning(asset_server) << "Will not keep mapping for" << key << "since it does not have a valid hash.";
+ continue;
+ }
+
+
+ qDebug() << "Added " << key << value.toString();
+ _fileMappings[key] = value.toString();
+ }
+
+ qCInfo(asset_server) << "Loaded" << _fileMappings.size() << "mappings from map file at" << mapFilePath;
return true;
}
}
- qCritical() << "Failed to read mapping file at" << mapFilePath;
+ qCCritical(asset_server) << "Failed to read mapping file at" << mapFilePath;
return false;
} else {
- qInfo() << "No existing mappings loaded from file since no file was found at" << mapFilePath;
+ qCInfo(asset_server) << "No existing mappings loaded from file since no file was found at" << mapFilePath;
}
return true;
@@ -545,17 +895,22 @@ bool AssetServer::writeMappingsToFile() {
QFile mapFile { mapFilePath };
if (mapFile.open(QIODevice::WriteOnly)) {
- auto jsonObject = QJsonObject::fromVariantHash(_fileMappings);
- QJsonDocument jsonDocument { jsonObject };
+ QJsonObject root;
+
+ for (auto it : _fileMappings) {
+ root[it.first] = it.second;
+ }
+
+ QJsonDocument jsonDocument { root };
if (mapFile.write(jsonDocument.toJson()) != -1) {
- qDebug() << "Wrote JSON mappings to file at" << mapFilePath;
+ qCDebug(asset_server) << "Wrote JSON mappings to file at" << mapFilePath;
return true;
} else {
- qWarning() << "Failed to write JSON mappings to file at" << mapFilePath;
+ qCWarning(asset_server) << "Failed to write JSON mappings to file at" << mapFilePath;
}
} else {
- qWarning() << "Failed to open map file at" << mapFilePath;
+ qCWarning(asset_server) << "Failed to open map file at" << mapFilePath;
}
return false;
@@ -565,17 +920,18 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
path = path.trimmed();
if (!isValidFilePath(path)) {
- qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
+ qCWarning(asset_server) << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
return false;
}
if (!isValidHash(hash)) {
- qWarning() << "Cannot set a mapping for invalid hash" << path << "=>" << hash;
+ qCWarning(asset_server) << "Cannot set a mapping for invalid hash" << path << "=>" << hash;
return false;
}
// remember what the old mapping was in case persistence fails
- auto oldMapping = _fileMappings.value(path).toString();
+ auto it = _fileMappings.find(path);
+ auto oldMapping = it != _fileMappings.end() ? it->second : "";
// update the in memory QHash
_fileMappings[path] = hash;
@@ -583,17 +939,18 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
// attempt to write to file
if (writeMappingsToFile()) {
// persistence succeeded, we are good to go
- qDebug() << "Set mapping:" << path << "=>" << hash;
+ qCDebug(asset_server) << "Set mapping:" << path << "=>" << hash;
+ maybeBake(path, hash);
return true;
} else {
// failed to persist this mapping to file - put back the old one in our in-memory representation
if (oldMapping.isEmpty()) {
- _fileMappings.remove(path);
+ _fileMappings.erase(_fileMappings.find(path));
} else {
_fileMappings[path] = oldMapping;
}
- qWarning() << "Failed to persist mapping:" << path << "=>" << hash;
+ qCWarning(asset_server) << "Failed to persist mapping:" << path << "=>" << hash;
return false;
}
@@ -603,16 +960,27 @@ bool pathIsFolder(const AssetPath& path) {
return path.endsWith('/');
}
-bool AssetServer::deleteMappings(AssetPathList& paths) {
+void AssetServer::removeBakedPathsForDeletedAsset(AssetHash hash) {
+ // we deleted the file with this hash
+
+ // check if we had baked content for that file that should also now be removed
+ // by calling deleteMappings for the hidden baked content folder for this hash
+ AssetPathList hiddenBakedFolder { HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" };
+
+ qCDebug(asset_server) << "Deleting baked content below" << hiddenBakedFolder << "since" << hash << "was deleted";
+
+ deleteMappings(hiddenBakedFolder);
+}
+
+bool AssetServer::deleteMappings(const AssetPathList& paths) {
// take a copy of the current mappings in case persistence of these deletes fails
auto oldMappings = _fileMappings;
QSet hashesToCheckForDeletion;
// enumerate the paths to delete and remove them all
- for (auto& path : paths) {
-
- path = path.trimmed();
+ for (const auto& rawPath : paths) {
+ auto path = rawPath.trimmed();
// figure out if this path will delete a file or folder
if (pathIsFolder(path)) {
@@ -621,9 +989,9 @@ bool AssetServer::deleteMappings(AssetPathList& paths) {
auto sizeBefore = _fileMappings.size();
while (it != _fileMappings.end()) {
- if (it.key().startsWith(path)) {
+ if (it->first.startsWith(path)) {
// add this hash to the list we need to check for asset removal from the server
- hashesToCheckForDeletion << it.value().toString();
+ hashesToCheckForDeletion << it->second;
it = _fileMappings.erase(it);
} else {
@@ -633,20 +1001,22 @@ bool AssetServer::deleteMappings(AssetPathList& paths) {
auto sizeNow = _fileMappings.size();
if (sizeBefore != sizeNow) {
- qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path;
+ qCDebug(asset_server) << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path;
} else {
- qDebug() << "Did not find any mappings to delete in folder:" << path;
+ qCDebug(asset_server) << "Did not find any mappings to delete in folder:" << path;
}
} else {
- auto oldMapping = _fileMappings.take(path);
- if (!oldMapping.isNull()) {
+ auto it = _fileMappings.find(path);
+ if (it != _fileMappings.end()) {
// add this hash to the list we need to check for asset removal from server
- hashesToCheckForDeletion << oldMapping.toString();
+ hashesToCheckForDeletion << it->second;
- qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString();
+ qCDebug(asset_server) << "Deleted a mapping:" << path << "=>" << it->second;
+
+ _fileMappings.erase(it);
} else {
- qDebug() << "Unable to delete a mapping that was not found:" << path;
+ qCDebug(asset_server) << "Unable to delete a mapping that was not found:" << path;
}
}
}
@@ -655,12 +1025,9 @@ bool AssetServer::deleteMappings(AssetPathList& paths) {
if (writeMappingsToFile()) {
// persistence succeeded we are good to go
- // grab the current mapped hashes
- auto mappedHashes = _fileMappings.values();
-
- // enumerate the mapped hashes and clear the list of hashes to check for anything that's present
- for (auto& hashVariant : mappedHashes) {
- auto it = hashesToCheckForDeletion.find(hashVariant.toString());
+ // TODO iterate through hashesToCheckForDeletion instead
+ for (auto& pair : _fileMappings) {
+ auto it = hashesToCheckForDeletion.find(pair.second);
if (it != hashesToCheckForDeletion.end()) {
hashesToCheckForDeletion.erase(it);
}
@@ -672,15 +1039,17 @@ bool AssetServer::deleteMappings(AssetPathList& paths) {
QFile removeableFile { _filesDirectory.absoluteFilePath(hash) };
if (removeableFile.remove()) {
- qDebug() << "\tDeleted" << hash << "from asset files directory since it is now unmapped.";
+ qCDebug(asset_server) << "\tDeleted" << hash << "from asset files directory since it is now unmapped.";
+
+ removeBakedPathsForDeletedAsset(hash);
} else {
- qDebug() << "\tAttempt to delete unmapped file" << hash << "failed";
+ qCDebug(asset_server) << "\tAttempt to delete unmapped file" << hash << "failed";
}
}
return true;
} else {
- qWarning() << "Failed to persist deleted mappings, rolling back";
+ qCWarning(asset_server) << "Failed to persist deleted mappings, rolling back";
// we didn't delete the previous mapping, put it back in our in-memory representation
_fileMappings = oldMappings;
@@ -694,7 +1063,7 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
newPath = newPath.trimmed();
if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) {
- qWarning() << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:"
+ qCWarning(asset_server) << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:"
<< oldPath << "=>" << newPath;
return false;
@@ -704,7 +1073,7 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
if (pathIsFolder(oldPath)) {
if (!pathIsFolder(newPath)) {
// we were asked to rename a path to a folder to a path that isn't a folder, this is a fail
- qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath;
+ qCWarning(asset_server) << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath;
return false;
}
@@ -716,13 +1085,14 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
auto it = oldMappings.begin();
while (it != oldMappings.end()) {
- if (it.key().startsWith(oldPath)) {
- auto newKey = it.key();
+ auto& oldKey = it->first;
+ if (oldKey.startsWith(oldPath)) {
+ auto newKey = oldKey;
newKey.replace(0, oldPath.size(), newPath);
// remove the old version from the in memory file mappings
- _fileMappings.remove(it.key());
- _fileMappings.insert(newKey, it.value());
+ _fileMappings.erase(_fileMappings.find(oldKey));
+ _fileMappings[newKey] = it->second;
}
++it;
@@ -730,52 +1100,54 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
if (writeMappingsToFile()) {
// persisted the changed mappings, return success
- qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath;
+ qCDebug(asset_server) << "Renamed folder mapping:" << oldPath << "=>" << newPath;
return true;
} else {
// couldn't persist the renamed paths, rollback and return failure
_fileMappings = oldMappings;
- qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath;
+ qCWarning(asset_server) << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath;
return false;
}
} else {
if (pathIsFolder(newPath)) {
// we were asked to rename a path to a file to a path that is a folder, this is a fail
- qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath;
+ qCWarning(asset_server) << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath;
return false;
}
// take the old hash to remove the old mapping
- auto oldSourceMapping = _fileMappings.take(oldPath).toString();
+ auto it = _fileMappings.find(oldPath);
+ auto oldSourceMapping = it->second;
+ _fileMappings.erase(it);
// in case we're overwriting, keep the current destination mapping for potential rollback
- auto oldDestinationMapping = _fileMappings.value(newPath);
+ auto oldDestinationIt = _fileMappings.find(newPath);
if (!oldSourceMapping.isEmpty()) {
_fileMappings[newPath] = oldSourceMapping;
if (writeMappingsToFile()) {
// persisted the renamed mapping, return success
- qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath;
+ qCDebug(asset_server) << "Renamed mapping:" << oldPath << "=>" << newPath;
return true;
} else {
// we couldn't persist the renamed mapping, rollback and return failure
_fileMappings[oldPath] = oldSourceMapping;
- if (!oldDestinationMapping.isNull()) {
+ if (oldDestinationIt != _fileMappings.end()) {
// put back the overwritten mapping for the destination path
- _fileMappings[newPath] = oldDestinationMapping.toString();
+ _fileMappings[newPath] = oldDestinationIt->second;
} else {
// clear the new mapping
- _fileMappings.remove(newPath);
+ _fileMappings.erase(_fileMappings.find(newPath));
}
- qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath;
+ qCDebug(asset_server) << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath;
return false;
}
@@ -785,3 +1157,255 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
}
}
}
+
+static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx";
+static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx";
+static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js";
+
+QString getBakeMapping(const AssetHash& hash, const QString& relativeFilePath) {
+ return HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath;
+}
+
+void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) {
+ qDebug() << "Failed: " << originalAssetHash << assetPath << errors;
+
+ bool loaded;
+ AssetMeta meta;
+
+ std::tie(loaded, meta) = readMetaFile(originalAssetHash);
+
+ meta.failedLastBake = true;
+ meta.lastBakeErrors = errors;
+
+ writeMetaFile(originalAssetHash, meta);
+
+ _pendingBakes.remove(originalAssetHash);
+}
+
+void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath,
+ QString bakedTempOutputDir, QVector bakedFilePaths) {
+ bool errorCompletingBake { false };
+ QString errorReason;
+
+ qDebug() << "Completing bake for " << originalAssetHash;
+
+ for (auto& filePath : bakedFilePaths) {
+ // figure out the hash for the contents of this file
+ QFile file(filePath);
+
+ qDebug() << "File path: " << filePath;
+
+ AssetHash bakedFileHash;
+
+ if (file.open(QIODevice::ReadOnly)) {
+ QCryptographicHash hasher(QCryptographicHash::Sha256);
+
+ if (hasher.addData(&file)) {
+ bakedFileHash = hasher.result().toHex();
+ } else {
+ // stop handling this bake, couldn't hash the contents of the file
+ errorCompletingBake = true;
+ errorReason = "Failed to finalize bake";
+ break;
+ }
+
+ // first check that we don't already have this bake file in our list
+ auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash);
+ if (!QFile::exists(bakeFileDestination)) {
+ // copy each to our files folder (with the hash as their filename)
+ if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) {
+ // stop handling this bake, couldn't copy the bake file into our files directory
+ errorCompletingBake = true;
+ errorReason = "Failed to copy baked assets to asset server";
+ break;
+ }
+ }
+
+ // setup the mapping for this bake file
+ auto relativeFilePath = QUrl(filePath).fileName();
+ qDebug() << "Relative file path is: " << relativeFilePath;
+ if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) {
+ // for an FBX file, we replace the filename with the simple name
+ // (to handle the case where two mapped assets have the same hash but different names)
+ relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME;
+ } else if (relativeFilePath.endsWith(".js", Qt::CaseInsensitive)) {
+ relativeFilePath = BAKED_ASSET_SIMPLE_JS_NAME;
+ } else if (!originalAssetPath.endsWith(".fbx", Qt::CaseInsensitive)) {
+ relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME;
+ }
+
+ QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath);
+
+ // add a mapping (under the hidden baked folder) for this file resulting from the bake
+ if (setMapping(bakeMapping, bakedFileHash)) {
+ qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash;
+ } else {
+ qDebug() << "Failed to set mapping";
+ // stop handling this bake, couldn't add a mapping for this bake file
+ errorCompletingBake = true;
+ errorReason = "Failed to finalize bake";
+ break;
+ }
+ } else {
+ qDebug() << "Failed to open baked file: " << filePath;
+ // stop handling this bake, we couldn't open one of the files for reading
+ errorCompletingBake = true;
+ errorReason = "Failed to finalize bake";
+ break;
+ }
+ }
+
+ for (auto& filePath : bakedFilePaths) {
+ QFile file(filePath);
+ if (!file.remove()) {
+ qWarning() << "Failed to remove temporary file:" << filePath;
+ }
+ }
+ if (!QDir(bakedTempOutputDir).rmdir(".")) {
+ qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir;
+ }
+
+ if (!errorCompletingBake) {
+ // create the meta file to store which version of the baking process we just completed
+ writeMetaFile(originalAssetHash);
+ } else {
+ qWarning() << "Could not complete bake for" << originalAssetHash;
+ AssetMeta meta;
+ meta.failedLastBake = true;
+ meta.lastBakeErrors = errorReason;
+ writeMetaFile(originalAssetHash, meta);
+ }
+
+ _pendingBakes.remove(originalAssetHash);
+}
+
+void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) {
+ // for an aborted bake we don't do anything but remove the BakeAssetTask from our pending bakes
+ _pendingBakes.remove(originalAssetHash);
+}
+
+static const QString BAKE_VERSION_KEY = "bake_version";
+static const QString FAILED_LAST_BAKE_KEY = "failed_last_bake";
+static const QString LAST_BAKE_ERRORS_KEY = "last_bake_errors";
+
+std::pair AssetServer::readMetaFile(AssetHash hash) {
+ auto metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json";
+
+ auto it = _fileMappings.find(metaFilePath);
+ if (it == _fileMappings.end()) {
+ return { false, {} };
+ }
+
+ auto metaFileHash = it->second;
+
+ QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash));
+
+ if (metaFile.open(QIODevice::ReadOnly)) {
+ auto data = metaFile.readAll();
+ metaFile.close();
+
+ QJsonParseError error;
+ auto doc = QJsonDocument::fromJson(data, &error);
+
+ if (error.error == QJsonParseError::NoError && doc.isObject()) {
+ auto root = doc.object();
+
+ auto bakeVersion = root[BAKE_VERSION_KEY].toInt(-1);
+ auto failedLastBake = root[FAILED_LAST_BAKE_KEY];
+ auto lastBakeErrors = root[LAST_BAKE_ERRORS_KEY];
+
+ if (bakeVersion != -1
+ && failedLastBake.isBool()
+ && lastBakeErrors.isString()) {
+
+ AssetMeta meta;
+ meta.bakeVersion = bakeVersion;
+ meta.failedLastBake = failedLastBake.toBool();
+ meta.lastBakeErrors = lastBakeErrors.toString();
+
+ return { true, meta };
+ } else {
+ qCWarning(asset_server) << "Metafile for" << hash << "has either missing or malformed data.";
+ }
+ }
+ }
+
+ return { false, {} };
+}
+
+bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta) {
+ // construct the JSON that will be in the meta file
+ QJsonObject metaFileObject;
+
+ metaFileObject[BAKE_VERSION_KEY] = meta.bakeVersion;
+ metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake;
+ metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors;
+
+ QJsonDocument metaFileDoc;
+ metaFileDoc.setObject(metaFileObject);
+
+ auto metaFileJSON = metaFileDoc.toJson();
+
+ // get a hash for the contents of the meta-file
+ AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex();
+
+ // create the meta file in our files folder, named by the hash of its contents
+ QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash));
+
+ if (metaFile.open(QIODevice::WriteOnly)) {
+ metaFile.write(metaFileJSON);
+ metaFile.close();
+
+ // add a mapping to the meta file so it doesn't get deleted because it is unmapped
+ auto metaFileMapping = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json";
+
+ return setMapping(metaFileMapping, metaFileHash);
+ } else {
+ return false;
+ }
+}
+
+bool AssetServer::setBakingEnabled(const AssetPathList& paths, bool enabled) {
+ for (const auto& path : paths) {
+ auto it = _fileMappings.find(path);
+ if (it != _fileMappings.end()) {
+ auto hash = it->second;
+
+ auto dotIndex = path.lastIndexOf(".");
+ if (dotIndex == -1) {
+ continue;
+ }
+
+ auto extension = path.mid(dotIndex + 1);
+
+ QString bakedFilename;
+
+ if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_MODEL_SIMPLE_NAME;
+ } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) {
+ bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
+ } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
+ bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
+ } else {
+ continue;
+ }
+
+ auto bakedMapping = getBakeMapping(hash, bakedFilename);
+
+ auto it = _fileMappings.find(bakedMapping);
+ bool currentlyDisabled = (it != _fileMappings.end() && it->second == hash);
+
+ if (enabled && currentlyDisabled) {
+ QStringList bakedMappings{ bakedMapping };
+ deleteMappings(bakedMappings);
+ maybeBake(path, hash);
+ qDebug() << "Enabled baking for" << path;
+ } else if (!enabled && !currentlyDisabled) {
+ removeBakedPathsForDeletedAsset(hash);
+ setMapping(bakedMapping, hash);
+ qDebug() << "Disabled baking for" << path;
+ }
+ }
+ }
+ return true;
+}
diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h
index 132fb51433..e6393e6a98 100644
--- a/assignment-client/src/assets/AssetServer.h
+++ b/assignment-client/src/assets/AssetServer.h
@@ -14,17 +14,39 @@
#include
#include
+#include
#include
#include "AssetUtils.h"
#include "ReceivedMessage.h"
+
+namespace std {
+ template <>
+ struct hash {
+ size_t operator()(const QString& v) const { return qHash(v); }
+ };
+}
+
+struct AssetMeta {
+ AssetMeta() {
+ }
+
+ int bakeVersion { 0 };
+ bool failedLastBake { false };
+ QString lastBakeErrors;
+};
+
+class BakeAssetTask;
+
class AssetServer : public ThreadedAssignment {
Q_OBJECT
public:
AssetServer(ReceivedMessage& message);
+ void aboutToFinish() override;
+
public slots:
void run() override;
@@ -39,13 +61,14 @@ private slots:
void sendStatsPacket() override;
private:
- using Mappings = QVariantHash;
+ using Mappings = std::unordered_map;
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
+ void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
// Mapping file operations must be called from main assignment thread only
bool loadMappingsFromFile();
@@ -55,19 +78,57 @@ private:
bool setMapping(AssetPath path, AssetHash hash);
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
- bool deleteMappings(AssetPathList& paths);
+ bool deleteMappings(const AssetPathList& paths);
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
bool renameMapping(AssetPath oldPath, AssetPath newPath);
- // deletes any unmapped files from the local asset directory
+ bool setBakingEnabled(const AssetPathList& paths, bool enabled);
+
+ /// Delete any unmapped files from the local asset directory
void cleanupUnmappedFiles();
+ QString getPathToAssetHash(const AssetHash& assetHash);
+
+ std::pair getAssetStatus(const AssetPath& path, const AssetHash& hash);
+
+ void bakeAssets();
+ void maybeBake(const AssetPath& path, const AssetHash& hash);
+ void createEmptyMetaFile(const AssetHash& hash);
+ bool hasMetaFile(const AssetHash& hash);
+ bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash);
+ void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
+
+ /// Move baked content for asset to baked directory and update baked status
+ void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir,
+ QVector bakedFilePaths);
+ void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors);
+ void handleAbortedBake(QString originalAssetHash, QString assetPath);
+
+ /// Create meta file to describe baked content for original asset
+ std::pair readMetaFile(AssetHash hash);
+ bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta());
+
+ /// Remove baked paths when the original asset is deleteds
+ void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash);
+
Mappings _fileMappings;
QDir _resourcesDirectory;
QDir _filesDirectory;
- QThreadPool _taskPool;
+
+ /// Task pool for handling uploads and downloads of assets
+ QThreadPool _transferTaskPool;
+
+ QHash> _pendingBakes;
+ QThreadPool _bakingTaskPool;
+
+ bool _wasColorTextureCompressionEnabled { false };
+ bool _wasGrayscaleTextureCompressionEnabled { false };
+ bool _wasNormalTextureCompressionEnabled { false };
+ bool _wasCubeTextureCompressionEnabled { false };
+
+ uint64_t _filesizeLimit;
};
#endif
diff --git a/assignment-client/src/assets/AssetServerLogging.cpp b/assignment-client/src/assets/AssetServerLogging.cpp
new file mode 100644
index 0000000000..39a02107ea
--- /dev/null
+++ b/assignment-client/src/assets/AssetServerLogging.cpp
@@ -0,0 +1,14 @@
+//
+// AssetServerLogging.cpp
+// assignment-client/src/assets
+//
+// Created by Clement Brisset on 8/9/17.
+// Copyright 2017 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 "AssetServerLogging.h"
+
+Q_LOGGING_CATEGORY(asset_server, "hifi.asset-server")
diff --git a/assignment-client/src/assets/AssetServerLogging.h b/assignment-client/src/assets/AssetServerLogging.h
new file mode 100644
index 0000000000..986e01ecc5
--- /dev/null
+++ b/assignment-client/src/assets/AssetServerLogging.h
@@ -0,0 +1,19 @@
+//
+// AssetServerLogging.h
+// assignment-client/src/assets
+//
+// Created by Clement Brisset on 8/9/17.
+// Copyright 2017 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
+//
+
+#ifndef hifi_AssetServerLogging_h
+#define hifi_AssetServerLogging_h
+
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(asset_server)
+
+#endif // hifi_AssetServerLogging_h
diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp
new file mode 100644
index 0000000000..6c78d2baf3
--- /dev/null
+++ b/assignment-client/src/assets/BakeAssetTask.cpp
@@ -0,0 +1,107 @@
+//
+// BakeAssetTask.cpp
+// assignment-client/src/assets
+//
+// Created by Stephen Birarda on 9/18/17.
+// Copyright 2017 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 "BakeAssetTask.h"
+
+#include
+
+#include
+#include
+#include
+
+BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) :
+ _assetHash(assetHash),
+ _assetPath(assetPath),
+ _filePath(filePath)
+{
+
+}
+
+void cleanupTempFiles(QString tempOutputDir, std::vector files) {
+ for (const auto& filename : files) {
+ QFile f { filename };
+ if (!f.remove()) {
+ qDebug() << "Failed to remove:" << filename;
+ }
+ }
+ if (!tempOutputDir.isEmpty()) {
+ QDir dir { tempOutputDir };
+ if (!dir.rmdir(".")) {
+ qDebug() << "Failed to remove temporary directory:" << tempOutputDir;
+ }
+ }
+};
+
+void BakeAssetTask::run() {
+ _isBaking.store(true);
+
+ qRegisterMetaType >("QVector");
+ TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); };
+
+ QString tempOutputDir;
+
+ if (_assetPath.endsWith(".fbx")) {
+ tempOutputDir = PathUtils::generateTemporaryDir();
+ _baker = std::unique_ptr {
+ new FBXBaker(QUrl("file:///" + _filePath), fn, tempOutputDir)
+ };
+ } else if (_assetPath.endsWith(".js", Qt::CaseInsensitive)) {
+ _baker = std::unique_ptr{
+ new JSBaker(QUrl("file:///" + _filePath), PathUtils::generateTemporaryDir())
+ };
+ } else {
+ tempOutputDir = PathUtils::generateTemporaryDir();
+ _baker = std::unique_ptr {
+ new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE,
+ tempOutputDir)
+ };
+ }
+
+ QEventLoop loop;
+ connect(_baker.get(), &Baker::finished, &loop, &QEventLoop::quit);
+ connect(_baker.get(), &Baker::aborted, &loop, &QEventLoop::quit);
+ QMetaObject::invokeMethod(_baker.get(), "bake", Qt::QueuedConnection);
+ loop.exec();
+
+ if (_baker->wasAborted()) {
+ qDebug() << "Aborted baking: " << _assetHash << _assetPath;
+
+ _wasAborted.store(true);
+
+ cleanupTempFiles(tempOutputDir, _baker->getOutputFiles());
+
+ emit bakeAborted(_assetHash, _assetPath);
+ } else if (_baker->hasErrors()) {
+ qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors();
+
+ auto errors = _baker->getErrors().join('\n'); // Join error list into a single string for convenience
+
+ _didFinish.store(true);
+
+ cleanupTempFiles(tempOutputDir, _baker->getOutputFiles());
+
+ emit bakeFailed(_assetHash, _assetPath, errors);
+ } else {
+ auto vectorOutputFiles = QVector::fromStdVector(_baker->getOutputFiles());
+
+ qDebug() << "Finished baking: " << _assetHash << _assetPath << vectorOutputFiles;
+
+ _didFinish.store(true);
+
+ emit bakeComplete(_assetHash, _assetPath, tempOutputDir, vectorOutputFiles);
+ }
+}
+
+void BakeAssetTask::abort() {
+ if (_baker) {
+ _baker->abort();
+ }
+}
diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h
new file mode 100644
index 0000000000..90458ac223
--- /dev/null
+++ b/assignment-client/src/assets/BakeAssetTask.h
@@ -0,0 +1,52 @@
+//
+// BakeAssetTask.h
+// assignment-client/src/assets
+//
+// Created by Stephen Birarda on 9/18/17.
+// Copyright 2017 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
+//
+
+#ifndef hifi_BakeAssetTask_h
+#define hifi_BakeAssetTask_h
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+class BakeAssetTask : public QObject, public QRunnable {
+ Q_OBJECT
+public:
+ BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
+
+ bool isBaking() { return _isBaking.load(); }
+
+ void run() override;
+
+ void abort();
+ bool wasAborted() const { return _wasAborted.load(); }
+ bool didFinish() const { return _didFinish.load(); }
+
+signals:
+ void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles);
+ void bakeFailed(QString assetHash, QString assetPath, QString errors);
+ void bakeAborted(QString assetHash, QString assetPath);
+
+private:
+ std::atomic _isBaking { false };
+ AssetHash _assetHash;
+ AssetPath _assetPath;
+ QString _filePath;
+ std::unique_ptr _baker;
+ std::atomic _wasAborted { false };
+ std::atomic _didFinish { false };
+};
+
+#endif // hifi_BakeAssetTask_h
diff --git a/assignment-client/src/assets/UploadAssetTask.cpp b/assignment-client/src/assets/UploadAssetTask.cpp
index 7e8e94c34d..5e6d59d032 100644
--- a/assignment-client/src/assets/UploadAssetTask.cpp
+++ b/assignment-client/src/assets/UploadAssetTask.cpp
@@ -22,10 +22,11 @@
UploadAssetTask::UploadAssetTask(QSharedPointer receivedMessage, SharedNodePointer senderNode,
- const QDir& resourcesDir) :
+ const QDir& resourcesDir, uint64_t filesizeLimit) :
_receivedMessage(receivedMessage),
_senderNode(senderNode),
- _resourcesDir(resourcesDir)
+ _resourcesDir(resourcesDir),
+ _filesizeLimit(filesizeLimit)
{
}
@@ -48,7 +49,7 @@ void UploadAssetTask::run() {
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply, -1, true);
replyPacket->writePrimitive(messageID);
- if (fileSize > MAX_UPLOAD_SIZE) {
+ if (fileSize > _filesizeLimit) {
replyPacket->writePrimitive(AssetServerError::AssetTooLarge);
} else {
QByteArray fileData = buffer.read(fileSize);
diff --git a/assignment-client/src/assets/UploadAssetTask.h b/assignment-client/src/assets/UploadAssetTask.h
index 700eecbf9a..8c9e0d234a 100644
--- a/assignment-client/src/assets/UploadAssetTask.h
+++ b/assignment-client/src/assets/UploadAssetTask.h
@@ -26,7 +26,8 @@ class Node;
class UploadAssetTask : public QRunnable {
public:
- UploadAssetTask(QSharedPointer message, QSharedPointer senderNode, const QDir& resourcesDir);
+ UploadAssetTask(QSharedPointer message, QSharedPointer senderNode,
+ const QDir& resourcesDir, uint64_t filesizeLimit);
void run() override;
@@ -34,6 +35,7 @@ private:
QSharedPointer _receivedMessage;
QSharedPointer _senderNode;
QDir _resourcesDir;
+ uint64_t _filesizeLimit;
};
#endif // hifi_UploadAssetTask_h
diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp
index 93b9b10eb7..9ed6c7fdbc 100644
--- a/assignment-client/src/audio/AudioMixer.cpp
+++ b/assignment-client/src/audio/AudioMixer.cpp
@@ -127,7 +127,7 @@ void AudioMixer::queueReplicatedAudioPacket(QSharedPointer mess
// construct a "fake" audio received message from the byte array and packet list information
auto audioData = message->getMessage().mid(NUM_BYTES_RFC4122_UUID);
- PacketType rewrittenType = REPLICATED_PACKET_MAPPING.key(message->getType());
+ PacketType rewrittenType = PacketTypeEnum::getReplicatedPacketMapping().key(message->getType());
if (rewrittenType == PacketType::Unknown) {
qDebug() << "Cannot unwrap replicated packet type not present in REPLICATED_PACKET_WRAPPING";
diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp
index 408ddf038c..9bba9c7f30 100644
--- a/assignment-client/src/audio/AudioMixerClientData.cpp
+++ b/assignment-client/src/audio/AudioMixerClientData.cpp
@@ -125,11 +125,11 @@ void AudioMixerClientData::optionallyReplicatePacket(ReceivedMessage& message, c
// now make sure it's a packet type that we want to replicate
// first check if it is an original type that we should replicate
- PacketType mirroredType = REPLICATED_PACKET_MAPPING.value(message.getType());
+ PacketType mirroredType = PacketTypeEnum::getReplicatedPacketMapping().value(message.getType());
if (mirroredType == PacketType::Unknown) {
// if it wasn't check if it is a replicated type that we should re-replicate
- if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
+ if (PacketTypeEnum::getReplicatedPacketMapping().key(message.getType()) != PacketType::Unknown) {
mirroredType = message.getType();
} else {
qDebug() << "Packet passed to optionallyReplicatePacket was not a replicatable type - returning";
diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp
index ed63bbc298..a131e266d2 100644
--- a/assignment-client/src/audio/AudioMixerSlave.cpp
+++ b/assignment-client/src/audio/AudioMixerSlave.cpp
@@ -558,7 +558,7 @@ float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const Positio
// produce an oriented angle about the y-axis
glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2));
- float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
+ float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
return (direction.x < 0.0f) ? -angle : angle;
} else {
diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp
index 643361ac5d..e28c96e259 100644
--- a/assignment-client/src/audio/AudioMixerSlavePool.cpp
+++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp
@@ -76,7 +76,7 @@ void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) {
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_function = &AudioMixerSlave::mix;
- _configure = [&](AudioMixerSlave& slave) {
+ _configure = [=](AudioMixerSlave& slave) {
slave.configureMix(_begin, _end, _frame, _throttlingRatio);
};
_frame = frame;
@@ -97,7 +97,11 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
#else
// fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
+#if defined(__clang__) && defined(Q_OS_LINUX)
+ _queue.push(node);
+#else
_queue.emplace(node);
+#endif
});
{
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index f218daed03..c67e998dd4 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -85,7 +85,22 @@ void AvatarMixer::handleReplicatedPacket(QSharedPointer message
auto nodeList = DependencyManager::get();
auto nodeID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID));
- auto replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
+ SharedNodePointer replicatedNode;
+
+ if (message->getType() == PacketType::ReplicatedKillAvatar) {
+ // this is a kill packet, which we should only process if we already have the node in our list
+ // since it of course does not make sense to add a node just to remove it an instant later
+ replicatedNode = nodeList->nodeWithUUID(nodeID);
+
+ if (!replicatedNode) {
+ return;
+ }
+ } else {
+ replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
+ }
+
+ // we better have a node to work with at this point
+ assert(replicatedNode);
if (message->getType() == PacketType::ReplicatedAvatarIdentity) {
handleAvatarIdentityPacket(message, replicatedNode);
@@ -129,10 +144,10 @@ void AvatarMixer::optionallyReplicatePacket(ReceivedMessage& message, const Node
// check if this is a packet type we replicate
// which means it must be a packet type present in REPLICATED_PACKET_MAPPING or must be the
// replicated version of one of those packet types
- PacketType replicatedType = REPLICATED_PACKET_MAPPING.value(message.getType());
+ PacketType replicatedType = PacketTypeEnum::getReplicatedPacketMapping().value(message.getType());
if (replicatedType == PacketType::Unknown) {
- if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
+ if (PacketTypeEnum::getReplicatedPacketMapping().key(message.getType()) != PacketType::Unknown) {
replicatedType = message.getType();
} else {
qDebug() << __FUNCTION__ << "called without replicatable packet type - returning";
@@ -285,6 +300,13 @@ void AvatarMixer::start() {
// is guaranteed to not be accessed by other thread
void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData());
+
+ // there is no need to manage identity data we haven't received yet
+ // so bail early if we've never received an identity packet for this avatar
+ if (!nodeData || !nodeData->getAvatar().hasProcessedFirstIdentity()) {
+ return;
+ }
+
bool sendIdentity = false;
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
AvatarData& avatar = nodeData->getAvatar();
diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp
index 4d80bc7d17..a4bf8fa253 100644
--- a/assignment-client/src/avatars/AvatarMixerClientData.cpp
+++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp
@@ -108,9 +108,6 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe
void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) {
if (isRadiusIgnoring(other)) {
_radiusIgnoredOthers.erase(other);
- auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID);
- exitingSpaceBubblePacket->write(other.toRfc4122());
- DependencyManager::get()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self);
}
}
diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp
index 4ff447a95a..5d36a6d261 100644
--- a/assignment-client/src/avatars/AvatarMixerSlave.cpp
+++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp
@@ -170,9 +170,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
// Define the minimum bubble size
- static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
+ static const glm::vec3 minBubbleSize = avatar.getSensorToWorldScale() * glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
- glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f;
+ glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f * avatar.getSensorToWorldScale();
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
@@ -209,7 +209,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
return nodeData->getLastBroadcastTime(avatarNode->getUUID());
}, [&](AvatarSharedPointer avatar)->float{
- glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner());
+ glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner() * avatar->getSensorToWorldScale());
return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z));
}, [&](AvatarSharedPointer avatar)->bool {
if (avatar == thisAvatar) {
@@ -244,9 +244,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
-
+ float sensorToWorldScale = avatarNodeData->getAvatarSharedPointer()->getSensorToWorldScale();
// Define the scale of the box for the current other node
- glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
+ glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f * sensorToWorldScale;
// Set up the bounding box for the current other node
AABox otherNodeBox(avatarNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
@@ -320,18 +320,23 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
++numOtherAvatars;
const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData());
+ const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
- if (nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
+ if (otherAvatar->hasProcessedFirstIdentity()
+ && nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
+
+ // remember the last time we sent identity details about this other node to the receiver
+ nodeData->setLastBroadcastTime(otherNode->getUUID(), usecTimestampNow());
}
- const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition();
+
// determine if avatar is in view, to determine how much data to include...
- glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
+ glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f * otherAvatar->getSensorToWorldScale();
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
@@ -379,11 +384,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
- }
- if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
- qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!";
- includeThisAvatar = false;
+ if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
+ qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!";
+ includeThisAvatar = false;
+ }
}
}
@@ -400,9 +405,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
-
- // remember the last time we sent details about this other node to the receiver
- nodeData->setLastBroadcastTime(otherNode->getUUID(), start);
}
}
diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
index cb5ae7735a..25b88686b7 100644
--- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
+++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
@@ -69,7 +69,7 @@ static AvatarMixerSlave slave;
void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) {
_function = &AvatarMixerSlave::processIncomingPackets;
- _configure = [&](AvatarMixerSlave& slave) {
+ _configure = [=](AvatarMixerSlave& slave) {
slave.configure(begin, end);
};
run(begin, end);
@@ -79,7 +79,7 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end,
p_high_resolution_clock::time_point lastFrameTimestamp,
float maxKbpsPerNode, float throttlingRatio) {
_function = &AvatarMixerSlave::broadcastAvatarData;
- _configure = [&](AvatarMixerSlave& slave) {
+ _configure = [=](AvatarMixerSlave& slave) {
slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio);
};
run(begin, end);
@@ -97,7 +97,11 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) {
#else
// fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
+#if defined(__clang__) && defined(Q_OS_LINUX)
+ _queue.push(node);
+#else
_queue.emplace(node);
+#endif
});
{
diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp
index 57456b00c3..5060891284 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.cpp
+++ b/assignment-client/src/avatars/ScriptableAvatar.cpp
@@ -13,12 +13,13 @@
#include
#include
+#include
#include
#include
#include "ScriptableAvatar.h"
-QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) {
+QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
_globalPosition = getPosition();
return AvatarData::toByteArrayStateful(dataDetail);
}
@@ -34,7 +35,7 @@ void ScriptableAvatar::startAnimation(const QString& url, float fps, float prior
return;
}
_animation = DependencyManager::get()->getAnimation(url);
- _animationDetails = AnimationDetails("", QUrl(url), fps, 0, loop, hold, false, firstFrame, lastFrame, true, firstFrame);
+ _animationDetails = AnimationDetails("", QUrl(url), fps, 0, loop, hold, false, firstFrame, lastFrame, true, firstFrame, false);
_maskedJoints = maskedJoints;
}
@@ -49,7 +50,7 @@ void ScriptableAvatar::stopAnimation() {
AnimationDetails ScriptableAvatar::getAnimationDetails() {
if (QThread::currentThread() != thread()) {
AnimationDetails result;
- QMetaObject::invokeMethod(this, "getAnimationDetails", Qt::BlockingQueuedConnection,
+ BLOCKING_INVOKE_METHOD(this, "getAnimationDetails",
Q_RETURN_ARG(AnimationDetails, result));
return result;
}
diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h
index 1028912e55..b1039b5ac0 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.h
+++ b/assignment-client/src/avatars/ScriptableAvatar.h
@@ -28,7 +28,7 @@ public:
Q_INVOKABLE AnimationDetails getAnimationDetails();
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
- virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override;
+ virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override;
private slots:
diff --git a/assignment-client/src/entities/EntityPriorityQueue.cpp b/assignment-client/src/entities/EntityPriorityQueue.cpp
new file mode 100644
index 0000000000..999a05f2e2
--- /dev/null
+++ b/assignment-client/src/entities/EntityPriorityQueue.cpp
@@ -0,0 +1,53 @@
+//
+// EntityPriorityQueue.cpp
+// assignment-client/src/entities
+//
+// Created by Andrew Meadows 2017.08.08
+// Copyright 2017 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 "EntityPriorityQueue.h"
+
+const float PrioritizedEntity::DO_NOT_SEND = -1.0e-6f;
+const float PrioritizedEntity::FORCE_REMOVE = -1.0e-5f;
+const float PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY = 1.0f;
+
+void ConicalView::set(const ViewFrustum& viewFrustum) {
+ // The ConicalView has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part.
+ // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum.
+ _position = viewFrustum.getPosition();
+ _direction = viewFrustum.getDirection();
+
+ // We cache the sin and cos of the half angle of the cone that bounds the frustum.
+ // (the math here is left as an exercise for the reader)
+ float A = viewFrustum.getAspectRatio();
+ float t = tanf(0.5f * viewFrustum.getFieldOfView());
+ _cosAngle = 1.0f / sqrtf(1.0f + (A * A + 1.0f) * (t * t));
+ _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle);
+
+ _radius = viewFrustum.getCenterRadius();
+}
+
+float ConicalView::computePriority(const AACube& cube) const {
+ glm::vec3 p = cube.calcCenter() - _position; // position of bounding sphere in view-frame
+ float d = glm::length(p); // distance to center of bounding sphere
+ float r = 0.5f * cube.getScale(); // radius of bounding sphere
+ if (d < _radius + r) {
+ return r;
+ }
+ // We check the angle between the center of the cube and the _direction of the view.
+ // If it is less than the sum of the half-angle from center of cone to outer edge plus
+ // the half apparent angle of the bounding sphere then it is in view.
+ //
+ // The math here is left as an exercise for the reader with the following hints:
+ // (1) We actually check the dot product of the cube's local position rather than the angle and
+ // (2) we take advantage of this trig identity: cos(A+B) = cos(A)*cos(B) - sin(A)*sin(B)
+ if (glm::dot(p, _direction) > sqrtf(d * d - r * r) * _cosAngle - r * _sinAngle) {
+ const float AVOID_DIVIDE_BY_ZERO = 0.001f;
+ return r / (d + AVOID_DIVIDE_BY_ZERO);
+ }
+ return PrioritizedEntity::DO_NOT_SEND;
+}
diff --git a/assignment-client/src/entities/EntityPriorityQueue.h b/assignment-client/src/entities/EntityPriorityQueue.h
new file mode 100644
index 0000000000..e308d9b549
--- /dev/null
+++ b/assignment-client/src/entities/EntityPriorityQueue.h
@@ -0,0 +1,66 @@
+//
+// EntityPriorityQueue.h
+// assignment-client/src/entities
+//
+// Created by Andrew Meadows 2017.08.08
+// Copyright 2017 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
+//
+
+#ifndef hifi_EntityPriorityQueue_h
+#define hifi_EntityPriorityQueue_h
+
+#include
+
+#include
+#include
+
+const float SQRT_TWO_OVER_TWO = 0.7071067811865f;
+const float DEFAULT_VIEW_RADIUS = 10.0f;
+
+// ConicalView is an approximation of a ViewFrustum for fast calculation of sort priority.
+class ConicalView {
+public:
+ ConicalView() {}
+ ConicalView(const ViewFrustum& viewFrustum) { set(viewFrustum); }
+ void set(const ViewFrustum& viewFrustum);
+ float computePriority(const AACube& cube) const;
+private:
+ glm::vec3 _position { 0.0f, 0.0f, 0.0f };
+ glm::vec3 _direction { 0.0f, 0.0f, 1.0f };
+ float _sinAngle { SQRT_TWO_OVER_TWO };
+ float _cosAngle { SQRT_TWO_OVER_TWO };
+ float _radius { DEFAULT_VIEW_RADIUS };
+};
+
+// PrioritizedEntity is a placeholder in a sorted queue.
+class PrioritizedEntity {
+public:
+ static const float DO_NOT_SEND;
+ static const float FORCE_REMOVE;
+ static const float WHEN_IN_DOUBT_PRIORITY;
+
+ PrioritizedEntity(EntityItemPointer entity, float priority, bool forceRemove = false) : _weakEntity(entity), _rawEntityPointer(entity.get()), _priority(priority), _forceRemove(forceRemove) {}
+ EntityItemPointer getEntity() const { return _weakEntity.lock(); }
+ EntityItem* getRawEntityPointer() const { return _rawEntityPointer; }
+ float getPriority() const { return _priority; }
+ bool shouldForceRemove() const { return _forceRemove; }
+
+ class Compare {
+ public:
+ bool operator() (const PrioritizedEntity& A, const PrioritizedEntity& B) { return A._priority < B._priority; }
+ };
+ friend class Compare;
+
+private:
+ EntityItemWeakPointer _weakEntity;
+ EntityItem* _rawEntityPointer;
+ float _priority;
+ bool _forceRemove;
+};
+
+using EntityPriorityQueue = std::priority_queue< PrioritizedEntity, std::vector, PrioritizedEntity::Compare >;
+
+#endif // hifi_EntityPriorityQueue_h
diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp
index ac686e2e0a..995a5bad27 100644
--- a/assignment-client/src/entities/EntityServer.cpp
+++ b/assignment-client/src/entities/EntityServer.cpp
@@ -16,6 +16,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include "AssignmentParentFinder.h"
#include "EntityNodeData.h"
@@ -29,15 +33,26 @@ const char* LOCAL_MODELS_PERSIST_FILE = "resources/models.svo";
EntityServer::EntityServer(ReceivedMessage& message) :
OctreeServer(message),
- _entitySimulation(NULL)
+ _entitySimulation(NULL),
+ _dynamicDomainVerificationTimer(this)
{
DependencyManager::set();
DependencyManager::set();
DependencyManager::set();
auto& packetReceiver = DependencyManager::get()->getPacketReceiver();
- packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase, PacketType::EntityPhysics },
- this, "handleEntityPacket");
+ packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,
+ PacketType::EntityEdit,
+ PacketType::EntityErase,
+ PacketType::EntityPhysics,
+ PacketType::ChallengeOwnership,
+ PacketType::ChallengeOwnershipRequest,
+ PacketType::ChallengeOwnershipReply },
+ this,
+ "handleEntityPacket");
+
+ connect(&_dynamicDomainVerificationTimer, &QTimer::timeout, this, &EntityServer::startDynamicDomainVerification);
+ _dynamicDomainVerificationTimer.setSingleShot(true);
}
EntityServer::~EntityServer() {
@@ -50,6 +65,12 @@ EntityServer::~EntityServer() {
tree->removeNewlyCreatedHook(this);
}
+void EntityServer::aboutToFinish() {
+ DependencyManager::get()->cleanup();
+
+ OctreeServer::aboutToFinish();
+}
+
void EntityServer::handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode) {
if (_octreeInboundPacketProcessor) {
_octreeInboundPacketProcessor->queueReceivedPacket(message, senderNode);
@@ -87,6 +108,9 @@ void EntityServer::beforeRun() {
connect(_pruneDeletedEntitiesTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedEntities()));
const int PRUNE_DELETED_MODELS_INTERVAL_MSECS = 1 * 1000; // once every second
_pruneDeletedEntitiesTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS);
+
+ DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler();
+ connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &EntityServer::domainSettingsRequestFailed);
}
void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) {
@@ -290,6 +314,18 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME);
}
+ int minTime;
+ if (readOptionInt("dynamicDomainVerificationTimeMin", settingsSectionObject, minTime)) {
+ _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = minTime * 1000;
+ }
+
+ int maxTime;
+ if (readOptionInt("dynamicDomainVerificationTimeMax", settingsSectionObject, maxTime)) {
+ _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = maxTime * 1000;
+ }
+
+ startDynamicDomainVerification();
+
tree->setWantEditLogging(wantEditLogging);
tree->setWantTerseEditLogging(wantTerseEditLogging);
@@ -404,3 +440,79 @@ QString EntityServer::serverSubclassStats() {
return statsString;
}
+
+void EntityServer::domainSettingsRequestFailed() {
+ auto nodeList = DependencyManager::get();
+ qCDebug(entities) << "The EntityServer couldn't get the Domain Settings. Starting dynamic domain verification with default values...";
+
+ _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS;
+ _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS;
+ startDynamicDomainVerification();
+}
+
+void EntityServer::startDynamicDomainVerification() {
+ qCDebug(entities) << "Starting Dynamic Domain Verification...";
+
+ QString thisDomainID = DependencyManager::get()->getDomainId().remove(QRegExp("\\{|\\}"));
+
+ EntityTreePointer tree = std::static_pointer_cast(_tree);
+ QHash localMap(tree->getEntityCertificateIDMap());
+
+ QHashIterator i(localMap);
+ qCDebug(entities) << localMap.size() << "entities in _entityCertificateIDMap";
+ while (i.hasNext()) {
+ i.next();
+
+ EntityItemPointer entity = tree->findEntityByEntityItemID(i.value());
+
+ if (entity) {
+ if (!entity->getProperties().verifyStaticCertificateProperties()) {
+ qCDebug(entities) << "During Dynamic Domain Verification, a certified entity with ID" << i.value() << "failed"
+ << "static certificate verification.";
+ // Delete the entity if it doesn't pass static certificate verification
+ tree->deleteEntity(i.value(), true);
+ } else {
+
+ QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+ QNetworkRequest networkRequest;
+ networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL;
+ requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location");
+ QJsonObject request;
+ request["certificate_id"] = i.key();
+ networkRequest.setUrl(requestURL);
+
+ QNetworkReply* networkReply = NULL;
+ networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
+
+ connect(networkReply, &QNetworkReply::finished, [=]() {
+ QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object();
+ jsonObject = jsonObject["data"].toObject();
+
+ if (networkReply->error() == QNetworkReply::NoError) {
+ if (jsonObject["domain_id"].toString() != thisDomainID) {
+ qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString()
+ << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << i.value();
+ tree->deleteEntity(i.value(), true);
+ } else {
+ qCDebug(entities) << "Entity passed dynamic domain verification:" << i.value();
+ }
+ } else {
+ qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << i.value()
+ << "More info:" << jsonObject;
+ tree->deleteEntity(i.value(), true);
+ }
+
+ networkReply->deleteLater();
+ });
+ }
+ } else {
+ qCWarning(entities) << "During DDV, an entity with ID" << i.value() << "was NOT found in the Entity Tree!";
+ }
+ }
+
+ int nextInterval = qrand() % ((_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS + 1) - _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS) + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS;
+ qCDebug(entities) << "Restarting Dynamic Domain Verification timer for" << nextInterval / 1000 << "seconds";
+ _dynamicDomainVerificationTimer.start(nextInterval);
+}
diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h
index 40676e79bd..05404b28c8 100644
--- a/assignment-client/src/entities/EntityServer.h
+++ b/assignment-client/src/entities/EntityServer.h
@@ -59,6 +59,8 @@ public:
virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override;
virtual void trackViewerGone(const QUuid& sessionID) override;
+ virtual void aboutToFinish() override;
+
public slots:
virtual void nodeAdded(SharedNodePointer node) override;
virtual void nodeKilled(SharedNodePointer node) override;
@@ -71,6 +73,7 @@ protected:
private slots:
void handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode);
+ void domainSettingsRequestFailed();
private:
SimpleEntitySimulationPointer _entitySimulation;
@@ -78,6 +81,13 @@ private:
QReadWriteLock _viewerSendingStatsLock;
QMap> _viewerSendingStats;
+
+ static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m
+ static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h
+ int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m
+ int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h
+ QTimer _dynamicDomainVerificationTimer;
+ void startDynamicDomainVerification();
};
#endif // hifi_EntityServer_h
diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp
index 7febdc67e1..11e4d533fb 100644
--- a/assignment-client/src/entities/EntityTreeSendThread.cpp
+++ b/assignment-client/src/entities/EntityTreeSendThread.cpp
@@ -13,9 +13,18 @@
#include
#include
+#include
#include "EntityServer.h"
+
+EntityTreeSendThread::EntityTreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) :
+ OctreeSendThread(myServer, node)
+{
+ connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::editingEntityPointer, this, &EntityTreeSendThread::editingEntityPointer, Qt::QueuedConnection);
+ connect(std::static_pointer_cast(myServer->getOctree()).get(), &EntityTree::deletingEntityPointer, this, &EntityTreeSendThread::deletingEntityPointer, Qt::QueuedConnection);
+}
+
void EntityTreeSendThread::preDistributionProcessing() {
auto node = _node.toStrongRef();
auto nodeData = static_cast(node->getLinkedData());
@@ -80,6 +89,72 @@ void EntityTreeSendThread::preDistributionProcessing() {
}
}
+void EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData,
+ bool viewFrustumChanged, bool isFullScene) {
+ if (viewFrustumChanged || _traversal.finished()) {
+ ViewFrustum viewFrustum;
+ nodeData->copyCurrentViewFrustum(viewFrustum);
+ EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot());
+ int32_t lodLevelOffset = nodeData->getBoundaryLevelAdjust() + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST);
+ startNewTraversal(viewFrustum, root, lodLevelOffset, nodeData->getUsesFrustum());
+
+ // When the viewFrustum changed the sort order may be incorrect, so we re-sort
+ // and also use the opportunity to cull anything no longer in view
+ if (viewFrustumChanged && !_sendQueue.empty()) {
+ EntityPriorityQueue prevSendQueue;
+ _sendQueue.swap(prevSendQueue);
+ _entitiesInQueue.clear();
+ // Re-add elements from previous traversal if they still need to be sent
+ float lodScaleFactor = _traversal.getCurrentLODScaleFactor();
+ glm::vec3 viewPosition = _traversal.getCurrentView().getPosition();
+ while (!prevSendQueue.empty()) {
+ EntityItemPointer entity = prevSendQueue.top().getEntity();
+ bool forceRemove = prevSendQueue.top().shouldForceRemove();
+ prevSendQueue.pop();
+ if (entity) {
+ if (!forceRemove) {
+ bool success = false;
+ AACube cube = entity->getQueryAACube(success);
+ if (success) {
+ if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) {
+ float priority = _conicalView.computePriority(cube);
+ if (priority != PrioritizedEntity::DO_NOT_SEND) {
+ float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE;
+ float angularDiameter = cube.getScale() / distance;
+ if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) {
+ _sendQueue.push(PrioritizedEntity(entity, priority));
+ _entitiesInQueue.insert(entity.get());
+ }
+ }
+ }
+ } else {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ } else {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::FORCE_REMOVE, true));
+ _entitiesInQueue.insert(entity.get());
+ }
+ }
+ }
+ }
+ }
+
+ if (!_traversal.finished()) {
+ quint64 startTime = usecTimestampNow();
+
+ #ifdef DEBUG
+ const uint64_t TIME_BUDGET = 400; // usec
+ #else
+ const uint64_t TIME_BUDGET = 200; // usec
+ #endif
+ _traversal.traverse(TIME_BUDGET);
+ OctreeServer::trackTreeTraverseTime((float)(usecTimestampNow() - startTime));
+ }
+
+ OctreeSendThread::traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene);
+}
+
bool EntityTreeSendThread::addAncestorsToExtraFlaggedEntities(const QUuid& filteredEntityID,
EntityItem& entityItem, EntityNodeData& nodeData) {
// check if this entity has a parent that is also an entity
@@ -100,7 +175,7 @@ bool EntityTreeSendThread::addAncestorsToExtraFlaggedEntities(const QUuid& filte
return parentWasNew || ancestorsWereNew;
}
- // since we didn't have a parent niether of our parents or ancestors could be new additions
+ // since we didn't have a parent, neither of our parents or ancestors could be new additions
return false;
}
@@ -129,4 +204,297 @@ bool EntityTreeSendThread::addDescendantsToExtraFlaggedEntities(const QUuid& fil
return hasNewChild || hasNewDescendants;
}
+void EntityTreeSendThread::startNewTraversal(const ViewFrustum& view, EntityTreeElementPointer root, int32_t lodLevelOffset,
+ bool usesViewFrustum) {
+ DiffTraversal::Type type = _traversal.prepareNewTraversal(view, root, lodLevelOffset, usesViewFrustum);
+ // there are three types of traversal:
+ //
+ // (1) FirstTime = at login --> find everything in view
+ // (2) Repeat = view hasn't changed --> find what has changed since last complete traversal
+ // (3) Differential = view has changed --> find what has changed or in new view but not old
+ //
+ // The "scanCallback" we provide to the traversal depends on the type:
+ //
+ // The _conicalView is updated here as a cached view approximation used by the lambdas for efficient
+ // computation of entity sorting priorities.
+ //
+ _conicalView.set(_traversal.getCurrentView());
+
+ switch (type) {
+ case DiffTraversal::First:
+ // When we get to a First traversal, clear the _knownState
+ _knownState.clear();
+ if (usesViewFrustum) {
+ float lodScaleFactor = _traversal.getCurrentLODScaleFactor();
+ glm::vec3 viewPosition = _traversal.getCurrentView().getPosition();
+ _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) {
+ next.element->forEachEntity([=](EntityItemPointer entity) {
+ // Bail early if we've already checked this entity this frame
+ if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) {
+ return;
+ }
+ bool success = false;
+ AACube cube = entity->getQueryAACube(success);
+ if (success) {
+ if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) {
+ // Check the size of the entity, it's possible that a "too small to see" entity is included in a
+ // larger octree cell because of its position (for example if it crosses the boundary of a cell it
+ // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen
+ // before we consider including it.
+ float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE;
+ float angularDiameter = cube.getScale() / distance;
+ if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) {
+ float priority = _conicalView.computePriority(cube);
+ _sendQueue.push(PrioritizedEntity(entity, priority));
+ _entitiesInQueue.insert(entity.get());
+ }
+ }
+ } else {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ });
+ });
+ } else {
+ _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) {
+ next.element->forEachEntity([this](EntityItemPointer entity) {
+ // Bail early if we've already checked this entity this frame
+ if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) {
+ return;
+ }
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ });
+ });
+ }
+ break;
+ case DiffTraversal::Repeat:
+ if (usesViewFrustum) {
+ float lodScaleFactor = _traversal.getCurrentLODScaleFactor();
+ glm::vec3 viewPosition = _traversal.getCurrentView().getPosition();
+ _traversal.setScanCallback([=](DiffTraversal::VisibleElement& next) {
+ uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal();
+ if (next.element->getLastChangedContent() > startOfCompletedTraversal) {
+ next.element->forEachEntity([=](EntityItemPointer entity) {
+ // Bail early if we've already checked this entity this frame
+ if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) {
+ return;
+ }
+ auto knownTimestamp = _knownState.find(entity.get());
+ if (knownTimestamp == _knownState.end()) {
+ bool success = false;
+ AACube cube = entity->getQueryAACube(success);
+ if (success) {
+ if (next.intersection == ViewFrustum::INSIDE || _traversal.getCurrentView().cubeIntersectsKeyhole(cube)) {
+ // See the DiffTraversal::First case for an explanation of the "entity is too small" check
+ float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE;
+ float angularDiameter = cube.getScale() / distance;
+ if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) {
+ float priority = _conicalView.computePriority(cube);
+ _sendQueue.push(PrioritizedEntity(entity, priority));
+ _entitiesInQueue.insert(entity.get());
+ }
+ }
+ } else {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ } else if (entity->getLastEdited() > knownTimestamp->second) {
+ // it is known and it changed --> put it on the queue with any priority
+ // TODO: sort these correctly
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ });
+ }
+ });
+ } else {
+ _traversal.setScanCallback([this](DiffTraversal::VisibleElement& next) {
+ uint64_t startOfCompletedTraversal = _traversal.getStartOfCompletedTraversal();
+ if (next.element->getLastChangedContent() > startOfCompletedTraversal) {
+ next.element->forEachEntity([this](EntityItemPointer entity) {
+ // Bail early if we've already checked this entity this frame
+ if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) {
+ return;
+ }
+ auto knownTimestamp = _knownState.find(entity.get());
+ if (knownTimestamp == _knownState.end() || entity->getLastEdited() > knownTimestamp->second) {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ });
+ }
+ });
+ }
+ break;
+ case DiffTraversal::Differential:
+ assert(usesViewFrustum);
+ float lodScaleFactor = _traversal.getCurrentLODScaleFactor();
+ glm::vec3 viewPosition = _traversal.getCurrentView().getPosition();
+ float completedLODScaleFactor = _traversal.getCompletedLODScaleFactor();
+ glm::vec3 completedViewPosition = _traversal.getCompletedView().getPosition();
+ _traversal.setScanCallback([=] (DiffTraversal::VisibleElement& next) {
+ next.element->forEachEntity([=](EntityItemPointer entity) {
+ // Bail early if we've already checked this entity this frame
+ if (_entitiesInQueue.find(entity.get()) != _entitiesInQueue.end()) {
+ return;
+ }
+ auto knownTimestamp = _knownState.find(entity.get());
+ if (knownTimestamp == _knownState.end()) {
+ bool success = false;
+ AACube cube = entity->getQueryAACube(success);
+ if (success) {
+ if (_traversal.getCurrentView().cubeIntersectsKeyhole(cube)) {
+ // See the DiffTraversal::First case for an explanation of the "entity is too small" check
+ float distance = glm::distance(cube.calcCenter(), viewPosition) + MIN_VISIBLE_DISTANCE;
+ float angularDiameter = cube.getScale() / distance;
+ if (angularDiameter > MIN_ENTITY_ANGULAR_DIAMETER * lodScaleFactor) {
+ if (!_traversal.getCompletedView().cubeIntersectsKeyhole(cube)) {
+ float priority = _conicalView.computePriority(cube);
+ _sendQueue.push(PrioritizedEntity(entity, priority));
+ _entitiesInQueue.insert(entity.get());
+ } else {
+ // If this entity was skipped last time because it was too small, we still need to send it
+ distance = glm::distance(cube.calcCenter(), completedViewPosition) + MIN_VISIBLE_DISTANCE;
+ angularDiameter = cube.getScale() / distance;
+ if (angularDiameter <= MIN_ENTITY_ANGULAR_DIAMETER * completedLODScaleFactor) {
+ // this object was skipped in last completed traversal
+ float priority = _conicalView.computePriority(cube);
+ _sendQueue.push(PrioritizedEntity(entity, priority));
+ _entitiesInQueue.insert(entity.get());
+ }
+ }
+ }
+ }
+ } else {
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ } else if (entity->getLastEdited() > knownTimestamp->second) {
+ // it is known and it changed --> put it on the queue with any priority
+ // TODO: sort these correctly
+ _sendQueue.push(PrioritizedEntity(entity, PrioritizedEntity::WHEN_IN_DOUBT_PRIORITY));
+ _entitiesInQueue.insert(entity.get());
+ }
+ });
+ });
+ break;
+ }
+}
+
+bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) {
+ if (_sendQueue.empty()) {
+ OctreeServer::trackEncodeTime(OctreeServer::SKIP_TIME);
+ return false;
+ }
+ quint64 encodeStart = usecTimestampNow();
+ if (!_packetData.hasContent()) {
+ // This is the beginning of a new packet.
+ // We pack minimal data for this to be accepted as an OctreeElement payload for the root element.
+ // The Octree header bytes look like this:
+ //
+ // 0x00 octalcode for root
+ // 0x00 colors (1 bit where recipient should call: child->readElementDataFromBuffer())
+ // 0xXX childrenInTreeMask (when params.includeExistsBits is true: 1 bit where child is existant)
+ // 0x00 childrenInBufferMask (1 bit where recipient should call: child->readElementData() recursively)
+ const uint8_t zeroByte = 0;
+ _packetData.appendValue(zeroByte); // octalcode
+ _packetData.appendValue(zeroByte); // colors
+ if (params.includeExistsBits) {
+ uint8_t childrenExistBits = 0;
+ EntityTreeElementPointer root = std::dynamic_pointer_cast(_myServer->getOctree()->getRoot());
+ for (int32_t i = 0; i < NUMBER_OF_CHILDREN; ++i) {
+ if (root->getChildAtIndex(i)) {
+ childrenExistBits += (1 << i);
+ }
+ }
+ _packetData.appendValue(childrenExistBits); // childrenInTreeMask
+ }
+ _packetData.appendValue(zeroByte); // childrenInBufferMask
+
+ // Pack zero for numEntities.
+ // But before we do: grab current byteOffset so we can come back later
+ // and update this with the real number.
+ _numEntities = 0;
+ _numEntitiesOffset = _packetData.getUncompressedByteOffset();
+ _packetData.appendValue(_numEntities);
+ }
+
+ LevelDetails entitiesLevel = _packetData.startLevel();
+ uint64_t sendTime = usecTimestampNow();
+ auto nodeData = static_cast(params.nodeData);
+ nodeData->stats.encodeStarted();
+ auto entityNode = _node.toStrongRef();
+ auto entityNodeData = static_cast