commit c07be8ad62f20d8f9daf2b53a6f81f1c6fa0311f Author: Snoweuph Date: Mon Nov 13 22:31:36 2023 +0100 Migrate to Gitea diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..296a22a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +.idea/** linguist-vendored +gradle/** linguist-vendored +profiler/** linguist-vendored \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d1d994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +/.idea/workspace.xml +/.idea/usage.statistics.xml +/.idea/uiDesigner.xml +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### OTHER ### +/wiki +.DS_Store +*.bat +*.conf +*.dll +*.dylib +*.jar +*.jpg +*.mhr +*.ogg +*.sh +*.so +*.ttf +*.wav +*.zip +touch.txt diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..659bf43 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..f9163b4 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..370437b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..bcb2b26 --- /dev/null +++ b/Readme.md @@ -0,0 +1,58 @@ +# GameEngine +*This GameEngine is sole written for fun, and provided as is* + +## Tech Stack: +- Java v16 +- OpenGL +- [LWJGL3](https://www.lwjgl.org/) +config: +```kt +import org.gradle.internal.os.OperatingSystem + +val lwjglVersion = "3.3.2" +val jomlVersion = "1.10.5" + +val lwjglNatives = Pair( + System.getProperty("os.name")!!, + System.getProperty("os.arch")!! +).let { (name, arch) -> + when { + arrayOf("Linux", "FreeBSD", "SunOS", "Unit").any { name.startsWith(it) } -> + if (arrayOf("arm", "aarch64").any { arch.startsWith(it) }) + "natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}" + else + "natives-linux" + arrayOf("Windows").any { name.startsWith(it) } -> + if (arch.contains("64")) + "natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}" + else + "natives-windows-x86" + else -> throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually") + } +} + + +repositories { + mavenCentral() +} + +dependencies { + implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) + + implementation("org.lwjgl", "lwjgl") + implementation("org.lwjgl", "lwjgl-assimp") + implementation("org.lwjgl", "lwjgl-glfw") + implementation("org.lwjgl", "lwjgl-openal") + implementation("org.lwjgl", "lwjgl-opengl") + implementation("org.lwjgl", "lwjgl-remotery") + implementation("org.lwjgl", "lwjgl-stb") + runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-remotery", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives) + implementation("org.joml", "joml", jomlVersion) +} +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d3e83b2 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,66 @@ +val lwjglVersion = "3.3.3" +val jomlVersion = "1.10.5" + +group = "dev.euph" +version = "1.0-SNAPSHOT" + +sourceSets { + main { + java.srcDirs("src") + resources.srcDir("res") + } +} + +plugins { + java + application +} + +val lwjglNatives = Pair( + System.getProperty("os.name")!!, + System.getProperty("os.arch")!! +).let { (name, arch) -> + when { + arrayOf("Linux", "FreeBSD", "SunOS", "Unit").any { name.startsWith(it) } -> + if (arrayOf("arm", "aarch64").any { arch.startsWith(it) }) + "natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}" + else if (arch.startsWith("ppc")) + "natives-linux-ppc64le" + else if (arch.startsWith("riscv")) + "natives-linux-riscv64" + else + "natives-linux" + arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } -> + "natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}" + arrayOf("Windows").any { name.startsWith(it) } -> + if (arch.contains("64")) + "natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}" + else + "natives-windows-x86" + else -> throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually") + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) + + implementation("org.lwjgl", "lwjgl") + implementation("org.lwjgl", "lwjgl-assimp") + implementation("org.lwjgl", "lwjgl-glfw") + implementation("org.lwjgl", "lwjgl-openal") + implementation("org.lwjgl", "lwjgl-opengl") + implementation("org.lwjgl", "lwjgl-remotery") + implementation("org.lwjgl", "lwjgl-stb") + runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-remotery", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives) + implementation("org.joml", "joml", jomlVersion) +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..62f495d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/profiler/LICENSE b/profiler/LICENSE new file mode 100644 index 0000000..b0fcd6d --- /dev/null +++ b/profiler/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/profiler/lib/Remotery.c b/profiler/lib/Remotery.c new file mode 100644 index 0000000..ba26535 --- /dev/null +++ b/profiler/lib/Remotery.c @@ -0,0 +1,10142 @@ +// +// Copyright 2014-2022 Celtoys Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* +@Contents: + + @DEPS: External Dependencies + @TIMERS: Platform-specific timers + @TLS: Thread-Local Storage + @ERROR: Error handling + @ATOMIC: Atomic Operations + @RNG: Random Number Generator + @LFSR: Galois Linear-feedback Shift Register + @VMBUFFER: Mirror Buffer using Virtual Memory for auto-wrap + @NEW: New/Delete operators with error values for simplifying object create/destroy + @SAFEC: Safe C Library excerpts + @OSTHREADS: Wrappers around OS-specific thread functions + @THREADS: Cross-platform thread object + @OBJALLOC: Reusable Object Allocator + @DYNBUF: Dynamic Buffer + @HASHTABLE: Integer pair hash map for inserts/finds. No removes for added simplicity. + @STRINGTABLE: Map from string hash to string offset in local buffer + @SOCKETS: Sockets TCP/IP Wrapper + @SHA1: SHA-1 Cryptographic Hash Function + @BASE64: Base-64 encoder + @MURMURHASH: Murmur-Hash 3 + @WEBSOCKETS: WebSockets + @MESSAGEQ: Multiple producer, single consumer message queue + @NETWORK: Network Server + @SAMPLE: Base Sample Description (CPU by default) + @SAMPLETREE: A tree of samples with their allocator + @TPROFILER: Thread Profiler data, storing both sampling and instrumentation results + @TGATHER: Thread Gatherer, periodically polling for newly created threads + @TSAMPLER: Sampling thread contexts + @REMOTERY: Remotery + @CUDA: CUDA event sampling + @D3D11: Direct3D 11 event sampling + @D3D12: Direct3D 12 event sampling + @OPENGL: OpenGL event sampling + @METAL: Metal event sampling + @SAMPLEAPI: Sample API for user callbacks + @PROPERTYAPI: Property API for user callbacks + @PROPERTIES: Property API +*/ + +#define RMT_IMPL +#include "Remotery.h" + +#ifdef RMT_PLATFORM_WINDOWS +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "winmm.lib") +#endif + +#if RMT_ENABLED + +// Global settings +static rmtSettings g_Settings; +static rmtBool g_SettingsInitialized = RMT_FALSE; + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @DEPS: External Dependencies +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// clang-format off + +// +// Required CRT dependencies +// +#if RMT_USE_TINYCRT + + #include + #include + #include + + #define CreateFileMapping CreateFileMappingA + #define RMT_ENABLE_THREAD_SAMPLER + +#else + + #ifdef RMT_PLATFORM_MACOS + #include + #include + #include + #include + #else + #if !defined(__FreeBSD__) && !defined(__OpenBSD__) + #include + #endif + #endif + + #include + #include + #include + #include + #include + #include + #include + + #ifdef RMT_PLATFORM_WINDOWS + #include + #ifndef __MINGW32__ + #include + #endif + #undef min + #undef max + #include + #include + #include + typedef long NTSTATUS; // winternl.h + + #ifdef _XBOX_ONE + #ifdef _DURANGO + #include "xmem.h" + #endif + #else + #define RMT_ENABLE_THREAD_SAMPLER + #endif + + #endif + + #ifdef RMT_PLATFORM_LINUX + #if defined(__FreeBSD__) || defined(__OpenBSD__) + #include + #else + #include + #endif + #endif + + #if defined(RMT_PLATFORM_POSIX) + #include + #include + #include + #include + #include + #include + #include + #include + #include + #endif + + #ifdef __MINGW32__ + #include + #endif + +#endif + +#if RMT_USE_CUDA + #include +#endif + +// clang-format on + +#if defined(_MSC_VER) && !defined(__clang__) + #define RMT_UNREFERENCED_PARAMETER(i) (i) +#else + #define RMT_UNREFERENCED_PARAMETER(i) (void)(1 ? (void)0 : ((void)i)) +#endif + +// Executes the given statement and returns from the calling function if it fails, returning the error with it +#define rmtTry(stmt) \ + { \ + rmtError error = stmt; \ + if (error != RMT_ERROR_NONE) \ + return error; \ + } + +static rmtU8 minU8(rmtU8 a, rmtU8 b) +{ + return a < b ? a : b; +} +static rmtU16 maxU16(rmtU16 a, rmtU16 b) +{ + return a > b ? a : b; +} +static rmtS32 minS32(rmtS32 a, rmtS32 b) +{ + return a < b ? a : b; +} +static rmtS32 maxS32(rmtS32 a, rmtS32 b) +{ + return a > b ? a : b; +} +static rmtU32 minU32(rmtU32 a, rmtU32 b) +{ + return a < b ? a : b; +} +static rmtU32 maxU32(rmtU32 a, rmtU32 b) +{ + return a > b ? a : b; +} +static rmtS64 maxS64(rmtS64 a, rmtS64 b) +{ + return a > b ? a : b; +} + +// Memory management functions +static void* rmtMalloc(rmtU32 size) +{ + return g_Settings.malloc(g_Settings.mm_context, size); +} + +static void* rmtRealloc(void* ptr, rmtU32 size) +{ + return g_Settings.realloc(g_Settings.mm_context, ptr, size); +} + +static void rmtFree(void* ptr) +{ + g_Settings.free(g_Settings.mm_context, ptr); +} + +// File system functions +static FILE* rmtOpenFile(const char* filename, const char* mode) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + FILE* fp; + return fopen_s(&fp, filename, mode) == 0 ? fp : NULL; +#else + return fopen(filename, mode); +#endif +} + +void rmtCloseFile(FILE* fp) +{ + if (fp != NULL) + { + fclose(fp); + } +} + +rmtBool rmtWriteFile(FILE* fp, const void* data, rmtU32 size) +{ + assert(fp != NULL); + return fwrite(data, size, 1, fp) == size ? RMT_TRUE : RMT_FALSE; +} + +#if RMT_USE_OPENGL +// DLL/Shared Library functions + +static void* rmtLoadLibrary(const char* path) +{ +#if defined(RMT_PLATFORM_WINDOWS) + return (void*)LoadLibraryA(path); +#elif defined(RMT_PLATFORM_POSIX) + return dlopen(path, RTLD_LOCAL | RTLD_LAZY); +#else + return NULL; +#endif +} + +static void rmtFreeLibrary(void* handle) +{ +#if defined(RMT_PLATFORM_WINDOWS) + FreeLibrary((HMODULE)handle); +#elif defined(RMT_PLATFORM_POSIX) + dlclose(handle); +#endif +} + +#if defined(RMT_PLATFORM_WINDOWS) +typedef FARPROC ProcReturnType; +#else +typedef void* ProcReturnType; +#endif + +static ProcReturnType rmtGetProcAddress(void* handle, const char* symbol) +{ +#if defined(RMT_PLATFORM_WINDOWS) + return GetProcAddress((HMODULE)handle, (LPCSTR)symbol); +#elif defined(RMT_PLATFORM_POSIX) + return dlsym(handle, symbol); +#endif +} + +#endif + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TIMERS: Platform-specific timers +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// Get millisecond timer value that has only one guarantee: multiple calls are consistently comparable. +// On some platforms, even though this returns milliseconds, the timer may be far less accurate. +// +static rmtU32 msTimer_Get() +{ +#ifdef RMT_PLATFORM_WINDOWS + + return (rmtU32)GetTickCount(); + +#else + + clock_t time = clock(); + +// CLOCKS_PER_SEC is 128 on FreeBSD, causing div/0 +#if defined(__FreeBSD__) || defined(__OpenBSD__) + rmtU32 msTime = (rmtU32)(time * 1000 / CLOCKS_PER_SEC); +#else + rmtU32 msTime = (rmtU32)(time / (CLOCKS_PER_SEC / 1000)); +#endif + + return msTime; + +#endif +} + +// +// Micro-second accuracy high performance counter +// +#ifndef RMT_PLATFORM_WINDOWS +typedef rmtU64 LARGE_INTEGER; +#endif +typedef struct +{ + LARGE_INTEGER counter_start; + double counter_scale; +} usTimer; + +static void usTimer_Init(usTimer* timer) +{ +#if defined(RMT_PLATFORM_WINDOWS) + LARGE_INTEGER performance_frequency; + + assert(timer != NULL); + + // Calculate the scale from performance counter to microseconds + QueryPerformanceFrequency(&performance_frequency); + timer->counter_scale = 1000000.0 / performance_frequency.QuadPart; + + // Record the offset for each read of the counter + QueryPerformanceCounter(&timer->counter_start); + +#elif defined(RMT_PLATFORM_MACOS) + + mach_timebase_info_data_t nsScale; + mach_timebase_info(&nsScale); + const double ns_per_us = 1.0e3; + timer->counter_scale = (double)(nsScale.numer) / ((double)nsScale.denom * ns_per_us); + + timer->counter_start = mach_absolute_time(); + +#elif defined(RMT_PLATFORM_LINUX) + + struct timespec tv; + clock_gettime(CLOCK_REALTIME, &tv); + timer->counter_start = (rmtU64)(tv.tv_sec * (rmtU64)1000000) + (rmtU64)(tv.tv_nsec * 0.001); + +#endif +} + +static rmtU64 usTimer_Get(usTimer* timer) +{ +#if defined(RMT_PLATFORM_WINDOWS) + LARGE_INTEGER performance_count; + + assert(timer != NULL); + + // Read counter and convert to microseconds + QueryPerformanceCounter(&performance_count); + return (rmtU64)((performance_count.QuadPart - timer->counter_start.QuadPart) * timer->counter_scale); + +#elif defined(RMT_PLATFORM_MACOS) + + rmtU64 curr_time = mach_absolute_time(); + return (rmtU64)((curr_time - timer->counter_start) * timer->counter_scale); + +#elif defined(RMT_PLATFORM_LINUX) + + struct timespec tv; + clock_gettime(CLOCK_REALTIME, &tv); + return ((rmtU64)(tv.tv_sec * (rmtU64)1000000) + (rmtU64)(tv.tv_nsec * 0.001)) - timer->counter_start; + +#endif +} + +static void msSleep(rmtU32 time_ms) +{ +#ifdef RMT_PLATFORM_WINDOWS + Sleep(time_ms); +#elif defined(RMT_PLATFORM_POSIX) + usleep(time_ms * 1000); +#endif +} + +static struct tm* TimeDateNow() +{ + time_t time_now = time(NULL); + +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + // Discard the thread-safety benefit of gmtime_s + static struct tm tm_now; + gmtime_s(&tm_now, &time_now); + return &tm_now; +#else + return gmtime(&time_now); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TLS: Thread-Local Storage +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define TLS_INVALID_HANDLE 0xFFFFFFFF + +#if defined(RMT_PLATFORM_WINDOWS) +typedef rmtU32 rmtTLS; +#else +typedef pthread_key_t rmtTLS; +#endif + +static rmtError tlsAlloc(rmtTLS* handle) +{ + assert(handle != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + *handle = (rmtTLS)TlsAlloc(); + if (*handle == TLS_OUT_OF_INDEXES) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } +#elif defined(RMT_PLATFORM_POSIX) + if (pthread_key_create(handle, NULL) != 0) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } +#endif + + return RMT_ERROR_NONE; +} + +static void tlsFree(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + TlsFree(handle); +#elif defined(RMT_PLATFORM_POSIX) + pthread_key_delete((pthread_key_t)handle); +#endif +} + +static void tlsSet(rmtTLS handle, void* value) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + TlsSetValue(handle, value); +#elif defined(RMT_PLATFORM_POSIX) + pthread_setspecific((pthread_key_t)handle, value); +#endif +} + +static void* tlsGet(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + return TlsGetValue(handle); +#elif defined(RMT_PLATFORM_POSIX) + return pthread_getspecific((pthread_key_t)handle); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @ERROR: Error handling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// Used to store per-thread error messages +// Static so that we can set error messages from code the Remotery object depends on +static rmtTLS g_lastErrorMessageTlsHandle = TLS_INVALID_HANDLE; +static const rmtU32 g_errorMessageSize = 1024; + +static rmtError rmtMakeError(rmtError error, rmtPStr error_message) +{ + char* thread_message_ptr; + rmtU32 error_len; + + // Allocate the TLS on-demand + // TODO(don): Make this thread-safe + if (g_lastErrorMessageTlsHandle == TLS_INVALID_HANDLE) + { + rmtTry(tlsAlloc(&g_lastErrorMessageTlsHandle)); + } + + // Allocate the string storage for the error message on-demand + thread_message_ptr = (char*)tlsGet(g_lastErrorMessageTlsHandle); + if (thread_message_ptr == NULL) + { + thread_message_ptr = (char*)rmtMalloc(g_errorMessageSize); + if (thread_message_ptr == NULL) + { + return RMT_ERROR_MALLOC_FAIL; + } + + tlsSet(g_lastErrorMessageTlsHandle, (void*)thread_message_ptr); + } + + // Safe copy of the error text without going via strcpy_s down below + error_len = strlen(error_message); + error_len = error_len >= g_errorMessageSize ? g_errorMessageSize - 1 : error_len; + memcpy(thread_message_ptr, error_message, error_len); + thread_message_ptr[error_len] = 0; + + return error; +} + +RMT_API rmtPStr rmt_GetLastErrorMessage() +{ + rmtPStr thread_message_ptr; + + // No message to specify if `rmtMakeError` failed or one hasn't been set yet + if (g_lastErrorMessageTlsHandle == TLS_INVALID_HANDLE) + { + return "No error message"; + } + thread_message_ptr = (rmtPStr)tlsGet(g_lastErrorMessageTlsHandle); + if (thread_message_ptr == NULL) + { + return "No error message"; + } + + return thread_message_ptr; +} + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MUTEX: Mutexes +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#ifdef RMT_PLATFORM_WINDOWS +typedef CRITICAL_SECTION rmtMutex; +#else +typedef pthread_mutex_t rmtMutex; +#endif + +static void mtxInit(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + InitializeCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_init(mutex, NULL); +#endif +} + +static void mtxLock(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + EnterCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_lock(mutex); +#endif +} + +static void mtxUnlock(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + LeaveCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_unlock(mutex); +#endif +} + +static void mtxDelete(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + DeleteCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_destroy(mutex); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @ATOMIC: Atomic Operations +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// TODO(don): The CAS loops possible with this API are suboptimal. For example, AtomicCompareAndSwapU32 discards the +// return value which tells you the current (potentially mismatching) value of the location you want to modify. This +// means the CAS loop has to explicitly re-load this location on each modify attempt. Instead, the return value should +// be used to update the old value and an initial load only made once before the loop starts. + +// TODO(don): Vary these types across versions of C and C++ +typedef volatile rmtS32 rmtAtomicS32; +typedef volatile rmtU32 rmtAtomicU32; +typedef volatile rmtU64 rmtAtomicU64; + +static rmtBool AtomicCompareAndSwapU32(rmtU32 volatile* val, long old_val, long new_val) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return _InterlockedCompareExchange((long volatile*)val, new_val, old_val) == old_val ? RMT_TRUE : RMT_FALSE; +#elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_bool_compare_and_swap(val, old_val, new_val) ? RMT_TRUE : RMT_FALSE; +#endif +} + +static rmtBool AtomicCompareAndSwapU64(rmtAtomicU64* val, rmtU64 old_value, rmtU64 new_val) +{ + #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return _InterlockedCompareExchange64((volatile LONG64*)val, (LONG64)new_val, (LONG64)old_value) == (LONG64)old_value + ? RMT_TRUE + : RMT_FALSE; + #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_bool_compare_and_swap(val, old_value, new_val) ? RMT_TRUE : RMT_FALSE; + #endif +} + +static rmtBool AtomicCompareAndSwapPointer(long* volatile* ptr, long* old_ptr, long* new_ptr) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) +#ifdef _WIN64 + return _InterlockedCompareExchange64((__int64 volatile*)ptr, (__int64)new_ptr, (__int64)old_ptr) == (__int64)old_ptr + ? RMT_TRUE + : RMT_FALSE; +#else + return _InterlockedCompareExchange((long volatile*)ptr, (long)new_ptr, (long)old_ptr) == (long)old_ptr ? RMT_TRUE + : RMT_FALSE; +#endif +#elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_bool_compare_and_swap(ptr, old_ptr, new_ptr) ? RMT_TRUE : RMT_FALSE; +#endif +} + +// +// NOTE: Does not guarantee a memory barrier +// TODO: Make sure all platforms don't insert a memory barrier as this is only for stats +// Alternatively, add strong/weak memory order equivalents +// +static rmtS32 AtomicAddS32(rmtAtomicS32* value, rmtS32 add) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return _InterlockedExchangeAdd((long volatile*)value, (long)add); +#elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_fetch_and_add(value, add); +#endif +} + +static rmtU32 AtomicAddU32(rmtAtomicU32* value, rmtU32 add) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return (rmtU32)_InterlockedExchangeAdd((long volatile*)value, (long)add); +#elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return (rmtU32)__sync_fetch_and_add(value, add); +#endif +} + +static void AtomicSubS32(rmtAtomicS32* value, rmtS32 sub) +{ + // Not all platforms have an implementation so just negate and add + AtomicAddS32(value, -sub); +} + +static void CompilerWriteFence() +{ +#if defined(__clang__) + __asm__ volatile("" : : : "memory"); +#elif defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + _WriteBarrier(); +#else + asm volatile("" : : : "memory"); +#endif +} + +static void CompilerReadFence() +{ +#if defined(__clang__) + __asm__ volatile("" : : : "memory"); +#elif defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + _ReadBarrier(); +#else + asm volatile("" : : : "memory"); +#endif +} + +static rmtU32 LoadAcquire(rmtAtomicU32* address) +{ + rmtU32 value = *address; + CompilerReadFence(); + return value; +} + +static long* LoadAcquirePointer(long* volatile* ptr) +{ + long* value = *ptr; + CompilerReadFence(); + return value; +} + +static void StoreRelease(rmtAtomicU32* address, rmtU32 value) +{ + CompilerWriteFence(); + *address = value; +} + +static void StoreReleasePointer(long* volatile* ptr, long* value) +{ + CompilerWriteFence(); + *ptr = value; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @RNG: Random Number Generator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// WELL: Well Equidistributed Long-period Linear +// These algorithms produce numbers with better equidistribution than MT19937 and improve upon "bit-mixing" properties. They are +// fast, come in many sizes, and produce higher quality random numbers. +// +// This implementation has a period of 2^512, or 10^154. +// +// Implementation from: Game Programming Gems 7, Random Number Generation Chris Lomont +// Documentation: http://www.lomont.org/Math/Papers/2008/Lomont_PRNG_2008.pdf +// + +// Global RNG state for now +// Far better than interfering with the user's rand() +#define Well512_StateSize 16 +static rmtU32 Well512_State[Well512_StateSize]; +static rmtU32 Well512_Index; + +static void Well512_Init(rmtU32 seed) +{ + rmtU32 i; + + // Generate initial state from seed + Well512_State[0] = seed; + for (i = 1; i < Well512_StateSize; i++) + { + rmtU32 prev = Well512_State[i - 1]; + Well512_State[i] = (1812433253 * (prev ^ (prev >> 30)) + i); + } + Well512_Index = 0; +} + +static rmtU32 Well512_RandomU32() +{ + rmtU32 a, b, c, d; + + a = Well512_State[Well512_Index]; + c = Well512_State[(Well512_Index + 13) & 15]; + b = a ^ c ^ (a << 16) ^ (c << 15); + c = Well512_State[(Well512_Index + 9) & 15]; + c ^= (c >> 11); + a = Well512_State[Well512_Index] = b ^ c; + d = a ^ ((a << 5) & 0xDA442D24UL); + Well512_Index = (Well512_Index + 15) & 15; + a = Well512_State[Well512_Index]; + Well512_State[Well512_Index] = a ^ b ^ d ^ (a << 2) ^ (b << 18) ^ (c << 28); + return Well512_State[Well512_Index]; +} + +static rmtU32 Well512_RandomOpenLimit(rmtU32 limit) +{ + // Using % to modulo with range is just masking out the higher bits, leaving a result that's objectively biased. + // Dividing by RAND_MAX is better but leads to increased repetition at low ranges due to very large bucket sizes. + // Instead use multiple passes with smaller bucket sizes, rejecting results that don't fit into this smaller range. + rmtU32 bucket_size = UINT_MAX / limit; + rmtU32 bucket_limit = bucket_size * limit; + rmtU32 r; + do + { + r = Well512_RandomU32(); + } while(r >= bucket_limit); + + return r / bucket_size; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @LFSR: Galois Linear-feedback Shift Register +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static rmtU32 Log2i(rmtU32 x) +{ + static const rmtU32 MultiplyDeBruijnBitPosition[32] = + { + 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 + }; + + // First round down to one less than a power of two + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + + return MultiplyDeBruijnBitPosition[(rmtU32)(x * 0x07C4ACDDU) >> 27]; +} + +static rmtU32 GaloisLFSRMask(rmtU32 table_size_log2) +{ + // Taps for 4 to 8 bit ranges + static const rmtU32 XORMasks[] = + { + ((1 << 0) | (1 << 1)), // 2 + ((1 << 1) | (1 << 2)), // 3 + ((1 << 2) | (1 << 3)), // 4 + ((1 << 2) | (1 << 4)), // 5 + ((1 << 4) | (1 << 5)), // 6 + ((1 << 5) | (1 << 6)), // 7 + ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 7)), // 8 + }; + + // Map table size to required XOR mask + assert(table_size_log2 >= 2); + assert(table_size_log2 <= 8); + return XORMasks[table_size_log2 - 2]; +} + +static rmtU32 GaloisLFSRNext(rmtU32 value, rmtU32 xor_mask) +{ + // Output bit + rmtU32 lsb = value & 1; + + // Apply the register shift + value >>= 1; + + // Apply toggle mask if the output bit is set + if (lsb != 0) + { + value ^= xor_mask; + } + + return value; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @NEW: New/Delete operators with error values for simplifying object create/destroy +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define rmtTryMalloc(type, obj) \ + obj = (type*)rmtMalloc(sizeof(type)); \ + if (obj == NULL) \ + { \ + return RMT_ERROR_MALLOC_FAIL; \ + } + +#define rmtTryMallocArray(type, obj, count) \ + obj = (type*)rmtMalloc((count) * sizeof(type)); \ + if (obj == NULL) \ + { \ + return RMT_ERROR_MALLOC_FAIL; \ + } + +// Ensures the pointer is non-NULL, calls the destructor, frees memory and sets the pointer to NULL +#define rmtDelete(type, obj) \ + if (obj != NULL) \ + { \ + type##_Destructor(obj); \ + rmtFree(obj); \ + obj = NULL; \ + } + +// New will allocate enough space for the object and call the constructor +// If allocation fails the constructor won't be called +// If the constructor fails, the destructor is called and memory is released +// NOTE: Use of sizeof() requires that the type be defined at the point of call +// This is a disadvantage over requiring only a custom Create function +#define rmtTryNew(type, obj, ...) \ + { \ + obj = (type*)rmtMalloc(sizeof(type)); \ + if (obj == NULL) \ + { \ + return RMT_ERROR_MALLOC_FAIL; \ + } \ + rmtError error = type##_Constructor(obj, ##__VA_ARGS__); \ + if (error != RMT_ERROR_NONE) \ + { \ + rmtDelete(type, obj); \ + return error; \ + } \ + } + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @VMBUFFER: Mirror Buffer using Virtual Memory for auto-wrap +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct VirtualMirrorBuffer +{ + // Page-rounded size of the buffer without mirroring + rmtU32 size; + + // Pointer to the first part of the mirror + // The second part comes directly after at ptr+size bytes + rmtU8* ptr; + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _DURANGO + size_t page_count; + size_t* page_mapping; +#else + HANDLE file_map_handle; +#endif +#endif + +} VirtualMirrorBuffer; + +#ifdef __ANDROID__ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#define ASHMEM_DEVICE "/dev/ashmem" + +/* + * ashmem_create_region - creates a new ashmem region and returns the file + * descriptor, or <0 on error + * + * `name' is an optional label to give the region (visible in /proc/pid/maps) + * `size' is the size of the region, in page-aligned bytes + */ +int ashmem_create_region(const char* name, size_t size) +{ + int fd, ret; + + fd = open(ASHMEM_DEVICE, O_RDWR); + if (fd < 0) + return fd; + + if (name) + { + char buf[ASHMEM_NAME_LEN] = {0}; + + strncpy(buf, name, sizeof(buf)); + buf[sizeof(buf) - 1] = 0; + ret = ioctl(fd, ASHMEM_SET_NAME, buf); + if (ret < 0) + goto error; + } + + ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) + goto error; + + return fd; + +error: + close(fd); + return ret; +} +#endif // __ANDROID__ + +static rmtError VirtualMirrorBuffer_Constructor(VirtualMirrorBuffer* buffer, rmtU32 size, int nb_attempts) +{ + static const rmtU32 k_64 = 64 * 1024; + RMT_UNREFERENCED_PARAMETER(nb_attempts); + +#ifdef RMT_PLATFORM_LINUX +#if defined(__FreeBSD__) || defined(__OpenBSD__) + char path[] = "/tmp/ring-buffer-XXXXXX"; +#else + char path[] = "/dev/shm/ring-buffer-XXXXXX"; +#endif + int file_descriptor; +#endif + + // Round up to page-granulation; the nearest 64k boundary for now + size = (size + k_64 - 1) / k_64 * k_64; + + // Set defaults + buffer->size = size; + buffer->ptr = NULL; +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _DURANGO + buffer->page_count = 0; + buffer->page_mapping = NULL; +#else + buffer->file_map_handle = INVALID_HANDLE_VALUE; +#endif +#endif + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _DURANGO + + // Xbox version based on Windows version and XDK reference + + buffer->page_count = size / k_64; + if (buffer->page_mapping) + { + free(buffer->page_mapping); + } + buffer->page_mapping = (size_t*)malloc(sizeof(ULONG) * buffer->page_count); + + while (nb_attempts-- > 0) + { + rmtU8* desired_addr; + + // Create a page mapping for pointing to its physical address with multiple virtual pages + if (!AllocateTitlePhysicalPages(GetCurrentProcess(), MEM_LARGE_PAGES, &buffer->page_count, + buffer->page_mapping)) + { + free(buffer->page_mapping); + buffer->page_mapping = NULL; + break; + } + + // Reserve two contiguous pages of virtual memory + desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS); + if (desired_addr == NULL) + break; + + // Release the range immediately but retain the address for the next sequence of code to + // try and map to it. In the mean-time some other OS thread may come along and allocate this + // address range from underneath us so multiple attempts need to be made. + VirtualFree(desired_addr, 0, MEM_RELEASE); + + // Immediately try to point both pages at the file mapping + if (MapTitlePhysicalPages(desired_addr, buffer->page_count, MEM_LARGE_PAGES, PAGE_READWRITE, + buffer->page_mapping) == desired_addr && + MapTitlePhysicalPages(desired_addr + size, buffer->page_count, MEM_LARGE_PAGES, PAGE_READWRITE, + buffer->page_mapping) == desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } + + // Failed to map the virtual pages; cleanup and try again + FreeTitlePhysicalPages(GetCurrentProcess(), buffer->page_count, buffer->page_mapping); + buffer->page_mapping = NULL; + } + +#else + + // Windows version based on https://gist.github.com/rygorous/3158316 + + while (nb_attempts-- > 0) + { + rmtU8* desired_addr; + + // Create a file mapping for pointing to its physical address with multiple virtual pages + buffer->file_map_handle = CreateFileMapping(INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, size, 0); + if (buffer->file_map_handle == NULL) + break; + +#ifndef _UWP // NON-UWP Windows Desktop Version + + // Reserve two contiguous pages of virtual memory + desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS); + if (desired_addr == NULL) + break; + + // Release the range immediately but retain the address for the next sequence of code to + // try and map to it. In the mean-time some other OS thread may come along and allocate this + // address range from underneath us so multiple attempts need to be made. + VirtualFree(desired_addr, 0, MEM_RELEASE); + + // Immediately try to point both pages at the file mapping + if (MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr) == desired_addr && + MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr + size) == + desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } + +#else // UWP + + // Implementation based on example from: + // https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-virtualalloc2 + // + // Notes + // - just replaced the non-uwp functions by the uwp variants. + // - Both versions could be rewritten to not need the try-loop, see the example mentioned above. I just keep it + // as is for now. + // - Successfully tested on Hololens + desired_addr = (rmtU8*)VirtualAlloc2FromApp(NULL, NULL, 2 * size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, + PAGE_NOACCESS, NULL, 0); + + // Split the placeholder region into two regions of equal size. + VirtualFree(desired_addr, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + + // Immediately try to point both pages at the file mapping. + if (MapViewOfFile3FromApp(buffer->file_map_handle, NULL, desired_addr, 0, size, MEM_REPLACE_PLACEHOLDER, + PAGE_READWRITE, NULL, 0) == desired_addr && + MapViewOfFile3FromApp(buffer->file_map_handle, NULL, desired_addr + size, 0, size, MEM_REPLACE_PLACEHOLDER, + PAGE_READWRITE, NULL, 0) == desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } +#endif + // Failed to map the virtual pages; cleanup and try again + CloseHandle(buffer->file_map_handle); + buffer->file_map_handle = NULL; + } + +#endif // _XBOX_ONE + +#endif + +#ifdef RMT_PLATFORM_MACOS + + // + // Mac version based on https://github.com/mikeash/MAMirroredQueue + // + // Copyright (c) 2010, Michael Ash + // All rights reserved. + // + // Redistribution and use in source and binary forms, with or without modification, are permitted provided that + // the following conditions are met: + // + // Redistributions of source code must retain the above copyright notice, this list of conditions and the following + // disclaimer. + // + // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + // following disclaimer in the documentation and/or other materials provided with the distribution. + // Neither the name of Michael Ash nor the names of its contributors may be used to endorse or promote products + // derived from this software without specific prior written permission. + // + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + // IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + // + + while (nb_attempts-- > 0) + { + vm_prot_t cur_prot, max_prot; + kern_return_t mach_error; + rmtU8* ptr = NULL; + rmtU8* target = NULL; + + // Allocate 2 contiguous pages of virtual memory + if (vm_allocate(mach_task_self(), (vm_address_t*)&ptr, size * 2, VM_FLAGS_ANYWHERE) != KERN_SUCCESS) + break; + + // Try to deallocate the last page, leaving its virtual memory address free + target = ptr + size; + if (vm_deallocate(mach_task_self(), (vm_address_t)target, size) != KERN_SUCCESS) + { + vm_deallocate(mach_task_self(), (vm_address_t)ptr, size * 2); + break; + } + + // Attempt to remap the page just deallocated to the buffer again + mach_error = vm_remap(mach_task_self(), (vm_address_t*)&target, size, + 0, // mask + 0, // anywhere + mach_task_self(), (vm_address_t)ptr, + 0, // copy + &cur_prot, &max_prot, VM_INHERIT_COPY); + + if (mach_error == KERN_NO_SPACE) + { + // Failed on this pass, cleanup and make another attempt + if (vm_deallocate(mach_task_self(), (vm_address_t)ptr, size) != KERN_SUCCESS) + break; + } + + else if (mach_error == KERN_SUCCESS) + { + // Leave the loop on success + buffer->ptr = ptr; + break; + } + + else + { + // Unknown error, can't recover + vm_deallocate(mach_task_self(), (vm_address_t)ptr, size); + break; + } + } + +#endif + +#ifdef RMT_PLATFORM_LINUX + + // Linux version based on now-defunct Wikipedia section + // http://en.wikipedia.org/w/index.php?title=Circular_buffer&oldid=600431497 + +#ifdef __ANDROID__ + file_descriptor = ashmem_create_region("remotery_shm", size * 2); + if (file_descriptor < 0) + { + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + } +#else + // Create a unique temporary filename in the shared memory folder + file_descriptor = mkstemp(path); + if (file_descriptor < 0) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + // Delete the name + if (unlink(path)) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + // Set the file size to twice the buffer size + // TODO: this 2x behaviour can be avoided with similar solution to Win/Mac + if (ftruncate(file_descriptor, size * 2)) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + +#endif + // Map 2 contiguous pages + buffer->ptr = (rmtU8*)mmap(NULL, size * 2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (buffer->ptr == MAP_FAILED) + { + buffer->ptr = NULL; + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + } + + // Point both pages to the same memory file + if (mmap(buffer->ptr, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != buffer->ptr || + mmap(buffer->ptr + size, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != + buffer->ptr + size) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + +#endif + + // Cleanup if exceeded number of attempts or failed + if (buffer->ptr == NULL) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + return RMT_ERROR_NONE; +} + +static void VirtualMirrorBuffer_Destructor(VirtualMirrorBuffer* buffer) +{ + assert(buffer != 0); + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _DURANGO + if (buffer->page_mapping != NULL) + { + VirtualFree(buffer->ptr, 0, MEM_DECOMMIT); // needed in conjunction with FreeTitlePhysicalPages + FreeTitlePhysicalPages(GetCurrentProcess(), buffer->page_count, buffer->page_mapping); + free(buffer->page_mapping); + buffer->page_mapping = NULL; + } +#else + if (buffer->file_map_handle != NULL) + { + // FIXME, don't we need to unmap the file views obtained in VirtualMirrorBuffer_Constructor, both for + // uwp/non-uwp See example + // https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-virtualalloc2 + + CloseHandle(buffer->file_map_handle); + buffer->file_map_handle = NULL; + } +#endif +#endif + +#ifdef RMT_PLATFORM_MACOS + if (buffer->ptr != NULL) + vm_deallocate(mach_task_self(), (vm_address_t)buffer->ptr, buffer->size * 2); +#endif + +#ifdef RMT_PLATFORM_LINUX + if (buffer->ptr != NULL) + munmap(buffer->ptr, buffer->size * 2); +#endif + + buffer->ptr = NULL; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAFEC: Safe C Library excerpts + http://sourceforge.net/projects/safeclib/ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +/*------------------------------------------------------------------ + * + * November 2008, Bo Berry + * + * Copyright (c) 2008-2011 by Cisco Systems, Inc + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + *------------------------------------------------------------------ + */ + +// NOTE: Microsoft also has its own version of these functions so I'm do some hacky PP to remove them +#define strnlen_s strnlen_s_safe_c +#define strncat_s strncat_s_safe_c +#define strcpy_s strcpy_s_safe_c + +#define RSIZE_MAX_STR (4UL << 10) /* 4KB */ +#define RCNEGATE(x) x + +#define EOK (0) +#define ESNULLP (400) /* null ptr */ +#define ESZEROL (401) /* length is zero */ +#define ESLEMAX (403) /* length exceeds max */ +#define ESOVRLP (404) /* overlap undefined */ +#define ESNOSPC (406) /* not enough space for s2 */ +#define ESUNTERM (407) /* unterminated string */ +#define ESNOTFND (409) /* not found */ + +#ifndef _ERRNO_T_DEFINED +#define _ERRNO_T_DEFINED +typedef int errno_t; +#endif + +// rsize_t equivalent without going to the hassle of detecting if a platform has implemented C11/K3.2 +typedef unsigned int r_size_t; + +static r_size_t strnlen_s(const char* dest, r_size_t dmax) +{ + r_size_t count; + + if (dest == NULL) + { + return RCNEGATE(0); + } + + if (dmax == 0) + { + return RCNEGATE(0); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(0); + } + + count = 0; + while (*dest && dmax) + { + count++; + dmax--; + dest++; + } + + return RCNEGATE(count); +} + +static errno_t strstr_s(char* dest, r_size_t dmax, const char* src, r_size_t slen, char** substring) +{ + r_size_t len; + r_size_t dlen; + int i; + + if (substring == NULL) + { + return RCNEGATE(ESNULLP); + } + *substring = NULL; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (src == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (slen == 0) + { + return RCNEGATE(ESZEROL); + } + + if (slen > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + /* + * src points to a string with zero length, or + * src equals dest, return dest + */ + if (*src == '\0' || dest == src) + { + *substring = dest; + return RCNEGATE(EOK); + } + + while (*dest && dmax) + { + i = 0; + len = slen; + dlen = dmax; + + while (src[i] && dlen) + { + + /* not a match, not a substring */ + if (dest[i] != src[i]) + { + break; + } + + /* move to the next char */ + i++; + len--; + dlen--; + + if (src[i] == '\0' || !len) + { + *substring = dest; + return RCNEGATE(EOK); + } + } + dest++; + dmax--; + } + + /* + * substring was not found, return NULL + */ + *substring = NULL; + return RCNEGATE(ESNOTFND); +} + +static errno_t strncat_s(char* dest, r_size_t dmax, const char* src, r_size_t slen) +{ + const char* overlap_bumper; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (src == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (slen > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + /* hold base of dest in case src was not copied */ + + if (dest < src) + { + overlap_bumper = src; + + /* Find the end of dest */ + while (*dest != '\0') + { + + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + dest++; + dmax--; + if (dmax == 0) + { + return RCNEGATE(ESUNTERM); + } + } + + while (dmax > 0) + { + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + /* + * Copying truncated before the source null is encountered + */ + if (slen == 0) + { + *dest = '\0'; + return RCNEGATE(EOK); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + slen--; + dest++; + src++; + } + } + else + { + overlap_bumper = dest; + + /* Find the end of dest */ + while (*dest != '\0') + { + + /* + * NOTE: no need to check for overlap here since src comes first + * in memory and we're not incrementing src here. + */ + dest++; + dmax--; + if (dmax == 0) + { + return RCNEGATE(ESUNTERM); + } + } + + while (dmax > 0) + { + if (src == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + /* + * Copying truncated + */ + if (slen == 0) + { + *dest = '\0'; + return RCNEGATE(EOK); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + slen--; + dest++; + src++; + } + } + + /* + * the entire src was not copied, so the string will be nulled. + */ + return RCNEGATE(ESNOSPC); +} + +errno_t strcpy_s(char* dest, r_size_t dmax, const char* src) +{ + const char* overlap_bumper; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (src == NULL) + { + *dest = '\0'; + return RCNEGATE(ESNULLP); + } + + if (dest == src) + { + return RCNEGATE(EOK); + } + + if (dest < src) + { + overlap_bumper = src; + + while (dmax > 0) + { + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + dest++; + src++; + } + } + else + { + overlap_bumper = dest; + + while (dmax > 0) + { + if (src == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + dest++; + src++; + } + } + + /* + * the entire src must have been copied, if not reset dest + * to null the string. + */ + return RCNEGATE(ESNOSPC); +} + +/* very simple integer to hex */ +static const char* hex_encoding_table = "0123456789ABCDEF"; + +static void itoahex_s(char* dest, r_size_t dmax, rmtS32 value) +{ + r_size_t len; + rmtS32 halfbytepos; + + halfbytepos = 8; + + /* strip leading 0's */ + while (halfbytepos > 1) + { + --halfbytepos; + if (value >> (4 * halfbytepos) & 0xF) + { + ++halfbytepos; + break; + } + } + + len = 0; + while (len + 1 < dmax && halfbytepos > 0) + { + --halfbytepos; + dest[len] = hex_encoding_table[value >> (4 * halfbytepos) & 0xF]; + ++len; + } + + if (len < dmax) + { + dest[len] = 0; + } +} + +static const char* itoa_s(rmtS32 value) +{ + static char temp_dest[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + int pos = 10; + + // Work back with the absolute value + rmtS32 abs_value = abs(value); + while (abs_value > 0) + { + temp_dest[pos--] = '0' + (abs_value % 10); + abs_value /= 10; + } + + // Place the negative + if (value < 0) + { + temp_dest[pos--] = '-'; + } + + return temp_dest + pos + 1; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @OSTHREADS: Wrappers around OS-specific thread functions +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#ifdef RMT_PLATFORM_WINDOWS +typedef DWORD rmtThreadId; +typedef HANDLE rmtThreadHandle; +#else +typedef uintptr_t rmtThreadId; +typedef pthread_t rmtThreadHandle; +#endif + +#ifdef RMT_PLATFORM_WINDOWS +typedef CONTEXT rmtCpuContext; +#else +typedef int rmtCpuContext; +#endif + +static rmtU32 rmtGetNbProcessors() +{ +#ifdef RMT_PLATFORM_WINDOWS + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + return system_info.dwNumberOfProcessors; +#else + // TODO: get_nprocs_conf / get_nprocs + return 0; +#endif +} + +static rmtThreadId rmtGetCurrentThreadId() +{ +#ifdef RMT_PLATFORM_WINDOWS + return GetCurrentThreadId(); +#else + return (rmtThreadId)pthread_self(); +#endif +} + +static rmtBool rmtSuspendThread(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + // SuspendThread is an async call to the scheduler and upon return the thread is not guaranteed to be suspended. + // Calling GetThreadContext will serialise that. + // See: https://github.com/mono/mono/blob/master/mono/utils/mono-threads-windows.c#L203 + return SuspendThread(thread_handle) == 0 ? RMT_TRUE : RMT_FALSE; +#else + return RMT_FALSE; +#endif +} + +static void rmtResumeThread(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + ResumeThread(thread_handle); +#endif +} + +#ifdef RMT_PLATFORM_WINDOWS +#ifndef CONTEXT_EXCEPTION_REQUEST +// These seem to be guarded by a _AMD64_ macro in winnt.h, which doesn't seem to be defined in older MSVC compilers. +// Which makes sense given this was a post-Vista/Windows 7 patch around errors in the WoW64 context switch. +// This bug was never fixed in the OS so defining these will only get this code to compile on Old Windows systems, with no +// guarantee of being stable at runtime. +#define CONTEXT_EXCEPTION_ACTIVE 0x8000000L +#define CONTEXT_SERVICE_ACTIVE 0x10000000L +#define CONTEXT_EXCEPTION_REQUEST 0x40000000L +#define CONTEXT_EXCEPTION_REPORTING 0x80000000L +#endif +#endif + +static rmtBool rmtGetUserModeThreadContext(rmtThreadHandle thread, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD kernel_mode_mask; + + // Request thread context with exception reporting + context->ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_EXCEPTION_REQUEST; + if (GetThreadContext(thread, context) == 0) + { + return RMT_FALSE; + } + + // Context on WoW64 is only valid and can only be set if the thread isn't in kernel mode + // Typical reference to this appears to be: http://zachsaw.blogspot.com/2010/11/wow64-bug-getthreadcontext-may-return.html + // Confirmed by MS here: https://social.msdn.microsoft.com/Forums/vstudio/en-US/aa176c36-6624-4776-9380-1c9cf37a314e/getthreadcontext-returns-stale-register-values-on-wow64?forum=windowscompatibility + kernel_mode_mask = CONTEXT_EXCEPTION_REPORTING | CONTEXT_EXCEPTION_ACTIVE | CONTEXT_SERVICE_ACTIVE; + return (context->ContextFlags & kernel_mode_mask) == CONTEXT_EXCEPTION_REPORTING ? RMT_TRUE : RMT_FALSE; +#else + return RMT_FALSE; +#endif +} + +static void rmtSetThreadContext(rmtThreadHandle thread_handle, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS + SetThreadContext(thread_handle, context); +#endif +} + +static rmtError rmtOpenThreadHandle(rmtThreadId thread_id, rmtThreadHandle* out_thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + // Open the thread with required access rights to get the thread handle + *out_thread_handle = OpenThread(THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, thread_id); + if (*out_thread_handle == NULL) + { + return RMT_ERROR_OPEN_THREAD_HANDLE_FAIL; + } +#endif + + return RMT_ERROR_NONE; +} + +static void rmtCloseThreadHandle(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + if (thread_handle != NULL) + { + CloseHandle(thread_handle); + } +#endif +} + +#ifdef RMT_ENABLE_THREAD_SAMPLER +DWORD_PTR GetThreadStartAddress(rmtThreadHandle thread_handle) +{ + // Get NtQueryInformationThread from ntdll + HMODULE ntdll = GetModuleHandleA("ntdll.dll"); + if (ntdll != NULL) + { + typedef NTSTATUS (WINAPI *NTQUERYINFOMATIONTHREAD)(HANDLE, LONG, PVOID, ULONG, PULONG); + NTQUERYINFOMATIONTHREAD NtQueryInformationThread = (NTQUERYINFOMATIONTHREAD)GetProcAddress(ntdll, "NtQueryInformationThread"); + + // Use it to query the start address + DWORD_PTR start_address; + NTSTATUS status = NtQueryInformationThread(thread_handle, 9, &start_address, sizeof(DWORD), NULL); + if (status == 0) + { + return start_address; + } + } + + return 0; +} + +const char* GetStartAddressModuleName(DWORD_PTR start_address) +{ + BOOL success; + MODULEENTRY32 module_entry; + + // Snapshot the modules + HANDLE handle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0); + if (handle == INVALID_HANDLE_VALUE) + { + return NULL; + } + + module_entry.dwSize = sizeof(MODULEENTRY32); + module_entry.th32ModuleID = 1; + + // Enumerate modules checking start address against their loaded address range + success = Module32First(handle, &module_entry); + while (success == TRUE) + { + if (start_address >= (DWORD_PTR)module_entry.modBaseAddr && start_address <= ((DWORD_PTR)module_entry.modBaseAddr + module_entry.modBaseSize)) + { + static char name[MAX_MODULE_NAME32 + 1]; +#ifdef UNICODE + int size = WideCharToMultiByte(CP_ACP, 0, module_entry.szModule, -1, name, MAX_MODULE_NAME32, NULL, NULL); + if (size < 1) + { + name[0] = '\0'; + } +#else + strcpy_s(name, sizeof(name), module_entry.szModule); +#endif + CloseHandle(handle); + return name; + } + + success = Module32Next(handle, &module_entry); + } + + CloseHandle(handle); + + return NULL; +} +#endif + +static void rmtGetThreadNameFallback(char* out_thread_name, rmtU32 thread_name_size) +{ + // In cases where we can't get a thread name from the OS + static rmtS32 countThreads = 0; + out_thread_name[0] = 0; + strncat_s(out_thread_name, thread_name_size, "Thread", 6); + itoahex_s(out_thread_name + 6, thread_name_size - 6, AtomicAddS32(&countThreads, 1)); +} + +static void rmtGetThreadName(rmtThreadId thread_id, rmtThreadHandle thread_handle, char* out_thread_name, rmtU32 thread_name_size) +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD_PTR address; + const char* module_name; + rmtU32 len; + + // Use the new Windows 10 GetThreadDescription function + HMODULE kernel32 = GetModuleHandleA("Kernel32.dll"); + if (kernel32 != NULL) + { + typedef HRESULT(WINAPI* GETTHREADDESCRIPTION)(HANDLE hThread, PWSTR *ppszThreadDescription); + GETTHREADDESCRIPTION GetThreadDescription = (GETTHREADDESCRIPTION)GetProcAddress(kernel32, "GetThreadDescription"); + if (GetThreadDescription != NULL) + { + int size; + + WCHAR* thread_name_w; + GetThreadDescription(thread_handle, &thread_name_w); + + // Returned size is the byte size, so will be 1 for a null-terminated strings + size = WideCharToMultiByte(CP_ACP, 0, thread_name_w, -1, out_thread_name, thread_name_size, NULL, NULL); + if (size > 1) + { + return; + } + } + } + + #ifndef _XBOX_ONE + // At this point GetThreadDescription hasn't returned anything so let's get the thread module name and use that + address = GetThreadStartAddress(thread_handle); + if (address == 0) + { + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + return; + } + module_name = GetStartAddressModuleName(address); + if (module_name == NULL) + { + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + return; + } + #else + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + return; + #endif + + // Concatenate thread name with then thread ID as that will be unique, whereas the start address won't be + memset(out_thread_name, 0, thread_name_size); + strcpy_s(out_thread_name, thread_name_size, module_name); + strncat_s(out_thread_name, thread_name_size, "!", 1); + len = strnlen_s(out_thread_name, thread_name_size); + itoahex_s(out_thread_name + len, thread_name_size - len, thread_id); + +#elif defined(RMT_PLATFORM_LINUX) && RMT_USE_POSIX_THREADNAMES && !defined(__FreeBSD__) && !defined(__OpenBSD__) + + prctl(PR_GET_NAME, out_thread_name, 0, 0, 0); + +#else + + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @THREADS: Cross-platform thread object +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct Thread_t rmtThread; +typedef rmtError (*ThreadProc)(rmtThread* thread); + +struct Thread_t +{ + rmtThreadHandle handle; + + // Callback executed when the thread is created + ThreadProc callback; + + // Caller-specified parameter passed to Thread_Create + void* param; + + // Error state returned from callback + rmtError error; + + // External threads can set this to request an exit + volatile rmtBool request_exit; +}; + +#if defined(RMT_PLATFORM_WINDOWS) + +static DWORD WINAPI ThreadProcWindows(LPVOID lpParameter) +{ + rmtThread* thread = (rmtThread*)lpParameter; + assert(thread != NULL); + thread->error = thread->callback(thread); + return thread->error == RMT_ERROR_NONE ? 0 : 1; +} + +#else +static void* StartFunc(void* pArgs) +{ + rmtThread* thread = (rmtThread*)pArgs; + assert(thread != NULL); + thread->error = thread->callback(thread); + return NULL; // returned error not use, check thread->error. +} +#endif + +static int rmtThread_Valid(rmtThread* thread) +{ + assert(thread != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + return thread->handle != NULL; +#else + return !pthread_equal(thread->handle, pthread_self()); +#endif +} + +static rmtError rmtThread_Constructor(rmtThread* thread, ThreadProc callback, void* param) +{ + assert(thread != NULL); + + thread->callback = callback; + thread->param = param; + thread->error = RMT_ERROR_NONE; + thread->request_exit = RMT_FALSE; + + // OS-specific thread creation + +#if defined(RMT_PLATFORM_WINDOWS) + + thread->handle = CreateThread(NULL, // lpThreadAttributes + 0, // dwStackSize + ThreadProcWindows, // lpStartAddress + thread, // lpParameter + 0, // dwCreationFlags + NULL); // lpThreadId + + if (thread->handle == NULL) + return RMT_ERROR_CREATE_THREAD_FAIL; + +#else + + int32_t error = pthread_create(&thread->handle, NULL, StartFunc, thread); + if (error) + { + // Contents of 'thread' parameter to pthread_create() are undefined after + // failure call so can't pre-set to invalid value before hand. + thread->handle = pthread_self(); + return RMT_ERROR_CREATE_THREAD_FAIL; + } + +#endif + + return RMT_ERROR_NONE; +} + +static void rmtThread_RequestExit(rmtThread* thread) +{ + // Not really worried about memory barriers or delayed visibility to the target thread + assert(thread != NULL); + thread->request_exit = RMT_TRUE; +} + +static void rmtThread_Join(rmtThread* thread) +{ + assert(rmtThread_Valid(thread)); + +#if defined(RMT_PLATFORM_WINDOWS) + WaitForSingleObject(thread->handle, INFINITE); +#else + pthread_join(thread->handle, NULL); +#endif +} + +static void rmtThread_Destructor(rmtThread* thread) +{ + assert(thread != NULL); + + if (rmtThread_Valid(thread)) + { + // Shutdown the thread + rmtThread_RequestExit(thread); + rmtThread_Join(thread); + + // OS-specific release of thread resources + +#if defined(RMT_PLATFORM_WINDOWS) + CloseHandle(thread->handle); + thread->handle = NULL; +#endif + } +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @OBJALLOC: Reusable Object Allocator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// All objects that require free-list-backed allocation need to inherit from this type. +// +typedef struct ObjectLink_s +{ + struct ObjectLink_s* volatile next; +} ObjectLink; + +static void ObjectLink_Constructor(ObjectLink* link) +{ + assert(link != NULL); + link->next = NULL; +} + +typedef rmtError (*ObjConstructor)(void*); +typedef void (*ObjDestructor)(void*); + +typedef struct +{ + // Object create/destroy parameters + rmtU32 object_size; + ObjConstructor constructor; + ObjDestructor destructor; + + // Number of objects in the free list + rmtAtomicS32 nb_free; + + // Number of objects used by callers + rmtAtomicS32 nb_inuse; + + // Total allocation count + rmtAtomicS32 nb_allocated; + + ObjectLink* first_free; +} ObjectAllocator; + +static rmtError ObjectAllocator_Constructor(ObjectAllocator* allocator, rmtU32 object_size, ObjConstructor constructor, + ObjDestructor destructor) +{ + allocator->object_size = object_size; + allocator->constructor = constructor; + allocator->destructor = destructor; + allocator->nb_free = 0; + allocator->nb_inuse = 0; + allocator->nb_allocated = 0; + allocator->first_free = NULL; + return RMT_ERROR_NONE; +} + +static void ObjectAllocator_Destructor(ObjectAllocator* allocator) +{ + // Ensure everything has been released to the allocator + assert(allocator != NULL); + assert(allocator->nb_inuse == 0); + + // Destroy all objects released to the allocator + while (allocator->first_free != NULL) + { + ObjectLink* next = allocator->first_free->next; + assert(allocator->destructor != NULL); + allocator->destructor(allocator->first_free); + rmtFree(allocator->first_free); + allocator->first_free = next; + } +} + +static void ObjectAllocator_Push(ObjectAllocator* allocator, ObjectLink* start, ObjectLink* end) +{ + assert(allocator != NULL); + assert(start != NULL); + assert(end != NULL); + + // CAS pop add range to the front of the list + for (;;) + { + ObjectLink* old_link = (ObjectLink*)allocator->first_free; + end->next = old_link; + if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)start) == + RMT_TRUE) + break; + } +} + +static ObjectLink* ObjectAllocator_Pop(ObjectAllocator* allocator) +{ + ObjectLink* link; + + assert(allocator != NULL); + + // CAS pop from the front of the list + for (;;) + { + ObjectLink* old_link = (ObjectLink*)allocator->first_free; + if (old_link == NULL) + { + return NULL; + } + ObjectLink* next_link = old_link->next; + if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)next_link) == + RMT_TRUE) + { + link = old_link; + break; + } + } + + link->next = NULL; + + return link; +} + +static rmtError ObjectAllocator_Alloc(ObjectAllocator* allocator, void** object) +{ + // This function only calls the object constructor on initial malloc of an object + + assert(allocator != NULL); + assert(object != NULL); + + // Pull available objects from the free list + *object = ObjectAllocator_Pop(allocator); + + // Has the free list run out? + if (*object == NULL) + { + rmtError error; + + // Allocate/construct a new object + *object = rmtMalloc(allocator->object_size); + if (*object == NULL) + return RMT_ERROR_MALLOC_FAIL; + assert(allocator->constructor != NULL); + error = allocator->constructor(*object); + if (error != RMT_ERROR_NONE) + { + // Auto-teardown on failure + assert(allocator->destructor != NULL); + allocator->destructor(*object); + rmtFree(*object); + return error; + } + + AtomicAddS32(&allocator->nb_allocated, 1); + } + else + { + AtomicSubS32(&allocator->nb_free, 1); + } + + AtomicAddS32(&allocator->nb_inuse, 1); + + return RMT_ERROR_NONE; +} + +static void ObjectAllocator_Free(ObjectAllocator* allocator, void* object) +{ + // Add back to the free-list + assert(allocator != NULL); + ObjectAllocator_Push(allocator, (ObjectLink*)object, (ObjectLink*)object); + AtomicSubS32(&allocator->nb_inuse, 1); + AtomicAddS32(&allocator->nb_free, 1); +} + +static void ObjectAllocator_FreeRange(ObjectAllocator* allocator, void* start, void* end, rmtU32 count) +{ + assert(allocator != NULL); + ObjectAllocator_Push(allocator, (ObjectLink*)start, (ObjectLink*)end); + AtomicSubS32(&allocator->nb_inuse, count); + AtomicAddS32(&allocator->nb_free, count); +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @DYNBUF: Dynamic Buffer +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct +{ + rmtU32 alloc_granularity; + + rmtU32 bytes_allocated; + rmtU32 bytes_used; + + rmtU8* data; +} Buffer; + +static rmtError Buffer_Constructor(Buffer* buffer, rmtU32 alloc_granularity) +{ + assert(buffer != NULL); + buffer->alloc_granularity = alloc_granularity; + buffer->bytes_allocated = 0; + buffer->bytes_used = 0; + buffer->data = NULL; + return RMT_ERROR_NONE; +} + +static void Buffer_Destructor(Buffer* buffer) +{ + assert(buffer != NULL); + + if (buffer->data != NULL) + { + rmtFree(buffer->data); + buffer->data = NULL; + } +} + +static rmtError Buffer_Grow(Buffer* buffer, rmtU32 length) +{ + // Calculate size increase rounded up to the requested allocation granularity + rmtU32 granularity = buffer->alloc_granularity; + rmtU32 allocate = buffer->bytes_allocated + length; + allocate = allocate + ((granularity - 1) - ((allocate - 1) % granularity)); + + buffer->bytes_allocated = allocate; + buffer->data = (rmtU8*)rmtRealloc(buffer->data, buffer->bytes_allocated); + if (buffer->data == NULL) + return RMT_ERROR_MALLOC_FAIL; + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_Pad(Buffer* buffer, rmtU32 length) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + length > buffer->bytes_allocated) + { + rmtTry(Buffer_Grow(buffer, length)); + } + + // Step by the pad amount + buffer->bytes_used += length; + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_AlignedPad(Buffer* buffer, rmtU32 start_pos) +{ + return Buffer_Pad(buffer, (4 - ((buffer->bytes_used - start_pos) & 3)) & 3); +} + +static rmtError Buffer_Write(Buffer* buffer, const void* data, rmtU32 length) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + length > buffer->bytes_allocated) + { + rmtTry(Buffer_Grow(buffer, length)); + } + + // Copy all bytes + memcpy(buffer->data + buffer->bytes_used, data, length); + buffer->bytes_used += length; + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_WriteStringZ(Buffer* buffer, rmtPStr string) +{ + assert(string != NULL); + return Buffer_Write(buffer, (void*)string, (rmtU32)strnlen_s(string, 2048) + 1); +} + +static void U32ToByteArray(rmtU8* dest, rmtU32 value) +{ + // Commit as little-endian + dest[0] = value & 255; + dest[1] = (value >> 8) & 255; + dest[2] = (value >> 16) & 255; + dest[3] = value >> 24; +} + +static rmtError Buffer_WriteBool(Buffer* buffer, rmtBool value) +{ + return Buffer_Write(buffer, &value, 1); +} + +static rmtError Buffer_WriteU32(Buffer* buffer, rmtU32 value) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + sizeof(value) > buffer->bytes_allocated) + { + rmtTry(Buffer_Grow(buffer, sizeof(value))); + } + +// Copy all bytes +#if RMT_ASSUME_LITTLE_ENDIAN + *(rmtU32*)(buffer->data + buffer->bytes_used) = value; +#else + U32ToByteArray(buffer->data + buffer->bytes_used, value); +#endif + + buffer->bytes_used += sizeof(value); + + return RMT_ERROR_NONE; +} + +static rmtBool IsLittleEndian() +{ + // Not storing this in a global variable allows the compiler to more easily optimise + // this away altogether. + union { + unsigned int i; + unsigned char c[sizeof(unsigned int)]; + } u; + u.i = 1; + return u.c[0] == 1 ? RMT_TRUE : RMT_FALSE; +} + +static rmtError Buffer_WriteF64(Buffer* buffer, rmtF64 value) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + sizeof(value) > buffer->bytes_allocated) + { + rmtTry(Buffer_Grow(buffer, sizeof(value))); + } + +// Copy all bytes +#if RMT_ASSUME_LITTLE_ENDIAN + *(rmtF64*)(buffer->data + buffer->bytes_used) = value; +#else + { + union { + double d; + unsigned char c[sizeof(double)]; + } u; + rmtU8* dest = buffer->data + buffer->bytes_used; + u.d = value; + if (IsLittleEndian()) + { + dest[0] = u.c[0]; + dest[1] = u.c[1]; + dest[2] = u.c[2]; + dest[3] = u.c[3]; + dest[4] = u.c[4]; + dest[5] = u.c[5]; + dest[6] = u.c[6]; + dest[7] = u.c[7]; + } + else + { + dest[0] = u.c[7]; + dest[1] = u.c[6]; + dest[2] = u.c[5]; + dest[3] = u.c[4]; + dest[4] = u.c[3]; + dest[5] = u.c[2]; + dest[6] = u.c[1]; + dest[7] = u.c[0]; + } + } +#endif + + buffer->bytes_used += sizeof(value); + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_WriteU64(Buffer* buffer, rmtU64 value) +{ + // Write as a double as Javascript DataView doesn't have a 64-bit integer read + return Buffer_WriteF64(buffer, (double)value); +} + +static rmtError Buffer_WriteStringWithLength(Buffer* buffer, rmtPStr string) +{ + rmtU32 length = (rmtU32)strnlen_s(string, 2048); + rmtTry(Buffer_WriteU32(buffer, length)); + return Buffer_Write(buffer, (void*)string, length); +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @HASHTABLE: Integer pair hash map for inserts/finds. No removes for added simplicity. +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define RMT_NOT_FOUND 0xffffffffffffffff + +typedef struct +{ + // Non-zero, pre-hashed key + rmtU32 key; + + // Value that's not equal to RMT_NOT_FOUND + rmtU64 value; +} HashSlot; + +typedef struct +{ + // Stats + rmtU32 maxNbSlots; + rmtU32 nbSlots; + + // Data + HashSlot* slots; +} rmtHashTable; + +static rmtError rmtHashTable_Constructor(rmtHashTable* table, rmtU32 max_nb_slots) +{ + // Default initialise + assert(table != NULL); + table->maxNbSlots = max_nb_slots; + table->nbSlots = 0; + + // Allocate and clear the hash slots + rmtTryMallocArray(HashSlot, table->slots, table->maxNbSlots); + memset(table->slots, 0, table->maxNbSlots * sizeof(HashSlot)); + + return RMT_ERROR_NONE; +} + +static void rmtHashTable_Destructor(rmtHashTable* table) +{ + assert(table != NULL); + + if (table->slots != NULL) + { + rmtFree(table->slots); + table->slots = NULL; + } +} + +static rmtError rmtHashTable_Resize(rmtHashTable* table); + +static rmtError rmtHashTable_Insert(rmtHashTable* table, rmtU32 key, rmtU64 value) +{ + HashSlot* slot = NULL; + rmtError error = RMT_ERROR_NONE; + + // Calculate initial slot location for this key + rmtU32 index_mask = table->maxNbSlots - 1; + rmtU32 index = key & index_mask; + + assert(key != 0); + assert(value != RMT_NOT_FOUND); + + // Linear probe for free slot, reusing any existing key matches + // There will always be at least one free slot due to load factor management + while (table->slots[index].key) + { + if (table->slots[index].key == key) + { + // Counter occupied slot increments below + table->nbSlots--; + break; + } + + index = (index + 1) & index_mask; + } + + // Just verify that I've got no errors in the code above + assert(index < table->maxNbSlots); + + // Add to the table + slot = table->slots + index; + slot->key = key; + slot->value = value; + table->nbSlots++; + + // Resize when load factor is greater than 2/3 + if (table->nbSlots > (table->maxNbSlots * 2) / 3) + { + error = rmtHashTable_Resize(table); + } + + return error; +} + +static rmtError rmtHashTable_Resize(rmtHashTable* table) +{ + rmtU32 old_max_nb_slots = table->maxNbSlots; + HashSlot* new_slots = NULL; + HashSlot* old_slots = table->slots; + rmtU32 i; + + // Increase the table size + rmtU32 new_max_nb_slots = table->maxNbSlots; + if (new_max_nb_slots < 8192 * 4) + { + new_max_nb_slots *= 4; + } + else + { + new_max_nb_slots *= 2; + } + + // Allocate and clear a new table + rmtTryMallocArray(HashSlot, new_slots, new_max_nb_slots); + memset(new_slots, 0, new_max_nb_slots * sizeof(HashSlot)); + + // Update fields of the table after successful allocation only + table->slots = new_slots; + table->maxNbSlots = new_max_nb_slots; + table->nbSlots = 0; + + // Reinsert all objects into the new table + for (i = 0; i < old_max_nb_slots; i++) + { + HashSlot* slot = old_slots + i; + if (slot->key != 0) + { + rmtHashTable_Insert(table, slot->key, slot->value); + } + } + + rmtFree(old_slots); + + return RMT_ERROR_NONE; +} + +static rmtU64 rmtHashTable_Find(rmtHashTable* table, rmtU32 key) +{ + // Calculate initial slot location for this key + rmtU32 index_mask = table->maxNbSlots - 1; + rmtU32 index = key & index_mask; + + // Linear probe for matching hash + while (table->slots[index].key) + { + HashSlot* slot = table->slots + index; + + if (slot->key == key) + { + return slot->value; + } + + index = (index + 1) & index_mask; + } + + return RMT_NOT_FOUND; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @STRINGTABLE: Map from string hash to string offset in local buffer +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct +{ + // Growable dynamic array of strings added so far + Buffer* text; + + // Map from text hash to text location in the buffer + rmtHashTable* text_map; +} StringTable; + +static rmtError StringTable_Constructor(StringTable* table) +{ + // Default initialise + assert(table != NULL); + table->text = NULL; + table->text_map = NULL; + + // Allocate reasonably storage for initial sample names + rmtTryNew(Buffer, table->text, 8 * 1024); + rmtTryNew(rmtHashTable, table->text_map, 1 * 1024); + + return RMT_ERROR_NONE; +} + +static void StringTable_Destructor(StringTable* table) +{ + assert(table != NULL); + + rmtDelete(rmtHashTable, table->text_map); + rmtDelete(Buffer, table->text); +} + +static rmtPStr StringTable_Find(StringTable* table, rmtU32 name_hash) +{ + rmtU64 text_offset = rmtHashTable_Find(table->text_map, name_hash); + if (text_offset != RMT_NOT_FOUND) + { + return (rmtPStr)(table->text->data + text_offset); + } + return NULL; +} + +static rmtBool StringTable_Insert(StringTable* table, rmtU32 name_hash, rmtPStr name) +{ + // Only add to the buffer if the string isn't already there + rmtU64 text_offset = rmtHashTable_Find(table->text_map, name_hash); + if (text_offset == RMT_NOT_FOUND) + { + // TODO: Allocation errors aren't being passed on to the caller + text_offset = table->text->bytes_used; + Buffer_WriteStringZ(table->text, name); + rmtHashTable_Insert(table->text_map, name_hash, text_offset); + return RMT_TRUE; + } + + return RMT_FALSE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SOCKETS: Sockets TCP/IP Wrapper +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#ifndef RMT_PLATFORM_WINDOWS +typedef int SOCKET; +#define INVALID_SOCKET -1 +#define SOCKET_ERROR -1 +#define SD_SEND SHUT_WR +#define closesocket close +#endif + +typedef struct +{ + SOCKET socket; +} TCPSocket; + +typedef struct +{ + rmtBool can_read; + rmtBool can_write; + rmtError error_state; +} SocketStatus; + +// +// Function prototypes +// +static void TCPSocket_Close(TCPSocket* tcp_socket); + +static rmtError InitialiseNetwork() +{ +#ifdef RMT_PLATFORM_WINDOWS + + WSADATA wsa_data; + if (WSAStartup(MAKEWORD(2, 2), &wsa_data)) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "WSAStartup failed"); + } + if (LOBYTE(wsa_data.wVersion) != 2 || HIBYTE(wsa_data.wVersion) != 2) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "WSAStartup returned incorrect version number"); + } + + return RMT_ERROR_NONE; + +#else + + return RMT_ERROR_NONE; + +#endif +} + +static void ShutdownNetwork() +{ +#ifdef RMT_PLATFORM_WINDOWS + WSACleanup(); +#endif +} + +static rmtError TCPSocket_Constructor(TCPSocket* tcp_socket) +{ + assert(tcp_socket != NULL); + tcp_socket->socket = INVALID_SOCKET; + return InitialiseNetwork(); +} + +static void TCPSocket_Destructor(TCPSocket* tcp_socket) +{ + assert(tcp_socket != NULL); + TCPSocket_Close(tcp_socket); + ShutdownNetwork(); +} + +static rmtError TCPSocket_RunServer(TCPSocket* tcp_socket, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + SOCKET s = INVALID_SOCKET; + struct sockaddr_in sin; +#ifdef RMT_PLATFORM_WINDOWS + u_long nonblock = 1; +#endif + + memset(&sin, 0, sizeof(sin)); + assert(tcp_socket != NULL); + + // Try to create the socket + s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "Can't create a socket for connection to the remote viewer"); + } + + if (reuse_open_port) + { + int enable = 1; + +// set SO_REUSEADDR so binding doesn't fail when restarting the application +// (otherwise the same port can't be reused within TIME_WAIT) +// I'm not checking for errors because if this fails (unlikely) we might still +// be able to bind to the socket anyway +#ifdef RMT_PLATFORM_POSIX + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); +#elif defined(RMT_PLATFORM_WINDOWS) + // windows also needs SO_EXCLUSEIVEADDRUSE, + // see http://www.andy-pearce.com/blog/posts/2013/Feb/so_reuseaddr-on-windows/ + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable)); + enable = 1; + setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&enable, sizeof(enable)); +#endif + } + + // Bind the socket to the incoming port + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(limit_connections_to_localhost ? INADDR_LOOPBACK : INADDR_ANY); + sin.sin_port = htons(port); + if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Can't bind a socket for the server"); + } + + // Connection is valid, remaining code is socket state modification + tcp_socket->socket = s; + + // Enter a listening state with a backlog of 1 connection + if (listen(s, 1) == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Created server socket failed to enter a listen state"); + } + +// Set as non-blocking +#ifdef RMT_PLATFORM_WINDOWS + if (ioctlsocket(tcp_socket->socket, FIONBIO, &nonblock) == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Created server socket failed to switch to a non-blocking state"); + } +#else + if (fcntl(tcp_socket->socket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Created server socket failed to switch to a non-blocking state"); + } +#endif + + return RMT_ERROR_NONE; +} + +static void TCPSocket_Close(TCPSocket* tcp_socket) +{ + assert(tcp_socket != NULL); + + if (tcp_socket->socket != INVALID_SOCKET) + { + // Shutdown the connection, stopping all sends + int result = shutdown(tcp_socket->socket, SD_SEND); + if (result != SOCKET_ERROR) + { + // Keep receiving until the peer closes the connection + int total = 0; + char temp_buf[128]; + while (result > 0) + { + result = (int)recv(tcp_socket->socket, temp_buf, sizeof(temp_buf), 0); + total += result; + } + } + + // Close the socket and issue a network shutdown request + closesocket(tcp_socket->socket); + tcp_socket->socket = INVALID_SOCKET; + } +} + +static SocketStatus TCPSocket_PollStatus(TCPSocket* tcp_socket) +{ + SocketStatus status; + fd_set fd_read, fd_write, fd_errors; + struct timeval tv; + + status.can_read = RMT_FALSE; + status.can_write = RMT_FALSE; + status.error_state = RMT_ERROR_NONE; + + assert(tcp_socket != NULL); + if (tcp_socket->socket == INVALID_SOCKET) + { + status.error_state = RMT_ERROR_SOCKET_INVALID_POLL; + return status; + } + + // Set read/write/error markers for the socket + FD_ZERO(&fd_read); + FD_ZERO(&fd_write); + FD_ZERO(&fd_errors); +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4127) // warning C4127: conditional expression is constant +#endif // _MSC_VER + FD_SET(tcp_socket->socket, &fd_read); + FD_SET(tcp_socket->socket, &fd_write); + FD_SET(tcp_socket->socket, &fd_errors); +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + + // Poll socket status without blocking + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select(((int)tcp_socket->socket) + 1, &fd_read, &fd_write, &fd_errors, &tv) == SOCKET_ERROR) + { + status.error_state = RMT_ERROR_SOCKET_SELECT_FAIL; + return status; + } + + status.can_read = FD_ISSET(tcp_socket->socket, &fd_read) != 0 ? RMT_TRUE : RMT_FALSE; + status.can_write = FD_ISSET(tcp_socket->socket, &fd_write) != 0 ? RMT_TRUE : RMT_FALSE; + status.error_state = FD_ISSET(tcp_socket->socket, &fd_errors) != 0 ? RMT_ERROR_SOCKET_POLL_ERRORS : RMT_ERROR_NONE; + return status; +} + +static rmtError TCPSocket_AcceptConnection(TCPSocket* tcp_socket, TCPSocket** client_socket) +{ + SocketStatus status; + SOCKET s; + + // Ensure there is an incoming connection + assert(tcp_socket != NULL); + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE || !status.can_read) + return status.error_state; + + // Accept the connection + s = accept(tcp_socket->socket, 0, 0); + if (s == SOCKET_ERROR) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "Server failed to accept connection from client"); + } + +#ifdef SO_NOSIGPIPE + // On POSIX systems, send() may send a SIGPIPE signal when writing to an + // already closed connection. By setting this option, we prevent the + // signal from being emitted and send will instead return an error and set + // errno to EPIPE. + // + // This is supported on BSD platforms and not on Linux. + { + int flag = 1; + setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(flag)); + } +#endif + // Create a client socket for the new connection + assert(client_socket != NULL); + rmtTryNew(TCPSocket, *client_socket); + (*client_socket)->socket = s; + + return RMT_ERROR_NONE; +} + +static int TCPTryAgain() +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD error = WSAGetLastError(); + return error == WSAEWOULDBLOCK; +#else +#if EAGAIN == EWOULDBLOCK + return errno == EAGAIN; +#else + return errno == EAGAIN || errno == EWOULDBLOCK; +#endif +#endif +} + +static rmtError TCPSocket_Send(TCPSocket* tcp_socket, const void* data, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data = NULL; + char* end_data = NULL; + rmtU32 start_ms = 0; + rmtU32 cur_ms = 0; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Loop until timeout checking whether data can be written + status.can_write = RMT_FALSE; + while (!status.can_write) + { + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + cur_ms = msTimer_Get(); + if (cur_ms - start_ms > timeout_ms) + { + return rmtMakeError(RMT_ERROR_TIMEOUT, "Timed out trying to send data"); + } + } + + cur_data = (char*)data; + end_data = cur_data + length; + + while (cur_data < end_data) + { + // Attempt to send the remaining chunk of data + int bytes_sent; + int send_flags = 0; +#ifdef MSG_NOSIGNAL + // On Linux this prevents send from emitting a SIGPIPE signal + // Equivalent on BSD to the SO_NOSIGPIPE option. + send_flags = MSG_NOSIGNAL; +#endif + bytes_sent = (int)send(tcp_socket->socket, cur_data, (int)(end_data - cur_data), send_flags); + + if (bytes_sent == SOCKET_ERROR || bytes_sent == 0) + { + // Close the connection if sending fails for any other reason other than blocking + if (bytes_sent != 0 && !TCPTryAgain()) + return RMT_ERROR_SOCKET_SEND_FAIL; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) endpoint is no longer there + // 2) endpoint can't consume quick enough + // 3) local buffers overflow + // + // As none of these are actually errors, we have to pass this timeout back to the caller. + // + // TODO: This strategy breaks down if a send partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return rmtMakeError(RMT_ERROR_TIMEOUT, "Timed out trying to send data"); + } + } + else + { + // Jump over the data sent + cur_data += bytes_sent; + } + } + + return RMT_ERROR_NONE; +} + +static rmtError TCPSocket_Receive(TCPSocket* tcp_socket, void* data, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data = NULL; + char* end_data = NULL; + rmtU32 start_ms = 0; + rmtU32 cur_ms = 0; + + assert(tcp_socket != NULL); + + // Ensure there is data to receive + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + if (!status.can_read) + return RMT_ERROR_SOCKET_RECV_NO_DATA; + + cur_data = (char*)data; + end_data = cur_data + length; + + // Loop until all data has been received + start_ms = msTimer_Get(); + while (cur_data < end_data) + { + int bytes_received = (int)recv(tcp_socket->socket, cur_data, (int)(end_data - cur_data), 0); + + if (bytes_received == SOCKET_ERROR || bytes_received == 0) + { + // Close the connection if receiving fails for any other reason other than blocking + if (bytes_received != 0 && !TCPTryAgain()) + return RMT_ERROR_SOCKET_RECV_FAILED; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) data is delayed by sender + // 2) sender fails to send a complete set of packets + // + // As not all of these scenarios are errors, we need to pass this information back to the caller. + // + // TODO: This strategy breaks down if a receive partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + } + } + else + { + // Jump over the data received + cur_data += bytes_received; + } + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SHA1: SHA-1 Cryptographic Hash Function +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// Typed to allow enforced data size specification +// +typedef struct +{ + rmtU8 data[20]; +} SHA1; + +/* + Copyright (c) 2011, Micael Hildenborg + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Micael Hildenborg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + Contributors: + Gustav + Several members in the gamedev.se forum. + Gregory Petrosyan + */ + +// Rotate an integer value to left. +static unsigned int rol(const unsigned int value, const unsigned int steps) +{ + return ((value << steps) | (value >> (32 - steps))); +} + +// Sets the first 16 integers in the buffert to zero. +// Used for clearing the W buffert. +static void clearWBuffert(unsigned int* buffert) +{ + int pos; + for (pos = 16; --pos >= 0;) + { + buffert[pos] = 0; + } +} + +static void innerHash(unsigned int* result, unsigned int* w) +{ + unsigned int a = result[0]; + unsigned int b = result[1]; + unsigned int c = result[2]; + unsigned int d = result[3]; + unsigned int e = result[4]; + + int round = 0; + +#define sha1macro(func, val) \ + { \ + const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \ + e = d; \ + d = c; \ + c = rol(b, 30); \ + b = a; \ + a = t; \ + } + + while (round < 16) + { + sha1macro((b & c) | (~b & d), 0x5a827999); + ++round; + } + while (round < 20) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (~b & d), 0x5a827999); + ++round; + } + while (round < 40) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0x6ed9eba1); + ++round; + } + while (round < 60) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc); + ++round; + } + while (round < 80) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0xca62c1d6); + ++round; + } + +#undef sha1macro + + result[0] += a; + result[1] += b; + result[2] += c; + result[3] += d; + result[4] += e; +} + +static void calc(const void* src, const int bytelength, unsigned char* hash) +{ + int roundPos; + int lastBlockBytes; + int hashByte; + + // Init the result array. + unsigned int result[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0}; + + // Cast the void src pointer to be the byte array we can work with. + const unsigned char* sarray = (const unsigned char*)src; + + // The reusable round buffer + unsigned int w[80]; + + // Loop through all complete 64byte blocks. + const int endOfFullBlocks = bytelength - 64; + int endCurrentBlock; + int currentBlock = 0; + + while (currentBlock <= endOfFullBlocks) + { + endCurrentBlock = currentBlock + 64; + + // Init the round buffer with the 64 byte block data. + for (roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4) + { + // This line will swap endian on big endian and keep endian on little endian. + w[roundPos++] = (unsigned int)sarray[currentBlock + 3] | (((unsigned int)sarray[currentBlock + 2]) << 8) | + (((unsigned int)sarray[currentBlock + 1]) << 16) | + (((unsigned int)sarray[currentBlock]) << 24); + } + innerHash(result, w); + } + + // Handle the last and not full 64 byte block if existing. + endCurrentBlock = bytelength - currentBlock; + clearWBuffert(w); + lastBlockBytes = 0; + for (; lastBlockBytes < endCurrentBlock; ++lastBlockBytes) + { + w[lastBlockBytes >> 2] |= (unsigned int)sarray[lastBlockBytes + currentBlock] + << ((3 - (lastBlockBytes & 3)) << 3); + } + w[lastBlockBytes >> 2] |= 0x80U << ((3 - (lastBlockBytes & 3)) << 3); + if (endCurrentBlock >= 56) + { + innerHash(result, w); + clearWBuffert(w); + } + w[15] = bytelength << 3; + innerHash(result, w); + + // Store hash in result pointer, and make sure we get in in the correct order on both endian models. + for (hashByte = 20; --hashByte >= 0;) + { + hash[hashByte] = (result[hashByte >> 2] >> (((3 - hashByte) & 0x3) << 3)) & 0xff; + } +} + +static SHA1 SHA1_Calculate(const void* src, unsigned int length) +{ + SHA1 hash; + assert((int)length >= 0); + calc(src, length, hash.data); + return hash; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @BASE64: Base-64 encoder +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static const char* b64_encoding_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static rmtU32 Base64_CalculateEncodedLength(rmtU32 length) +{ + // ceil(l * 4/3) + return 4 * ((length + 2) / 3); +} + +static void Base64_Encode(const rmtU8* in_bytes, rmtU32 length, rmtU8* out_bytes) +{ + rmtU32 i; + rmtU32 encoded_length; + rmtU32 remaining_bytes; + + rmtU8* optr = out_bytes; + + for (i = 0; i < length;) + { + // Read input 3 values at a time, null terminating + rmtU32 c0 = i < length ? in_bytes[i++] : 0; + rmtU32 c1 = i < length ? in_bytes[i++] : 0; + rmtU32 c2 = i < length ? in_bytes[i++] : 0; + + // Encode 4 bytes for ever 3 input bytes + rmtU32 triple = (c0 << 0x10) + (c1 << 0x08) + c2; + *optr++ = b64_encoding_table[(triple >> 3 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 2 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 1 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 0 * 6) & 0x3F]; + } + + // Pad output to multiple of 3 bytes with terminating '=' + encoded_length = Base64_CalculateEncodedLength(length); + remaining_bytes = (3 - ((length + 2) % 3)) - 1; + for (i = 0; i < remaining_bytes; i++) + out_bytes[encoded_length - 1 - i] = '='; + + // Null terminate + out_bytes[encoded_length] = 0; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MURMURHASH: MurmurHash3 + https://code.google.com/p/smhasher +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. +//----------------------------------------------------------------------------- + +#if RMT_USE_INTERNAL_HASH_FUNCTION + +static rmtU32 rotl32(rmtU32 x, rmtS8 r) +{ + return (x << r) | (x >> (32 - r)); +} + +// Block read - if your platform needs to do endian-swapping, do the conversion here +static rmtU32 getblock32(const rmtU32* p, int i) +{ + rmtU32 result; + const rmtU8* src = ((const rmtU8*)p) + i * (int)sizeof(rmtU32); + memcpy(&result, src, sizeof(result)); + return result; +} + +// Finalization mix - force all bits of a hash block to avalanche +static rmtU32 fmix32(rmtU32 h) +{ + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + return h; +} + +static rmtU32 MurmurHash3_x86_32(const void* key, int len, rmtU32 seed) +{ + const rmtU8* data = (const rmtU8*)key; + const int nblocks = len / 4; + + rmtU32 h1 = seed; + + const rmtU32 c1 = 0xcc9e2d51; + const rmtU32 c2 = 0x1b873593; + + int i; + + const rmtU32* blocks = (const rmtU32*)(data + nblocks * 4); + const rmtU8* tail = (const rmtU8*)(data + nblocks * 4); + + rmtU32 k1 = 0; + + //---------- + // body + + for (i = -nblocks; i; i++) + { + rmtU32 k2 = getblock32(blocks, i); + + k2 *= c1; + k2 = rotl32(k2, 15); + k2 *= c2; + + h1 ^= k2; + h1 = rotl32(h1, 13); + h1 = h1 * 5 + 0xe6546b64; + } + + //---------- + // tail + + switch (len & 3) + { + case 3: + k1 ^= tail[2] << 16; // fallthrough + case 2: + k1 ^= tail[1] << 8; // fallthrough + case 1: + k1 ^= tail[0]; + k1 *= c1; + k1 = rotl32(k1, 15); + k1 *= c2; + h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + + h1 = fmix32(h1); + + return h1; +} + +RMT_API rmtU32 _rmt_HashString32(const char* s, int len, rmtU32 seed) +{ + return MurmurHash3_x86_32(s, len, seed); +} + +#else + #if defined(__cplusplus) + extern "C" + #endif + RMT_API rmtU32 _rmt_HashString32(const char* s, int len, rmtU32 seed); + +#endif // RMT_USE_INTERNAL_HASH_FUNCTION + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @WEBSOCKETS: WebSockets +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +enum WebSocketMode +{ + WEBSOCKET_NONE = 0, + WEBSOCKET_TEXT = 1, + WEBSOCKET_BINARY = 2, +}; + +typedef struct +{ + TCPSocket* tcp_socket; + + enum WebSocketMode mode; + + rmtU32 frame_bytes_remaining; + rmtU32 mask_offset; + + union { + rmtU8 mask[4]; + rmtU32 mask_u32; + } data; + +} WebSocket; + +static void WebSocket_Close(WebSocket* web_socket); + +static char* GetField(char* buffer, r_size_t buffer_length, rmtPStr field_name) +{ + char* field = NULL; + char* buffer_end = buffer + buffer_length - 1; + + r_size_t field_length = strnlen_s(field_name, buffer_length); + if (field_length == 0) + return NULL; + + // Search for the start of the field + if (strstr_s(buffer, buffer_length, field_name, field_length, &field) != EOK) + return NULL; + + // Field name is now guaranteed to be in the buffer so its safe to jump over it without hitting the bounds + field += strlen(field_name); + + // Skip any trailing whitespace + while (*field == ' ') + { + if (field >= buffer_end) + return NULL; + field++; + } + + return field; +} + +static const char websocket_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +static const char websocket_response[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: "; + +static rmtError WebSocketHandshake(TCPSocket* tcp_socket, rmtPStr limit_host) +{ + rmtU32 start_ms, now_ms; + + // Parsing scratchpad + char buffer[1024]; + char* buffer_ptr = buffer; + int buffer_len = sizeof(buffer) - 1; + char* buffer_end = buffer + buffer_len; + + char response_buffer[256]; + int response_buffer_len = sizeof(response_buffer) - 1; + + char* version; + char* host; + char* key; + char* key_end; + SHA1 hash; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Really inefficient way of receiving the handshake data from the browser + // Not really sure how to do this any better, as the termination requirement is \r\n\r\n + while (buffer_ptr - buffer < buffer_len) + { + rmtError error = TCPSocket_Receive(tcp_socket, buffer_ptr, 1, 20); + if (error == RMT_ERROR_SOCKET_RECV_FAILED) + return error; + + // If there's a stall receiving the data, check for a handshake timeout + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + now_ms = msTimer_Get(); + if (now_ms - start_ms > 1000) + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + + continue; + } + + // Just in case new enums are added... + assert(error == RMT_ERROR_NONE); + + if (buffer_ptr - buffer >= 4) + { + if (*(buffer_ptr - 3) == '\r' && *(buffer_ptr - 2) == '\n' && *(buffer_ptr - 1) == '\r' && + *(buffer_ptr - 0) == '\n') + break; + } + + buffer_ptr++; + } + *buffer_ptr = 0; + + // HTTP GET instruction + if (memcmp(buffer, "GET", 3) != 0) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET; + + // Look for the version number and verify that it's supported + version = GetField(buffer, buffer_len, "Sec-WebSocket-Version:"); + if (version == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION; + if (buffer_end - version < 2 || (version[0] != '8' && (version[0] != '1' || version[1] != '3'))) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION; + + // Make sure this connection comes from a known host + host = GetField(buffer, buffer_len, "Host:"); + if (host == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST; + if (limit_host != NULL) + { + r_size_t limit_host_len = strnlen_s(limit_host, 128); + char* found = NULL; + if (strstr_s(host, (r_size_t)(buffer_end - host), limit_host, limit_host_len, &found) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST; + } + + // Look for the key start and null-terminate it within the receive buffer + key = GetField(buffer, buffer_len, "Sec-WebSocket-Key:"); + if (key == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY; + if (strstr_s(key, (r_size_t)(buffer_end - key), "\r\n", 2, &key_end) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY; + *key_end = 0; + + // Concatenate the browser's key with the WebSocket Protocol GUID and base64 encode + // the hash, to prove to the browser that this is a bonafide WebSocket server + buffer[0] = 0; + if (strncat_s(buffer, buffer_len, key, (r_size_t)(key_end - key)) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(buffer, buffer_len, websocket_guid, sizeof(websocket_guid)) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + hash = SHA1_Calculate(buffer, (rmtU32)strnlen_s(buffer, buffer_len)); + Base64_Encode(hash.data, sizeof(hash.data), (rmtU8*)buffer); + + // Send the response back to the server with a longer timeout than usual + response_buffer[0] = 0; + if (strncat_s(response_buffer, response_buffer_len, websocket_response, sizeof(websocket_response)) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(response_buffer, response_buffer_len, buffer, buffer_len) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(response_buffer, response_buffer_len, "\r\n\r\n", 4) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + + return TCPSocket_Send(tcp_socket, response_buffer, (rmtU32)strnlen_s(response_buffer, response_buffer_len), 1000); +} + +static rmtError WebSocket_Constructor(WebSocket* web_socket, TCPSocket* tcp_socket) +{ + rmtError error = RMT_ERROR_NONE; + + assert(web_socket != NULL); + web_socket->tcp_socket = tcp_socket; + web_socket->mode = WEBSOCKET_NONE; + web_socket->frame_bytes_remaining = 0; + web_socket->mask_offset = 0; + web_socket->data.mask[0] = 0; + web_socket->data.mask[1] = 0; + web_socket->data.mask[2] = 0; + web_socket->data.mask[3] = 0; + + // Caller can optionally specify which TCP socket to use + if (web_socket->tcp_socket == NULL) + rmtTryNew(TCPSocket, web_socket->tcp_socket); + + return error; +} + +static void WebSocket_Destructor(WebSocket* web_socket) +{ + WebSocket_Close(web_socket); +} + +static rmtError WebSocket_RunServer(WebSocket* web_socket, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost, enum WebSocketMode mode) +{ + // Create the server's listening socket + assert(web_socket != NULL); + web_socket->mode = mode; + return TCPSocket_RunServer(web_socket->tcp_socket, port, reuse_open_port, limit_connections_to_localhost); +} + +static void WebSocket_Close(WebSocket* web_socket) +{ + assert(web_socket != NULL); + rmtDelete(TCPSocket, web_socket->tcp_socket); +} + +static SocketStatus WebSocket_PollStatus(WebSocket* web_socket) +{ + assert(web_socket != NULL); + return TCPSocket_PollStatus(web_socket->tcp_socket); +} + +static rmtError WebSocket_AcceptConnection(WebSocket* web_socket, WebSocket** client_socket) +{ + TCPSocket* tcp_socket = NULL; + + // Is there a waiting connection? + assert(web_socket != NULL); + rmtTry(TCPSocket_AcceptConnection(web_socket->tcp_socket, &tcp_socket)); + if (tcp_socket == NULL) + return RMT_ERROR_NONE; + + // Need a successful handshake between client/server before allowing the connection + // TODO: Specify limit_host + rmtTry(WebSocketHandshake(tcp_socket, NULL)); + + // Allocate and return a new client socket + assert(client_socket != NULL); + rmtTryNew(WebSocket, *client_socket, tcp_socket); + + (*client_socket)->mode = web_socket->mode; + + return RMT_ERROR_NONE; +} + +static void WriteSize(rmtU32 size, rmtU8* dest, rmtU32 dest_size, rmtU32 dest_offset) +{ + int size_size = dest_size - dest_offset; + rmtU32 i; + for (i = 0; i < dest_size; i++) + { + int j = i - dest_offset; + dest[i] = (j < 0) ? 0 : (size >> ((size_size - j - 1) * 8)) & 0xFF; + } +} + +// For send buffers to preallocate +#define WEBSOCKET_MAX_FRAME_HEADER_SIZE 10 + +static void WebSocket_PrepareBuffer(Buffer* buffer) +{ + char empty_frame_header[WEBSOCKET_MAX_FRAME_HEADER_SIZE]; + + assert(buffer != NULL); + + // Reset to start + buffer->bytes_used = 0; + + // Allocate enough space for a maximum-sized frame header + Buffer_Write(buffer, empty_frame_header, sizeof(empty_frame_header)); +} + +static rmtU32 WebSocket_FrameHeaderSize(rmtU32 length) +{ + if (length <= 125) + return 2; + if (length <= 65535) + return 4; + return 10; +} + +static void WebSocket_WriteFrameHeader(WebSocket* web_socket, rmtU8* dest, rmtU32 length) +{ + rmtU8 final_fragment = 0x1 << 7; + rmtU8 frame_type = (rmtU8)web_socket->mode; + + dest[0] = final_fragment | frame_type; + + // Construct the frame header, correctly applying the narrowest size + if (length <= 125) + { + dest[1] = (rmtU8)length; + } + else if (length <= 65535) + { + dest[1] = 126; + WriteSize(length, dest + 2, 2, 0); + } + else + { + dest[1] = 127; + WriteSize(length, dest + 2, 8, 4); + } +} + +static rmtError WebSocket_Send(WebSocket* web_socket, const void* data, rmtU32 length, rmtU32 timeout_ms) +{ + rmtError error; + SocketStatus status; + rmtU32 payload_length, frame_header_size, delta; + + assert(web_socket != NULL); + assert(data != NULL); + + // Can't send if there are socket errors + status = WebSocket_PollStatus(web_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + // Assume space for max frame header has been allocated in the incoming data + payload_length = length - WEBSOCKET_MAX_FRAME_HEADER_SIZE; + frame_header_size = WebSocket_FrameHeaderSize(payload_length); + delta = WEBSOCKET_MAX_FRAME_HEADER_SIZE - frame_header_size; + data = (void*)((rmtU8*)data + delta); + length -= delta; + WebSocket_WriteFrameHeader(web_socket, (rmtU8*)data, payload_length); + + // Send frame header and data together + error = TCPSocket_Send(web_socket->tcp_socket, data, length, timeout_ms); + return error; +} + +static rmtError ReceiveFrameHeader(WebSocket* web_socket) +{ + // TODO: Specify infinite timeout? + + rmtU8 msg_header[2] = {0, 0}; + int msg_length, size_bytes_remaining, i; + rmtBool mask_present; + + assert(web_socket != NULL); + + // Get message header + rmtTry(TCPSocket_Receive(web_socket->tcp_socket, msg_header, 2, 20)); + + // Check for WebSocket Protocol disconnect + if (msg_header[0] == 0x88) + return RMT_ERROR_WEBSOCKET_DISCONNECTED; + + // Check that the client isn't sending messages we don't understand + if (msg_header[0] != 0x81 && msg_header[0] != 0x82) + return RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER; + + // Get message length and check to see if it's a marker for a wider length + msg_length = msg_header[1] & 0x7F; + size_bytes_remaining = 0; + switch (msg_length) + { + case 126: + size_bytes_remaining = 2; + break; + case 127: + size_bytes_remaining = 8; + break; + } + + if (size_bytes_remaining > 0) + { + // Receive the wider bytes of the length + rmtU8 size_bytes[8]; + rmtTry(TCPSocket_Receive(web_socket->tcp_socket, size_bytes, size_bytes_remaining, 20)); + + // Calculate new length, MSB first + msg_length = 0; + for (i = 0; i < size_bytes_remaining; i++) + msg_length |= size_bytes[i] << ((size_bytes_remaining - 1 - i) * 8); + } + + // Receive any message data masks + mask_present = (msg_header[1] & 0x80) != 0 ? RMT_TRUE : RMT_FALSE; + if (mask_present) + { + rmtTry(TCPSocket_Receive(web_socket->tcp_socket, web_socket->data.mask, 4, 20)); + } + + web_socket->frame_bytes_remaining = msg_length; + web_socket->mask_offset = 0; + + return RMT_ERROR_NONE; +} + +static rmtError WebSocket_Receive(WebSocket* web_socket, void* data, rmtU32* msg_len, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data; + char* end_data; + rmtU32 start_ms, now_ms; + rmtU32 bytes_to_read; + rmtError error; + + assert(web_socket != NULL); + + // Can't read with any socket errors + status = WebSocket_PollStatus(web_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + cur_data = (char*)data; + end_data = cur_data + length; + + start_ms = msTimer_Get(); + while (cur_data < end_data) + { + // Get next WebSocket frame if we've run out of data to read from the socket + if (web_socket->frame_bytes_remaining == 0) + { + rmtTry(ReceiveFrameHeader(web_socket)); + + // Set output message length only on initial receive + if (msg_len != NULL) + *msg_len = web_socket->frame_bytes_remaining; + } + + // Read as much required data as possible + bytes_to_read = web_socket->frame_bytes_remaining < length ? web_socket->frame_bytes_remaining : length; + error = TCPSocket_Receive(web_socket->tcp_socket, cur_data, bytes_to_read, 20); + if (error == RMT_ERROR_SOCKET_RECV_FAILED) + return error; + + // If there's a stall receiving the data, check for timeout + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + now_ms = msTimer_Get(); + if (now_ms - start_ms > timeout_ms) + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + continue; + } + + // Apply data mask + if (web_socket->data.mask_u32 != 0) + { + rmtU32 i; + for (i = 0; i < bytes_to_read; i++) + { + *((rmtU8*)cur_data + i) ^= web_socket->data.mask[web_socket->mask_offset & 3]; + web_socket->mask_offset++; + } + } + + cur_data += bytes_to_read; + web_socket->frame_bytes_remaining -= bytes_to_read; + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MESSAGEQ: Multiple producer, single consumer message queue +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef enum MessageID +{ + MsgID_NotReady, + MsgID_AddToStringTable, + MsgID_LogText, + MsgID_SampleTree, + MsgID_ProcessorThreads, + MsgID_None, + MsgID_PropertySnapshot, + MsgID_Force32Bits = 0xFFFFFFFF, +} MessageID; + +typedef struct Message +{ + MessageID id; + + rmtU32 payload_size; + + // For telling which thread the message came from in the debugger + struct ThreadProfiler* threadProfiler; + + rmtU8 payload[1]; +} Message; + +// Multiple producer, single consumer message queue that uses its own data buffer +// to store the message data. +typedef struct rmtMessageQueue +{ + rmtU32 size; + + // The physical address of this data buffer is pointed to by two sequential + // virtual memory pages, allowing automatic wrap-around of any reads or writes + // that exceed the limits of the buffer. + VirtualMirrorBuffer* data; + + // Read/write position never wrap allowing trivial overflow checks + // with easier debugging + rmtAtomicU32 read_pos; + rmtAtomicU32 write_pos; + +} rmtMessageQueue; + +static rmtError rmtMessageQueue_Constructor(rmtMessageQueue* queue, rmtU32 size) +{ + assert(queue != NULL); + + // Set defaults + queue->size = 0; + queue->data = NULL; + queue->read_pos = 0; + queue->write_pos = 0; + + rmtTryNew(VirtualMirrorBuffer, queue->data, size, 10); + + // The mirror buffer needs to be page-aligned and will change the requested + // size to match that. + queue->size = queue->data->size; + + // Set the entire buffer to not ready message + memset(queue->data->ptr, MsgID_NotReady, queue->size); + + return RMT_ERROR_NONE; +} + +static void rmtMessageQueue_Destructor(rmtMessageQueue* queue) +{ + assert(queue != NULL); + rmtDelete(VirtualMirrorBuffer, queue->data); +} + +static rmtU32 rmtMessageQueue_SizeForPayload(rmtU32 payload_size) +{ + // Add message header and align for ARM platforms + rmtU32 size = sizeof(Message) + payload_size; +#if defined(RMT_ARCH_64BIT) + size = (size + 7) & ~7U; +#else + size = (size + 3) & ~3U; +#endif + return size; +} + +static Message* rmtMessageQueue_AllocMessage(rmtMessageQueue* queue, rmtU32 payload_size, + struct ThreadProfiler* thread_profiler) +{ + Message* msg; + + rmtU32 write_size = rmtMessageQueue_SizeForPayload(payload_size); + + assert(queue != NULL); + + for (;;) + { + // Check for potential overflow + // Order of loads means allocation failure can happen when enough space has just been freed + // However, incorrect overflows are not possible + rmtU32 s = queue->size; + rmtU32 w = LoadAcquire(&queue->write_pos); + rmtU32 r = LoadAcquire(&queue->read_pos); + if ((int)(w - r) > ((int)(s - write_size))) + return NULL; + + // Point to the newly allocated space + msg = (Message*)(queue->data->ptr + (w & (s - 1))); + + // Increment the write position, leaving the loop if this is the thread that succeeded + if (AtomicCompareAndSwapU32(&queue->write_pos, w, w + write_size) == RMT_TRUE) + { + // Safe to set payload size after thread claims ownership of this allocated range + msg->payload_size = payload_size; + msg->threadProfiler = thread_profiler; + break; + } + } + + return msg; +} + +static void rmtMessageQueue_CommitMessage(Message* message, MessageID id) +{ + assert(message != NULL); + + // Setting the message ID signals to the consumer that the message is ready + assert(LoadAcquire((rmtU32*)&message->id) == MsgID_NotReady); + StoreRelease((rmtU32*)&message->id, id); +} + +Message* rmtMessageQueue_PeekNextMessage(rmtMessageQueue* queue) +{ + Message* ptr; + rmtU32 r, w; + MessageID id; + + assert(queue != NULL); + + // First check that there are bytes queued + w = LoadAcquire(&queue->write_pos); + r = queue->read_pos; + if (w - r == 0) + return NULL; + + // Messages are in the queue but may not have been commit yet + // Messages behind this one may have been commit but it's not reachable until + // the next one in the queue is ready. + r = r & (queue->size - 1); + ptr = (Message*)(queue->data->ptr + r); + id = (MessageID)LoadAcquire((rmtU32*)&ptr->id); + if (id != MsgID_NotReady) + return ptr; + + return NULL; +} + +static void rmtMessageQueue_ConsumeNextMessage(rmtMessageQueue* queue, Message* message) +{ + rmtU32 message_size, read_pos; + + assert(queue != NULL); + assert(message != NULL); + + // Setting the message ID to "not ready" serves as a marker to the consumer that even though + // space has been allocated for a message, the message isn't ready to be consumed + // yet. + // + // We can't do that when allocating the message because multiple threads will be fighting for + // the same location. Instead, clear out any messages just read by the consumer before advancing + // the read position so that a winning thread's allocation will inherit the "not ready" state. + // + // This costs some write bandwidth and has the potential to flush cache to other cores. + message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + memset(message, MsgID_NotReady, message_size); + + // Advance read position + read_pos = queue->read_pos + message_size; + StoreRelease(&queue->read_pos, read_pos); +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @NETWORK: Network Server +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef rmtError (*Server_ReceiveHandler)(void*, char*, rmtU32); + +typedef struct +{ + WebSocket* listen_socket; + + WebSocket* client_socket; + + rmtU32 last_ping_time; + + rmtU16 port; + + rmtBool reuse_open_port; + rmtBool limit_connections_to_localhost; + + // A dynamically-sized buffer used for binary-encoding messages and sending to the client + Buffer* bin_buf; + + // Handler for receiving messages from the client + Server_ReceiveHandler receive_handler; + void* receive_handler_context; +} Server; + +static rmtError Server_CreateListenSocket(Server* server, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + rmtTryNew(WebSocket, server->listen_socket, NULL); + rmtTry(WebSocket_RunServer(server->listen_socket, port, reuse_open_port, limit_connections_to_localhost, WEBSOCKET_BINARY)); + return RMT_ERROR_NONE; +} + +static rmtError Server_Constructor(Server* server, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + assert(server != NULL); + server->listen_socket = NULL; + server->client_socket = NULL; + server->last_ping_time = 0; + server->port = port; + server->reuse_open_port = reuse_open_port; + server->limit_connections_to_localhost = limit_connections_to_localhost; + server->bin_buf = NULL; + server->receive_handler = NULL; + server->receive_handler_context = NULL; + + // Create the binary serialisation buffer + rmtTryNew(Buffer, server->bin_buf, 4096); + + // Create the listening WebSocket + return Server_CreateListenSocket(server, port, reuse_open_port, limit_connections_to_localhost); +} + +static void Server_Destructor(Server* server) +{ + assert(server != NULL); + rmtDelete(WebSocket, server->client_socket); + rmtDelete(WebSocket, server->listen_socket); + rmtDelete(Buffer, server->bin_buf); +} + +static rmtBool Server_IsClientConnected(Server* server) +{ + assert(server != NULL); + return server->client_socket != NULL ? RMT_TRUE : RMT_FALSE; +} + +static void Server_DisconnectClient(Server* server) +{ + WebSocket* client_socket; + + assert(server != NULL); + + // NULL the variable before destroying the socket + client_socket = server->client_socket; + server->client_socket = NULL; + CompilerWriteFence(); + rmtDelete(WebSocket, client_socket); +} + +static rmtError Server_Send(Server* server, const void* data, rmtU32 length, rmtU32 timeout) +{ + assert(server != NULL); + if (Server_IsClientConnected(server)) + { + rmtError error = WebSocket_Send(server->client_socket, data, length, timeout); + if (error == RMT_ERROR_SOCKET_SEND_FAIL) + Server_DisconnectClient(server); + + return error; + } + + return RMT_ERROR_NONE; +} + +static rmtError Server_ReceiveMessage(Server* server, char message_first_byte, rmtU32 message_length) +{ + char message_data[1024]; + + // Check for potential message data overflow + if (message_length >= sizeof(message_data) - 1) + { + rmt_LogText("Ignoring console input bigger than internal receive buffer (1024 bytes)"); + return RMT_ERROR_NONE; + } + + // Receive the rest of the message + message_data[0] = message_first_byte; + rmtTry(WebSocket_Receive(server->client_socket, message_data + 1, NULL, message_length - 1, 100)); + message_data[message_length] = 0; + + // Each message must have a descriptive 4 byte header + if (message_length < 4) + return RMT_ERROR_NONE; + + // Dispatch to handler + if (server->receive_handler) + rmtTry(server->receive_handler(server->receive_handler_context, message_data, message_length)); + + return RMT_ERROR_NONE; +} + +static rmtError bin_MessageHeader(Buffer* buffer, const char* id, rmtU32* out_write_start_offset) +{ + // Record where the header starts before writing it + *out_write_start_offset = buffer->bytes_used; + rmtTry(Buffer_Write(buffer, (void*)id, 4)); + rmtTry(Buffer_Write(buffer, (void*)" ", 4)); + return RMT_ERROR_NONE; +} + +static rmtError bin_MessageFooter(Buffer* buffer, rmtU32 write_start_offset) +{ + // Align message size to 32-bits so that the viewer can alias float arrays within log files + rmtTry(Buffer_AlignedPad(buffer, write_start_offset)); + + // Patch message size, including padding at the end + U32ToByteArray(buffer->data + write_start_offset + 4, (buffer->bytes_used - write_start_offset)); + + return RMT_ERROR_NONE; +} + +static void Server_Update(Server* server) +{ + rmtU32 cur_time; + + assert(server != NULL); + + // Recreate the listening socket if it's been destroyed earlier + if (server->listen_socket == NULL) + Server_CreateListenSocket(server, server->port, server->reuse_open_port, + server->limit_connections_to_localhost); + + if (server->listen_socket != NULL && server->client_socket == NULL) + { + // Accept connections as long as there is no client connected + WebSocket* client_socket = NULL; + rmtError error = WebSocket_AcceptConnection(server->listen_socket, &client_socket); + if (error == RMT_ERROR_NONE) + { + server->client_socket = client_socket; + } + else + { + // Destroy the listen socket on failure to accept + // It will get recreated in another update + rmtDelete(WebSocket, server->listen_socket); + } + } + + else + { + // Loop checking for incoming messages + for (;;) + { + // Inspect first byte to see if a message is there + char message_first_byte; + rmtU32 message_length; + rmtError error = WebSocket_Receive(server->client_socket, &message_first_byte, &message_length, 1, 0); + if (error == RMT_ERROR_NONE) + { + // Parse remaining message + error = Server_ReceiveMessage(server, message_first_byte, message_length); + if (error != RMT_ERROR_NONE) + { + Server_DisconnectClient(server); + break; + } + + // Check for more... + continue; + } + + // Passable errors... + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA) + { + // No data available + break; + } + + if (error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + // Data not available yet, can afford to ignore as we're only reading the first byte + break; + } + + // Anything else is an error that may have closed the connection + Server_DisconnectClient(server); + break; + } + } + + // Send pings to the client every second + cur_time = msTimer_Get(); + if (cur_time - server->last_ping_time > 1000) + { + Buffer* bin_buf = server->bin_buf; + rmtU32 write_start_offset; + WebSocket_PrepareBuffer(bin_buf); + bin_MessageHeader(bin_buf, "PING", &write_start_offset); + bin_MessageFooter(bin_buf, write_start_offset); + Server_Send(server, bin_buf->data, bin_buf->bytes_used, 10); + server->last_ping_time = cur_time; + } +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAMPLE: Base Sample Description for CPU by default +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define SAMPLE_NAME_LEN 128 + +typedef struct Sample +{ + // Inherit so that samples can be quickly allocated + ObjectLink Link; + + enum rmtSampleType type; + + // Hash generated from sample name + rmtU32 name_hash; + + // Unique, persistent ID among all samples + rmtU32 unique_id; + + // RGB8 unique colour generated from the unique ID + rmtU8 uniqueColour[3]; + + // Links to related samples in the tree + struct Sample* parent; + struct Sample* first_child; + struct Sample* last_child; + struct Sample* next_sibling; + + // Keep track of child count to distinguish from repeated calls to the same function at the same stack level + // This is also mixed with the callstack hash to allow consistent addressing of any point in the tree + rmtU32 nb_children; + + // Sample end points and length in microseconds + rmtU64 us_start; + rmtU64 us_end; + rmtU64 us_length; + + // Total sampled length of all children + rmtU64 us_sampled_length; + + // If this is a GPU sample, when the sample was issued on the GPU + rmtU64 usGpuIssueOnCpu; + + // Number of times this sample was used in a call in aggregate mode, 1 otherwise + rmtU32 call_count; + + // Current and maximum sample recursion depths + rmtU16 recurse_depth; + rmtU16 max_recurse_depth; + +} Sample; + +static rmtError Sample_Constructor(Sample* sample) +{ + assert(sample != NULL); + + ObjectLink_Constructor((ObjectLink*)sample); + + sample->type = RMT_SampleType_CPU; + sample->name_hash = 0; + sample->unique_id = 0; + sample->uniqueColour[0] = 0; + sample->uniqueColour[1] = 0; + sample->uniqueColour[2] = 0; + sample->parent = NULL; + sample->first_child = NULL; + sample->last_child = NULL; + sample->next_sibling = NULL; + sample->nb_children = 0; + sample->us_start = 0; + sample->us_end = 0; + sample->us_length = 0; + sample->us_sampled_length = 0; + sample->usGpuIssueOnCpu = 0; + sample->call_count = 0; + sample->recurse_depth = 0; + sample->max_recurse_depth = 0; + + return RMT_ERROR_NONE; +} + +static void Sample_Destructor(Sample* sample) +{ + RMT_UNREFERENCED_PARAMETER(sample); +} + +static void Sample_Prepare(Sample* sample, rmtU32 name_hash, Sample* parent) +{ + sample->name_hash = name_hash; + sample->unique_id = 0; + sample->parent = parent; + sample->first_child = NULL; + sample->last_child = NULL; + sample->next_sibling = NULL; + sample->nb_children = 0; + sample->us_start = 0; + sample->us_end = 0; + sample->us_length = 0; + sample->us_sampled_length = 0; + sample->usGpuIssueOnCpu = 0; + sample->call_count = 1; + sample->recurse_depth = 0; + sample->max_recurse_depth = 0; +} + +static void Sample_Close(Sample* sample, rmtS64 us_end) +{ + // Aggregate samples use us_end to store start so that us_start is preserved + rmtS64 us_length = 0; + if (sample->call_count > 1 && sample->max_recurse_depth == 0) + { + us_length = maxS64(us_end - sample->us_end, 0); + } + else + { + us_length = maxS64(us_end - sample->us_start, 0); + } + + sample->us_length += us_length; + + // Sum length on the parent to track un-sampled time in the parent + if (sample->parent != NULL) + { + sample->parent->us_sampled_length += us_length; + } +} + +static void Sample_CopyState(Sample* dst_sample, const Sample* src_sample) +{ + // Copy fields that don't override destination allocator links or transfer source sample tree positioning + // Also ignoring uniqueColour as that's calculated in the Remotery thread + dst_sample->type = src_sample->type; + dst_sample->name_hash = src_sample->name_hash; + dst_sample->unique_id = src_sample->unique_id; + dst_sample->nb_children = src_sample->nb_children; + dst_sample->us_start = src_sample->us_start; + dst_sample->us_end = src_sample->us_end; + dst_sample->us_length = src_sample->us_length; + dst_sample->us_sampled_length = src_sample->us_sampled_length; + dst_sample->usGpuIssueOnCpu = src_sample->usGpuIssueOnCpu; + dst_sample->call_count = src_sample->call_count; + dst_sample->recurse_depth = src_sample->recurse_depth; + dst_sample->max_recurse_depth = src_sample->max_recurse_depth; + + // Prepare empty tree links + dst_sample->parent = NULL; + dst_sample->first_child = NULL; + dst_sample->last_child = NULL; + dst_sample->next_sibling = NULL; +} + +static rmtError bin_SampleArray(Buffer* buffer, Sample* parent_sample, rmtU8 depth); + +static rmtError bin_Sample(Buffer* buffer, Sample* sample, rmtU8 depth) +{ + assert(sample != NULL); + + rmtTry(Buffer_WriteU32(buffer, sample->name_hash)); + rmtTry(Buffer_WriteU32(buffer, sample->unique_id)); + rmtTry(Buffer_Write(buffer, sample->uniqueColour, 3)); + rmtTry(Buffer_Write(buffer, &depth, 1)); + rmtTry(Buffer_WriteU64(buffer, sample->us_start)); + rmtTry(Buffer_WriteU64(buffer, sample->us_length)); + rmtTry(Buffer_WriteU64(buffer, maxS64(sample->us_length - sample->us_sampled_length, 0))); + rmtTry(Buffer_WriteU64(buffer, sample->usGpuIssueOnCpu)); + rmtTry(Buffer_WriteU32(buffer, sample->call_count)); + rmtTry(Buffer_WriteU32(buffer, sample->max_recurse_depth)); + rmtTry(bin_SampleArray(buffer, sample, depth + 1)); + + return RMT_ERROR_NONE; +} + +static rmtError bin_SampleArray(Buffer* buffer, Sample* parent_sample, rmtU8 depth) +{ + Sample* sample; + + rmtTry(Buffer_WriteU32(buffer, parent_sample->nb_children)); + for (sample = parent_sample->first_child; sample != NULL; sample = sample->next_sibling) + rmtTry(bin_Sample(buffer, sample, depth)); + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAMPLETREE: A tree of samples with their allocator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct SampleTree +{ + // Allocator for all samples + ObjectAllocator* allocator; + + // Root sample for all samples created by this thread + Sample* root; + + // Most recently pushed sample + Sample* currentParent; + + // Last time this sample tree was completed and sent to listeners, for stall detection + rmtAtomicU32 msLastTreeSendTime; + + // Lightweight flag, changed with release/acquire semantics to inform the stall detector the state of the tree is unreliable + rmtAtomicU32 treeBeingModified; + + // Send this popped sample to the log/viewer on close? + Sample* sendSampleOnClose; + +} SampleTree; + +// Notify tree watchers that its structure is in the process of being changed +#define ModifySampleTree(tree, statements) \ + StoreRelease(&tree->treeBeingModified, 1); \ + statements; \ + StoreRelease(&tree->treeBeingModified, 0); + +static rmtError SampleTree_Constructor(SampleTree* tree, rmtU32 sample_size, ObjConstructor constructor, + ObjDestructor destructor) +{ + assert(tree != NULL); + + tree->allocator = NULL; + tree->root = NULL; + tree->currentParent = NULL; + StoreRelease(&tree->msLastTreeSendTime, 0); + StoreRelease(&tree->treeBeingModified, 0); + tree->sendSampleOnClose = NULL; + + // Create the sample allocator + rmtTryNew(ObjectAllocator, tree->allocator, sample_size, constructor, destructor); + + // Create a root sample that's around for the lifetime of the thread + rmtTry(ObjectAllocator_Alloc(tree->allocator, (void**)&tree->root)); + Sample_Prepare(tree->root, 0, NULL); + tree->currentParent = tree->root; + + return RMT_ERROR_NONE; +} + +static void SampleTree_Destructor(SampleTree* tree) +{ + assert(tree != NULL); + + if (tree->root != NULL) + { + ObjectAllocator_Free(tree->allocator, tree->root); + tree->root = NULL; + } + + rmtDelete(ObjectAllocator, tree->allocator); +} + +static rmtU32 HashCombine(rmtU32 hash_a, rmtU32 hash_b) +{ + // A sequence of 32 uniformly random bits so that each bit of the combined hash is changed on application + // Derived from the golden ratio: UINT_MAX / ((1 + sqrt(5)) / 2) + // In reality it's just an arbitrary value which happens to work well, avoiding mapping all zeros to zeros. + // http://burtleburtle.net/bob/hash/doobs.html + static rmtU32 random_bits = 0x9E3779B9; + hash_a ^= hash_b + random_bits + (hash_a << 6) + (hash_a >> 2); + return hash_a; +} + +static rmtError SampleTree_Push(SampleTree* tree, rmtU32 name_hash, rmtU32 flags, Sample** sample) +{ + Sample* parent; + rmtU32 unique_id; + + // As each tree has a root sample node allocated, a parent must always be present + assert(tree != NULL); + assert(tree->currentParent != NULL); + parent = tree->currentParent; + + // Assume no flags is the common case and predicate branch checks + if (flags != 0) + { + // Check root status + if ((flags & RMTSF_Root) != 0) + { + assert(parent->parent == NULL); + } + + if ((flags & RMTSF_Aggregate) != 0) + { + // Linear search for previous instance of this sample name + Sample* sibling; + for (sibling = parent->first_child; sibling != NULL; sibling = sibling->next_sibling) + { + if (sibling->name_hash == name_hash) + { + tree->currentParent = sibling; + sibling->call_count++; + *sample = sibling; + return RMT_ERROR_NONE; + } + } + } + + // Collapse sample on recursion + if ((flags & RMTSF_Recursive) != 0 && parent->name_hash == name_hash) + { + parent->recurse_depth++; + parent->max_recurse_depth = maxU16(parent->max_recurse_depth, parent->recurse_depth); + parent->call_count++; + *sample = parent; + return RMT_ERROR_RECURSIVE_SAMPLE; + } + + // Allocate a new sample for subsequent flag checks to reference + rmtTry(ObjectAllocator_Alloc(tree->allocator, (void**)sample)); + Sample_Prepare(*sample, name_hash, parent); + + // Check for sending this sample on close + if ((flags & RMTSF_SendOnClose) != 0) + { + assert(tree->currentParent != NULL); + assert(tree->sendSampleOnClose == NULL); + tree->sendSampleOnClose = *sample; + } + } + + else + { + // Allocate a new sample + rmtTry(ObjectAllocator_Alloc(tree->allocator, (void**)sample)); + Sample_Prepare(*sample, name_hash, parent); + } + + // Generate a unique ID for this sample in the tree + unique_id = parent->unique_id; + unique_id = HashCombine(unique_id, (*sample)->name_hash); + unique_id = HashCombine(unique_id, parent->nb_children); + (*sample)->unique_id = unique_id; + + // Add sample to its parent + parent->nb_children++; + if (parent->first_child == NULL) + { + parent->first_child = *sample; + parent->last_child = *sample; + } + else + { + assert(parent->last_child != NULL); + parent->last_child->next_sibling = *sample; + parent->last_child = *sample; + } + + // Make this sample the new parent of any newly created samples + tree->currentParent = *sample; + + return RMT_ERROR_NONE; +} + +static void SampleTree_Pop(SampleTree* tree, Sample* sample) +{ + assert(tree != NULL); + assert(sample != NULL); + assert(sample != tree->root); + tree->currentParent = sample->parent; +} + +static ObjectLink* FlattenSamples(Sample* sample, rmtU32* nb_samples) +{ + Sample* child; + ObjectLink* cur_link = &sample->Link; + + assert(sample != NULL); + assert(nb_samples != NULL); + + *nb_samples += 1; + sample->Link.next = (ObjectLink*)sample->first_child; + + // Link all children together + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + ObjectLink* last_link = FlattenSamples(child, nb_samples); + last_link->next = (ObjectLink*)child->next_sibling; + cur_link = last_link; + } + + // Clear child info + sample->first_child = NULL; + sample->last_child = NULL; + sample->nb_children = 0; + + return cur_link; +} + +static void FreeSamples(Sample* sample, ObjectAllocator* allocator) +{ + // Chain all samples together in a flat list + rmtU32 nb_cleared_samples = 0; + ObjectLink* last_link = FlattenSamples(sample, &nb_cleared_samples); + + // Release the complete sample memory range + if (sample->Link.next != NULL) + { + ObjectAllocator_FreeRange(allocator, sample, last_link, nb_cleared_samples); + } + else + { + ObjectAllocator_Free(allocator, sample); + } +} + +static rmtError SampleTree_CopySample(Sample** out_dst_sample, Sample* dst_parent_sample, ObjectAllocator* allocator, const Sample* src_sample) +{ + Sample* src_child; + + // Allocate a copy of the sample + Sample* dst_sample; + rmtTry(ObjectAllocator_Alloc(allocator, (void**)&dst_sample)); + Sample_CopyState(dst_sample, src_sample); + + // Link the newly created/copied sample to its parent + // Note that metrics including nb_children have already been copied by the Sample_CopyState call + if (dst_parent_sample != NULL) + { + if (dst_parent_sample->first_child == NULL) + { + dst_parent_sample->first_child = dst_sample; + dst_parent_sample->last_child = dst_sample; + } + else + { + assert(dst_parent_sample->last_child != NULL); + dst_parent_sample->last_child->next_sibling = dst_sample; + dst_parent_sample->last_child = dst_sample; + } + } + + // Copy all children + for (src_child = src_sample->first_child; src_child != NULL; src_child = src_child->next_sibling) + { + Sample* dst_child; + rmtTry(SampleTree_CopySample(&dst_child, dst_sample, allocator, src_child)); + } + + *out_dst_sample = dst_sample; + + return RMT_ERROR_NONE; +} + +static rmtError SampleTree_Copy(SampleTree* dst_tree, const SampleTree* src_tree) +{ + // Sample trees are allocated at startup and their allocators are persistent for the lifetime of the Remotery object. + // It's safe to reference the allocator and use it for sample lifetime. + ObjectAllocator* allocator = src_tree->allocator; + dst_tree->allocator = allocator; + + // Copy from the root + rmtTry(SampleTree_CopySample(&dst_tree->root, NULL, allocator, src_tree->root)); + dst_tree->currentParent = dst_tree->root; + + return RMT_ERROR_NONE; +} + +typedef struct Msg_SampleTree +{ + Sample* rootSample; + + ObjectAllocator* allocator; + + rmtPStr threadName; + + // Data specific to the sample tree that downstream users can inspect/use + rmtU32 userData; + + rmtBool partialTree; +} Msg_SampleTree; + +static void QueueSampleTree(rmtMessageQueue* queue, Sample* sample, ObjectAllocator* allocator, rmtPStr thread_name, rmtU32 user_data, + struct ThreadProfiler* thread_profiler, rmtBool partial_tree) +{ + Msg_SampleTree* payload; + + // Attempt to allocate a message for sending the tree to the viewer + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_SampleTree), thread_profiler); + if (message == NULL) + { + // Discard tree samples on failure + FreeSamples(sample, allocator); + return; + } + + // Populate and commit + payload = (Msg_SampleTree*)message->payload; + payload->rootSample = sample; + payload->allocator = allocator; + payload->threadName = thread_name; + payload->userData = user_data; + payload->partialTree = partial_tree; + rmtMessageQueue_CommitMessage(message, MsgID_SampleTree); +} + +typedef struct Msg_AddToStringTable +{ + rmtU32 hash; + rmtU32 length; +} Msg_AddToStringTable; + +static rmtBool QueueAddToStringTable(rmtMessageQueue* queue, rmtU32 hash, const char* string, size_t length, struct ThreadProfiler* thread_profiler) +{ + Msg_AddToStringTable* payload; + + // Attempt to allocate a message om the queue + size_t nb_string_bytes = length + 1; + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_AddToStringTable) + nb_string_bytes, thread_profiler); + if (message == NULL) + { + return RMT_FALSE; + } + + // Populate and commit + payload = (Msg_AddToStringTable*)message->payload; + payload->hash = hash; + payload->length = length; + memcpy(payload + 1, string, nb_string_bytes); + rmtMessageQueue_CommitMessage(message, MsgID_AddToStringTable); + + return RMT_TRUE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TPROFILER: Thread Profiler data, storing both sampling and instrumentation results +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_D3D11 +typedef struct D3D11 D3D11; +static rmtError D3D11_Create(D3D11** d3d11); +static void D3D11_Destructor(D3D11* d3d11); +#endif + +#if RMT_USE_D3D12 +typedef struct D3D12ThreadData D3D12ThreadData; +static rmtError D3D12ThreadData_Create(D3D12ThreadData** d3d12); +static void D3D12ThreadData_Destructor(D3D12ThreadData* d3d12); +#endif + +typedef struct ThreadProfiler +{ + // Storage for backing up initial register values when modifying a thread's context + rmtU64 registerBackup0; // 0 + rmtU64 registerBackup1; // 8 + rmtU64 registerBackup2; // 16 + + // Used to schedule callbacks taking into account some threads may be sleeping + rmtAtomicS32 nbSamplesWithoutCallback; // 24 + + // Index of the processor the thread was last seen running on + rmtU32 processorIndex; // 28 + rmtU32 lastProcessorIndex; + + // OS thread ID/handle + rmtThreadId threadId; + rmtThreadHandle threadHandle; + + // Thread name stored for sending to the viewer + char threadName[64]; + rmtU32 threadNameHash; + + // Store a unique sample tree for each type + SampleTree* sampleTrees[RMT_SampleType_Count]; + +#if RMT_USE_D3D11 + D3D11* d3d11; +#endif + +#if RMT_USE_D3D12 + D3D12ThreadData* d3d12ThreadData; +#endif +} ThreadProfiler; + +static rmtError ThreadProfiler_Constructor(rmtMessageQueue* mq_to_rmt, ThreadProfiler* thread_profiler, rmtThreadId thread_id) +{ + rmtU32 name_length; + + // Set defaults + thread_profiler->nbSamplesWithoutCallback = 0; + thread_profiler->processorIndex = (rmtU32)-1; + thread_profiler->lastProcessorIndex = (rmtU32)-1; + thread_profiler->threadId = thread_id; + memset(thread_profiler->sampleTrees, 0, sizeof(thread_profiler->sampleTrees)); + +#if RMT_USE_D3D11 + thread_profiler->d3d11 = NULL; +#endif + +#if RMT_USE_D3D12 + thread_profiler->d3d12ThreadData = NULL; +#endif + + // Pre-open the thread handle + rmtTry(rmtOpenThreadHandle(thread_id, &thread_profiler->threadHandle)); + + // Name the thread and add to the string table + // Users can override this at a later point with the Remotery thread name API + rmtGetThreadName(thread_id, thread_profiler->threadHandle, thread_profiler->threadName, sizeof(thread_profiler->threadName)); + name_length = strnlen_s(thread_profiler->threadName, 64); + thread_profiler->threadNameHash = _rmt_HashString32(thread_profiler->threadName, name_length, 0); + QueueAddToStringTable(mq_to_rmt, thread_profiler->threadNameHash, thread_profiler->threadName, name_length, thread_profiler); + + // Create the CPU sample tree only. The rest are created on-demand as they need extra context to function correctly. + rmtTryNew(SampleTree, thread_profiler->sampleTrees[RMT_SampleType_CPU], sizeof(Sample), (ObjConstructor)Sample_Constructor, + (ObjDestructor)Sample_Destructor); + +#if RMT_USE_D3D11 + rmtTry(D3D11_Create(&thread_profiler->d3d11)); +#endif + +#if RMT_USE_D3D12 + rmtTry(D3D12ThreadData_Create(&thread_profiler->d3d12ThreadData)); +#endif + + return RMT_ERROR_NONE; +} + +static void ThreadProfiler_Destructor(ThreadProfiler* thread_profiler) +{ + rmtU32 index; + +#if RMT_USE_D3D12 + rmtDelete(D3D12ThreadData, thread_profiler->d3d12ThreadData); +#endif + +#if RMT_USE_D3D11 + rmtDelete(D3D11, thread_profiler->d3d11); +#endif + + for (index = 0; index < RMT_SampleType_Count; index++) + { + rmtDelete(SampleTree, thread_profiler->sampleTrees[index]); + } + + rmtCloseThreadHandle(thread_profiler->threadHandle); +} + +static rmtError ThreadProfiler_Push(SampleTree* tree, rmtU32 name_hash, rmtU32 flags, Sample** sample) +{ + rmtError error; + ModifySampleTree(tree, + error = SampleTree_Push(tree, name_hash, flags, sample); + ); + return error; +} + +static void CloseOpenSamples(Sample* sample, rmtU64 sample_time_us, rmtU32 parents_are_last) +{ + Sample* child_sample; + + // Depth-first search into children as we want to close child samples before their parents + for (child_sample = sample->first_child; child_sample != NULL; child_sample = child_sample->next_sibling) + { + rmtU32 is_last = parents_are_last & (child_sample == sample->last_child ? 1 : 0); + CloseOpenSamples(child_sample, sample_time_us, is_last); + } + + // A chain of open samples will be linked from the root to the deepest, currently open sample + if (parents_are_last > 0) + { + Sample_Close(sample, sample_time_us); + } +} + +static rmtError MakePartialTreeCopy(SampleTree* sample_tree, rmtU64 sample_time_us, SampleTree* out_sample_tree_copy) +{ + rmtU32 sample_time_s = (rmtU32)(sample_time_us / 1000); + StoreRelease(&sample_tree->msLastTreeSendTime, sample_time_s); + + // Make a local copy of the tree as we want to keep the current tree for active profiling + rmtTry(SampleTree_Copy(out_sample_tree_copy, sample_tree)); + + // Close all samples from the deepest open sample, right back to the root + CloseOpenSamples(out_sample_tree_copy->root, sample_time_us, 1); + + return RMT_ERROR_NONE; +} + +static rmtBool ThreadProfiler_Pop(ThreadProfiler* thread_profiler, rmtMessageQueue* queue, Sample* sample, rmtU32 msg_user_data) +{ + SampleTree* tree = thread_profiler->sampleTrees[sample->type]; + SampleTree_Pop(tree, sample); + + // Are we back at the root? + if (tree->currentParent == tree->root) + { + Sample* root; + + // Disconnect all samples from the root and pack in the chosen message queue + ModifySampleTree(tree, + root = tree->root; + root->first_child = NULL; + root->last_child = NULL; + root->nb_children = 0; + ); + QueueSampleTree(queue, sample, tree->allocator, thread_profiler->threadName, msg_user_data, thread_profiler, RMT_FALSE); + + // Update the last send time for this tree, for stall detection + StoreRelease(&tree->msLastTreeSendTime, (rmtU32)(sample->us_end / 1000)); + + return RMT_TRUE; + } + + if (tree->sendSampleOnClose == sample) + { + // Copy the sample tree as it is and send as a partial tree + SampleTree partial_tree; + if (MakePartialTreeCopy(tree, sample->us_start + sample->us_length, &partial_tree) == RMT_ERROR_NONE) + { + Sample* sample = partial_tree.root->first_child; + assert(sample != NULL); + QueueSampleTree(queue, sample, partial_tree.allocator, thread_profiler->threadName, msg_user_data, thread_profiler, RMT_TRUE); + } + + // Tree has been copied away to the message queue so free up the samples + if (partial_tree.root != NULL) + { + FreeSamples(partial_tree.root, partial_tree.allocator); + } + + tree->sendSampleOnClose = NULL; + } + + return RMT_FALSE; +} + +static rmtU32 ThreadProfiler_GetNameHash(ThreadProfiler* thread_profiler, rmtMessageQueue* queue, rmtPStr name, rmtU32* hash_cache) +{ + size_t name_len; + rmtU32 name_hash; + + // Hash cache provided? + if (hash_cache != NULL) + { + // Calculate the hash first time round only + name_hash = *hash_cache; + if (name_hash == 0) + { + assert(name != NULL); + name_len = strnlen_s(name, 256); + name_hash = _rmt_HashString32(name, name_len, 0); + + // Queue the string for the string table and only cache the hash if it succeeds + if (QueueAddToStringTable(queue, name_hash, name, name_len, thread_profiler) == RMT_TRUE) + { + *hash_cache = name_hash; + } + } + + return name_hash; + } + + // Have to recalculate and speculatively insert the name every time when no cache storage exists + name_len = strnlen_s(name, 256); + name_hash = _rmt_HashString32(name, name_len, 0); + QueueAddToStringTable(queue, name_hash, name, name_len, thread_profiler); + return name_hash; +} + +typedef struct ThreadProfilers +{ + // Timer shared with Remotery threads + usTimer* timer; + + // Queue between clients and main remotery thread + rmtMessageQueue* mqToRmtThread; + + // On x64 machines this points to the sample function + void* compiledSampleFn; + rmtU32 compiledSampleFnSize; + + // Used to store thread profilers bound to an OS thread + rmtTLS threadProfilerTlsHandle; + + // Array of preallocated ThreadProfiler objects + // Read iteration is safe given that no incomplete ThreadProfiler objects will be encountered during iteration. + // The ThreadProfiler count is only incremented once a new ThreadProfiler is fully defined and ready to be used. + // Do not use this list to verify if a ThreadProfiler exists for a given thread. Use the mutex-guarded Get functions instead. + ThreadProfiler threadProfilers[256]; + rmtAtomicU32 nbThreadProfilers; + rmtU32 maxNbThreadProfilers; + + // Guards creation and existence-testing of the ThreadProfiler list + rmtMutex threadProfilerMutex; + + // Periodic thread sampling thread + rmtThread* threadSampleThread; + + // Periodic thread to processor gatherer + rmtThread* threadGatherThread; +} ThreadProfilers; + +static rmtError SampleThreadsLoop(rmtThread* rmt_thread); + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT +static void* CreateSampleCallback(rmtU32* out_size); +#endif +#endif + +static rmtError ThreadProfilers_Constructor(ThreadProfilers* thread_profilers, usTimer* timer, rmtMessageQueue* mq_to_rmt_thread) +{ + // Set to default + thread_profilers->timer = timer; + thread_profilers->mqToRmtThread = mq_to_rmt_thread; + thread_profilers->compiledSampleFn = NULL; + thread_profilers->compiledSampleFnSize = 0; + thread_profilers->threadProfilerTlsHandle = TLS_INVALID_HANDLE; + thread_profilers->nbThreadProfilers = 0; + thread_profilers->maxNbThreadProfilers = sizeof(thread_profilers->threadProfilers) / sizeof(thread_profilers->threadProfilers[0]); + mtxInit(&thread_profilers->threadProfilerMutex); + thread_profilers->threadSampleThread = NULL; + thread_profilers->threadGatherThread = NULL; + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT + thread_profilers->compiledSampleFn = CreateSampleCallback(&thread_profilers->compiledSampleFnSize); + if (thread_profilers->compiledSampleFn == NULL) + { + return RMT_ERROR_MALLOC_FAIL; + } +#endif +#endif + + // Allocate a TLS handle for the thread profilers + rmtTry(tlsAlloc(&thread_profilers->threadProfilerTlsHandle)); + + // Kick-off the thread sampler + if (g_Settings.enableThreadSampler == RMT_TRUE) + { + rmtTryNew(rmtThread, thread_profilers->threadSampleThread, SampleThreadsLoop, thread_profilers); + } + + return RMT_ERROR_NONE; +} + +static void ThreadProfilers_Destructor(ThreadProfilers* thread_profilers) +{ + rmtU32 thread_index; + + rmtDelete(rmtThread, thread_profilers->threadSampleThread); + + // Delete all profilers + for (thread_index = 0; thread_index < thread_profilers->nbThreadProfilers; thread_index++) + { + ThreadProfiler* thread_profiler = thread_profilers->threadProfilers + thread_index; + ThreadProfiler_Destructor(thread_profiler); + } + + if (thread_profilers->threadProfilerTlsHandle != TLS_INVALID_HANDLE) + { + tlsFree(thread_profilers->threadProfilerTlsHandle); + } + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT + if (thread_profilers->compiledSampleFn != NULL) + { + VirtualFree(thread_profilers->compiledSampleFn, 0, MEM_RELEASE); + } +#endif +#endif + + mtxDelete(&thread_profilers->threadProfilerMutex); +} + +static rmtError ThreadProfilers_GetThreadProfiler(ThreadProfilers* thread_profilers, rmtThreadId thread_id, ThreadProfiler** out_thread_profiler) +{ + rmtU32 profiler_index; + ThreadProfiler* thread_profiler; + rmtError error; + + mtxLock(&thread_profilers->threadProfilerMutex); + + // Linear search for a matching thread id + for (profiler_index = 0; profiler_index < thread_profilers->nbThreadProfilers; profiler_index++) + { + thread_profiler = thread_profilers->threadProfilers + profiler_index; + if (thread_profiler->threadId == thread_id) + { + *out_thread_profiler = thread_profiler; + mtxUnlock(&thread_profilers->threadProfilerMutex); + return RMT_ERROR_NONE; + } + } + + // Thread info not found so create a new one at the end + thread_profiler = thread_profilers->threadProfilers + thread_profilers->nbThreadProfilers; + error = ThreadProfiler_Constructor(thread_profilers->mqToRmtThread, thread_profiler, thread_id); + if (error != RMT_ERROR_NONE) + { + ThreadProfiler_Destructor(thread_profiler); + mtxUnlock(&thread_profilers->threadProfilerMutex); + return error; + } + *out_thread_profiler = thread_profiler; + + // Increment count for consume by read iterators + // Within the mutex so that there are no race conditions creating thread profilers + // Using release semantics to ensure a memory barrier for read iterators + StoreRelease(&thread_profilers->nbThreadProfilers, thread_profilers->nbThreadProfilers + 1); + + mtxUnlock(&thread_profilers->threadProfilerMutex); + + return RMT_ERROR_NONE; +} + +static rmtError ThreadProfilers_GetCurrentThreadProfiler(ThreadProfilers* thread_profilers, ThreadProfiler** out_thread_profiler) +{ + // Is there a thread profiler associated with this thread yet? + *out_thread_profiler = (ThreadProfiler*)tlsGet(thread_profilers->threadProfilerTlsHandle); + if (*out_thread_profiler == NULL) + { + // Allocate on-demand + rmtTry(ThreadProfilers_GetThreadProfiler(thread_profilers, rmtGetCurrentThreadId(), out_thread_profiler)); + + // Bind to the curren thread + tlsSet(thread_profilers->threadProfilerTlsHandle, *out_thread_profiler); + } + + return RMT_ERROR_NONE; +} + +static rmtBool ThreadProfilers_ThreadInCallback(ThreadProfilers* thread_profilers, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_32BIT + if (context->Eip >= (DWORD)thread_profilers->compiledSampleFn && + context->Eip < (DWORD)((char*)thread_profilers->compiledSampleFn + thread_profilers->compiledSampleFnSize)) + { + return RMT_TRUE; + } +#else + if (context->Rip >= (DWORD64)thread_profilers->compiledSampleFn && + context->Rip < (DWORD64)((char*)thread_profilers->compiledSampleFn + thread_profilers->compiledSampleFnSize)) + { + return RMT_TRUE; + } +#endif +#endif + return RMT_FALSE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TGATHER: Thread Gatherer, periodically polling for newly created threads +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static void GatherThreads(ThreadProfilers* thread_profilers) +{ + rmtThreadHandle handle; + + assert(thread_profilers != NULL); + +#ifdef RMT_ENABLE_THREAD_SAMPLER + + // Create the snapshot - this is a slow call + handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (handle != INVALID_HANDLE_VALUE) + { + BOOL success; + + THREADENTRY32 thread_entry; + thread_entry.dwSize = sizeof(thread_entry); + + // Loop through all threads owned by this process + success = Thread32First(handle, &thread_entry); + while (success == TRUE) + { + if (thread_entry.th32OwnerProcessID == GetCurrentProcessId()) + { + // Create thread profilers on-demand if there're not already there + ThreadProfiler* thread_profiler; + rmtError error = ThreadProfilers_GetThreadProfiler(thread_profilers, thread_entry.th32ThreadID, &thread_profiler); + if (error != RMT_ERROR_NONE) + { + // Not really worth bringing the whole profiler down here + rmt_LogText("REMOTERY ERROR: Failed to create Thread Profiler"); + } + } + + success = Thread32Next(handle, &thread_entry); + } + + CloseHandle(handle); + } + +#endif +} + +static rmtError GatherThreadsLoop(rmtThread* thread) +{ + ThreadProfilers* thread_profilers = (ThreadProfilers*)thread->param; + rmtU32 sleep_time = 100; + + assert(thread_profilers != NULL); + + rmt_SetCurrentThreadName("RemoteryGatherThreads"); + + while (thread->request_exit == RMT_FALSE) + { + // We want a long period of time between scanning for new threads as the process is a little expensive (~30ms here). + // However not too long so as to miss potentially detailed process startup data. + // Use reduced sleep time at startup to catch as many early thread creations as possible. + // TODO(don): We could get processes to register themselves to ensure no startup data is lost but the scan must still + // be present, to catch threads in a process that the user doesn't create (e.g. graphics driver threads). + GatherThreads(thread_profilers); + msSleep(sleep_time); + sleep_time = minU32(sleep_time * 2, 2000); + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TSAMPLER: Sampling thread contexts +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct Processor +{ + // Current thread profiler sampling this processor + ThreadProfiler* threadProfiler; + + rmtU32 sampleCount; + rmtU64 sampleTime; +} Processor; + +typedef struct Msg_ProcessorThreads +{ + // Running index of processor messages + rmtU64 messageIndex; + + // Processor array, leaking into the memory behind the struct + rmtU32 nbProcessors; + Processor processors[1]; +} Msg_ProcessorThreads; + +static void QueueProcessorThreads(rmtMessageQueue* queue, rmtU64 message_index, rmtU32 nb_processors, Processor* processors) +{ + Msg_ProcessorThreads* payload; + + // Attempt to allocate a message for sending processors to the viewer + rmtU32 array_size = (nb_processors - 1) * sizeof(Processor); + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_ProcessorThreads) + array_size, NULL); + if (message == NULL) + { + return; + } + + // Populate and commit + payload = (Msg_ProcessorThreads*)message->payload; + payload->messageIndex = message_index; + payload->nbProcessors = nb_processors; + memcpy(payload->processors, processors, nb_processors * sizeof(Processor)); + rmtMessageQueue_CommitMessage(message, MsgID_ProcessorThreads); +} + +#ifdef RMT_ARCH_32BIT +__declspec(naked) static void SampleCallback() +{ + // + // It's important to realise that this call can be pre-empted by the scheduler and shifted to another processor *while we are + // sampling which processor this thread is on*. + // + // This has two very important implications: + // + // * What we are sampling here is an *approximation* of the path of threads across processors. + // * These samples can't be used to "open" and "close" sample periods on a processor as it's highly likely you'll get many + // open events without a close, or vice versa. + // + // As such, we can only choose a sampling period and for each sample register which threads are on which processor. + // + // This is very different to hooking up the Event Tracing API (requiring Administrator elevation), which raises events for + // each context switch, directly from the kernel. + // + + __asm + { + // Push the EIP return address used by the final ret instruction + push ebx + + // We might be in the middle of something like a cmp/jmp instruction pair so preserve EFLAGS + // (Classic example which seems to pop up regularly is _RTC_CheckESP, with cmp/call/jne) + pushfd + + // Push all volatile registers as we don't know what the function calls below will destroy + push eax + push ecx + push edx + + // Retrieve and store the current processor index + call esi + mov [edi].processorIndex, eax + + // Mark as ready for scheduling another callback + // Intel x86 store release + mov [edi].nbSamplesWithoutCallback, 0 + + // Restore preserved register state + pop edx + pop ecx + pop eax + + // Restore registers used to provide parameters to the callback + mov ebx, dword ptr [edi].registerBackup0 + mov esi, dword ptr [edi].registerBackup1 + mov edi, dword ptr [edi].registerBackup2 + + // Restore EFLAGS + popfd + + // Pops the original EIP off the stack and jmps to origin suspend point in the thread + ret + } +} +#elif defined(RMT_ARCH_64BIT) +// Generated with https://defuse.ca/online-x86-assembler.htm +static rmtU8 SampleCallbackBytes[] = +{ + // Push the RIP return address used by the final ret instruction + 0x53, // push rbx + + // We might be in the middle of something like a cmp/jmp instruction pair so preserve RFLAGS + // (Classic example which seems to pop up regularly is _RTC_CheckESP, with cmp/call/jne) + 0x9C, // pushfq + + // Push all volatile registers as we don't know what the function calls below will destroy + 0x50, // push rax + 0x51, // push rcx + 0x52, // push rdx + 0x41, 0x50, // push r8 + 0x41, 0x51, // push r9 + 0x41, 0x52, // push r10 + 0x41, 0x53, // push r11 + + // Retrieve and store the current processor index + 0xFF, 0xD6, // call rsi + 0x89, 0x47, 0x1C, // mov dword ptr [rdi + 28], eax + + // Mark as ready for scheduling another callback + // Intel x64 store release + 0xC7, 0x47, 0x18, 0x00, 0x00, 0x00, 0x00, // mov dword ptr [rdi + 24], 0 + + // Restore preserved register state + 0x41, 0x5B, // pop r11 + 0x41, 0x5A, // pop r10 + 0x41, 0x59, // pop r9 + 0x41, 0x58, // pop r8 + 0x5A, // pop rdx + 0x59, // pop rcx + 0x58, // pop rax + + // Restore registers used to provide parameters to the callback + 0x48, 0x8B, 0x1F, // mov rbx, qword ptr [rdi + 0] + 0x48, 0x8B, 0x77, 0x08, // mov rsi, qword ptr [rdi + 8] + 0x48, 0x8B, 0x7F, 0x10, // mov rdi, qword ptr [rdi + 16] + + // Restore RFLAGS + 0x9D, // popfq + + // Pops the original EIP off the stack and jmps to origin suspend point in the thread + 0xC3 // ret +}; +#ifdef RMT_PLATFORM_WINDOWS +static void* CreateSampleCallback(rmtU32* out_size) +{ + // Allocate page for the generated code + DWORD size = 4096; + DWORD old_protect; + void* function = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (function == NULL) + { + return NULL; + } + + // Clear whole allocation to int 3h + memset(function, 0xCC, size); + + // Copy over the generated code + memcpy(function, SampleCallbackBytes, sizeof(SampleCallbackBytes)); + *out_size = sizeof(SampleCallbackBytes); + + // Enable execution + VirtualProtect(function, size, PAGE_EXECUTE_READ, &old_protect); + return function; +} +#endif +#endif + +#if defined(__cplusplus) && __cplusplus >= 201103L +static_assert(offsetof(ThreadProfiler, nbSamplesWithoutCallback) == 24, ""); +static_assert(offsetof(ThreadProfiler, processorIndex) == 28, ""); +#endif + +static rmtError CheckForStallingSamples(SampleTree* stalling_sample_tree, ThreadProfiler* thread_profiler, rmtU64 sample_time_us) +{ + SampleTree* sample_tree; + rmtU32 sample_time_s = (rmtU32)(sample_time_us / 1000); + + // Initialise to empty + stalling_sample_tree->root = NULL; + stalling_sample_tree->allocator = NULL; + + // Skip the stall check if the tree is being modified + sample_tree = thread_profiler->sampleTrees[RMT_SampleType_CPU]; + if (LoadAcquire(&sample_tree->treeBeingModified) != 0) + { + return RMT_ERROR_NONE; + } + + if (sample_tree != NULL) + { + // The root is a dummy root inserted on tree creation so check that for children + Sample* root_sample = sample_tree->root; + if (root_sample != NULL && root_sample->nb_children > 0) + { + if (sample_time_s - LoadAcquire(&sample_tree->msLastTreeSendTime) > 1000) + { + rmtTry(MakePartialTreeCopy(sample_tree, sample_time_us, stalling_sample_tree)); + } + } + } + + return RMT_ERROR_NONE; +} + +static rmtError InitThreadSampling(ThreadProfilers* thread_profilers) +{ + rmt_SetCurrentThreadName("RemoterySampleThreads"); + + // Make an initial gather so that we have something to work with + GatherThreads(thread_profilers); + +#ifdef RMT_ENABLE_THREAD_SAMPLER + // Ensure we can wake up every millisecond + if (timeBeginPeriod(1) != TIMERR_NOERROR) + { + return RMT_ERROR_UNKNOWN; + } +#endif + + // Kick-off the background thread that watches for new threads + rmtTryNew(rmtThread, thread_profilers->threadGatherThread, GatherThreadsLoop, thread_profilers); + + // We're going to be shuffling thread visits to avoid the scheduler trying to predict a work-load based on sampling + // Use the global RNG with a random seed to start the shuffle + Well512_Init((rmtU32)time(NULL)); + + return RMT_ERROR_NONE; +} + +static rmtError SampleThreadsLoop(rmtThread* rmt_thread) +{ + rmtCpuContext context; + rmtU32 processor_message_index = 0; + rmtU32 nb_processors; + Processor* processors; + rmtU32 processor_index; + + ThreadProfilers* thread_profilers = (ThreadProfilers*)rmt_thread->param; + + // If we can't figure out how many processors there are then we are running on an unsupported platform + nb_processors = rmtGetNbProcessors(); + if (nb_processors == 0) + { + return RMT_ERROR_UNKNOWN; + } + + rmtTry(InitThreadSampling(thread_profilers)); + + // An array entry for each processor + rmtTryMallocArray(Processor, processors, nb_processors); + for (processor_index = 0; processor_index < nb_processors; processor_index++) + { + processors[processor_index].threadProfiler = NULL; + processors[processor_index].sampleTime = 0; + } + + while (rmt_thread->request_exit == RMT_FALSE) + { + rmtU32 lfsr_seed; + rmtU32 lfsr_value; + + // Query how many threads the gather knows about this time round + rmtU32 nb_thread_profilers = LoadAcquire(&thread_profilers->nbThreadProfilers); + + // Calculate table size log2 required to fit count entries. Normally we would adjust the log2 input by -1 so that + // power-of-2 counts map to their exact bit offset and don't require a twice larger table. You can iterate indices + // 0 to (1<= nb_thread_profilers) + { + continue; + } + + // Ignore our own thread + thread_id = rmtGetCurrentThreadId(); + thread_profiler = thread_profilers->threadProfilers + thread_index; + if (thread_profiler->threadId == thread_id) + { + continue; + } + + // Suspend the thread so we can insert a callback + thread_handle = thread_profiler->threadHandle; + if (rmtSuspendThread(thread_handle) == RMT_FALSE) + { + continue; + } + + // Mark the processor this thread was last recorded as running on. + // Note that a thread might be pre-empted multiple times in-between sampling. Given a sampling rate equal to the + // scheduling quantum, this doesn't happen too often. However in such cases, whoever marks the processor last is + // the one that gets recorded. + sample_time_us = usTimer_Get(thread_profilers->timer); + sample_count = AtomicAddS32(&thread_profiler->nbSamplesWithoutCallback, 1); + processor_index = thread_profiler->processorIndex; + if (processor_index != (rmtU32)-1) + { + assert(processor_index < nb_processors); + processors[processor_index].threadProfiler = thread_profiler; + processors[processor_index].sampleCount = sample_count; + processors[processor_index].sampleTime = sample_time_us; + } + + // Swap in a new context with our callback if one is not already scheduled on this thread + if (sample_count == 0) + { + if (rmtGetUserModeThreadContext(thread_handle, &context) == RMT_TRUE && + // There is a slight window of opportunity, after which the callback sets nbSamplesWithoutCallback=0, + // for this loop to suspend a thread while it's executing the last instructions of the callback. + ThreadProfilers_ThreadInCallback(thread_profilers, &context) == RMT_FALSE) + { + #ifdef RMT_PLATFORM_WINDOWS + #ifdef RMT_ARCH_64BIT + thread_profiler->registerBackup0 = context.Rbx; + thread_profiler->registerBackup1 = context.Rsi; + thread_profiler->registerBackup2 = context.Rdi; + context.Rbx = context.Rip; + context.Rsi = (rmtU64)GetCurrentProcessorNumber; + context.Rdi = (rmtU64)thread_profiler; + context.Rip = (DWORD64)thread_profilers->compiledSampleFn; + #endif + #ifdef RMT_ARCH_32BIT + thread_profiler->registerBackup0 = context.Ebx; + thread_profiler->registerBackup1 = context.Esi; + thread_profiler->registerBackup2 = context.Edi; + context.Ebx = context.Eip; + context.Esi = (rmtU32)GetCurrentProcessorNumber; + context.Edi = (rmtU32)thread_profiler; + context.Eip = (DWORD)&SampleCallback; + #endif + #endif + + rmtSetThreadContext(thread_handle, &context); + } + else + { + AtomicAddS32(&thread_profiler->nbSamplesWithoutCallback, -1); + } + } + + // While the thread is suspended take the chance to check for samples trees that may never complete + // Because SuspendThread on Windows is an async request, this needs to be placed at a point where the request completes + // Calling GetThreadContext will ensure the request is completed so this stall check is placed after that + if (RMT_ERROR_NONE != CheckForStallingSamples(&stalling_sample_tree, thread_profiler, sample_time_us)) + { + assert(stalling_sample_tree.allocator != NULL); + if (stalling_sample_tree.root != NULL) + { + FreeSamples(stalling_sample_tree.root, stalling_sample_tree.allocator); + } + } + + rmtResumeThread(thread_handle); + + if (stalling_sample_tree.root != NULL) + { + // If there is stalling sample tree on this thread then send it to listeners. + // Do the send *outside* of all Suspend/Resume calls as we have no way of knowing who is reading/writing the queue + // Mark this as partial so that the listeners know it will be overwritten. + Sample* sample = stalling_sample_tree.root->first_child; + assert(sample != NULL); + QueueSampleTree(thread_profilers->mqToRmtThread, sample, stalling_sample_tree.allocator, thread_profiler->threadName, 0, thread_profiler, RMT_TRUE); + + // The stalling_sample_tree.root->first_child has been sent to the main Remotery thread. This will get released later + // when the Remotery thread has processed it. This leaves the stalling_sample_tree.root here that must be freed. + // Before freeing the root sample we have to detach the children though. + stalling_sample_tree.root->first_child = NULL; + stalling_sample_tree.root->last_child = NULL; + stalling_sample_tree.root->nb_children = 0; + assert(stalling_sample_tree.allocator != NULL); + FreeSamples(stalling_sample_tree.root, stalling_sample_tree.allocator); + } + + + } while (lfsr_value != lfsr_seed); + + // Filter all processor samples made in this pass + for (processor_index = 0; processor_index < nb_processors; processor_index++) + { + Processor* processor = processors + processor_index; + ThreadProfiler* thread_profiler = processor->threadProfiler; + + if (thread_profiler != NULL) + { + // If this thread was on another processor on a previous pass and that processor is still tracking that thread, + // remove the thread from it. + rmtU32 last_processor_index = thread_profiler->lastProcessorIndex; + if (last_processor_index != (rmtU32)-1 && last_processor_index != processor_index) + { + assert(last_processor_index < nb_processors); + if (processors[last_processor_index].threadProfiler == thread_profiler) + { + processors[last_processor_index].threadProfiler = NULL; + } + } + + // When the thread is still on the same processor, check to see if it hasn't triggered the callback within another + // pass. This suggests the thread has gone to sleep and is no longer assigned to any thread. + else if (processor->sampleCount > 1) + { + processor->threadProfiler = NULL; + } + + thread_profiler->lastProcessorIndex = thread_profiler->processorIndex; + } + } + + // Send current processor state off to remotery + QueueProcessorThreads(thread_profilers->mqToRmtThread, processor_message_index++, nb_processors, processors); + } + + rmtDelete(rmtThread, thread_profilers->threadGatherThread); + +#ifdef RMT_ENABLE_THREAD_SAMPLER + timeEndPeriod(1); +#endif + + rmtFree(processors); + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @REMOTERY: Remotery +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_OPENGL +typedef struct OpenGL_t OpenGL; +static rmtError OpenGL_Create(OpenGL** opengl); +static void OpenGL_Destructor(OpenGL* opengl); +#endif + +#if RMT_USE_METAL +typedef struct Metal_t Metal; +static rmtError Metal_Create(Metal** metal); +static void Metal_Destructor(Metal* metal); +#endif + +typedef struct PropertySnapshot +{ + // Inherit so that property states can be quickly allocated + ObjectLink Link; + + // Data copied from the property at the time of the snapshot + rmtPropertyType type; + rmtPropertyValue value; + rmtPropertyValue prevValue; + rmtU32 prevValueFrame; + rmtU32 nameHash; + rmtU32 uniqueID; + + // Depth calculated as part of the walk + rmtU8 depth; + + // Link to the next property snapshot + rmtU32 nbChildren; + struct PropertySnapshot* nextSnapshot; +} PropertySnapshot; + +typedef struct Msg_PropertySnapshot +{ + PropertySnapshot* rootSnapshot; + rmtU32 nbSnapshots; + rmtU32 propertyFrame; +} Msg_PropertySnapshot; + +static rmtError PropertySnapshot_Constructor(PropertySnapshot* snapshot) +{ + assert(snapshot != NULL); + + ObjectLink_Constructor((ObjectLink*)snapshot); + + snapshot->type = RMT_PropertyType_rmtBool; + snapshot->value.Bool = RMT_FALSE; + snapshot->nameHash = 0; + snapshot->uniqueID = 0; + snapshot->nbChildren = 0; + snapshot->depth = 0; + snapshot->nextSnapshot = NULL; + + return RMT_ERROR_NONE; +} + +static void PropertySnapshot_Destructor(PropertySnapshot* snapshot) +{ + RMT_UNREFERENCED_PARAMETER(snapshot); +} + +struct Remotery +{ + Server* server; + + // Microsecond accuracy timer for CPU timestamps + usTimer timer; + + // Queue between clients and main remotery thread + rmtMessageQueue* mq_to_rmt_thread; + + // The main server thread + rmtThread* thread; + + // String table shared by all threads + StringTable* string_table; + + // Open logfile handle to append events to + FILE* logfile; + + // Set to trigger a map of each message on the remotery thread message queue + void (*map_message_queue_fn)(Remotery* rmt, Message*); + void* map_message_queue_data; + +#if RMT_USE_CUDA + rmtCUDABind cuda; +#endif + +#if RMT_USE_OPENGL + OpenGL* opengl; +#endif + +#if RMT_USE_METAL + Metal* metal; +#endif + +#if RMT_USE_D3D12 + // Linked list of all D3D12 queue samplers + rmtMutex d3d12BindsMutex; + struct D3D12BindImpl* d3d12Binds; +#endif + + ThreadProfilers* threadProfilers; + + // Root of all registered properties, guarded by mutex as property register can come from any thread + rmtMutex propertyMutex; + rmtProperty rootProperty; + + // Allocator for property values that get sent to the viewer + ObjectAllocator* propertyAllocator; + + // Frame used to determine age of property changes + rmtU32 propertyFrame; +}; + +// +// Global remotery context +// +static Remotery* g_Remotery = NULL; + +// +// This flag marks the EXE/DLL that created the global remotery instance. We want to allow +// only the creating EXE/DLL to destroy the remotery instance. +// +static rmtBool g_RemoteryCreated = RMT_FALSE; + +static double saturate(double v) +{ + if (v < 0) + { + return 0; + } + if (v > 1) + { + return 1; + } + return v; +} + +static void PostProcessSamples(Sample* sample, rmtU32* nb_samples) +{ + Sample* child; + + assert(sample != NULL); + assert(nb_samples != NULL); + + (*nb_samples)++; + + { + // Hash integer line position to full hue + double h = (double)sample->name_hash / (double)0xFFFFFFFF; + double r = saturate(fabs(fmod(h * 6 + 0, 6) - 3) - 1); + double g = saturate(fabs(fmod(h * 6 + 4, 6) - 3) - 1); + double b = saturate(fabs(fmod(h * 6 + 2, 6) - 3) - 1); + + // Cubic smooth + r = r * r * (3 - 2 * r); + g = g * g * (3 - 2 * g); + b = b * b * (3 - 2 * b); + + // Lerp to HSV lightness a little + double k = 0.4; + r = r * k + (1 - k); + g = g * k + (1 - k); + b = b * k + (1 - k); + + // To RGB8 + sample->uniqueColour[0] = (rmtU8)maxS32(minS32((rmtS32)(r * 255), 255), 0); + sample->uniqueColour[1] = (rmtU8)maxS32(minS32((rmtS32)(g * 255), 255), 0); + sample->uniqueColour[2] = (rmtU8)maxS32(minS32((rmtS32)(b * 255), 255), 0); + + //rmtU32 hash = sample->name_hash; + //sample->uniqueColour[0] = 127 + ((hash & 255) >> 1); + //sample->uniqueColour[1] = 127 + (((hash >> 4) & 255) >> 1); + //sample->uniqueColour[2] = 127 + (((hash >> 8) & 255) >> 1); + } + + // Concatenate children + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + PostProcessSamples(child, nb_samples); + } +} + +static rmtError Remotery_SendLogTextMessage(Remotery* rmt, Message* message) +{ + rmtError error = RMT_ERROR_NONE; + Buffer* bin_buf; + rmtU32 write_start_offset; + + // Build the buffer as if it's being sent to the server + assert(rmt != NULL); + assert(message != NULL); + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + rmtTry(bin_MessageHeader(bin_buf, "LOGM", &write_start_offset)); + rmtTry(Buffer_Write(bin_buf, message->payload, message->payload_size)); + rmtTry(bin_MessageFooter(bin_buf, write_start_offset)); + + // Pass to either the server or the log file + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 20); + } + if (rmt->logfile != NULL) + { + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return error; +} + +static rmtError bin_SampleName(Buffer* buffer, const char* name, rmtU32 name_hash, rmtU32 name_length) +{ + rmtU32 write_start_offset; + rmtTry(bin_MessageHeader(buffer, "SSMP", &write_start_offset)); + rmtTry(Buffer_WriteU32(buffer, name_hash)); + rmtTry(Buffer_WriteU32(buffer, name_length)); + rmtTry(Buffer_Write(buffer, (void*)name, name_length)); + rmtTry(bin_MessageFooter(buffer, write_start_offset)); + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_AddToStringTable(Remotery* rmt, Message* message) +{ + // Add to the string table + Msg_AddToStringTable* payload = (Msg_AddToStringTable*)message->payload; + const char* name = (const char*)(payload + 1); + rmtBool name_inserted = StringTable_Insert(rmt->string_table, payload->hash, name); + + // Emit to log file if one is open + if (name_inserted == RMT_TRUE && rmt->logfile != NULL) + { + Buffer* bin_buf = rmt->server->bin_buf; + bin_buf->bytes_used = 0; + rmtTry(bin_SampleName(bin_buf, name, payload->hash, payload->length)); + + rmtWriteFile(rmt->logfile, bin_buf->data, bin_buf->bytes_used); + } + + return RMT_ERROR_NONE; +} + +static rmtError bin_SampleTree(Buffer* buffer, Msg_SampleTree* msg) +{ + Sample* root_sample; + char thread_name[256]; + rmtU32 nb_samples = 0; + rmtU32 write_start_offset = 0; + + assert(buffer != NULL); + assert(msg != NULL); + + // Get the message root sample + root_sample = msg->rootSample; + assert(root_sample != NULL); + + // Add any sample types as a thread name post-fix to ensure they get their own viewer + thread_name[0] = 0; + strncat_s(thread_name, sizeof(thread_name), msg->threadName, strnlen_s(msg->threadName, 255)); + if (root_sample->type == RMT_SampleType_CUDA) + { + strncat_s(thread_name, sizeof(thread_name), " (CUDA)", 7); + } + if (root_sample->type == RMT_SampleType_D3D11) + { + strncat_s(thread_name, sizeof(thread_name), " (D3D11)", 8); + } + if (root_sample->type == RMT_SampleType_D3D12) + { + strncat_s(thread_name, sizeof(thread_name), " (D3D12)", 8); + } + if (root_sample->type == RMT_SampleType_OpenGL) + { + strncat_s(thread_name, sizeof(thread_name), " (OpenGL)", 9); + } + if (root_sample->type == RMT_SampleType_Metal) + { + strncat_s(thread_name, sizeof(thread_name), " (Metal)", 8); + } + + // Get digest hash of samples so that viewer can efficiently rebuild its tables + PostProcessSamples(root_sample, &nb_samples); + + // Write sample message header + rmtTry(bin_MessageHeader(buffer, "SMPL", &write_start_offset)); + rmtTry(Buffer_WriteStringWithLength(buffer, thread_name)); + rmtTry(Buffer_WriteU32(buffer, nb_samples)); + rmtTry(Buffer_WriteU32(buffer, msg->partialTree ? 1 : 0)); + + // Align serialised sample tree to 32-bit boundary + rmtTry(Buffer_AlignedPad(buffer, write_start_offset)); + + // Write entire sample tree + rmtTry(bin_Sample(buffer, root_sample, 0)); + + rmtTry(bin_MessageFooter(buffer, write_start_offset)); + + return RMT_ERROR_NONE; +} + +#if RMT_USE_CUDA +static rmtBool AreCUDASamplesReady(Sample* sample); +static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample); +#endif + +static rmtError Remotery_SendToViewerAndLog(Remotery* rmt, Buffer* bin_buf, rmtU32 timeout) +{ + rmtError error = RMT_ERROR_NONE; + + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + rmt_BeginCPUSample(Server_Send, RMTSF_Aggregate); + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, timeout); + rmt_EndCPUSample(); + } + + if (rmt->logfile != NULL) + { + // Write the data after the websocket header + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return error; +} + +static rmtError Remotery_SendSampleTreeMessage(Remotery* rmt, Message* message) +{ + rmtError error = RMT_ERROR_NONE; + + Msg_SampleTree* sample_tree; + Sample* sample; + Buffer* bin_buf; + + assert(rmt != NULL); + assert(message != NULL); + + // Get the message root sample + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample != NULL); + +#if RMT_USE_CUDA + if (sample->type == RMT_SampleType_CUDA) + { + // If these CUDA samples aren't ready yet, stick them to the back of the queue and continue + rmtBool are_samples_ready; + rmt_BeginCPUSample(AreCUDASamplesReady, 0); + are_samples_ready = AreCUDASamplesReady(sample); + rmt_EndCPUSample(); + if (!are_samples_ready) + { + QueueSampleTree(rmt->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, + message->threadProfiler, RMT_FALSE); + return RMT_ERROR_NONE; + } + + // Retrieve timing of all CUDA samples + rmt_BeginCPUSample(GetCUDASampleTimes, 0); + GetCUDASampleTimes(sample->parent, sample); + rmt_EndCPUSample(); + } +#endif + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the sample tree + rmt_BeginCPUSample(bin_SampleTree, RMTSF_Aggregate); + error = bin_SampleTree(bin_buf, sample_tree); + rmt_EndCPUSample(); + + if (g_Settings.sampletree_handler != NULL) + { + g_Settings.sampletree_handler(g_Settings.sampletree_context, sample_tree); + } + + // Release sample tree samples back to their allocator + FreeSamples(sample, sample_tree->allocator); + + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Send to the viewer with a reasonably long timeout as the size of the sample data may be large + return Remotery_SendToViewerAndLog(rmt, bin_buf, 50000); +} + +static rmtError Remotery_SendProcessorThreads(Remotery* rmt, Message* message) +{ + rmtU32 processor_index; + rmtError error = RMT_ERROR_NONE; + + Msg_ProcessorThreads* processor_threads = (Msg_ProcessorThreads*)message->payload; + + Buffer* bin_buf; + rmtU32 write_start_offset; + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the message + rmtTry(bin_MessageHeader(bin_buf, "PRTH", &write_start_offset)); + rmtTry(Buffer_WriteU32(bin_buf, processor_threads->nbProcessors)); + rmtTry(Buffer_WriteU64(bin_buf, processor_threads->messageIndex)); + for (processor_index = 0; processor_index < processor_threads->nbProcessors; processor_index++) + { + Processor* processor = processor_threads->processors + processor_index; + if (processor->threadProfiler != NULL) + { + rmtTry(Buffer_WriteU32(bin_buf, processor->threadProfiler->threadId)); + rmtTry(Buffer_WriteU32(bin_buf, processor->threadProfiler->threadNameHash)); + rmtTry(Buffer_WriteU64(bin_buf, processor->sampleTime)); + } + else + { + rmtTry(Buffer_WriteU32(bin_buf, (rmtU32)-1)); + rmtTry(Buffer_WriteU32(bin_buf, 0)); + rmtTry(Buffer_WriteU64(bin_buf, 0)); + } + } + + rmtTry(bin_MessageFooter(bin_buf, write_start_offset)); + + return Remotery_SendToViewerAndLog(rmt, bin_buf, 50); +} + +static void FreePropertySnapshots(PropertySnapshot* snapshot) +{ + // Allows root call to pass null + if (snapshot == NULL) + { + return; + } + + // Depth first free + if (snapshot->nextSnapshot != NULL) + { + FreePropertySnapshots(snapshot->nextSnapshot); + } + + ObjectAllocator_Free(g_Remotery->propertyAllocator, snapshot); +} + +static rmtError Remotery_SerialisePropertySnapshots(Buffer* bin_buf, Msg_PropertySnapshot* msg_snapshot) +{ + PropertySnapshot* snapshot; + rmtU8 empty_group[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + rmtU32 write_start_offset; + + // Header + rmtTry(bin_MessageHeader(bin_buf, "PSNP", &write_start_offset)); + rmtTry(Buffer_WriteU32(bin_buf, msg_snapshot->nbSnapshots)); + rmtTry(Buffer_WriteU32(bin_buf, msg_snapshot->propertyFrame)); + + // Linearised snapshots + for (snapshot = msg_snapshot->rootSnapshot; snapshot != NULL; snapshot = snapshot->nextSnapshot) + { + rmtU8 colour_depth[4] = {0, 0, 0}; + + // Same place as samples so that the GPU renderer can easily pick them out + rmtTry(Buffer_WriteU32(bin_buf, snapshot->nameHash)); + rmtTry(Buffer_WriteU32(bin_buf, snapshot->uniqueID)); + + // 3 byte place holder for viewer-side colour, with snapshot depth packed next to it + colour_depth[3] = snapshot->depth; + rmtTry(Buffer_Write(bin_buf, colour_depth, 4)); + + // Dispatch on property type, but maintaining 64-bits per value + rmtTry(Buffer_WriteU32(bin_buf, snapshot->type)); + switch (snapshot->type) + { + // Empty + case RMT_PropertyType_rmtGroup: + rmtTry(Buffer_Write(bin_buf, empty_group, 16)); + break; + + // All value ranges here are double-representable, so convert them early in C where it's cheap + case RMT_PropertyType_rmtBool: + rmtTry(Buffer_WriteF64(bin_buf, snapshot->value.Bool)); + rmtTry(Buffer_WriteF64(bin_buf, snapshot->prevValue.Bool)); + break; + case RMT_PropertyType_rmtS32: + rmtTry(Buffer_WriteF64(bin_buf, snapshot->value.S32)); + rmtTry(Buffer_WriteF64(bin_buf, snapshot->prevValue.S32)); + break; + case RMT_PropertyType_rmtU32: + rmtTry(Buffer_WriteF64(bin_buf, snapshot->value.U32)); + rmtTry(Buffer_WriteF64(bin_buf, snapshot->prevValue.U32)); + break; + case RMT_PropertyType_rmtF32: + rmtTry(Buffer_WriteF64(bin_buf, snapshot->value.F32)); + rmtTry(Buffer_WriteF64(bin_buf, snapshot->prevValue.F32)); + break; + + // The high end of these are not double representable but store their full pattern so we don't lose data + case RMT_PropertyType_rmtS64: + case RMT_PropertyType_rmtU64: + rmtTry(Buffer_WriteU64(bin_buf, snapshot->value.U64)); + rmtTry(Buffer_WriteU64(bin_buf, snapshot->prevValue.U64)); + break; + + case RMT_PropertyType_rmtF64: + rmtTry(Buffer_WriteF64(bin_buf, snapshot->value.F64)); + rmtTry(Buffer_WriteF64(bin_buf, snapshot->prevValue.F64)); + break; + } + + rmtTry(Buffer_WriteU32(bin_buf, snapshot->prevValueFrame)); + rmtTry(Buffer_WriteU32(bin_buf, snapshot->nbChildren)); + } + + rmtTry(bin_MessageFooter(bin_buf, write_start_offset)); + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_SendPropertySnapshot(Remotery* rmt, Message* message) +{ + Msg_PropertySnapshot* msg_snapshot = (Msg_PropertySnapshot*)message->payload; + + rmtError error = RMT_ERROR_NONE; + + Buffer* bin_buf; + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the message and send + error = Remotery_SerialisePropertySnapshots(bin_buf, msg_snapshot); + if (error == RMT_ERROR_NONE) + { + error = Remotery_SendToViewerAndLog(rmt, bin_buf, 50); + } + + FreePropertySnapshots(msg_snapshot->rootSnapshot); + + return error; +} + +static rmtError Remotery_ConsumeMessageQueue(Remotery* rmt) +{ + rmtU32 nb_messages_sent = 0; + const rmtU32 maxNbMessagesPerUpdate = g_Settings.maxNbMessagesPerUpdate; + + assert(rmt != NULL); + + // Loop reading the max number of messages for this update + // Note some messages don't consume the sent message count as they are small enough to not cause performance issues + while (nb_messages_sent < maxNbMessagesPerUpdate) + { + rmtError error = RMT_ERROR_NONE; + Message* message = rmtMessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread); + if (message == NULL) + break; + + switch (message->id) + { + // This shouldn't be possible + case MsgID_NotReady: + assert(RMT_FALSE); + break; + + // Dispatch to message handler + case MsgID_AddToStringTable: + error = Remotery_AddToStringTable(rmt, message); + break; + case MsgID_LogText: + error = Remotery_SendLogTextMessage(rmt, message); + nb_messages_sent++; + break; + case MsgID_SampleTree: + rmt_BeginCPUSample(SendSampleTreeMessage, RMTSF_Aggregate); + error = Remotery_SendSampleTreeMessage(rmt, message); + nb_messages_sent++; + rmt_EndCPUSample(); + break; + case MsgID_ProcessorThreads: + Remotery_SendProcessorThreads(rmt, message); + nb_messages_sent++; + break; + case MsgID_PropertySnapshot: + error = Remotery_SendPropertySnapshot(rmt, message); + break; + + default: + break; + } + + // Consume the message before reacting to any errors + rmtMessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message); + if (error != RMT_ERROR_NONE) + { + return error; + } + } + + return RMT_ERROR_NONE; +} + +static void Remotery_FlushMessageQueue(Remotery* rmt) +{ + assert(rmt != NULL); + + // Loop reading all remaining messages + for (;;) + { + Message* message = rmtMessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread); + if (message == NULL) + break; + + switch (message->id) + { + // These can be safely ignored + case MsgID_NotReady: + case MsgID_AddToStringTable: + case MsgID_LogText: + break; + + // Release all samples back to their allocators + case MsgID_SampleTree: { + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + FreeSamples(sample_tree->rootSample, sample_tree->allocator); + break; + } + + case MsgID_PropertySnapshot: { + Msg_PropertySnapshot* msg_snapshot = (Msg_PropertySnapshot*)message->payload; + FreePropertySnapshots(msg_snapshot->rootSnapshot); + break; + } + + default: + break; + } + + rmtMessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message); + } +} + +static void Remotery_MapMessageQueue(Remotery* rmt) +{ + rmtU32 read_pos, write_pos; + rmtMessageQueue* queue; + + assert(rmt != NULL); + + // Wait until the caller sets the custom data + while (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_data) == NULL) + msSleep(1); + + // Snapshot the current write position so that we're not constantly chasing other threads + // that can have no effect on the thread requesting the map. + queue = rmt->mq_to_rmt_thread; + write_pos = LoadAcquire(&queue->write_pos); + + // Walk every message in the queue and call the map function + read_pos = queue->read_pos; + while (read_pos < write_pos) + { + rmtU32 r = read_pos & (queue->size - 1); + Message* message = (Message*)(queue->data->ptr + r); + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + rmt->map_message_queue_fn(rmt, message); + read_pos += message_size; + } + + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_data, NULL); +} + +static rmtError Remotery_ThreadMain(rmtThread* thread) +{ + Remotery* rmt = (Remotery*)thread->param; + assert(rmt != NULL); + + rmt_SetCurrentThreadName("Remotery"); + + while (thread->request_exit == RMT_FALSE) + { + rmt_BeginCPUSample(Wakeup, 0); + + rmt_BeginCPUSample(ServerUpdate, 0); + Server_Update(rmt->server); + rmt_EndCPUSample(); + + rmt_BeginCPUSample(ConsumeMessageQueue, 0); + Remotery_ConsumeMessageQueue(rmt); + rmt_EndCPUSample(); + + rmt_EndCPUSample(); + + // Process any queue map requests + if (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_fn) != NULL) + { + Remotery_MapMessageQueue(rmt); + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_fn, NULL); + } + + // + // [NOTE-A] + // + // Possible sequence of user events at this point: + // + // 1. Add samples to the queue. + // 2. Shutdown remotery. + // + // This loop will exit with unrelease samples. + // + + msSleep(g_Settings.msSleepBetweenServerUpdates); + } + + // Release all samples to their allocators as a consequence of [NOTE-A] + Remotery_FlushMessageQueue(rmt); + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_ReceiveMessage(void* context, char* message_data, rmtU32 message_length) +{ + Remotery* rmt = (Remotery*)context; + +// Manual dispatch on 4-byte message headers (message ID is little-endian encoded) +#define FOURCC(a, b, c, d) (rmtU32)(((d) << 24) | ((c) << 16) | ((b) << 8) | (a)) + rmtU32 message_id = *(rmtU32*)message_data; + + switch (message_id) + { + case FOURCC('C', 'O', 'N', 'I'): { + rmt_LogText("Console message received..."); + rmt_LogText(message_data + 4); + + // Pass on to any registered handler + if (g_Settings.input_handler != NULL) + g_Settings.input_handler(message_data + 4, g_Settings.input_handler_context); + + break; + } + + case FOURCC('G', 'S', 'M', 'P'): { + rmtPStr name; + + // Convert name hash to integer + rmtU32 name_hash = 0; + const char* cur = message_data + 4; + const char* end = cur + message_length - 4; + while (cur < end) + name_hash = name_hash * 10 + *cur++ - '0'; + + // Search for a matching string hash + name = StringTable_Find(rmt->string_table, name_hash); + if (name != NULL) + { + rmtU32 name_length = (rmtU32)strnlen_s_safe_c(name, 256 - 12); + + // Construct a response message containing the matching name + Buffer* bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + bin_SampleName(bin_buf, name, name_hash, name_length); + + // Send back immediately as we're on the server thread + return Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 10); + } + + break; + } + } + +#undef FOURCC + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_Constructor(Remotery* rmt) +{ + assert(rmt != NULL); + + // Set default state + rmt->server = NULL; + rmt->mq_to_rmt_thread = NULL; + rmt->thread = NULL; + rmt->string_table = NULL; + rmt->logfile = NULL; + rmt->map_message_queue_fn = NULL; + rmt->map_message_queue_data = NULL; + rmt->threadProfilers = NULL; + mtxInit(&rmt->propertyMutex); + rmt->propertyAllocator = NULL; + rmt->propertyFrame = 0; + + // Set default state on the root property + rmtProperty* root_property = &rmt->rootProperty; + root_property->initialised = RMT_TRUE; + root_property->type = RMT_PropertyType_rmtGroup; + root_property->value.Bool = RMT_FALSE; + root_property->flags = RMT_PropertyFlags_NoFlags; + root_property->name = "Root Property"; + root_property->description = ""; + root_property->defaultValue.Bool = RMT_FALSE; + root_property->parent = NULL; + root_property->firstChild = NULL; + root_property->lastChild = NULL; + root_property->nextSibling = NULL; + root_property->nameHash = 0; + root_property->uniqueID = 0; + +#if RMT_USE_CUDA + rmt->cuda.CtxSetCurrent = NULL; + rmt->cuda.EventCreate = NULL; + rmt->cuda.EventDestroy = NULL; + rmt->cuda.EventElapsedTime = NULL; + rmt->cuda.EventQuery = NULL; + rmt->cuda.EventRecord = NULL; +#endif + +#if RMT_USE_OPENGL + rmt->opengl = NULL; +#endif + +#if RMT_USE_METAL + rmt->metal = NULL; +#endif + +#if RMT_USE_D3D12 + mtxInit(&rmt->d3d12BindsMutex); + rmt->d3d12Binds = NULL; +#endif + + // Kick-off the timer + usTimer_Init(&rmt->timer); + + // Create the server + rmtTryNew(Server, rmt->server, g_Settings.port, g_Settings.reuse_open_port, g_Settings.limit_connections_to_localhost); + + // Setup incoming message handler + rmt->server->receive_handler = Remotery_ReceiveMessage; + rmt->server->receive_handler_context = rmt; + + // Create the main message thread with only one page + rmtTryNew(rmtMessageQueue, rmt->mq_to_rmt_thread, g_Settings.messageQueueSizeInBytes); + + // Create sample name string table + rmtTryNew(StringTable, rmt->string_table); + + if (g_Settings.logPath != NULL) + { + // Get current date/time + struct tm* now_tm = TimeDateNow(); + + // Start the log path off + char filename[512] = { 0 }; + strncat_s(filename, sizeof(filename), g_Settings.logPath, 512); + strncat_s(filename, sizeof(filename), "/remotery-log-", 14); + + // Append current date and time + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_year + 1900), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_mon + 1), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_mday), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_hour), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_min), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_sec), 11); + + // Just append a custom extension + strncat_s(filename, sizeof(filename), ".rbin", 5); + + // Open and assume any failure simply sets NULL and the file isn't written + rmt->logfile = rmtOpenFile(filename, "w"); + + // Write the header + if (rmt->logfile != NULL) + { + rmtWriteFile(rmt->logfile, "RMTBLOGF", 8); + } + } + +#if RMT_USE_OPENGL + rmtTry(OpenGL_Create(&rmt->opengl)); +#endif + +#if RMT_USE_METAL + rmtTry(Metal_Create(&rmt->metal)); +#endif + + // Create the thread profilers container + rmtTryNew(ThreadProfilers, rmt->threadProfilers, &rmt->timer, rmt->mq_to_rmt_thread); + + // Create the property state allocator + rmtTryNew(ObjectAllocator, rmt->propertyAllocator, sizeof(PropertySnapshot), (ObjConstructor)PropertySnapshot_Constructor, (ObjDestructor)PropertySnapshot_Destructor); + + // Set as the global instance before creating any threads that uses it for sampling itself + assert(g_Remotery == NULL); + g_Remotery = rmt; + g_RemoteryCreated = RMT_TRUE; + + // Ensure global instance writes complete before other threads get a chance to use it + CompilerWriteFence(); + + // Create the main update thread once everything has been defined for the global remotery object + rmtTryNew(rmtThread, rmt->thread, Remotery_ThreadMain, rmt); + + return RMT_ERROR_NONE; +} + +static void Remotery_Destructor(Remotery* rmt) +{ + assert(rmt != NULL); + + // Join the remotery thread before clearing the global object as the thread is profiling itself + rmtDelete(rmtThread, rmt->thread); + + if (g_RemoteryCreated) + { + g_Remotery = NULL; + g_RemoteryCreated = RMT_FALSE; + } + + rmtDelete(ObjectAllocator, rmt->propertyAllocator); + + + rmtDelete(ThreadProfilers, rmt->threadProfilers); + +#if RMT_USE_D3D12 + while (rmt->d3d12Binds != NULL) + { + _rmt_UnbindD3D12((rmtD3D12Bind*)rmt->d3d12Binds); + } + mtxDelete(&rmt->d3d12BindsMutex); +#endif + +#if RMT_USE_OPENGL + rmtDelete(OpenGL, rmt->opengl); +#endif + +#if RMT_USE_METAL + rmtDelete(Metal, rmt->metal); +#endif + + rmtCloseFile(rmt->logfile); + + rmtDelete(StringTable, rmt->string_table); + rmtDelete(rmtMessageQueue, rmt->mq_to_rmt_thread); + + rmtDelete(Server, rmt->server); + + // Free the error message TLS + // TODO(don): The allocated messages will need to be freed as well + if (g_lastErrorMessageTlsHandle != TLS_INVALID_HANDLE) + { + tlsFree(g_lastErrorMessageTlsHandle); + g_lastErrorMessageTlsHandle = TLS_INVALID_HANDLE; + } + + mtxDelete(&rmt->propertyMutex); +} + +static void* CRTMalloc(void* mm_context, rmtU32 size) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + return malloc((size_t)size); +} + +static void CRTFree(void* mm_context, void* ptr) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + free(ptr); +} + +static void* CRTRealloc(void* mm_context, void* ptr, rmtU32 size) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + return realloc(ptr, size); +} + +RMT_API rmtSettings* _rmt_Settings(void) +{ + // Default-initialize on first call + if (g_SettingsInitialized == RMT_FALSE) + { + g_Settings.port = 0x4597; + g_Settings.reuse_open_port = RMT_FALSE; + g_Settings.limit_connections_to_localhost = RMT_FALSE; + g_Settings.enableThreadSampler = RMT_TRUE; + g_Settings.msSleepBetweenServerUpdates = 4; + g_Settings.messageQueueSizeInBytes = 1024 * 1024; + g_Settings.maxNbMessagesPerUpdate = 1000; + g_Settings.malloc = CRTMalloc; + g_Settings.free = CRTFree; + g_Settings.realloc = CRTRealloc; + g_Settings.input_handler = NULL; + g_Settings.input_handler_context = NULL; + g_Settings.logPath = NULL; + g_Settings.sampletree_handler = NULL; + g_Settings.sampletree_context = NULL; + g_Settings.snapshot_callback = NULL; + g_Settings.snapshot_context = NULL; + + g_SettingsInitialized = RMT_TRUE; + } + + return &g_Settings; +} + +RMT_API rmtError _rmt_CreateGlobalInstance(Remotery** remotery) +{ + // Ensure load/acquire store/release operations match this enum size + assert(sizeof(MessageID) == sizeof(rmtU32)); + + // Default-initialise if user has not set values + rmt_Settings(); + + // Creating the Remotery instance also records it as the global instance + assert(remotery != NULL); + rmtTryNew(Remotery, *remotery); + return RMT_ERROR_NONE; +} + +RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery) +{ + // Ensure this is the module that created it + assert(g_RemoteryCreated == RMT_TRUE); + assert(g_Remotery == remotery); + rmtDelete(Remotery, remotery); +} + +RMT_API void _rmt_SetGlobalInstance(Remotery* remotery) +{ + // Default-initialise if user has not set values + rmt_Settings(); + + g_Remotery = remotery; +} + +RMT_API Remotery* _rmt_GetGlobalInstance(void) +{ + return g_Remotery; +} + +#ifdef RMT_PLATFORM_WINDOWS +#pragma pack(push, 8) +typedef struct tagTHREADNAME_INFO +{ + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. +} THREADNAME_INFO; +#pragma pack(pop) +#endif + +wchar_t* MakeWideString(const char* string) +{ + size_t wlen; + wchar_t* wstr; + + // First get the converted length +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + if (mbstowcs_s(&wlen, NULL, 0, string, INT_MAX) != 0) + { + return NULL; + } +#else + wlen = mbstowcs(NULL, string, 256); +#endif + + // Allocate enough words for the converted result + wstr = (wchar_t*)(rmtMalloc((wlen + 1) * sizeof(wchar_t))); + if (wstr == NULL) + { + return NULL; + } + + // Convert +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + if (mbstowcs_s(&wlen, wstr, wlen + 1, string, wlen) != 0) +#else + if (mbstowcs(wstr, string, wlen + 1) != wlen) +#endif + { + rmtFree(wstr); + return NULL; + } + + return wstr; +} + +static void SetDebuggerThreadName(const char* name) +{ +#ifdef RMT_PLATFORM_WINDOWS + THREADNAME_INFO info; + + // See if SetThreadDescription is available in this version of Windows + // Introduced in Windows 10 build 1607 + HMODULE kernel32 = GetModuleHandleA("Kernel32.dll"); + if (kernel32 != NULL) + { + typedef HRESULT(WINAPI* SETTHREADDESCRIPTION)(HANDLE hThread, PCWSTR lpThreadDescription); + SETTHREADDESCRIPTION SetThreadDescription = (SETTHREADDESCRIPTION)GetProcAddress(kernel32, "SetThreadDescription"); + if (SetThreadDescription != NULL) + { + // Create a wide-string version of the thread name + wchar_t* wstr = MakeWideString(name); + if (wstr != NULL) + { + // Set and return, leaving a fall-through for any failure cases to use the old exception method + SetThreadDescription(GetCurrentThread(), wstr); + rmtFree(wstr); + return; + } + } + } + + info.dwType = 0x1000; + info.szName = name; + info.dwThreadID = (DWORD)-1; + info.dwFlags = 0; + +#ifndef __MINGW32__ + __try + { + RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } + __except (1 /* EXCEPTION_EXECUTE_HANDLER */) + { + } +#endif +#else + RMT_UNREFERENCED_PARAMETER(name); +#endif + +#ifdef RMT_PLATFORM_LINUX + // pthread_setname_np is a non-standard GNU extension. + char name_clamp[16]; + name_clamp[0] = 0; + strncat_s(name_clamp, sizeof(name_clamp), name, 15); +#if defined(__FreeBSD__) || defined(__OpenBSD__) + pthread_set_name_np(pthread_self(), name_clamp); +#else + prctl(PR_SET_NAME, name_clamp, 0, 0, 0); +#endif +#endif +} + +RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name) +{ + ThreadProfiler* thread_profiler; + rmtU32 name_length; + + if (g_Remotery == NULL) + { + return; + } + + // Get data for this thread + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) != RMT_ERROR_NONE) + { + return; + } + + // Copy name and apply to the debugger + strcpy_s(thread_profiler->threadName, sizeof(thread_profiler->threadName), thread_name); + thread_profiler->threadNameHash = _rmt_HashString32(thread_name, strnlen_s(thread_name, 64), 0); + SetDebuggerThreadName(thread_name); + + // Send the thread name for lookup +#ifdef RMT_PLATFORM_WINDOWS + name_length = strnlen_s(thread_profiler->threadName, 64); + QueueAddToStringTable(g_Remotery->mq_to_rmt_thread, thread_profiler->threadNameHash, thread_name, name_length, NULL); +#endif +} + +static rmtBool QueueLine(rmtMessageQueue* queue, unsigned char* text, rmtU32 size, struct ThreadProfiler* thread_profiler) +{ + Message* message; + rmtU32 text_size; + + assert(queue != NULL); + + // Patch line size + text_size = size - 4; + U32ToByteArray(text, text_size); + + // Allocate some space for the line + message = rmtMessageQueue_AllocMessage(queue, size, thread_profiler); + if (message == NULL) + return RMT_FALSE; + + // Copy the text and commit the message + memcpy(message->payload, text, size); + rmtMessageQueue_CommitMessage(message, MsgID_LogText); + + return RMT_TRUE; +} + +RMT_API void _rmt_LogText(rmtPStr text) +{ + int start_offset, offset, i; + unsigned char line_buffer[1024] = {0}; + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) != RMT_ERROR_NONE) + { + return; + } + + // Start with empty line size + // Fill with spaces to enable viewing line_buffer without offset in a debugger + // (will be overwritten later by QueueLine/rmtMessageQueue_AllocMessage) + line_buffer[0] = ' '; + line_buffer[1] = ' '; + line_buffer[2] = ' '; + line_buffer[3] = ' '; + start_offset = 4; + + // There might be newlines in the buffer, so split them into multiple network calls + offset = start_offset; + for (i = 0; text[i] != 0; i++) + { + char c = text[i]; + + // Line wrap when too long or newline encountered + if (offset == sizeof(line_buffer) - 1 || c == '\n') + { + // Send the line up to now + if (QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, offset, thread_profiler) == RMT_FALSE) + return; + + // Restart line + offset = start_offset; + + // Don't add the newline character (if this was the reason for the flush) + // to the restarted line_buffer, let's skip it + if (c == '\n') + continue; + } + + line_buffer[offset++] = c; + } + + // Send the last line + if (offset > start_offset) + { + assert(offset < (int)sizeof(line_buffer)); + QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, offset, thread_profiler); + } +} + +RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32 flags, rmtU32* hash_cache) +{ + // 'hash_cache' stores a pointer to a sample name's hash value. Internally this is used to identify unique + // callstacks and it would be ideal that it's not recalculated each time the sample is used. This can be statically + // cached at the point of call or stored elsewhere when dynamic names are required. + // + // If 'hash_cache' is NULL then this call becomes more expensive, as it has to recalculate the hash of the name. + + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + // TODO: Time how long the bits outside here cost and subtract them from the parent + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + if (ThreadProfiler_Push(thread_profiler->sampleTrees[RMT_SampleType_CPU], name_hash, flags, &sample) == RMT_ERROR_NONE) + { + // If this is an aggregate sample, store the time in 'end' as we want to preserve 'start' + if (sample->call_count > 1) + sample->us_end = usTimer_Get(&g_Remotery->timer); + else + sample->us_start = usTimer_Get(&g_Remotery->timer); + } + } +} + +RMT_API void _rmt_EndCPUSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample = thread_profiler->sampleTrees[RMT_SampleType_CPU]->currentParent; + + if (sample->recurse_depth > 0) + { + sample->recurse_depth--; + } + else + { + rmtU64 us_end = usTimer_Get(&g_Remotery->timer); + Sample_Close(sample, us_end); + ThreadProfiler_Pop(thread_profiler, g_Remotery->mq_to_rmt_thread, sample, 0); + } + } +} + +#if RMT_USE_D3D12 +static rmtError D3D12MarkFrame(struct D3D12BindImpl* bind); +#endif + +RMT_API rmtError _rmt_MarkFrame(void) +{ + if (g_Remotery == NULL) + { + return RMT_ERROR_REMOTERY_NOT_CREATED; + } + + #if RMT_USE_D3D12 + // This will kick off mark frames on the complete chain of binds + rmtTry(D3D12MarkFrame(g_Remotery->d3d12Binds)); + #endif + + return RMT_ERROR_NONE; +} + +#if RMT_USE_OPENGL || RMT_USE_D3D11 || RMT_USE_D3D12 +static void Remotery_DeleteSampleTree(Remotery* rmt, enum rmtSampleType sample_type) +{ + ThreadProfiler* thread_profiler; + + // Get the attached thread sampler and delete the sample tree + assert(rmt != NULL); + if (ThreadProfilers_GetCurrentThreadProfiler(rmt->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + SampleTree* sample_tree = thread_profiler->sampleTrees[sample_type]; + if (sample_tree != NULL) + { + rmtDelete(SampleTree, sample_tree); + thread_profiler->sampleTrees[sample_type] = NULL; + } + } +} + +static rmtBool rmtMessageQueue_IsEmpty(rmtMessageQueue* queue) +{ + assert(queue != NULL); + return queue->write_pos - queue->read_pos == 0; +} + +typedef struct +{ + rmtSampleType sample_type; + Buffer* flush_samples; +} GatherQueuedSampleData; + +static void MapMessageQueueAndWait(Remotery* rmt, void (*map_message_queue_fn)(Remotery* rmt, Message*), void* data) +{ + // Basic spin lock on the map function itself + while (AtomicCompareAndSwapPointer((long* volatile*)&rmt->map_message_queue_fn, NULL, + (long*)map_message_queue_fn) == RMT_FALSE) + msSleep(1); + + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_data, (long*)data); + + // Wait until map completes + while (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_fn) != NULL) + msSleep(1); +} + +static void GatherQueuedSamples(Remotery* rmt, Message* message) +{ + GatherQueuedSampleData* gather_data = (GatherQueuedSampleData*)rmt->map_message_queue_data; + + // Filter sample trees + if (message->id == MsgID_SampleTree) + { + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + Sample* sample = sample_tree->rootSample; + if (sample->type == gather_data->sample_type) + { + // Make a copy of the entire sample tree as the remotery thread may overwrite it while + // the calling thread tries to delete + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + Buffer_Write(gather_data->flush_samples, message, message_size); + + // Mark the message empty + message->id = MsgID_None; + } + } +} + +static void FreePendingSampleTrees(Remotery* rmt, rmtSampleType sample_type, Buffer* flush_samples) +{ + rmtU8* data; + rmtU8* data_end; + + // Gather all sample trees currently queued for the Remotery thread + GatherQueuedSampleData gather_data; + gather_data.sample_type = sample_type; + gather_data.flush_samples = flush_samples; + MapMessageQueueAndWait(rmt, GatherQueuedSamples, &gather_data); + + // Release all sample trees to their allocators + data = flush_samples->data; + data_end = data + flush_samples->bytes_used; + while (data < data_end) + { + Message* message = (Message*)data; + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + FreeSamples(sample_tree->rootSample, sample_tree->allocator); + data += message_size; + } +} + +#endif + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @CUDA: CUDA event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_CUDA + +typedef struct CUDASample +{ + // IS-A inheritance relationship + Sample base; + + // Pair of events that wrap the sample + CUevent event_start; + CUevent event_end; + +} CUDASample; + +static rmtError MapCUDAResult(CUresult result) +{ + switch (result) + { + case CUDA_SUCCESS: + return RMT_ERROR_NONE; + case CUDA_ERROR_DEINITIALIZED: + return RMT_ERROR_CUDA_DEINITIALIZED; + case CUDA_ERROR_NOT_INITIALIZED: + return RMT_ERROR_CUDA_NOT_INITIALIZED; + case CUDA_ERROR_INVALID_CONTEXT: + return RMT_ERROR_CUDA_INVALID_CONTEXT; + case CUDA_ERROR_INVALID_VALUE: + return RMT_ERROR_CUDA_INVALID_VALUE; + case CUDA_ERROR_INVALID_HANDLE: + return RMT_ERROR_CUDA_INVALID_HANDLE; + case CUDA_ERROR_OUT_OF_MEMORY: + return RMT_ERROR_CUDA_OUT_OF_MEMORY; + case CUDA_ERROR_NOT_READY: + return RMT_ERROR_ERROR_NOT_READY; + default: + return RMT_ERROR_CUDA_UNKNOWN; + } +} + +#define CUDA_MAKE_FUNCTION(name, params) \ + typedef CUresult(CUDAAPI* name##Ptr) params; \ + name##Ptr name = (name##Ptr)g_Remotery->cuda.name; + +#define CUDA_GUARD(call) \ + { \ + rmtError error = call; \ + if (error != RMT_ERROR_NONE) \ + return error; \ + } + +// Wrappers around CUDA driver functions that manage the active context. +static rmtError CUDASetContext(void* context) +{ + CUDA_MAKE_FUNCTION(CtxSetCurrent, (CUcontext ctx)); + assert(CtxSetCurrent != NULL); + return MapCUDAResult(CtxSetCurrent((CUcontext)context)); +} +static rmtError CUDAGetContext(void** context) +{ + CUDA_MAKE_FUNCTION(CtxGetCurrent, (CUcontext * ctx)); + assert(CtxGetCurrent != NULL); + return MapCUDAResult(CtxGetCurrent((CUcontext*)context)); +} +static rmtError CUDAEnsureContext() +{ + void* current_context; + CUDA_GUARD(CUDAGetContext(¤t_context)); + + assert(g_Remotery != NULL); + if (current_context != g_Remotery->cuda.context) + CUDA_GUARD(CUDASetContext(g_Remotery->cuda.context)); + + return RMT_ERROR_NONE; +} + +// Wrappers around CUDA driver functions that manage events +static rmtError CUDAEventCreate(CUevent* phEvent, unsigned int Flags) +{ + CUDA_MAKE_FUNCTION(EventCreate, (CUevent * phEvent, unsigned int Flags)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventCreate(phEvent, Flags)); +} +static rmtError CUDAEventDestroy(CUevent hEvent) +{ + CUDA_MAKE_FUNCTION(EventDestroy, (CUevent hEvent)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventDestroy(hEvent)); +} +static rmtError CUDAEventRecord(CUevent hEvent, void* hStream) +{ + CUDA_MAKE_FUNCTION(EventRecord, (CUevent hEvent, CUstream hStream)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventRecord(hEvent, (CUstream)hStream)); +} +static rmtError CUDAEventQuery(CUevent hEvent) +{ + CUDA_MAKE_FUNCTION(EventQuery, (CUevent hEvent)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventQuery(hEvent)); +} +static rmtError CUDAEventElapsedTime(float* pMilliseconds, CUevent hStart, CUevent hEnd) +{ + CUDA_MAKE_FUNCTION(EventElapsedTime, (float* pMilliseconds, CUevent hStart, CUevent hEnd)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventElapsedTime(pMilliseconds, hStart, hEnd)); +} + +static rmtError CUDASample_Constructor(CUDASample* sample) +{ + rmtError error; + + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = RMT_SampleType_CUDA; + sample->event_start = NULL; + sample->event_end = NULL; + + // Create non-blocking events with timing + assert(g_Remotery != NULL); + error = CUDAEventCreate(&sample->event_start, CU_EVENT_DEFAULT); + if (error == RMT_ERROR_NONE) + error = CUDAEventCreate(&sample->event_end, CU_EVENT_DEFAULT); + return error; +} + +static void CUDASample_Destructor(CUDASample* sample) +{ + assert(sample != NULL); + + // Destroy events + if (sample->event_start != NULL) + CUDAEventDestroy(sample->event_start); + if (sample->event_end != NULL) + CUDAEventDestroy(sample->event_end); + + Sample_Destructor((Sample*)sample); +} + +static rmtBool AreCUDASamplesReady(Sample* sample) +{ + rmtError error; + Sample* child; + + CUDASample* cuda_sample = (CUDASample*)sample; + assert(sample->type == RMT_SampleType_CUDA); + + // Check to see if both of the CUDA events have been processed + error = CUDAEventQuery(cuda_sample->event_start); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + error = CUDAEventQuery(cuda_sample->event_end); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + + // Check child sample events + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!AreCUDASamplesReady(child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample) +{ + Sample* child; + + CUDASample* cuda_root_sample = (CUDASample*)root_sample; + CUDASample* cuda_sample = (CUDASample*)sample; + + float ms_start, ms_end; + + assert(root_sample != NULL); + assert(sample != NULL); + + // Get millisecond timing of each sample event, relative to initial root sample + if (CUDAEventElapsedTime(&ms_start, cuda_root_sample->event_start, cuda_sample->event_start) != RMT_ERROR_NONE) + return RMT_FALSE; + if (CUDAEventElapsedTime(&ms_end, cuda_root_sample->event_start, cuda_sample->event_end) != RMT_ERROR_NONE) + return RMT_FALSE; + + // Convert to microseconds and add to the sample + sample->us_start = (rmtU64)(ms_start * 1000); + sample->us_end = (rmtU64)(ms_end * 1000); + sample->us_length = sample->us_end - sample->us_start; + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetCUDASampleTimes(root_sample, child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind) +{ + assert(bind != NULL); + if (g_Remotery != NULL) + g_Remotery->cuda = *bind; +} + +RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + rmtError error; + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the CUDA tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a CUDA binding is not yet available. + SampleTree** cuda_tree = &thread_profiler->sampleTrees[RMT_SampleType_CUDA]; + if (*cuda_tree == NULL) + { + CUDASample* root_sample; + + rmtTryNew(SampleTree, *cuda_tree, sizeof(CUDASample), (ObjConstructor)CUDASample_Constructor, + (ObjDestructor)CUDASample_Destructor); + if (error != RMT_ERROR_NONE) + return; + + // Record an event once on the root sample, used to measure absolute sample + // times since this point + root_sample = (CUDASample*)(*cuda_tree)->root; + error = CUDAEventRecord(root_sample->event_start, stream); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the same and record its event + if (ThreadProfiler_Push(*cuda_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + CUDASample* cuda_sample = (CUDASample*)sample; + cuda_sample->base.usGpuIssueOnCpu = usTimer_Get(&g_Remotery->timer); + CUDAEventRecord(cuda_sample->event_start, stream); + } + } +} + +RMT_API void _rmt_EndCUDASample(void* stream) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + CUDASample* sample = (CUDASample*)thread_profiler->sampleTrees[RMT_SampleType_CUDA]->currentParent; + if (sample->base.recurse_depth > 0) + { + sample->base.recurse_depth--; + } + else + { + CUDAEventRecord(sample->event_end, stream); + ThreadProfiler_Pop(thread_profiler, g_Remotery->mq_to_rmt_thread, (Sample*)sample, 0); + } + } +} + +#endif // RMT_USE_CUDA + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @D3D11: Direct3D 11 event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_D3D11 + +// As clReflect has no way of disabling C++ compile mode, this forces C interfaces everywhere... +#define CINTERFACE + +// ...unfortunately these C++ helpers aren't wrapped by the same macro but they can be disabled individually +#define D3D11_NO_HELPERS + +// Allow use of the D3D11 helper macros for accessing the C-style vtable +#define COBJMACROS + +#ifdef _MSC_VER +// Disable for d3d11.h +// warning C4201: nonstandard extension used : nameless struct/union +#pragma warning(push) +#pragma warning(disable : 4201) +#endif + +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +typedef struct D3D11 +{ + // Context set by user + ID3D11Device* device; + ID3D11DeviceContext* context; + + HRESULT last_error; + + // Queue to the D3D 11 main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_d3d11_main; + + // Mark the first time so that remaining timestamps are offset from this + rmtU64 first_timestamp; + // Last time in us (CPU time, via usTimer_Get) since we last resync'ed CPU & GPU + rmtU64 last_resync; + + // Sample trees in transit in the message queue for release on shutdown + Buffer* flush_samples; +} D3D11; + +static rmtError D3D11_Create(D3D11** d3d11) +{ + assert(d3d11 != NULL); + + // Allocate space for the D3D11 data + rmtTryMalloc(D3D11, *d3d11); + + // Set defaults + (*d3d11)->device = NULL; + (*d3d11)->context = NULL; + (*d3d11)->last_error = S_OK; + (*d3d11)->mq_to_d3d11_main = NULL; + (*d3d11)->first_timestamp = 0; + (*d3d11)->last_resync = 0; + (*d3d11)->flush_samples = NULL; + + rmtTryNew(rmtMessageQueue, (*d3d11)->mq_to_d3d11_main, g_Settings.messageQueueSizeInBytes); + rmtTryNew(Buffer, (*d3d11)->flush_samples, 8 * 1024); + + return RMT_ERROR_NONE; +} + +static void D3D11_Destructor(D3D11* d3d11) +{ + assert(d3d11 != NULL); + rmtDelete(Buffer, d3d11->flush_samples); + rmtDelete(rmtMessageQueue, d3d11->mq_to_d3d11_main); +} + +static HRESULT rmtD3D11Finish(ID3D11Device* device, ID3D11DeviceContext* context, rmtU64* out_timestamp, + double* out_frequency) +{ + HRESULT result; + ID3D11Query* full_stall_fence; + ID3D11Query* query_disjoint; + D3D11_QUERY_DESC query_desc; + D3D11_QUERY_DESC disjoint_desc; + UINT64 timestamp; + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint; + + query_desc.Query = D3D11_QUERY_TIMESTAMP; + query_desc.MiscFlags = 0; + result = ID3D11Device_CreateQuery(device, &query_desc, &full_stall_fence); + if (result != S_OK) + return result; + + disjoint_desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + disjoint_desc.MiscFlags = 0; + result = ID3D11Device_CreateQuery(device, &disjoint_desc, &query_disjoint); + if (result != S_OK) + { + ID3D11Query_Release(full_stall_fence); + return result; + } + + ID3D11DeviceContext_Begin(context, (ID3D11Asynchronous*)query_disjoint); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)full_stall_fence); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)query_disjoint); + + result = S_FALSE; + + while (result == S_FALSE) + { + result = + ID3D11DeviceContext_GetData(context, (ID3D11Asynchronous*)query_disjoint, &disjoint, sizeof(disjoint), 0); + if (result != S_OK && result != S_FALSE) + { + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; + } + if (result == S_OK) + { + result = ID3D11DeviceContext_GetData(context, (ID3D11Asynchronous*)full_stall_fence, ×tamp, + sizeof(timestamp), 0); + if (result != S_OK && result != S_FALSE) + { + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; + } + } + // Give HyperThreading threads a breath on this spinlock. + YieldProcessor(); + } + + if (disjoint.Disjoint == FALSE) + { + double frequency = disjoint.Frequency / 1000000.0; + *out_timestamp = timestamp; + *out_frequency = frequency; + } + else + { + result = S_FALSE; + } + + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; +} + +static HRESULT SyncD3D11CpuGpuTimes(ID3D11Device* device, ID3D11DeviceContext* context, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + rmtU64 cpu_time_start = 0; + rmtU64 cpu_time_stop = 0; + rmtU64 average_half_RTT = 0; // RTT = Rountrip Time. + UINT64 gpu_base = 0; + double frequency = 1; + int i; + + HRESULT result; + result = rmtD3D11Finish(device, context, &gpu_base, &frequency); + if (result != S_OK && result != S_FALSE) + return result; + + for (i = 0; i < RMT_GPU_CPU_SYNC_NUM_ITERATIONS; ++i) + { + rmtU64 half_RTT; + cpu_time_start = usTimer_Get(&g_Remotery->timer); + result = rmtD3D11Finish(device, context, &gpu_base, &frequency); + cpu_time_stop = usTimer_Get(&g_Remotery->timer); + + if (result != S_OK && result != S_FALSE) + return result; + + // Ignore attempts where there was a disjoint, since there would + // be a lot of noise in those readings for measuring the RTT + if (result == S_OK) + { + // Average the time it takes a roundtrip from CPU to GPU + // while doing nothing other than getting timestamps + half_RTT = (cpu_time_stop - cpu_time_start) >> 1ULL; + if (i == 0) + average_half_RTT = half_RTT; + else + average_half_RTT = (average_half_RTT + half_RTT) >> 1ULL; + } + } + + // All GPU times are offset from gpu_base, and then taken to + // the same relative origin CPU timestamps are based on. + // CPU is in us, we must translate it to ns. + *out_first_timestamp = gpu_base - (rmtU64)((cpu_time_start + average_half_RTT) * frequency); + *out_last_resync = cpu_time_stop; + + return result; +} + +typedef struct D3D11Timestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Pair of timestamp queries that wrap the sample + ID3D11Query* query_start; + ID3D11Query* query_end; + + // A disjoint to measure frequency/stability + // TODO: Does *each* sample need one of these? + ID3D11Query* query_disjoint; + + rmtU64 cpu_timestamp; +} D3D11Timestamp; + +static rmtError D3D11Timestamp_Constructor(D3D11Timestamp* stamp) +{ + ThreadProfiler* thread_profiler; + D3D11_QUERY_DESC timestamp_desc; + D3D11_QUERY_DESC disjoint_desc; + ID3D11Device* device; + HRESULT* last_error; + rmtError rmt_error; + + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->query_start = NULL; + stamp->query_end = NULL; + stamp->query_disjoint = NULL; + stamp->cpu_timestamp = 0; + + assert(g_Remotery != NULL); + rmt_error = ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler); + if (rmt_error != RMT_ERROR_NONE) + { + return rmt_error; + } + assert(thread_profiler->d3d11 != NULL); + device = thread_profiler->d3d11->device; + last_error = &thread_profiler->d3d11->last_error; + + // Create start/end timestamp queries + timestamp_desc.Query = D3D11_QUERY_TIMESTAMP; + timestamp_desc.MiscFlags = 0; + *last_error = ID3D11Device_CreateQuery(device, ×tamp_desc, &stamp->query_start); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + *last_error = ID3D11Device_CreateQuery(device, ×tamp_desc, &stamp->query_end); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + + // Create disjoint query + disjoint_desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + disjoint_desc.MiscFlags = 0; + *last_error = ID3D11Device_CreateQuery(device, &disjoint_desc, &stamp->query_disjoint); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + + return RMT_ERROR_NONE; +} + +static void D3D11Timestamp_Destructor(D3D11Timestamp* stamp) +{ + assert(stamp != NULL); + + // Destroy queries + if (stamp->query_disjoint != NULL) + ID3D11Query_Release(stamp->query_disjoint); + if (stamp->query_end != NULL) + ID3D11Query_Release(stamp->query_end); + if (stamp->query_start != NULL) + ID3D11Query_Release(stamp->query_start); +} + +static void D3D11Timestamp_Begin(D3D11Timestamp* stamp, ID3D11DeviceContext* context) +{ + assert(stamp != NULL); + + // Start of disjoint and first query + stamp->cpu_timestamp = usTimer_Get(&g_Remotery->timer); + ID3D11DeviceContext_Begin(context, (ID3D11Asynchronous*)stamp->query_disjoint); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_start); +} + +static void D3D11Timestamp_End(D3D11Timestamp* stamp, ID3D11DeviceContext* context) +{ + assert(stamp != NULL); + + // End of disjoint and second query + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_end); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_disjoint); +} + +static HRESULT D3D11Timestamp_GetData(D3D11Timestamp* stamp, ID3D11Device* device, ID3D11DeviceContext* context, + rmtU64* out_start, rmtU64* out_end, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + ID3D11Asynchronous* query_start; + ID3D11Asynchronous* query_end; + ID3D11Asynchronous* query_disjoint; + HRESULT result; + + UINT64 start; + UINT64 end; + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint; + + assert(stamp != NULL); + query_start = (ID3D11Asynchronous*)stamp->query_start; + query_end = (ID3D11Asynchronous*)stamp->query_end; + query_disjoint = (ID3D11Asynchronous*)stamp->query_disjoint; + + // Check to see if all queries are ready + // If any fail to arrive, wait until later + result = ID3D11DeviceContext_GetData(context, query_start, &start, sizeof(start), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + result = ID3D11DeviceContext_GetData(context, query_end, &end, sizeof(end), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + result = ID3D11DeviceContext_GetData(context, query_disjoint, &disjoint, sizeof(disjoint), + D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + + if (disjoint.Disjoint == FALSE) + { + double frequency = disjoint.Frequency / 1000000.0; + + // Mark the first timestamp. We may resync if we detect the GPU timestamp is in the + // past (i.e. happened before the CPU command) since it should be impossible. + assert(out_first_timestamp != NULL); + if (*out_first_timestamp == 0 || ((start - *out_first_timestamp) / frequency) < stamp->cpu_timestamp) + { + result = SyncD3D11CpuGpuTimes(device, context, out_first_timestamp, out_last_resync); + if (result != S_OK) + return result; + } + + // Calculate start and end timestamps from the disjoint info + *out_start = (rmtU64)((start - *out_first_timestamp) / frequency); + *out_end = (rmtU64)((end - *out_first_timestamp) / frequency); + } + else + { +#if RMT_D3D11_RESYNC_ON_DISJOINT + result = SyncD3D11CpuGpuTimes(device, context, out_first_timestamp, out_last_resync); + if (result != S_OK) + return result; +#endif + } + + return S_OK; +} + +typedef struct D3D11Sample +{ + // IS-A inheritance relationship + Sample base; + + D3D11Timestamp* timestamp; + +} D3D11Sample; + +static rmtError D3D11Sample_Constructor(D3D11Sample* sample) +{ + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = RMT_SampleType_D3D11; + rmtTryNew(D3D11Timestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void D3D11Sample_Destructor(D3D11Sample* sample) +{ + rmtDelete(D3D11Timestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +RMT_API void _rmt_BindD3D11(void* device, void* context) +{ + if (g_Remotery != NULL) + { + ThreadProfiler* thread_profiler; + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + assert(thread_profiler->d3d11 != NULL); + + assert(device != NULL); + thread_profiler->d3d11->device = (ID3D11Device*)device; + assert(context != NULL); + thread_profiler->d3d11->context = (ID3D11DeviceContext*)context; + } + } +} + +static void UpdateD3D11Frame(ThreadProfiler* thread_profiler); + +RMT_API void _rmt_UnbindD3D11(void) +{ + if (g_Remotery != NULL) + { + ThreadProfiler* thread_profiler; + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + D3D11* d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + // Stall waiting for the D3D queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(d3d11->mq_to_d3d11_main)) + UpdateD3D11Frame(thread_profiler); + + // There will be a whole bunch of D3D11 sample trees queued up the remotery queue that need releasing + FreePendingSampleTrees(g_Remotery, RMT_SampleType_D3D11, d3d11->flush_samples); + + // Inform sampler to not add any more samples + d3d11->device = NULL; + d3d11->context = NULL; + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_DeleteSampleTree(g_Remotery, RMT_SampleType_D3D11); + } + } +} + +static rmtError AllocateD3D11SampleTree(SampleTree** d3d_tree) +{ + rmtTryNew(SampleTree, *d3d_tree, sizeof(D3D11Sample), (ObjConstructor)D3D11Sample_Constructor, + (ObjDestructor)D3D11Sample_Destructor); + return RMT_ERROR_NONE; +} + +RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash; + SampleTree** d3d_tree; + + // Has D3D11 been unbound? + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the D3D11 tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a D3D11 binding is not yet available. + d3d_tree = &thread_profiler->sampleTrees[RMT_SampleType_D3D11]; + if (*d3d_tree == NULL) + { + AllocateD3D11SampleTree(d3d_tree); + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*d3d_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + D3D11Sample* d3d_sample = (D3D11Sample*)sample; + d3d_sample->base.usGpuIssueOnCpu = usTimer_Get(&g_Remotery->timer); + D3D11Timestamp_Begin(d3d_sample->timestamp, d3d11->context); + } + } +} + +static rmtBool GetD3D11SampleTimes(Sample* sample, ThreadProfiler* thread_profiler, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + Sample* child; + + D3D11Sample* d3d_sample = (D3D11Sample*)sample; + + assert(sample != NULL); + if (d3d_sample->timestamp != NULL) + { + HRESULT result; + + D3D11* d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + assert(out_last_resync != NULL); + +#if (RMT_GPU_CPU_SYNC_SECONDS > 0) + if (*out_last_resync < d3d_sample->timestamp->cpu_timestamp) + { + // Convert from us to seconds. + rmtU64 time_diff = (d3d_sample->timestamp->cpu_timestamp - *out_last_resync) / 1000000ULL; + if (time_diff > RMT_GPU_CPU_SYNC_SECONDS) + { + result = SyncD3D11CpuGpuTimes(d3d11->device, d3d11->context, out_first_timestamp, out_last_resync); + if (result != S_OK) + { + d3d11->last_error = result; + return RMT_FALSE; + } + } + } +#endif + + result = D3D11Timestamp_GetData(d3d_sample->timestamp, d3d11->device, d3d11->context, &sample->us_start, + &sample->us_end, out_first_timestamp, out_last_resync); + + if (result != S_OK) + { + d3d11->last_error = result; + return RMT_FALSE; + } + + sample->us_length = sample->us_end - sample->us_start; + } + + // Sum length on the parent to track un-sampled time in the parent + if (sample->parent != NULL) + { + sample->parent->us_sampled_length += sample->us_length; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetD3D11SampleTimes(child, thread_profiler, out_first_timestamp, out_last_resync)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateD3D11Frame(ThreadProfiler* thread_profiler) +{ + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + rmt_BeginCPUSample(rmt_UpdateD3D11Frame, 0); + + // Process all messages in the D3D queue + for (;;) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(d3d11->mq_to_d3d11_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == RMT_SampleType_D3D11); + + // Retrieve timing of all D3D11 samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetD3D11SampleTimes(sample, thread_profiler, &d3d11->first_timestamp, &d3d11->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, 0, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(d3d11->mq_to_d3d11_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndD3D11Sample(void) +{ + ThreadProfiler* thread_profiler; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + D3D11Sample* d3d_sample; + + // Has D3D11 been unbound? + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + // Close the timestamp + d3d_sample = (D3D11Sample*)thread_profiler->sampleTrees[RMT_SampleType_D3D11]->currentParent; + if (d3d_sample->base.recurse_depth > 0) + { + d3d_sample->base.recurse_depth--; + } + else + { + if (d3d_sample->timestamp != NULL) + D3D11Timestamp_End(d3d_sample->timestamp, d3d11->context); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, d3d11->mq_to_d3d11_main, (Sample*)d3d_sample, 0)) + // Perform ready-polling on popping of the root sample + UpdateD3D11Frame(thread_profiler); + } + } +} + +#endif // RMT_USE_D3D11 + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @D3D12: Direct3D 12 event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_D3D12 + +// As clReflect has no way of disabling C++ compile mode, this forces C interfaces everywhere... +#define CINTERFACE + +#include + +typedef struct D3D12ThreadData +{ + rmtU32 lastAllocatedQueryIndex; + + // Sample trees in transit in the message queue for release on shutdown + Buffer* flushSamples; +} D3D12ThreadData; + +static rmtError D3D12ThreadData_Create(D3D12ThreadData** d3d12_thread_data) +{ + assert(d3d12_thread_data != NULL); + + // Allocate space for the D3D12 data + rmtTryMalloc(D3D12ThreadData, *d3d12_thread_data); + + // Set defaults + (*d3d12_thread_data)->lastAllocatedQueryIndex = 0; + (*d3d12_thread_data)->flushSamples = NULL; + + rmtTryNew(Buffer, (*d3d12_thread_data)->flushSamples, 8 * 1024); + + return RMT_ERROR_NONE; +} + +static void D3D12ThreadData_Destructor(D3D12ThreadData* d3d12_thread_data) +{ + assert(d3d12_thread_data != NULL); + rmtDelete(Buffer, d3d12_thread_data->flushSamples); +} + +typedef struct D3D12Sample +{ + // IS-A inheritance relationship + Sample base; + + // Cached bind and command list used to create the sample so that the user doesn't have to pass it + struct D3D12BindImpl* bind; + ID3D12GraphicsCommandList* commandList; + + // Begin/End timestamp indices in the query heap + rmtU32 queryIndex; + +} D3D12Sample; + +static rmtError D3D12Sample_Constructor(D3D12Sample* sample) +{ + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = RMT_SampleType_D3D12; + sample->bind = NULL; + sample->commandList = NULL; + sample->queryIndex = 0; + + return RMT_ERROR_NONE; +} + +static void D3D12Sample_Destructor(D3D12Sample* sample) +{ + Sample_Destructor((Sample*)sample); +} + +typedef struct D3D12BindImpl +{ + rmtD3D12Bind base; + + // Ring buffer of GPU timestamp destinations for all queries + rmtU32 maxNbQueries; + ID3D12QueryHeap* gpuTimestampRingBuffer; + + // CPU-accessible copy destination for all timestamps + ID3D12Resource* cpuTimestampRingBuffer; + + // Pointers to samples that expect the result of timestamps + D3D12Sample** sampleRingBuffer; + + // Read/write positions of the ring buffer allocator, synchronising access to all the ring buffers at once + // TODO(don): Separate by cache line? + rmtAtomicU32 ringBufferRead; + rmtAtomicU32 ringBufferWrite; + + ID3D12Fence* gpuQueryFence; + + + + // Queue to the D3D 12 main update thread + rmtMessageQueue* mqToD3D12Update; + + struct D3D12BindImpl* next; + +} D3D12BindImpl; + +#ifdef IID_PPV_ARGS +#define C_IID_PPV_ARGS(iid, addr) IID_PPV_ARGS(addr) +#else +#define C_IID_PPV_ARGS(iid, addr) &iid, (void**)addr +#endif + +#include + +static rmtError CreateQueryHeap(D3D12BindImpl* bind, ID3D12Device* d3d_device, ID3D12CommandQueue* d3d_queue, rmtU32 nb_queries) +{ + HRESULT hr; + D3D12_QUERY_HEAP_TYPE query_heap_type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP; + D3D12_COMMAND_QUEUE_DESC queue_desc; + D3D12_QUERY_HEAP_DESC query_heap_desc; + + // Select the correct query heap type for the copy queue + #if WDK_NTDDI_VERSION >= NTDDI_WIN10_CO + //d3d_queue->lpVtbl->GetDesc(d3d_queue, &queue_desc); + /*if (queue_desc.Type == D3D12_COMMAND_LIST_TYPE_COPY) + { + D3D12_FEATURE_DATA_D3D12_OPTIONS3 feature_data; + hr = d3d_device->lpVtbl->CheckFeatureSupport(d3d_device, D3D12_FEATURE_D3D12_OPTIONS3, &feature_data, sizeof(feature_data)); + if (hr != S_OK || feature_data.CopyQueueTimestampQueriesSupported == FALSE) + { + return rmtMakeError(RMT_ERROR_INVALID_INPUT, "Copy queues on this device do not support timestamps"); + } + + query_heap_type = D3D12_QUERY_HEAP_TYPE_COPY_QUEUE_TIMESTAMP; + }*/ + #else + if (queue_desc.Type == D3D12_COMMAND_LIST_TYPE_COPY) + { + // On old versions of Windows SDK the D3D C headers incorrectly returned structures + // The ABI is different and C++ expects return structures to be silently passed as parameters + // The newer headers add an extra out parameter to make this explicit + return rmtMakeError(RMT_ERROR_INVALID_INPUT, "Your Win10 SDK version is too old to determine if this device supports timestamps on copy queues"); + } + #endif + + // Create the heap for all the queries + ZeroMemory(&query_heap_desc, sizeof(query_heap_desc)); + query_heap_desc.Type = query_heap_type; + query_heap_desc.Count = nb_queries; + hr = d3d_device->lpVtbl->CreateQueryHeap(d3d_device, &query_heap_desc, C_IID_PPV_ARGS(IID_ID3D12QueryHeap, &bind->gpuTimestampRingBuffer)); + if (hr != S_OK) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "Failed to create D3D12 Query Heap"); + } + + return RMT_ERROR_NONE; +} + +static rmtError CreateCpuQueries(D3D12BindImpl* bind, ID3D12Device* d3d_device) +{ + D3D12_HEAP_PROPERTIES results_heap_props; + HRESULT hr; + + // We want a readback resource that the GPU can copy to and the CPU can read from + ZeroMemory(&results_heap_props, sizeof(results_heap_props)); + results_heap_props.Type = D3D12_HEAP_TYPE_READBACK; + + // Describe resource dimensions, enough to store a timestamp for each query + D3D12_RESOURCE_DESC results_desc; + ZeroMemory(&results_desc, sizeof(results_desc)); + results_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + results_desc.Width = bind->maxNbQueries * sizeof(rmtU64); + results_desc.Height = 1; + results_desc.DepthOrArraySize = 1; + results_desc.MipLevels = 1; + results_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + results_desc.SampleDesc.Count = 1; + + hr = d3d_device->lpVtbl->CreateCommittedResource(d3d_device, &results_heap_props, D3D12_HEAP_FLAG_NONE, + &results_desc, D3D12_RESOURCE_STATE_COPY_DEST, NULL, + C_IID_PPV_ARGS(IID_ID3D12Resource, &bind->cpuTimestampRingBuffer)); + if (hr != S_OK) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "Failed to create D3D12 Query Results Buffer"); + } + + return RMT_ERROR_NONE; +} + +static rmtError CreateQueryFence(D3D12BindImpl* bind, ID3D12Device* d3d_device) +{ + HRESULT hr = d3d_device->lpVtbl->CreateFence(d3d_device, 0, D3D12_FENCE_FLAG_NONE, C_IID_PPV_ARGS(IID_ID3D12Fence, &bind->gpuQueryFence)); + if (hr != S_OK) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "Failed to create D3D12 Query Fence"); + } + + return RMT_ERROR_NONE; +} + +static rmtError CopyTimestamps(D3D12BindImpl* bind, rmtU32 ring_pos_a, rmtU32 ring_pos_b, double gpu_ticks_to_us, rmtS64 gpu_to_cpu_timestamp_us) +{ + rmtU32 query_index; + D3D12_RANGE map; + rmtU64* cpu_timestamps; + + ID3D12Resource* cpu_timestamp_buffer = (ID3D12Resource*)bind->cpuTimestampRingBuffer; + D3D12Sample** cpu_sample_buffer = bind->sampleRingBuffer; + + // Map the range we're interesting in reading + map.Begin = ring_pos_a * sizeof(rmtU64); + map.End = ring_pos_b * sizeof(rmtU64); + if (cpu_timestamp_buffer->lpVtbl->Map(cpu_timestamp_buffer, 0, &map, (void**)&cpu_timestamps) != S_OK) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Failed to Map D3D12 CPU Timestamp Ring Buffer"); + } + + // Copy all timestamps to their expectant samples + for (query_index = ring_pos_a; query_index < ring_pos_b; query_index += 2) + { + rmtU64 us_start = (rmtU64)(cpu_timestamps[query_index] * gpu_ticks_to_us + gpu_to_cpu_timestamp_us); + rmtU64 us_end = (rmtU64)(cpu_timestamps[query_index + 1] * gpu_ticks_to_us + gpu_to_cpu_timestamp_us); + + D3D12Sample* sample = cpu_sample_buffer[query_index >> 1]; + sample->base.us_start = us_start; + Sample_Close(&sample->base, us_end); + sample->base.us_end = us_end; + } + + cpu_timestamp_buffer->lpVtbl->Unmap(cpu_timestamp_buffer, 0, NULL); + + return RMT_ERROR_NONE; +} + +static rmtError D3D12MarkFrame(D3D12BindImpl* bind) +{ + if (bind == NULL) + { + return RMT_ERROR_NONE; + } + + rmtU32 index_mask = bind->maxNbQueries - 1; + rmtU32 current_read_cpu = LoadAcquire(&bind->ringBufferRead); + rmtU32 current_write_cpu = LoadAcquire(&bind->ringBufferWrite); + + // Tell the GPU where the CPU write position is + ID3D12CommandQueue* d3d_queue = (ID3D12CommandQueue*)bind->base.queue; + d3d_queue->lpVtbl->Signal(d3d_queue, bind->gpuQueryFence, current_write_cpu); + + // Has the GPU processed any writes? + rmtU32 current_write_gpu = (rmtU32)bind->gpuQueryFence->lpVtbl->GetCompletedValue(bind->gpuQueryFence); + if (current_write_gpu > current_read_cpu) + { + rmtU64 gpu_tick_frequency; + double gpu_ticks_to_us; + rmtU64 gpu_timestamp_us; + rmtU64 cpu_timestamp_us; + rmtS64 gpu_to_cpu_timestamp_us; + + // Physical ring buffer positions + rmtU32 ring_pos_a = current_read_cpu & index_mask; + rmtU32 ring_pos_b = current_write_gpu & index_mask; + + // Get current ticks of both CPU and GPU for synchronisation + rmtU64 gpu_timestamp_ticks; + rmtU64 cpu_timestamp_ticks; + if (d3d_queue->lpVtbl->GetClockCalibration(d3d_queue, &gpu_timestamp_ticks, &cpu_timestamp_ticks) != S_OK) + { + return rmtMakeError(RMT_ERROR_RESOURCE_ACCESS_FAIL, "Failed to D3D12 CPU/GPU Clock Calibration"); + } + + // Convert GPU ticks to microseconds + d3d_queue->lpVtbl->GetTimestampFrequency(d3d_queue, &gpu_tick_frequency); + gpu_ticks_to_us = 1000000.0 / gpu_tick_frequency; + gpu_timestamp_us = (rmtU64)(gpu_timestamp_ticks * gpu_ticks_to_us); + + // Convert CPU ticks to microseconds, offset from the global timer start + cpu_timestamp_us = (rmtU64)((cpu_timestamp_ticks - g_Remotery->timer.counter_start.QuadPart) * g_Remotery->timer.counter_scale); + + // And we now have the offset from GPU microseconds to CPU microseconds + gpu_to_cpu_timestamp_us = cpu_timestamp_us - gpu_timestamp_us; + + // Copy resulting timestamps to their samples + // Will have to split the copies into two passes if they cross the ring buffer wrap around + if (ring_pos_b < ring_pos_a) + { + rmtTry(CopyTimestamps(bind, ring_pos_a, bind->maxNbQueries, gpu_ticks_to_us, gpu_to_cpu_timestamp_us)); + rmtTry(CopyTimestamps(bind, 0, ring_pos_b, gpu_ticks_to_us, gpu_to_cpu_timestamp_us)); + } + else + { + rmtTry(CopyTimestamps(bind, ring_pos_a, ring_pos_b, gpu_ticks_to_us, gpu_to_cpu_timestamp_us)); + } + + // Release the ring buffer entries just processed + StoreRelease(&bind->ringBufferRead, current_write_gpu); + } + + // Attempt to empty the queue of complete message trees + Message* message; + while ((message = rmtMessageQueue_PeekNextMessage(bind->mqToD3D12Update))) + { + Msg_SampleTree* msg_sample_tree; + Sample* root_sample; + + // Ensure only D3D12 sample tree messages come through here + assert(message->id == MsgID_SampleTree); + msg_sample_tree = (Msg_SampleTree*)message->payload; + root_sample = msg_sample_tree->rootSample; + assert(root_sample->type == RMT_SampleType_D3D12); + + // If the last-allocated query in this tree has been GPU-processed it's safe to now send the tree to Remotery thread + if (current_write_gpu > msg_sample_tree->userData) + { + QueueSampleTree(g_Remotery->mq_to_rmt_thread, root_sample, msg_sample_tree->allocator, msg_sample_tree->threadName, + 0, message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(bind->mqToD3D12Update, message); + } + else + { + break; + } + } + + // Chain to the next bind here so that root calling code doesn't need to know the definition of D3D12BindImpl + rmtTry(D3D12MarkFrame(bind->next)); + + return RMT_ERROR_NONE; +} + +static rmtError SampleD3D12GPUThreadLoop(rmtThread* rmt_thread) +{ + D3D12BindImpl* bind = (D3D12BindImpl*)rmt_thread->param; + + while (rmt_thread->request_exit == RMT_FALSE) + { + msSleep(15); + } + + return RMT_ERROR_NONE; +} + +RMT_API rmtError _rmt_BindD3D12(void* device, void* queue, rmtD3D12Bind** out_bind) +{ + D3D12BindImpl* bind; + ID3D12Device* d3d_device = (ID3D12Device*)device; + ID3D12CommandQueue* d3d_queue = (ID3D12CommandQueue*)queue; + + if (g_Remotery == NULL) + { + return RMT_ERROR_REMOTERY_NOT_CREATED; + } + + assert(device != NULL); + assert(queue != NULL); + assert(out_bind != NULL); + + // Allocate the bind container + rmtTryMalloc(D3D12BindImpl, bind); + + // Set default state + bind->base.device = device; + bind->base.queue = queue; + bind->maxNbQueries = 32 * 1024; + bind->gpuTimestampRingBuffer = NULL; + bind->cpuTimestampRingBuffer = NULL; + bind->sampleRingBuffer = NULL; + bind->ringBufferRead = 0; + bind->ringBufferWrite = 0; + bind->gpuQueryFence = NULL; + bind->mqToD3D12Update = NULL; + bind->next = NULL; + + // Create the independent ring buffer storage items + // TODO(don): Leave space beetween start and end to stop invalidating cache lines? + // NOTE(don): ABA impossible due to non-wrapping ring buffer indices + rmtTry(CreateQueryHeap(bind, d3d_device, d3d_queue, bind->maxNbQueries)); + rmtTry(CreateCpuQueries(bind, d3d_device)); + rmtTryMallocArray(D3D12Sample*, bind->sampleRingBuffer, bind->maxNbQueries / 2); + rmtTry(CreateQueryFence(bind, d3d_device)); + + rmtTryNew(rmtMessageQueue, bind->mqToD3D12Update, g_Settings.messageQueueSizeInBytes); + + // Add to the global linked list of binds + { + mtxLock(&g_Remotery->d3d12BindsMutex); + bind->next = g_Remotery->d3d12Binds; + g_Remotery->d3d12Binds = bind; + mtxUnlock(&g_Remotery->d3d12BindsMutex); + } + + *out_bind = &bind->base; + + return RMT_ERROR_NONE; +} + +RMT_API void _rmt_UnbindD3D12(rmtD3D12Bind* bind) +{ + D3D12BindImpl* d3d_bind = (D3D12BindImpl*)bind; + + assert(bind != NULL); + + // Remove from the linked list + { + mtxLock(&g_Remotery->d3d12BindsMutex); + D3D12BindImpl* cur = g_Remotery->d3d12Binds; + D3D12BindImpl* prev = NULL; + for ( ; cur != NULL; cur = cur->next) + { + if (cur == d3d_bind) + { + if (prev != NULL) + { + prev->next = cur->next; + } + else + { + g_Remotery->d3d12Binds = cur->next; + } + + break; + } + } + mtxUnlock(&g_Remotery->d3d12BindsMutex); + } + + if (d3d_bind->gpuQueryFence != NULL) + { + d3d_bind->gpuQueryFence->lpVtbl->Release(d3d_bind->gpuQueryFence); + } + + rmtFree(d3d_bind->sampleRingBuffer); + + if (d3d_bind->cpuTimestampRingBuffer != NULL) + { + d3d_bind->cpuTimestampRingBuffer->lpVtbl->Release(d3d_bind->cpuTimestampRingBuffer); + } + + if (d3d_bind->gpuTimestampRingBuffer != NULL) + { + d3d_bind->gpuTimestampRingBuffer->lpVtbl->Release(d3d_bind->gpuTimestampRingBuffer); + } +} + +static rmtError AllocateD3D12SampleTree(SampleTree** d3d_tree) +{ + rmtTryNew(SampleTree, *d3d_tree, sizeof(D3D12Sample), (ObjConstructor)D3D12Sample_Constructor, + (ObjDestructor)D3D12Sample_Destructor); + return RMT_ERROR_NONE; +} + +static rmtError AllocQueryPair(D3D12BindImpl* d3d_bind, rmtAtomicU32* out_allocation_index) +{ + // Check for overflow against a tail which is only ever written by one thread + rmtU32 read = LoadAcquire(&d3d_bind->ringBufferRead); + rmtU32 write = LoadAcquire(&d3d_bind->ringBufferWrite); + rmtU32 nb_queries = (write - read); + rmtU32 queries_left = d3d_bind->maxNbQueries - nb_queries; + if (queries_left < 2) + { + return rmtMakeError(RMT_ERROR_RESOURCE_CREATE_FAIL, "D3D12 query ring buffer overflow"); + } + + *out_allocation_index = AtomicAddU32(&d3d_bind->ringBufferWrite, 2); + return RMT_ERROR_NONE; +} + +RMT_API void _rmt_BeginD3D12Sample(rmtD3D12Bind* bind, void* command_list, rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL || bind == NULL) + return; + + assert(command_list != NULL); + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash; + SampleTree** d3d_tree; + + name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the D3D12 tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a D3D12 binding is not yet available. + d3d_tree = &thread_profiler->sampleTrees[RMT_SampleType_D3D12]; + if (*d3d_tree == NULL) + { + AllocateD3D12SampleTree(d3d_tree); + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*d3d_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + rmtError error; + + D3D12BindImpl* d3d_bind = (D3D12BindImpl*)bind; + ID3D12GraphicsCommandList* d3d_command_list = (ID3D12GraphicsCommandList*)command_list; + + D3D12Sample* d3d_sample = (D3D12Sample*)sample; + d3d_sample->bind = d3d_bind; + d3d_sample->commandList = d3d_command_list; + d3d_sample->base.usGpuIssueOnCpu = usTimer_Get(&g_Remotery->timer); + + error = AllocQueryPair(d3d_bind, &d3d_sample->queryIndex); + if (error == RMT_ERROR_NONE) + { + rmtU32 physical_query_index = d3d_sample->queryIndex & (d3d_bind->maxNbQueries - 1); + d3d_command_list->lpVtbl->EndQuery(d3d_command_list, d3d_bind->gpuTimestampRingBuffer, D3D12_QUERY_TYPE_TIMESTAMP, physical_query_index); + + // Track which D3D sample expects the timestamp results + d3d_bind->sampleRingBuffer[physical_query_index / 2] = d3d_sample; + + // Keep track of the last allocated query so we can check when the GPU has finished with them all + thread_profiler->d3d12ThreadData->lastAllocatedQueryIndex = d3d_sample->queryIndex; + } + else + { + // SET QUERY INDEX TO INVALID so that pop doesn't release it + } + } + } +} + +RMT_API void _rmt_EndD3D12Sample() +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + D3D12ThreadData* d3d_thread_data = thread_profiler->d3d12ThreadData; + D3D12Sample* d3d_sample; + + // Sample tree isn't there if D3D12 hasn't been initialised + SampleTree* d3d_tree = thread_profiler->sampleTrees[RMT_SampleType_D3D12]; + if (d3d_tree == NULL) + { + return; + } + + // Close the timestamp + d3d_sample = (D3D12Sample*)d3d_tree->currentParent; + if (d3d_sample->base.recurse_depth > 0) + { + d3d_sample->base.recurse_depth--; + } + else + { + // Issue the timestamp query for the end of the sample + D3D12BindImpl* d3d_bind = d3d_sample->bind; + ID3D12GraphicsCommandList* d3d_command_list = d3d_sample->commandList; + rmtU32 query_index = d3d_sample->queryIndex & (d3d_bind->maxNbQueries - 1); + d3d_command_list->lpVtbl->EndQuery(d3d_command_list, d3d_bind->gpuTimestampRingBuffer, D3D12_QUERY_TYPE_TIMESTAMP, + query_index + 1); + + // Immediately schedule resolve of the timestamps to CPU-visible memory + d3d_command_list->lpVtbl->ResolveQueryData(d3d_command_list, d3d_bind->gpuTimestampRingBuffer, + D3D12_QUERY_TYPE_TIMESTAMP, query_index, 2, + d3d_bind->cpuTimestampRingBuffer, query_index * sizeof(rmtU64)); + + if (ThreadProfiler_Pop(thread_profiler, d3d_bind->mqToD3D12Update, (Sample*)d3d_sample, + d3d_thread_data->lastAllocatedQueryIndex)) + { + } + } + } +} + +#endif // RMT_USE_D3D12 + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@OpenGL: OpenGL event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_OPENGL + +#ifndef APIENTRY +#if defined(__MINGW32__) || defined(__CYGWIN__) +#define APIENTRY __stdcall +#elif (defined(_MSC_VER) && (_MSC_VER >= 800)) || defined(_STDCALL_SUPPORTED) || defined(__BORLANDC__) +#define APIENTRY __stdcall +#else +#define APIENTRY +#endif +#endif + +#ifndef GLAPI +#if defined(__MINGW32__) || defined(__CYGWIN__) +#define GLAPI extern +#elif defined(_WIN32) +#define GLAPI WINGDIAPI +#else +#define GLAPI extern +#endif +#endif + +#ifndef GLAPIENTRY +#define GLAPIENTRY APIENTRY +#endif + +typedef rmtU32 GLenum; +typedef rmtU32 GLuint; +typedef rmtS32 GLint; +typedef rmtS32 GLsizei; +typedef rmtU64 GLuint64; +typedef rmtS64 GLint64; +typedef unsigned char GLubyte; + +typedef GLenum(GLAPIENTRY* PFNGLGETERRORPROC)(void); +typedef void(GLAPIENTRY* PFNGLGENQUERIESPROC)(GLsizei n, GLuint* ids); +typedef void(GLAPIENTRY* PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint* ids); +typedef void(GLAPIENTRY* PFNGLBEGINQUERYPROC)(GLenum target, GLuint id); +typedef void(GLAPIENTRY* PFNGLENDQUERYPROC)(GLenum target); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTUIVPROC)(GLuint id, GLenum pname, GLuint* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64* params); +typedef void(GLAPIENTRY* PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target); +typedef void(GLAPIENTRY* PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64* data); +typedef void(GLAPIENTRY* PFNGLFINISHPROC)(void); + +#define GL_NO_ERROR 0 +#define GL_QUERY_RESULT 0x8866 +#define GL_QUERY_RESULT_AVAILABLE 0x8867 +#define GL_TIME_ELAPSED 0x88BF +#define GL_TIMESTAMP 0x8E28 + +#define RMT_GL_GET_FUN(x) \ + assert(g_Remotery->opengl->x != NULL); \ + g_Remotery->opengl->x + +#define rmtglGenQueries RMT_GL_GET_FUN(__glGenQueries) +#define rmtglDeleteQueries RMT_GL_GET_FUN(__glDeleteQueries) +#define rmtglBeginQuery RMT_GL_GET_FUN(__glBeginQuery) +#define rmtglEndQuery RMT_GL_GET_FUN(__glEndQuery) +#define rmtglGetQueryObjectiv RMT_GL_GET_FUN(__glGetQueryObjectiv) +#define rmtglGetQueryObjectuiv RMT_GL_GET_FUN(__glGetQueryObjectuiv) +#define rmtglGetQueryObjecti64v RMT_GL_GET_FUN(__glGetQueryObjecti64v) +#define rmtglGetQueryObjectui64v RMT_GL_GET_FUN(__glGetQueryObjectui64v) +#define rmtglQueryCounter RMT_GL_GET_FUN(__glQueryCounter) +#define rmtglGetInteger64v RMT_GL_GET_FUN(__glGetInteger64v) +#define rmtglFinish RMT_GL_GET_FUN(__glFinish) + +struct OpenGL_t +{ + // Handle to the OS OpenGL DLL + void* dll_handle; + + PFNGLGETERRORPROC __glGetError; + PFNGLGENQUERIESPROC __glGenQueries; + PFNGLDELETEQUERIESPROC __glDeleteQueries; + PFNGLBEGINQUERYPROC __glBeginQuery; + PFNGLENDQUERYPROC __glEndQuery; + PFNGLGETQUERYOBJECTIVPROC __glGetQueryObjectiv; + PFNGLGETQUERYOBJECTUIVPROC __glGetQueryObjectuiv; + PFNGLGETQUERYOBJECTI64VPROC __glGetQueryObjecti64v; + PFNGLGETQUERYOBJECTUI64VPROC __glGetQueryObjectui64v; + PFNGLQUERYCOUNTERPROC __glQueryCounter; + PFNGLGETINTEGER64VPROC __glGetInteger64v; + PFNGLFINISHPROC __glFinish; + + // Queue to the OpenGL main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_opengl_main; + + // Mark the first time so that remaining timestamps are offset from this + rmtU64 first_timestamp; + // Last time in us (CPU time, via usTimer_Get) since we last resync'ed CPU & GPU + rmtU64 last_resync; + + // Sample trees in transit in the message queue for release on shutdown + Buffer* flush_samples; +}; + +static GLenum rmtglGetError(void) +{ + if (g_Remotery != NULL) + { + assert(g_Remotery->opengl != NULL); + if (g_Remotery->opengl->__glGetError != NULL) + return g_Remotery->opengl->__glGetError(); + } + + return (GLenum)0; +} + +#ifdef RMT_PLATFORM_LINUX +#ifdef __cplusplus +extern "C" void* glXGetProcAddressARB(const GLubyte*); +#else +extern void* glXGetProcAddressARB(const GLubyte*); +#endif +#endif + +static ProcReturnType rmtglGetProcAddress(OpenGL* opengl, const char* symbol) +{ +#if defined(RMT_PLATFORM_WINDOWS) + { + // Get OpenGL extension-loading function for each call + typedef ProcReturnType(WINAPI * wglGetProcAddressFn)(LPCSTR); + assert(opengl != NULL); + { + wglGetProcAddressFn wglGetProcAddress = + (wglGetProcAddressFn)rmtGetProcAddress(opengl->dll_handle, "wglGetProcAddress"); + if (wglGetProcAddress != NULL) + return wglGetProcAddress(symbol); + } + } + +#elif defined(RMT_PLATFORM_MACOS) && !defined(GLEW_APPLE_GLX) + + return rmtGetProcAddress(opengl->dll_handle, symbol); + +#elif defined(RMT_PLATFORM_LINUX) + + return glXGetProcAddressARB((const GLubyte*)symbol); + +#endif + + return NULL; +} + +static rmtError OpenGL_Create(OpenGL** opengl) +{ + assert(opengl != NULL); + + rmtTryMalloc(OpenGL, *opengl); + + (*opengl)->dll_handle = NULL; + + (*opengl)->__glGetError = NULL; + (*opengl)->__glGenQueries = NULL; + (*opengl)->__glDeleteQueries = NULL; + (*opengl)->__glBeginQuery = NULL; + (*opengl)->__glEndQuery = NULL; + (*opengl)->__glGetQueryObjectiv = NULL; + (*opengl)->__glGetQueryObjectuiv = NULL; + (*opengl)->__glGetQueryObjecti64v = NULL; + (*opengl)->__glGetQueryObjectui64v = NULL; + (*opengl)->__glQueryCounter = NULL; + (*opengl)->__glGetInteger64v = NULL; + (*opengl)->__glFinish = NULL; + + (*opengl)->mq_to_opengl_main = NULL; + (*opengl)->first_timestamp = 0; + (*opengl)->last_resync = 0; + (*opengl)->flush_samples = NULL; + + rmtTryNew(Buffer, (*opengl)->flush_samples, 8 * 1024); + rmtTryNew(rmtMessageQueue, (*opengl)->mq_to_opengl_main, g_Settings.messageQueueSizeInBytes); + + return RMT_ERROR_NONE; +} + +static void OpenGL_Destructor(OpenGL* opengl) +{ + assert(opengl != NULL); + rmtDelete(rmtMessageQueue, opengl->mq_to_opengl_main); + rmtDelete(Buffer, opengl->flush_samples); +} + +static void SyncOpenGLCpuGpuTimes(rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + rmtU64 cpu_time_start = 0; + rmtU64 cpu_time_stop = 0; + rmtU64 average_half_RTT = 0; // RTT = Rountrip Time. + GLint64 gpu_base = 0; + int i; + + rmtglFinish(); + + for (i = 0; i < RMT_GPU_CPU_SYNC_NUM_ITERATIONS; ++i) + { + rmtU64 half_RTT; + + rmtglFinish(); + cpu_time_start = usTimer_Get(&g_Remotery->timer); + rmtglGetInteger64v(GL_TIMESTAMP, &gpu_base); + cpu_time_stop = usTimer_Get(&g_Remotery->timer); + // Average the time it takes a roundtrip from CPU to GPU + // while doing nothing other than getting timestamps + half_RTT = (cpu_time_stop - cpu_time_start) >> 1ULL; + if (i == 0) + average_half_RTT = half_RTT; + else + average_half_RTT = (average_half_RTT + half_RTT) >> 1ULL; + } + + // All GPU times are offset from gpu_base, and then taken to + // the same relative origin CPU timestamps are based on. + // CPU is in us, we must translate it to ns. + *out_first_timestamp = (rmtU64)(gpu_base) - (cpu_time_start + average_half_RTT) * 1000ULL; + *out_last_resync = cpu_time_stop; +} + +typedef struct OpenGLTimestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Pair of timestamp queries that wrap the sample + GLuint queries[2]; + rmtU64 cpu_timestamp; +} OpenGLTimestamp; + +static rmtError OpenGLTimestamp_Constructor(OpenGLTimestamp* stamp) +{ + GLenum error; + + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->queries[0] = stamp->queries[1] = 0; + stamp->cpu_timestamp = 0; + + // Empty the error queue before using it for glGenQueries + while ((error = rmtglGetError()) != GL_NO_ERROR) + ; + + // Create start/end timestamp queries + assert(g_Remotery != NULL); + rmtglGenQueries(2, stamp->queries); + error = rmtglGetError(); + if (error != GL_NO_ERROR) + return RMT_ERROR_OPENGL_ERROR; + + return RMT_ERROR_NONE; +} + +static void OpenGLTimestamp_Destructor(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // Destroy queries + if (stamp->queries[0] != 0) + rmtglDeleteQueries(2, stamp->queries); +} + +static void OpenGLTimestamp_Begin(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // First query + assert(g_Remotery != NULL); + stamp->cpu_timestamp = usTimer_Get(&g_Remotery->timer); + rmtglQueryCounter(stamp->queries[0], GL_TIMESTAMP); +} + +static void OpenGLTimestamp_End(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // Second query + assert(g_Remotery != NULL); + rmtglQueryCounter(stamp->queries[1], GL_TIMESTAMP); +} + +static rmtBool OpenGLTimestamp_GetData(OpenGLTimestamp* stamp, rmtU64* out_start, rmtU64* out_end, + rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + GLuint64 start = 0, end = 0; + GLint startAvailable = 0, endAvailable = 0; + + assert(g_Remotery != NULL); + + assert(stamp != NULL); + assert(stamp->queries[0] != 0 && stamp->queries[1] != 0); + + // Check to see if all queries are ready + // If any fail to arrive, wait until later + rmtglGetQueryObjectiv(stamp->queries[0], GL_QUERY_RESULT_AVAILABLE, &startAvailable); + if (!startAvailable) + return RMT_FALSE; + rmtglGetQueryObjectiv(stamp->queries[1], GL_QUERY_RESULT_AVAILABLE, &endAvailable); + if (!endAvailable) + return RMT_FALSE; + + rmtglGetQueryObjectui64v(stamp->queries[0], GL_QUERY_RESULT, &start); + rmtglGetQueryObjectui64v(stamp->queries[1], GL_QUERY_RESULT, &end); + + // Mark the first timestamp. We may resync if we detect the GPU timestamp is in the + // past (i.e. happened before the CPU command) since it should be impossible. + assert(out_first_timestamp != NULL); + if (*out_first_timestamp == 0 || ((start - *out_first_timestamp) / 1000ULL) < stamp->cpu_timestamp) + SyncOpenGLCpuGpuTimes(out_first_timestamp, out_last_resync); + + // Calculate start and end timestamps (we want us, the queries give us ns) + *out_start = (rmtU64)(start - *out_first_timestamp) / 1000ULL; + *out_end = (rmtU64)(end - *out_first_timestamp) / 1000ULL; + + return RMT_TRUE; +} + +typedef struct OpenGLSample +{ + // IS-A inheritance relationship + Sample base; + + OpenGLTimestamp* timestamp; + +} OpenGLSample; + +static rmtError OpenGLSample_Constructor(OpenGLSample* sample) +{ + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = RMT_SampleType_OpenGL; + rmtTryNew(OpenGLTimestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void OpenGLSample_Destructor(OpenGLSample* sample) +{ + rmtDelete(OpenGLTimestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +RMT_API void _rmt_BindOpenGL() +{ + if (g_Remotery != NULL) + { + OpenGL* opengl = g_Remotery->opengl; + assert(opengl != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + opengl->dll_handle = rmtLoadLibrary("opengl32.dll"); +#elif defined(RMT_PLATFORM_MACOS) + opengl->dll_handle = rmtLoadLibrary("/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL"); +#elif defined(RMT_PLATFORM_LINUX) + opengl->dll_handle = rmtLoadLibrary("libGL.so"); +#endif + + opengl->__glGetError = (PFNGLGETERRORPROC)rmtGetProcAddress(opengl->dll_handle, "glGetError"); + opengl->__glGenQueries = (PFNGLGENQUERIESPROC)rmtglGetProcAddress(opengl, "glGenQueries"); + opengl->__glDeleteQueries = (PFNGLDELETEQUERIESPROC)rmtglGetProcAddress(opengl, "glDeleteQueries"); + opengl->__glBeginQuery = (PFNGLBEGINQUERYPROC)rmtglGetProcAddress(opengl, "glBeginQuery"); + opengl->__glEndQuery = (PFNGLENDQUERYPROC)rmtglGetProcAddress(opengl, "glEndQuery"); + opengl->__glGetQueryObjectiv = (PFNGLGETQUERYOBJECTIVPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectiv"); + opengl->__glGetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectuiv"); + opengl->__glGetQueryObjecti64v = + (PFNGLGETQUERYOBJECTI64VPROC)rmtglGetProcAddress(opengl, "glGetQueryObjecti64v"); + opengl->__glGetQueryObjectui64v = + (PFNGLGETQUERYOBJECTUI64VPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectui64v"); + opengl->__glQueryCounter = (PFNGLQUERYCOUNTERPROC)rmtglGetProcAddress(opengl, "glQueryCounter"); + opengl->__glGetInteger64v = (PFNGLGETINTEGER64VPROC)rmtglGetProcAddress(opengl, "glGetInteger64v"); + opengl->__glFinish = (PFNGLFINISHPROC)rmtGetProcAddress(opengl->dll_handle, "glFinish"); + } +} + +static void UpdateOpenGLFrame(void); + +RMT_API void _rmt_UnbindOpenGL(void) +{ + if (g_Remotery != NULL) + { + OpenGL* opengl = g_Remotery->opengl; + assert(opengl != NULL); + + // Stall waiting for the OpenGL queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(opengl->mq_to_opengl_main)) + UpdateOpenGLFrame(); + + // There will be a whole bunch of OpenGL sample trees queued up the remotery queue that need releasing + FreePendingSampleTrees(g_Remotery, RMT_SampleType_OpenGL, opengl->flush_samples); + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_DeleteSampleTree(g_Remotery, RMT_SampleType_OpenGL); + + // Release reference to the OpenGL DLL + if (opengl->dll_handle != NULL) + { + rmtFreeLibrary(opengl->dll_handle); + opengl->dll_handle = NULL; + } + } +} + +static rmtError AllocateOpenGLSampleTree(SampleTree** ogl_tree) +{ + rmtTryNew(SampleTree, *ogl_tree, sizeof(OpenGLSample), (ObjConstructor)OpenGLSample_Constructor, + (ObjDestructor)OpenGLSample_Destructor); + return RMT_ERROR_NONE; +} + +RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the OpenGL tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a OpenGL binding is not yet available. + SampleTree** ogl_tree = &thread_profiler->sampleTrees[RMT_SampleType_OpenGL]; + if (*ogl_tree == NULL) + { + AllocateOpenGLSampleTree(ogl_tree); + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*ogl_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + OpenGLSample* ogl_sample = (OpenGLSample*)sample; + ogl_sample->base.usGpuIssueOnCpu = usTimer_Get(&g_Remotery->timer); + OpenGLTimestamp_Begin(ogl_sample->timestamp); + } + } +} + +static rmtBool GetOpenGLSampleTimes(Sample* sample, rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + Sample* child; + + OpenGLSample* ogl_sample = (OpenGLSample*)sample; + + assert(sample != NULL); + if (ogl_sample->timestamp != NULL) + { + assert(out_last_resync != NULL); +#if (RMT_GPU_CPU_SYNC_SECONDS > 0) + if (*out_last_resync < ogl_sample->timestamp->cpu_timestamp) + { + // Convert from us to seconds. + rmtU64 time_diff = (ogl_sample->timestamp->cpu_timestamp - *out_last_resync) / 1000000ULL; + if (time_diff > RMT_GPU_CPU_SYNC_SECONDS) + SyncOpenGLCpuGpuTimes(out_first_timestamp, out_last_resync); + } +#endif + + if (!OpenGLTimestamp_GetData(ogl_sample->timestamp, &sample->us_start, &sample->us_end, out_first_timestamp, + out_last_resync)) + return RMT_FALSE; + + sample->us_length = sample->us_end - sample->us_start; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetOpenGLSampleTimes(child, out_first_timestamp, out_last_resync)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateOpenGLFrame(void) +{ + OpenGL* opengl; + + if (g_Remotery == NULL) + return; + + opengl = g_Remotery->opengl; + assert(opengl != NULL); + + rmt_BeginCPUSample(rmt_UpdateOpenGLFrame, 0); + + // Process all messages in the OpenGL queue + while (1) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(opengl->mq_to_opengl_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == RMT_SampleType_OpenGL); + + // Retrieve timing of all OpenGL samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetOpenGLSampleTimes(sample, &opengl->first_timestamp, &opengl->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, 0, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(opengl->mq_to_opengl_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndOpenGLSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + // Close the timestamp + OpenGLSample* ogl_sample = (OpenGLSample*)thread_profiler->sampleTrees[RMT_SampleType_OpenGL]->currentParent; + if (ogl_sample->base.recurse_depth > 0) + { + ogl_sample->base.recurse_depth--; + } + else + { + if (ogl_sample->timestamp != NULL) + OpenGLTimestamp_End(ogl_sample->timestamp); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, g_Remotery->opengl->mq_to_opengl_main, (Sample*)ogl_sample, 0)) + // Perform ready-polling on popping of the root sample + UpdateOpenGLFrame(); + } + } +} + +#endif // RMT_USE_OPENGL + +/* + ------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------------------------------------------------------ + @Metal: Metal event sampling + ------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------------------------------------------------------ + */ + +#if RMT_USE_METAL + +struct Metal_t +{ + // Queue to the Metal main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_metal_main; +}; + +static rmtError Metal_Create(Metal** metal) +{ + assert(metal != NULL); + + rmtTryMallocArray(Metal, *metal); + + (*metal)->mq_to_metal_main = NULL; + + rmtTryNew(rmtMessageQueue, (*metal)->mq_to_metal_main, g_Settings.messageQueueSizeInBytes); + return error; +} + +static void Metal_Destructor(Metal* metal) +{ + assert(metal != NULL); + rmtDelete(rmtMessageQueue, metal->mq_to_metal_main); +} + +typedef struct MetalTimestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Output from GPU callbacks + rmtU64 start; + rmtU64 end; + rmtBool ready; +} MetalTimestamp; + +static rmtError MetalTimestamp_Constructor(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->start = 0; + stamp->end = 0; + stamp->ready = RMT_FALSE; + + return RMT_ERROR_NONE; +} + +static void MetalTimestamp_Destructor(MetalTimestamp* stamp) +{ + assert(stamp != NULL); +} + +rmtU64 rmtMetal_usGetTime() +{ + // Share the CPU timer for auto-sync + assert(g_Remotery != NULL); + return usTimer_Get(&g_Remotery->timer); +} + +void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready); + +static void MetalTimestamp_Begin(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + stamp->ready = RMT_FALSE; + + // Metal can currently only issue callbacks at the command buffer level + // So for now measure execution of the entire command buffer + rmtMetal_MeasureCommandBuffer(&stamp->start, &stamp->end, &stamp->ready); +} + +static void MetalTimestamp_End(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + + // As Metal can currently only measure entire command buffers, this function is a no-op + // as the completed handler was already issued in Begin +} + +static rmtBool MetalTimestamp_GetData(MetalTimestamp* stamp, rmtU64* out_start, rmtU64* out_end) +{ + assert(g_Remotery != NULL); + assert(stamp != NULL); + + // GPU writes ready flag when complete handler is called + if (stamp->ready == RMT_FALSE) + return RMT_FALSE; + + *out_start = stamp->start; + *out_end = stamp->end; + + return RMT_TRUE; +} + +typedef struct MetalSample +{ + // IS-A inheritance relationship + Sample base; + + MetalTimestamp* timestamp; + +} MetalSample; + +static rmtError MetalSample_Constructor(MetalSample* sample) +{ + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = RMT_SampleType_Metal; + rmtTryNew(MetalTimestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void MetalSample_Destructor(MetalSample* sample) +{ + rmtDelete(MetalTimestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +static void UpdateOpenGLFrame(void); + +/*RMT_API void _rmt_UnbindMetal(void) +{ + if (g_Remotery != NULL) + { + Metal* metal = g_Remotery->metal; + assert(metal != NULL); + + // Stall waiting for the Metal queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(metal->mq_to_metal_main)) + UpdateMetalFrame(); + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_BlockingDeleteSampleTree(g_Remotery, RMT_SampleType_Metal); + } +}*/ + +RMT_API void _rmt_BeginMetalSample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the Metal tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a Metal binding is not yet available. + SampleTree** metal_tree = &thread_profiler->sampleTrees[RMT_SampleType_Metal]; + if (*metal_tree == NULL) + { + rmtError error; + rmtTryNew(SampleTree, *metal_tree, sizeof(MetalSample), (ObjConstructor)MetalSample_Constructor, + (ObjDestructor)MetalSample_Destructor); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*metal_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + MetalSample* metal_sample = (MetalSample*)sample; + metal_sample->base.usGpuIssueOnCpu = usTimer_Get(&g_Remotery->timer); + MetalTimestamp_Begin(metal_sample->timestamp); + } + } +} + +static rmtBool GetMetalSampleTimes(Sample* sample) +{ + Sample* child; + + MetalSample* metal_sample = (MetalSample*)sample; + + assert(sample != NULL); + if (metal_sample->timestamp != NULL) + { + if (!MetalTimestamp_GetData(metal_sample->timestamp, &sample->us_start, &sample->us_end)) + return RMT_FALSE; + + sample->us_length = sample->us_end - sample->us_start; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetMetalSampleTimes(child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateMetalFrame(void) +{ + Metal* metal; + + if (g_Remotery == NULL) + return; + + metal = g_Remotery->metal; + assert(metal != NULL); + + rmt_BeginCPUSample(rmt_UpdateMetalFrame, 0); + + // Process all messages in the Metal queue + while (1) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(metal->mq_to_metal_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == RMT_SampleType_Metal); + + // Retrieve timing of all Metal samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetMetalSampleTimes(sample)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, 0, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(metal->mq_to_metal_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndMetalSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + // Close the timestamp + MetalSample* metal_sample = (MetalSample*)thread_profiler->sampleTrees[RMT_SampleType_Metal]->currentParent; + if (metal_sample->base.recurse_depth > 0) + { + metal_sample->base.recurse_depth--; + } + else + { + if (metal_sample->timestamp != NULL) + MetalTimestamp_End(metal_sample->timestamp); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, g_Remotery->metal->mq_to_metal_main, (Sample*)metal_sample, 0)) + // Perform ready-polling on popping of the root sample + UpdateMetalFrame(); + } + } +} + +#endif // RMT_USE_METAL + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@SAMPLEAPI: Sample API for user callbacks +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// Iterator +RMT_API void _rmt_IterateChildren(rmtSampleIterator* iterator, rmtSample* sample) +{ + iterator->sample = 0; + iterator->initial = sample != NULL ? sample->first_child : 0; +} + +RMT_API rmtBool _rmt_IterateNext(rmtSampleIterator* iter) +{ + if (iter->initial != NULL) + { + iter->sample = iter->initial; + iter->initial = 0; + } + else + { + if (iter->sample != NULL) + iter->sample = iter->sample->next_sibling; + } + + return iter->sample != NULL ? RMT_TRUE : RMT_FALSE; +} + +// Sample tree accessors +RMT_API const char* _rmt_SampleTreeGetThreadName(rmtSampleTree* sample_tree) +{ + return sample_tree->threadName; +} + +RMT_API rmtSample* _rmt_SampleTreeGetRootSample(rmtSampleTree* sample_tree) +{ + return sample_tree->rootSample; +} + +// Sample accessors +RMT_API const char* _rmt_SampleGetName(rmtSample* sample) +{ + const char* name = StringTable_Find(g_Remotery->string_table, sample->name_hash); + if (name == NULL) + { + return "null"; + } + return name; +} + +RMT_API rmtU32 _rmt_SampleGetNameHash(rmtSample* sample) +{ + return sample->name_hash; +} + +RMT_API rmtU32 _rmt_SampleGetCallCount(rmtSample* sample) +{ + return sample->call_count; +} + +RMT_API rmtU64 _rmt_SampleGetStart(rmtSample* sample) +{ + return sample->us_start; +} + +RMT_API rmtU64 _rmt_SampleGetTime(rmtSample* sample) +{ + return sample->us_length; +} + +RMT_API rmtU64 _rmt_SampleGetSelfTime(rmtSample* sample) +{ + return (rmtU64)maxS64(sample->us_length - sample->us_sampled_length, 0); +} + +RMT_API rmtSampleType _rmt_SampleGetType(rmtSample* sample) +{ + return sample->type; +} + +RMT_API void _rmt_SampleGetColour(rmtSample* sample, rmtU8* r, rmtU8* g, rmtU8* b) +{ + *r = sample->uniqueColour[0]; + *g = sample->uniqueColour[1]; + *b = sample->uniqueColour[2]; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@PROPERTYAPI: Property API for user callbacks +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// Iterator +RMT_API void _rmt_PropertyIterateChildren(rmtPropertyIterator* iterator, rmtProperty* property) +{ + iterator->property = 0; + iterator->initial = property != NULL ? property->firstChild : 0; +} + +RMT_API rmtBool _rmt_PropertyIterateNext(rmtPropertyIterator* iter) +{ + if (iter->initial != NULL) + { + iter->property = iter->initial; + iter->initial = 0; + } + else + { + if (iter->property != NULL) + iter->property = iter->property->nextSibling; + } + + return iter->property != NULL ? RMT_TRUE : RMT_FALSE; +} + +// Property accessors +RMT_API const char* _rmt_PropertyGetName(rmtProperty* property) +{ + return property->name; +} + +RMT_API const char* _rmt_PropertyGetDescription(rmtProperty* property) +{ + return property->description; +} + +RMT_API rmtU32 _rmt_PropertyGetNameHash(rmtProperty* property) +{ + return property->nameHash; +} + +RMT_API rmtPropertyType _rmt_PropertyGetType(rmtProperty* property) +{ + return property->type; +} + +RMT_API rmtPropertyValue _rmt_PropertyGetValue(rmtProperty* property) +{ + return property->value; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@PROPERTIES: Property API +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static void RegisterProperty(rmtProperty* property, rmtBool can_lock) +{ + if (property->initialised == RMT_FALSE) + { + // Apply for a lock once at the start of the recursive walk + if (can_lock) + { + mtxLock(&g_Remotery->propertyMutex); + } + + // Multiple threads accessing the same property can apply for the lock at the same time as the `initialised` property for + // each of them may not be set yet. One thread only will get the lock successfully while the others will only come through + // here when the first thread has finished initialising. The first thread through will have `initialised` set to RMT_FALSE + // while all other threads will see it in its initialised state. Skip those so that we don't register multiple times. + if (property->initialised == RMT_FALSE) + { + rmtU32 name_len; + + // With no parent, add this to the root property + rmtProperty* parent_property = property->parent; + if (parent_property == NULL) + { + property->parent = &g_Remotery->rootProperty; + parent_property = property->parent; + } + + // Walk up to parent properties first in case they haven't been registered + RegisterProperty(parent_property, RMT_FALSE); + + // Link this property into the parent's list + if (parent_property->firstChild != NULL) + { + parent_property->lastChild->nextSibling = property; + parent_property->lastChild = property; + } + else + { + parent_property->firstChild = property; + parent_property->lastChild = property; + } + + // Calculate the name hash and send it to the viewer + name_len = strnlen_s(property->name, 256); + property->nameHash = _rmt_HashString32(property->name, name_len, 0); + QueueAddToStringTable(g_Remotery->mq_to_rmt_thread, property->nameHash, property->name, name_len, NULL); + + // Generate a unique ID for this property in the tree + property->uniqueID = parent_property->uniqueID; + property->uniqueID = HashCombine(property->uniqueID, property->nameHash); + + property->initialised = RMT_TRUE; + } + + // Unlock on the way out of recursive walk + if (can_lock) + { + mtxUnlock(&g_Remotery->propertyMutex); + } + } +} + +RMT_API void _rmt_PropertySetValue(rmtProperty* property) +{ + if (g_Remotery == NULL) + { + return; + } + + RegisterProperty(property, RMT_TRUE); + + // on this thread, create a new sample that encodes the value just set + + // send the sample to remotery UI and disk log + + // value resets and sets don't have delta values, really +} + +RMT_API void _rmt_PropertyAddValue(rmtProperty* property, rmtPropertyValue add_value) +{ + if (g_Remotery == NULL) + { + return; + } + + RegisterProperty(property, RMT_TRUE); + + // use `add_value` to determine how much this property was changed + + // on this thread, create a new sample that encodes the delta and parents itself to `property` + // could also encode the current value of the property at this point + + // send the sample to remotery UI and disk log +} + +static rmtError TakePropertySnapshot(rmtProperty* property, PropertySnapshot* parent_snapshot, PropertySnapshot** first_snapshot, PropertySnapshot** prev_snapshot, rmtU32 depth) +{ + rmtError error; + rmtProperty* child_property; + + // Allocate some state for the property + PropertySnapshot* snapshot; + error = ObjectAllocator_Alloc(g_Remotery->propertyAllocator, (void**)&snapshot); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Snapshot the property + snapshot->type = property->type; + snapshot->value = property->value; + snapshot->prevValue = property->prevValue; + snapshot->prevValueFrame = property->prevValueFrame; + snapshot->nameHash = property->nameHash; + snapshot->uniqueID = property->uniqueID; + snapshot->nbChildren = 0; + snapshot->depth = depth; + snapshot->nextSnapshot = NULL; + + // Keep count of the number of children in the parent + if (parent_snapshot != NULL) + { + parent_snapshot->nbChildren++; + } + + // Link into the linear list + if (*first_snapshot == NULL) + { + *first_snapshot = snapshot; + } + if (*prev_snapshot != NULL) + { + (*prev_snapshot)->nextSnapshot = snapshot; + } + *prev_snapshot = snapshot; + + // Snapshot the children + for (child_property = property->firstChild; child_property != NULL; child_property = child_property->nextSibling) + { + error = TakePropertySnapshot(child_property, snapshot, first_snapshot, prev_snapshot, depth + 1); + if (error != RMT_ERROR_NONE) + { + return error; + } + } + + return RMT_ERROR_NONE; +} + +RMT_API rmtError _rmt_PropertySnapshotAll() +{ + rmtError error; + PropertySnapshot* first_snapshot; + PropertySnapshot* prev_snapshot; + Msg_PropertySnapshot* payload; + Message* message; + rmtU32 nb_snapshot_allocs; + + if (g_Remotery == NULL) + { + return RMT_ERROR_REMOTERY_NOT_CREATED; + } + + // Don't do anything if any properties haven't been registered yet + if (g_Remotery->rootProperty.firstChild == NULL) + { + return RMT_ERROR_NONE; + } + + // Mark current allocation count so we can quickly calculate the number of snapshots being sent + nb_snapshot_allocs = g_Remotery->propertyAllocator->nb_inuse; + + // Snapshot from the root into a linear list + first_snapshot = NULL; + prev_snapshot = NULL; + mtxLock(&g_Remotery->propertyMutex); + error = TakePropertySnapshot(&g_Remotery->rootProperty, NULL, &first_snapshot, &prev_snapshot, 0); + + if (g_Settings.snapshot_callback != NULL) + { + g_Settings.snapshot_callback(g_Settings.snapshot_context, &g_Remotery->rootProperty); + } + + mtxUnlock(&g_Remotery->propertyMutex); + if (error != RMT_ERROR_NONE) + { + FreePropertySnapshots(first_snapshot); + return error; + } + + // Attempt to allocate a message for sending the snapshot to the viewer + message = rmtMessageQueue_AllocMessage(g_Remotery->mq_to_rmt_thread, sizeof(Msg_PropertySnapshot), NULL); + if (message == NULL) + { + FreePropertySnapshots(first_snapshot); + return RMT_ERROR_UNKNOWN; + } + + // Populate and commit + payload = (Msg_PropertySnapshot*)message->payload; + payload->rootSnapshot = first_snapshot; + payload->nbSnapshots = g_Remotery->propertyAllocator->nb_inuse - nb_snapshot_allocs; + payload->propertyFrame = g_Remotery->propertyFrame; + rmtMessageQueue_CommitMessage(message, MsgID_PropertySnapshot); + + return RMT_ERROR_NONE; +} + +static void PropertyFrameReset(Remotery* rmt, rmtProperty* first_property) +{ + rmtProperty* property; + for (property = first_property; property != NULL; property = property->nextSibling) + { + // TODO(don): It might actually be quicker to sign-extend assignments but this gives me a nice debug hook for now + rmtBool changed = RMT_FALSE; + switch (property->type) + { + case RMT_PropertyType_rmtGroup: + PropertyFrameReset(rmt, property->firstChild); + break; + + case RMT_PropertyType_rmtBool: + changed = property->lastFrameValue.Bool != property->value.Bool; + break; + + case RMT_PropertyType_rmtS32: + case RMT_PropertyType_rmtU32: + case RMT_PropertyType_rmtF32: + changed = property->lastFrameValue.U32 != property->value.U32; + break; + + case RMT_PropertyType_rmtS64: + case RMT_PropertyType_rmtU64: + case RMT_PropertyType_rmtF64: + changed = property->lastFrameValue.U64 != property->value.U64; + break; + } + + if (changed) + { + property->prevValue = property->lastFrameValue; + property->prevValueFrame = rmt->propertyFrame; + } + + property->lastFrameValue = property->value; + + if ((property->flags & RMT_PropertyFlags_FrameReset) != 0) + { + property->value = property->defaultValue; + } + } +} + +RMT_API void _rmt_PropertyFrameResetAll() +{ + if (g_Remotery == NULL) + { + return; + } + + mtxLock(&g_Remotery->propertyMutex); + PropertyFrameReset(g_Remotery, g_Remotery->rootProperty.firstChild); + mtxUnlock(&g_Remotery->propertyMutex); + + g_Remotery->propertyFrame++; +} + +#endif // RMT_ENABLED diff --git a/profiler/lib/Remotery.h b/profiler/lib/Remotery.h new file mode 100644 index 0000000..9cd0c85 --- /dev/null +++ b/profiler/lib/Remotery.h @@ -0,0 +1,1095 @@ + +/* +Copyright 2014-2022 Celtoys Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + +Compiling +--------- + +* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include + directories to add Remotery/lib path. The required library ws2_32.lib should be picked + up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c. + +* Mac OS X (XCode) - simply add lib/Remotery.c and lib/Remotery.h to your program. + +* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for + library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c + -I lib -pthread -lm + +You can define some extra macros to modify what features are compiled into Remotery. These are +documented just below this comment. + +*/ + + +#ifndef RMT_INCLUDED_H +#define RMT_INCLUDED_H + + +// Set to 0 to not include any bits of Remotery in your build +#ifndef RMT_ENABLED +#define RMT_ENABLED 1 +#endif + +// Help performance of the server sending data to the client by marking this machine as little-endian +#ifndef RMT_ASSUME_LITTLE_ENDIAN +#define RMT_ASSUME_LITTLE_ENDIAN 0 +#endif + +// Used by the Celtoys TinyCRT library (not released yet) +#ifndef RMT_USE_TINYCRT +#define RMT_USE_TINYCRT 0 +#endif + +// Assuming CUDA headers/libs are setup, allow CUDA profiling +#ifndef RMT_USE_CUDA +#define RMT_USE_CUDA 0 +#endif + +// Assuming Direct3D 11 headers/libs are setup, allow D3D11 profiling +#ifndef RMT_USE_D3D11 +#define RMT_USE_D3D11 0 +#endif + +// Allow D3D12 profiling +#ifndef RMT_USE_D3D12 +#define RMT_USE_D3D12 0 +#endif + +// Allow OpenGL profiling +#ifndef RMT_USE_OPENGL +#define RMT_USE_OPENGL 0 +#endif + +// Allow Metal profiling +#ifndef RMT_USE_METAL +#define RMT_USE_METAL 0 +#endif + +// Initially use POSIX thread names to name threads instead of Thread0, 1, ... +#ifndef RMT_USE_POSIX_THREADNAMES +#define RMT_USE_POSIX_THREADNAMES 0 +#endif + +// How many times we spin data back and forth between CPU & GPU +// to calculate average RTT (Roundtrip Time). Cannot be 0. +// Affects OpenGL & D3D11 +#ifndef RMT_GPU_CPU_SYNC_NUM_ITERATIONS +#define RMT_GPU_CPU_SYNC_NUM_ITERATIONS 16 +#endif + +// Time in seconds between each resync to compensate for drifting between GPU & CPU timers, +// effects of power saving, etc. Resyncs can cause stutter, lag spikes, stalls. +// Set to 0 for never. +// Affects OpenGL & D3D11 +#ifndef RMT_GPU_CPU_SYNC_SECONDS +#define RMT_GPU_CPU_SYNC_SECONDS 30 +#endif + +// Whether we should automatically resync if we detect a timer disjoint (e.g. +// changed from AC power to battery, GPU is overheating, or throttling up/down +// due to laptop savings events). Set it to 0 to avoid resync in such events. +// Useful if for some odd reason a driver reports a lot of disjoints. +// Affects D3D11 +#ifndef RMT_D3D11_RESYNC_ON_DISJOINT +#define RMT_D3D11_RESYNC_ON_DISJOINT 1 +#endif + +// If RMT_USE_INTERNAL_HASH_FUNCTION is defined to 1, the internal hash function for strings is used. +// This is the default setting. +// If you set RMT_USE_INTERNAL_HASH_FUNCTION to 0, you must implement rmt_HashString32 yourself. +#ifndef RMT_USE_INTERNAL_HASH_FUNCTION +#define RMT_USE_INTERNAL_HASH_FUNCTION 1 +#endif + +/*-------------------------------------------------------------------------------------------------------------------------------- + Compiler/Platform Detection and Preprocessor Utilities +---------------------------------------------------------------------------------------------------------------------------------*/ + + +// Platform identification +#if defined(_WINDOWS) || defined(_WIN32) + #define RMT_PLATFORM_WINDOWS +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #define RMT_PLATFORM_LINUX + #define RMT_PLATFORM_POSIX +#elif defined(__APPLE__) + #define RMT_PLATFORM_MACOS + #define RMT_PLATFORM_POSIX +#endif + +// Architecture identification +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _M_AMD64 +#define RMT_ARCH_64BIT +#else +#define RMT_ARCH_32BIT +#endif +#endif + +#ifdef RMT_DLL + #if defined (RMT_PLATFORM_WINDOWS) + #if defined (RMT_IMPL) + #define RMT_API __declspec(dllexport) + #else + #define RMT_API __declspec(dllimport) + #endif + #elif defined (RMT_PLATFORM_POSIX) + #if defined (RMT_IMPL) + #define RMT_API __attribute__((visibility("default"))) + #else + #define RMT_API + #endif + #endif +#else + #define RMT_API +#endif + +// Allows macros to be written that can work around the inability to do: #define(x) #ifdef x +// with the C preprocessor. +#if RMT_ENABLED + #define IFDEF_RMT_ENABLED(t, f) t +#else + #define IFDEF_RMT_ENABLED(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_CUDA + #define IFDEF_RMT_USE_CUDA(t, f) t +#else + #define IFDEF_RMT_USE_CUDA(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_D3D11 + #define IFDEF_RMT_USE_D3D11(t, f) t +#else + #define IFDEF_RMT_USE_D3D11(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_D3D12 + #define IFDEF_RMT_USE_D3D12(t, f) t +#else + #define IFDEF_RMT_USE_D3D12(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_OPENGL + #define IFDEF_RMT_USE_OPENGL(t, f) t +#else + #define IFDEF_RMT_USE_OPENGL(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_METAL + #define IFDEF_RMT_USE_METAL(t, f) t +#else + #define IFDEF_RMT_USE_METAL(t, f) f +#endif + + +// Public interface is written in terms of these macros to easily enable/disable itself +#define RMT_OPTIONAL(macro, x) IFDEF_ ## macro(x, ) +#define RMT_OPTIONAL_RET(macro, x, y) IFDEF_ ## macro(x, (y)) + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Types +--------------------------------------------------------------------------------------------------------------------------------*/ + + +// Boolean +typedef unsigned int rmtBool; +#define RMT_TRUE ((rmtBool)1) +#define RMT_FALSE ((rmtBool)0) + +// Unsigned integer types +typedef unsigned char rmtU8; +typedef unsigned short rmtU16; +typedef unsigned int rmtU32; +typedef unsigned long long rmtU64; + +// Signed integer types +typedef char rmtS8; +typedef short rmtS16; +typedef int rmtS32; +typedef long long rmtS64; + +// Float types +typedef float rmtF32; +typedef double rmtF64; + +// Const, null-terminated string pointer +typedef const char* rmtPStr; + +// Opaque pointer for a sample graph tree +typedef struct Msg_SampleTree rmtSampleTree; + +// Opaque pointer to a node in the sample graph tree +typedef struct Sample rmtSample; + +// Handle to the main remotery instance +typedef struct Remotery Remotery; + +// Forward declaration +struct rmtProperty; + +typedef enum rmtSampleType +{ + RMT_SampleType_CPU, + RMT_SampleType_CUDA, + RMT_SampleType_D3D11, + RMT_SampleType_D3D12, + RMT_SampleType_OpenGL, + RMT_SampleType_Metal, + RMT_SampleType_Count, +} rmtSampleType; + +// All possible error codes +// clang-format off +typedef enum rmtError +{ + RMT_ERROR_NONE, + RMT_ERROR_RECURSIVE_SAMPLE, // Not an error but an internal message to calling code + RMT_ERROR_UNKNOWN, // An error with a message yet to be defined, only for internal error handling + RMT_ERROR_INVALID_INPUT, // An invalid input to a function call was provided + RMT_ERROR_RESOURCE_CREATE_FAIL, // Creation of an internal resource failed + RMT_ERROR_RESOURCE_ACCESS_FAIL, // Access of an internal resource failed + RMT_ERROR_TIMEOUT, // Internal system timeout + + // System errors + RMT_ERROR_MALLOC_FAIL, // Malloc call within remotery failed + RMT_ERROR_TLS_ALLOC_FAIL, // Attempt to allocate thread local storage failed + RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL, // Failed to create a virtual memory mirror buffer + RMT_ERROR_CREATE_THREAD_FAIL, // Failed to create a thread for the server + RMT_ERROR_OPEN_THREAD_HANDLE_FAIL, // Failed to open a thread handle, given a thread id + + // Network TCP/IP socket errors + RMT_ERROR_SOCKET_INVALID_POLL, // Poll attempt on an invalid socket + RMT_ERROR_SOCKET_SELECT_FAIL, // Server failed to call select on socket + RMT_ERROR_SOCKET_POLL_ERRORS, // Poll notified that the socket has errors + RMT_ERROR_SOCKET_SEND_FAIL, // Unrecoverable error occured while client/server tried to send data + RMT_ERROR_SOCKET_RECV_NO_DATA, // No data available when attempting a receive + RMT_ERROR_SOCKET_RECV_TIMEOUT, // Timed out trying to receive data + RMT_ERROR_SOCKET_RECV_FAILED, // Unrecoverable error occured while client/server tried to receive data + + // WebSocket errors + RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET, // WebSocket server handshake failed, not HTTP GET + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION, // WebSocket server handshake failed, can't locate WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION, // WebSocket server handshake failed, unsupported WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST, // WebSocket server handshake failed, can't locate host + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST, // WebSocket server handshake failed, host is not allowed to connect + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY, // WebSocket server handshake failed, can't locate WebSocket key + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY, // WebSocket server handshake failed, WebSocket key is ill-formed + RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL, // WebSocket server handshake failed, internal error, bad string code + RMT_ERROR_WEBSOCKET_DISCONNECTED, // WebSocket server received a disconnect request and closed the socket + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER, // Couldn't parse WebSocket frame header + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE, // Partially received wide frame header size + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_MASK, // Partially received frame header data mask + RMT_ERROR_WEBSOCKET_RECEIVE_TIMEOUT, // Timeout receiving frame header + + RMT_ERROR_REMOTERY_NOT_CREATED, // Remotery object has not been created + RMT_ERROR_SEND_ON_INCOMPLETE_PROFILE, // An attempt was made to send an incomplete profile tree to the client + + // CUDA error messages + RMT_ERROR_CUDA_DEINITIALIZED, // This indicates that the CUDA driver is in the process of shutting down + RMT_ERROR_CUDA_NOT_INITIALIZED, // This indicates that the CUDA driver has not been initialized with cuInit() or that initialization has failed + RMT_ERROR_CUDA_INVALID_CONTEXT, // This most frequently indicates that there is no context bound to the current thread + RMT_ERROR_CUDA_INVALID_VALUE, // This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values + RMT_ERROR_CUDA_INVALID_HANDLE, // This indicates that a resource handle passed to the API call was not valid + RMT_ERROR_CUDA_OUT_OF_MEMORY, // The API call failed because it was unable to allocate enough memory to perform the requested operation + RMT_ERROR_ERROR_NOT_READY, // This indicates that a resource handle passed to the API call was not valid + + // Direct3D 11 error messages + RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY, // Failed to create query for sample + + // OpenGL error messages + RMT_ERROR_OPENGL_ERROR, // Generic OpenGL error, no need to expose detail since app will need an OpenGL error callback registered + + RMT_ERROR_CUDA_UNKNOWN, +} rmtError; +// clang-format on + +// Gets the last error message issued on the calling thread +RMT_API rmtPStr rmt_GetLastErrorMessage(); + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Runtime Settings +--------------------------------------------------------------------------------------------------------------------------------*/ + + +// Callback function pointer types +typedef void* (*rmtMallocPtr)(void* mm_context, rmtU32 size); +typedef void* (*rmtReallocPtr)(void* mm_context, void* ptr, rmtU32 size); +typedef void (*rmtFreePtr)(void* mm_context, void* ptr); +typedef void (*rmtInputHandlerPtr)(const char* text, void* context); +typedef void (*rmtSampleTreeHandlerPtr)(void* cbk_context, rmtSampleTree* sample_tree); +typedef void (*rmtPropertyHandlerPtr)(void* cbk_context, struct rmtProperty* root); + +// Struture to fill in to modify Remotery default settings +typedef struct rmtSettings +{ + // Which port to listen for incoming connections on + rmtU16 port; + + // When this server exits it can leave the port open in TIME_WAIT state for a while. This forces + // subsequent server bind attempts to fail when restarting. If you find restarts fail repeatedly + // with bind attempts, set this to true to forcibly reuse the open port. + rmtBool reuse_open_port; + + // Only allow connections on localhost? + // For dev builds you may want to access your game from other devices but if + // you distribute a game to your players with Remotery active, probably best + // to limit connections to localhost. + rmtBool limit_connections_to_localhost; + + // Whether to enable runtime thread sampling that discovers which processors a thread is running + // on. This will suspend and resume threads from outside repeatdly and inject code into each + // thread that automatically instruments the processor. + // Default: Enabled + rmtBool enableThreadSampler; + + // How long to sleep between server updates, hopefully trying to give + // a little CPU back to other threads. + rmtU32 msSleepBetweenServerUpdates; + + // Size of the internal message queues Remotery uses + // Will be rounded to page granularity of 64k + rmtU32 messageQueueSizeInBytes; + + // If the user continuously pushes to the message queue, the server network + // code won't get a chance to update unless there's an upper-limit on how + // many messages can be consumed per loop. + rmtU32 maxNbMessagesPerUpdate; + + // Callback pointers for memory allocation + rmtMallocPtr malloc; + rmtReallocPtr realloc; + rmtFreePtr free; + void* mm_context; + + // Callback pointer for receiving input from the Remotery console + rmtInputHandlerPtr input_handler; + + // Callback pointer for traversing the sample tree graph + rmtSampleTreeHandlerPtr sampletree_handler; + void* sampletree_context; + + // Callback pointer for traversing the prpperty graph + rmtPropertyHandlerPtr snapshot_callback; + void* snapshot_context; + + // Context pointer that gets sent to Remotery console callback function + void* input_handler_context; + + rmtPStr logPath; +} rmtSettings; + +// Retrieve and configure the global rmtSettings object; returns `rmtSettings*`. +// This can be done before or after Remotery is initialised, however some fields are only referenced on initialisation. +#define rmt_Settings() \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_Settings(), NULL ) + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Initialisation/Shutdown +--------------------------------------------------------------------------------------------------------------------------------*/ + + +// Can call remotery functions on a null pointer +// TODO: Can embed extern "C" in these macros? + +// Initialises Remotery and sets its internal global instance pointer. +// Parameter is `Remotery**`, returning you the pointer for further use. +#define rmt_CreateGlobalInstance(rmt) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_CreateGlobalInstance(rmt), RMT_ERROR_NONE) + +// Shutsdown Remotery, requiring its pointer to be passed to ensure you are destroying the correct instance. +#define rmt_DestroyGlobalInstance(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_DestroyGlobalInstance(rmt)) + +// For use in the presence of DLLs/SOs if each of them are linking Remotery statically. +// If Remotery is hosted in its own DLL and linked dynamically then there is no need to use this. +// Otherwise, pass the result of `rmt_CreateGlobalInstance` from your main DLL to this in your other DLLs. +#define rmt_SetGlobalInstance(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_SetGlobalInstance(rmt)) + +// Get a pointer to the current global Remotery instance. +#define rmt_GetGlobalInstance() \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_GetGlobalInstance(), NULL) + + +/*-------------------------------------------------------------------------------------------------------------------------------- + CPU Sampling +--------------------------------------------------------------------------------------------------------------------------------*/ + + +#define rmt_SetCurrentThreadName(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_SetCurrentThreadName(rmt)) + +#define rmt_LogText(text) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_LogText(text)) + +#define rmt_BeginCPUSample(name, flags) \ + RMT_OPTIONAL(RMT_ENABLED, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginCPUSample(#name, flags, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginCPUSampleDynamic(namestr, flags) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_BeginCPUSample(namestr, flags, NULL)) + +#define rmt_EndCPUSample() \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_EndCPUSample()) + +// Used for both CPU and GPU profiling +// Essential to call this every frame, ever since D3D12 support was added +// D3D12 Requirements: Don't sample any command lists that begin before this call and end after it +#define rmt_MarkFrame() \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_MarkFrame(), RMT_ERROR_NONE) + + +/*-------------------------------------------------------------------------------------------------------------------------------- + GPU Sampling +--------------------------------------------------------------------------------------------------------------------------------*/ + + +// Structure to fill in when binding CUDA to Remotery +typedef struct rmtCUDABind +{ + // The main context that all driver functions apply before each call + void* context; + + // Driver API function pointers that need to be pointed to + // Untyped so that the CUDA headers are not required in this file + // NOTE: These are named differently to the CUDA functions because the CUDA API has a habit of using + // macros to point function calls to different versions, e.g. cuEventDestroy is a macro for + // cuEventDestroy_v2. + void* CtxSetCurrent; + void* CtxGetCurrent; + void* EventCreate; + void* EventDestroy; + void* EventRecord; + void* EventQuery; + void* EventElapsedTime; + +} rmtCUDABind; + +// Call once after you've initialised CUDA to bind it to Remotery +#define rmt_BindCUDA(bind) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_BindCUDA(bind)) + +// Mark the beginning of a CUDA sample on the specified asynchronous stream +#define rmt_BeginCUDASample(name, stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginCUDASample(#name, &rmt_sample_hash_##name, stream); \ + }) + +// Mark the end of a CUDA sample on the specified asynchronous stream +#define rmt_EndCUDASample(stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_EndCUDASample(stream)) + + +#define rmt_BindD3D11(device, context) \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BindD3D11(device, context)) + +#define rmt_UnbindD3D11() \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_UnbindD3D11()) + +#define rmt_BeginD3D11Sample(name) \ + RMT_OPTIONAL(RMT_USE_D3D11, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginD3D11Sample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginD3D11SampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BeginD3D11Sample(namestr, NULL)) + +#define rmt_EndD3D11Sample() \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_EndD3D11Sample()) + + +typedef struct rmtD3D12Bind +{ + // The main device shared by all threads + void* device; + + // The queue command lists are executed on for profiling + void* queue; + +} rmtD3D12Bind; + +// Create a D3D12 binding for the given device/queue pair +#define rmt_BindD3D12(device, queue, out_bind) \ + RMT_OPTIONAL_RET(RMT_USE_D3D12, _rmt_BindD3D12(device, queue, out_bind), NULL) + +#define rmt_UnbindD3D12(bind) \ + RMT_OPTIONAL(RMT_USE_D3D12, _rmt_UnbindD3D12(bind)) + +#define rmt_BeginD3D12Sample(bind, command_list, name) \ + RMT_OPTIONAL(RMT_USE_D3D12, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginD3D12Sample(bind, command_list, #name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginD3D12SampleDynamic(bind, command_list, namestr) \ + RMT_OPTIONAL(RMT_USE_D3D12, _rmt_BeginD3D12Sample(bind, command_list, namestr, NULL)) + +#define rmt_EndD3D12Sample() \ + RMT_OPTIONAL(RMT_USE_D3D12, _rmt_EndD3D12Sample()) + + +#define rmt_BindOpenGL() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BindOpenGL()) + +#define rmt_UnbindOpenGL() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_UnbindOpenGL()) + +#define rmt_BeginOpenGLSample(name) \ + RMT_OPTIONAL(RMT_USE_OPENGL, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginOpenGLSample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginOpenGLSampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BeginOpenGLSample(namestr, NULL)) + +#define rmt_EndOpenGLSample() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_EndOpenGLSample()) + + +#define rmt_BindMetal(command_buffer) \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_BindMetal(command_buffer)); + +#define rmt_UnbindMetal() \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_UnbindMetal()); + +#define rmt_BeginMetalSample(name) \ + RMT_OPTIONAL(RMT_USE_METAL, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginMetalSample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginMetalSampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_BeginMetalSample(namestr, NULL)) + +#define rmt_EndMetalSample() \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_EndMetalSample()) + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Runtime Properties +--------------------------------------------------------------------------------------------------------------------------------*/ + + +/* --- Public API --------------------------------------------------------------------------------------------------------------*/ + + +// Flags that control property behaviour +typedef enum +{ + RMT_PropertyFlags_NoFlags = 0, + + // Reset property back to its default value on each new frame + RMT_PropertyFlags_FrameReset = 1, +} rmtPropertyFlags; + +// All possible property types that can be recorded and sent to the viewer +typedef enum +{ + RMT_PropertyType_rmtGroup, + RMT_PropertyType_rmtBool, + RMT_PropertyType_rmtS32, + RMT_PropertyType_rmtU32, + RMT_PropertyType_rmtF32, + RMT_PropertyType_rmtS64, + RMT_PropertyType_rmtU64, + RMT_PropertyType_rmtF64, +} rmtPropertyType; + +// A property value as a union of all its possible types +typedef union rmtPropertyValue +{ + // C++ requires function-based construction of property values because it has no designated initialiser support until C++20 + #ifdef __cplusplus + // These are static Make calls, rather than overloaded constructors, because `rmtBool` is the same type as `rmtU32` + static rmtPropertyValue MakeBool(rmtBool v) { rmtPropertyValue pv; pv.Bool = v; return pv; } + static rmtPropertyValue MakeS32(rmtS32 v) { rmtPropertyValue pv; pv.S32 = v; return pv; } + static rmtPropertyValue MakeU32(rmtU32 v) { rmtPropertyValue pv; pv.U32 = v; return pv; } + static rmtPropertyValue MakeF32(rmtF32 v) { rmtPropertyValue pv; pv.F32 = v; return pv; } + static rmtPropertyValue MakeS64(rmtS64 v) { rmtPropertyValue pv; pv.S64 = v; return pv; } + static rmtPropertyValue MakeU64(rmtU64 v) { rmtPropertyValue pv; pv.U64 = v; return pv; } + static rmtPropertyValue MakeF64(rmtF64 v) { rmtPropertyValue pv; pv.F64 = v; return pv; } + #endif + + rmtBool Bool; + rmtS32 S32; + rmtU32 U32; + rmtF32 F32; + rmtS64 S64; + rmtU64 U64; + rmtF64 F64; +} rmtPropertyValue; + +// Definition of a property that should be stored globally +// Note: +// Use the callback api and the rmt_PropertyGetxxx accessors to traverse this structure +typedef struct rmtProperty +{ + // Gets set to RMT_TRUE after a property has been modified, when it gets initialised for the first time + rmtBool initialised; + + // Runtime description + rmtPropertyType type; + rmtPropertyFlags flags; + + // Current value + rmtPropertyValue value; + + // Last frame value to see if previous value needs to be updated + rmtPropertyValue lastFrameValue; + + // Previous value only if it's different from the current value, and when it changed + rmtPropertyValue prevValue; + rmtU32 prevValueFrame; + + // Text description + const char* name; + const char* description; + + // Default value for Reset calls + rmtPropertyValue defaultValue; + + // Parent link specifically placed after default value so that variadic macro can initialise it + struct rmtProperty* parent; + + // Links within the property tree + struct rmtProperty* firstChild; + struct rmtProperty* lastChild; + struct rmtProperty* nextSibling; + + // Hash for efficient sending of properties to the viewer + rmtU32 nameHash; + + // Unique, persistent ID among all properties + rmtU32 uniqueID; +} rmtProperty; + +// Define properties of different types at global scope: +// +// * Never define properties in a header file that gets included multiple times. +// * The property gets defined exactly as `name` in the global scope. +// * `flag` is specified without the `RMT_PropertyFlags_` prefix. +// * Property parents are optional and can be specified as the last parameter, referencing `&name`. +// +#define rmt_PropertyDefine_Group(name, desc, ...) _rmt_PropertyDefine(rmtGroup, name, _rmt_MakePropertyValue(Bool, 0), RMT_PropertyFlags_NoFlags, desc, __VA_ARGS__) +#define rmt_PropertyDefine_Bool(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtBool, name, _rmt_MakePropertyValue(Bool, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_S32(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtS32, name, _rmt_MakePropertyValue(S32, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_U32(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtU32, name, _rmt_MakePropertyValue(U32, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_F32(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtF32, name, _rmt_MakePropertyValue(F32, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_S64(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtS64, name, _rmt_MakePropertyValue(S64, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_U64(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtU64, name, _rmt_MakePropertyValue(U64, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) +#define rmt_PropertyDefine_F64(name, default_value, flag, desc, ...) _rmt_PropertyDefine(rmtF64, name, _rmt_MakePropertyValue(F64, default_value), RMT_PropertyFlags_##flag, desc, __VA_ARGS__) + +// As properties need to be defined at global scope outside header files, use this to declare properties in header files to be +// modified in other translation units. +// +// If you don't want to include Remotery.h in your shared header you can forward declare the `rmtProperty` type and then forward +// declare the property name yourself. +#define rmt_PropertyExtern(name) extern rmtProperty name; + +// Set properties to the given value +#define rmt_PropertySet_Bool(name, set_value) _rmt_PropertySet(Bool, name, set_value) +#define rmt_PropertySet_S32(name, set_value) _rmt_PropertySet(S32, name, set_value) +#define rmt_PropertySet_U32(name, set_value) _rmt_PropertySet(U32, name, set_value) +#define rmt_PropertySet_F32(name, set_value) _rmt_PropertySet(F32, name, set_value) +#define rmt_PropertySet_S64(name, set_value) _rmt_PropertySet(S64, name, set_value) +#define rmt_PropertySet_U64(name, set_value) _rmt_PropertySet(U64, name, set_value) +#define rmt_PropertySet_F64(name, set_value) _rmt_PropertySet(F64, name, set_value) + +// Add the given value to properties +#define rmt_PropertyAdd_S32(name, add_value) _rmt_PropertyAdd(S32, name, add_value) +#define rmt_PropertyAdd_U32(name, add_value) _rmt_PropertyAdd(U32, name, add_value) +#define rmt_PropertyAdd_F32(name, add_value) _rmt_PropertyAdd(F32, name, add_value) +#define rmt_PropertyAdd_S64(name, add_value) _rmt_PropertyAdd(S64, name, add_value) +#define rmt_PropertyAdd_U64(name, add_value) _rmt_PropertyAdd(U64, name, add_value) +#define rmt_PropertyAdd_F64(name, add_value) _rmt_PropertyAdd(F64, name, add_value) + +// Reset properties to their default value +#define rmt_PropertyReset(name) \ + { \ + name.value = name.defaultValue; \ + _rmt_PropertySetValue(&name); \ + } + +// Send all properties and their values to the viewer and log to file +#define rmt_PropertySnapshotAll() _rmt_PropertySnapshotAll() + +// Reset all RMT_PropertyFlags_FrameReset properties to their default value +#define rmt_PropertyFrameResetAll() _rmt_PropertyFrameResetAll() + +/* --- Private Details ---------------------------------------------------------------------------------------------------------*/ + + +// Used to define properties from typed macro callers +#define _rmt_PropertyDefine(type, name, default_value, flags, desc, ...) \ + rmtProperty name = { RMT_FALSE, RMT_PropertyType_##type, flags, default_value, default_value, default_value, 0, #name, desc, default_value, __VA_ARGS__ }; + +// C++ doesn't support designated initialisers until C++20 +// Worth checking for C++ designated initialisers to remove the function call in debug builds +#ifdef __cplusplus +#define _rmt_MakePropertyValue(field, value) rmtPropertyValue::Make##field(value) +#else +#define _rmt_MakePropertyValue(field, value) { .field = value } +#endif + +// Used to set properties from typed macro callers +#define _rmt_PropertySet(field, name, set_value) \ + { \ + name.value.field = set_value; \ + _rmt_PropertySetValue(&name); \ + } + +// Used to add properties from typed macro callers +#define _rmt_PropertyAdd(field, name, add_value) \ + { \ + name.value.field += add_value; \ + rmtPropertyValue delta_value = _rmt_MakePropertyValue(field, add_value); \ + _rmt_PropertyAddValue(&name, delta_value); \ + } + + +#ifdef __cplusplus +extern "C" { +#endif + +RMT_API void _rmt_PropertySetValue(rmtProperty* property); +RMT_API void _rmt_PropertyAddValue(rmtProperty* property, rmtPropertyValue add_value); +RMT_API rmtError _rmt_PropertySnapshotAll(); +RMT_API void _rmt_PropertyFrameResetAll(); +RMT_API rmtU32 _rmt_HashString32(const char* s, int len, rmtU32 seed); + +#ifdef __cplusplus +} +#endif + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Sample Tree API for walking `rmtSampleTree` Objects in the Sample Tree Handler. +--------------------------------------------------------------------------------------------------------------------------------*/ + + +typedef enum rmtSampleFlags +{ + // Default behaviour + RMTSF_None = 0, + + // Search parent for same-named samples and merge timing instead of adding a new sample + RMTSF_Aggregate = 1, + + // Merge sample with parent if it's the same sample + RMTSF_Recursive = 2, + + // Set this flag on any of your root samples so that Remotery will assert if it ends up *not* being the root sample. + // This will quickly allow you to detect Begin/End mismatches causing a sample tree imbalance. + RMTSF_Root = 4, + + // Mainly for platforms other than Windows that don't support the thread sampler and can't detect stalling samples. + // Where you have a non-root sample that stays open indefinitely and never sends its contents to log/viewer. + // Send this sample to log/viewer when it closes. + // You can not have more than one sample open with this flag on the same thread at a time. + // This flag will be removed in a future version when all platforms support stalling samples. + RMTSF_SendOnClose = 8, +} rmtSampleFlags; + +// Struct to hold iterator info +typedef struct rmtSampleIterator +{ +// public + rmtSample* sample; +// private + rmtSample* initial; +} rmtSampleIterator; + +#define rmt_IterateChildren(iter, sample) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_IterateChildren(iter, sample)) + +#define rmt_IterateNext(iter) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_IterateNext(iter), RMT_FALSE) + +#define rmt_SampleTreeGetThreadName(sample_tree) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleTreeGetThreadName(sample_tree), NULL) + +#define rmt_SampleTreeGetRootSample(sample_tree) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleTreeGetRootSample(sample_tree), NULL) + +// Should only called from within the sample tree callback, +// when the internal string lookup table is valid (i.e. on the main Remotery thread) +#define rmt_SampleGetName(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetName(sample), NULL) + +#define rmt_SampleGetNameHash(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetNameHash(sample), 0U) + +#define rmt_SampleGetCallCount(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetCallCount(sample), 0U) + +#define rmt_SampleGetStart(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetStart(sample), 0LLU) + +#define rmt_SampleGetTime(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetTime(sample), 0LLU) + +#define rmt_SampleGetSelfTime(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetSelfTime(sample), 0LLU) + +#define rmt_SampleGetColour(sample, r, g, b) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_SampleGetColour(sample, r, g, b)) + +#define rmt_SampleGetType(sample) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_SampleGetType(sample), RMT_SampleType_Count) + + +// Struct to hold iterator info +typedef struct rmtPropertyIterator +{ +// public + rmtProperty* property; +// private + rmtProperty* initial; +} rmtPropertyIterator; + +#define rmt_PropertyIterateChildren(iter, property) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_PropertyIterateChildren(iter, property)) + +#define rmt_PropertyIterateNext(iter) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_PropertyIterateNext(iter), RMT_FALSE) + +// Should only called from within the property callback, +// when the internal string lookup table is valid (i.e. on the main Remotery thread) + +#define rmt_PropertyGetType(property) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_PropertyGetType(property), RMT_PropertyType_Count) + +#define rmt_PropertyGetName(property) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_PropertyGetName(property), NULL) + +#define rmt_PropertyGetDescription(property) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_PropertyGetDescription(property), 0U) + +#define rmt_PropertyGetValue(property) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_PropertyGetValue(property), 0U) + + + +/*-------------------------------------------------------------------------------------------------------------------------------- + C++ Public Interface Extensions +--------------------------------------------------------------------------------------------------------------------------------*/ + + +#ifdef __cplusplus + + +#if RMT_ENABLED + +// Types that end samples in their destructors +extern "C" RMT_API void _rmt_EndCPUSample(void); +struct rmt_EndCPUSampleOnScopeExit +{ + ~rmt_EndCPUSampleOnScopeExit() + { + _rmt_EndCPUSample(); + } +}; + +#if RMT_USE_CUDA +extern "C" RMT_API void _rmt_EndCUDASample(void* stream); +struct rmt_EndCUDASampleOnScopeExit +{ + rmt_EndCUDASampleOnScopeExit(void* stream) : stream(stream) + { + } + ~rmt_EndCUDASampleOnScopeExit() + { + _rmt_EndCUDASample(stream); + } + void* stream; +}; + +#endif +#if RMT_USE_D3D11 +extern "C" RMT_API void _rmt_EndD3D11Sample(void); +struct rmt_EndD3D11SampleOnScopeExit +{ + ~rmt_EndD3D11SampleOnScopeExit() + { + _rmt_EndD3D11Sample(); + } +}; +#endif + +#if RMT_USE_D3D12 +extern "C" RMT_API void _rmt_EndD3D12Sample(); +struct rmt_EndD3D12SampleOnScopeExit +{ + ~rmt_EndD3D12SampleOnScopeExit() + { + _rmt_EndD3D12Sample(); + } +}; +#endif + +#if RMT_USE_OPENGL +extern "C" RMT_API void _rmt_EndOpenGLSample(void); +struct rmt_EndOpenGLSampleOnScopeExit +{ + ~rmt_EndOpenGLSampleOnScopeExit() + { + _rmt_EndOpenGLSample(); + } +}; +#endif + +#if RMT_USE_METAL +extern "C" RMT_API void _rmt_EndMetalSample(void); +struct rmt_EndMetalSampleOnScopeExit +{ + ~rmt_EndMetalSampleOnScopeExit() + { + _rmt_EndMetalSample(); + } +}; +#endif + +#endif + + +// Pairs a call to rmt_BeginSample with its call to rmt_EndSample when leaving scope +#define rmt_ScopedCPUSample(name, flags) \ + RMT_OPTIONAL(RMT_ENABLED, rmt_BeginCPUSample(name, flags)); \ + RMT_OPTIONAL(RMT_ENABLED, rmt_EndCPUSampleOnScopeExit rmt_ScopedCPUSample##name); +#define rmt_ScopedCUDASample(name, stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, rmt_BeginCUDASample(name, stream)); \ + RMT_OPTIONAL(RMT_USE_CUDA, rmt_EndCUDASampleOnScopeExit rmt_ScopedCUDASample##name(stream)); +#define rmt_ScopedD3D11Sample(name) \ + RMT_OPTIONAL(RMT_USE_D3D11, rmt_BeginD3D11Sample(name)); \ + RMT_OPTIONAL(RMT_USE_D3D11, rmt_EndD3D11SampleOnScopeExit rmt_ScopedD3D11Sample##name); +#define rmt_ScopedD3D12Sample(bind, command_list, name) \ + RMT_OPTIONAL(RMT_USE_D3D12, rmt_BeginD3D12Sample(bind, command_list, name)); \ + RMT_OPTIONAL(RMT_USE_D3D12, rmt_EndD3D12SampleOnScopeExit rmt_ScopedD3D12Sample##name()); +#define rmt_ScopedOpenGLSample(name) \ + RMT_OPTIONAL(RMT_USE_OPENGL, rmt_BeginOpenGLSample(name)); \ + RMT_OPTIONAL(RMT_USE_OPENGL, rmt_EndOpenGLSampleOnScopeExit rmt_ScopedOpenGLSample##name); +#define rmt_ScopedMetalSample(name) \ + RMT_OPTIONAL(RMT_USE_METAL, rmt_BeginMetalSample(name)); \ + RMT_OPTIONAL(RMT_USE_METAL, rmt_EndMetalSampleOnScopeExit rmt_ScopedMetalSample##name); + +#endif + + +/*-------------------------------------------------------------------------------------------------------------------------------- + Private Interface - don't directly call these +--------------------------------------------------------------------------------------------------------------------------------*/ + + +#if RMT_ENABLED + +#ifdef __cplusplus +extern "C" { +#endif + +RMT_API rmtSettings* _rmt_Settings( void ); +RMT_API enum rmtError _rmt_CreateGlobalInstance(Remotery** remotery); +RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery); +RMT_API void _rmt_SetGlobalInstance(Remotery* remotery); +RMT_API Remotery* _rmt_GetGlobalInstance(void); +RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name); +RMT_API void _rmt_LogText(rmtPStr text); +RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32 flags, rmtU32* hash_cache); +RMT_API void _rmt_EndCPUSample(void); +RMT_API rmtError _rmt_MarkFrame(void); + +#if RMT_USE_CUDA +RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind); +RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream); +RMT_API void _rmt_EndCUDASample(void* stream); +#endif + +#if RMT_USE_D3D11 +RMT_API void _rmt_BindD3D11(void* device, void* context); +RMT_API void _rmt_UnbindD3D11(void); +RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndD3D11Sample(void); +#endif + +#if RMT_USE_D3D12 +RMT_API rmtError _rmt_BindD3D12(void* device, void* queue, rmtD3D12Bind** out_bind); +RMT_API void _rmt_UnbindD3D12(rmtD3D12Bind* bind); +RMT_API void _rmt_BeginD3D12Sample(rmtD3D12Bind* bind, void* command_list, rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndD3D12Sample(); +#endif + +#if RMT_USE_OPENGL +RMT_API void _rmt_BindOpenGL(); +RMT_API void _rmt_UnbindOpenGL(void); +RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndOpenGLSample(void); +#endif + +#if RMT_USE_METAL +RMT_API void _rmt_BeginMetalSample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndMetalSample(void); +#endif + +// Sample iterator +RMT_API void _rmt_IterateChildren(rmtSampleIterator* iter, rmtSample* sample); +RMT_API rmtBool _rmt_IterateNext(rmtSampleIterator* iter); + +// SampleTree accessors +RMT_API const char* _rmt_SampleTreeGetThreadName(rmtSampleTree* sample_tree); +RMT_API rmtSample* _rmt_SampleTreeGetRootSample(rmtSampleTree* sample_tree); + +// Sample accessors +RMT_API const char* _rmt_SampleGetName(rmtSample* sample); +RMT_API rmtU32 _rmt_SampleGetNameHash(rmtSample* sample); +RMT_API rmtU32 _rmt_SampleGetCallCount(rmtSample* sample); +RMT_API rmtU64 _rmt_SampleGetStart(rmtSample* sample); +RMT_API rmtU64 _rmt_SampleGetTime(rmtSample* sample); +RMT_API rmtU64 _rmt_SampleGetSelfTime(rmtSample* sample); +RMT_API void _rmt_SampleGetColour(rmtSample* sample, rmtU8* r, rmtU8* g, rmtU8* b); +RMT_API rmtSampleType _rmt_SampleGetType(rmtSample* sample); + +// Property iterator +RMT_API void _rmt_PropertyIterateChildren(rmtPropertyIterator* iter, rmtProperty* property); +RMT_API rmtBool _rmt_PropertyIterateNext(rmtPropertyIterator* iter); + +// Property accessors +RMT_API rmtPropertyType _rmt_PropertyGetType(rmtProperty* property); +RMT_API rmtU32 _rmt_PropertyGetNameHash(rmtProperty* property); +RMT_API const char* _rmt_PropertyGetName(rmtProperty* property); +RMT_API const char* _rmt_PropertyGetDescription(rmtProperty* property); +RMT_API rmtPropertyValue _rmt_PropertyGetValue(rmtProperty* property); + +#ifdef __cplusplus + +} +#endif + +#if RMT_USE_METAL +#ifdef __OBJC__ +RMT_API void _rmt_BindMetal(id command_buffer); +RMT_API void _rmt_UnbindMetal(); +#endif +#endif + +#endif // RMT_ENABLED + + +#endif diff --git a/profiler/lib/RemoteryMetal.mm b/profiler/lib/RemoteryMetal.mm new file mode 100644 index 0000000..437890b --- /dev/null +++ b/profiler/lib/RemoteryMetal.mm @@ -0,0 +1,59 @@ +// +// Copyright 2014-2018 Celtoys Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include +#include +#include + +#import + +// Store command buffer in thread-local so that each thread can point to its own +static void SetCommandBuffer(id command_buffer) +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + thread_data[@"rmtMTLCommandBuffer"] = command_buffer; +} + +static id GetCommandBuffer() +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + return thread_data[@"rmtMTLCommandBuffer"]; +} + +extern "C" void _rmt_BindMetal(id command_buffer) +{ + SetCommandBuffer(command_buffer); +} + +extern "C" void _rmt_UnbindMetal() +{ + SetCommandBuffer(0); +} + +// Needs to be in the same lib for this to work +extern "C" unsigned long long rmtMetal_usGetTime(); + +static void SetTimestamp(void* data) +{ + *((unsigned long long*)data) = rmtMetal_usGetTime(); +} + +extern "C" void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready) +{ + id command_buffer = GetCommandBuffer(); + [command_buffer addScheduledHandler:^(id ){ SetTimestamp(out_start); }]; + [command_buffer addCompletedHandler:^(id ){ SetTimestamp(out_end); *out_ready = 1; }]; +} diff --git a/profiler/readme.md b/profiler/readme.md new file mode 100644 index 0000000..35dbc9d --- /dev/null +++ b/profiler/readme.md @@ -0,0 +1,232 @@ +Remotery +-------- + +[![Build](https://github.com/Celtoys/Remotery/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/Celtoys/Remotery/actions/workflows/build.yml) + +A realtime CPU/GPU profiler hosted in a single C file with a viewer that runs in a web browser. + +![screenshot](screenshot.png?raw=true) + +Features: + +* Lightweight instrumentation of multiple threads running on the CPU. +* Web viewer that runs in Chrome, Firefox and Safari; on Desktops, Mobiles or Tablets. +* GPU UI rendering, bypassing the DOM completely, for real-time 60hz viewer updates at 10,000x the performance. +* Automatic thread sampler that tells you what processor cores your threads are running on without requiring Administrator privileges. +* Drop saved traces onto the Remotery window to load historical runs for inspection. +* Console output for logging text. +* Console input for sending commands to your game. +* A Property API for recording named/typed values over time, alongside samples. +* Profiles itself and shows how it's performing in the viewer. + +Supported Profiling Platforms: + +* Windows 7/8/10/11/UWP (Hololens), Linux, OSX, iOS, Android, Xbox One/Series, Free BSD. + +Supported GPU Profiling APIS: + +* D3D 11/12, OpenGL, CUDA, Metal. + +Compiling +--------- + +* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include + directories to add Remotery/lib path. The required library ws2_32.lib should be picked + up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c. + +* Mac OS X (XCode) - simply add lib/Remotery.c, lib/Remotery.h and lib/Remotery.mm to your program. + +* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for + library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c + -I lib -pthread -lm + +* FreeBSD - the easiest way is to take a look at the official port + ([devel/remotery](https://www.freshports.org/devel/remotery/)) and modify the port's + Makefile if needed. There is also a package available via `pkg install remotery`. + +You can define some extra macros to modify what features are compiled into Remotery: + + Macro Default Description + + RMT_ENABLED 1 Disable this to not include any bits of Remotery in your build + RMT_USE_TINYCRT 0 Used by the Celtoys TinyCRT library (not released yet) + RMT_USE_CUDA 0 Assuming CUDA headers/libs are setup, allow CUDA profiling + RMT_USE_D3D11 0 Assuming Direct3D 11 headers/libs are setup, allow D3D11 GPU profiling + RMT_USE_D3D12 0 Allow D3D12 GPU profiling + RMT_USE_OPENGL 0 Allow OpenGL GPU profiling (dynamically links OpenGL libraries on available platforms) + RMT_USE_METAL 0 Allow Metal profiling of command buffers + + +Basic Use +--------- + +See the sample directory for further examples. A quick example: + + int main() + { + // Create the main instance of Remotery. + // You need only do this once per program. + Remotery* rmt; + rmt_CreateGlobalInstance(&rmt); + + // Explicit begin/end for C + { + rmt_BeginCPUSample(LogText, 0); + rmt_LogText("Time me, please!"); + rmt_EndCPUSample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedCPUSample(LogText, 0); + rmt_LogText("Time me, too!"); + } + + // Destroy the main instance of Remotery. + rmt_DestroyGlobalInstance(rmt); + } + + +Running the Viewer +------------------ + +Double-click or launch `vis/index.html` from the browser. + + +Sampling CUDA GPU activity +-------------------------- + +Remotery allows for profiling multiple threads of CUDA execution using different asynchronous streams +that must all share the same context. After initialising both Remotery and CUDA you need to bind the +two together using the call: + + rmtCUDABind bind; + bind.context = m_Context; + bind.CtxSetCurrent = &cuCtxSetCurrent; + bind.CtxGetCurrent = &cuCtxGetCurrent; + bind.EventCreate = &cuEventCreate; + bind.EventDestroy = &cuEventDestroy; + bind.EventRecord = &cuEventRecord; + bind.EventQuery = &cuEventQuery; + bind.EventElapsedTime = &cuEventElapsedTime; + rmt_BindCUDA(&bind); + +Explicitly pointing to the CUDA interface allows Remotery to be included anywhere in your project without +need for you to link with the required CUDA libraries. After the bind completes you can safely sample any +CUDA activity: + + CUstream stream; + + // Explicit begin/end for C + { + rmt_BeginCUDASample(UnscopedSample, stream); + // ... CUDA code ... + rmt_EndCUDASample(stream); + } + + // Scoped begin/end for C++ + { + rmt_ScopedCUDASample(ScopedSample, stream); + // ... CUDA code ... + } + +Remotery supports only one context for all threads and will use cuCtxGetCurrent and cuCtxSetCurrent to +ensure the current thread has the context you specify in rmtCUDABind.context. + + +Sampling Direct3D 11 GPU activity +--------------------------------- + +Remotery allows sampling of D3D11 GPU activity on multiple devices on multiple threads. After initialising Remotery, you need to bind it to D3D11 with a single call from the thread that owns the device context: + + // Parameters are ID3D11Device* and ID3D11DeviceContext* + rmt_BindD3D11(d3d11_device, d3d11_context); + +Sampling is then a simple case of: + + // Explicit begin/end for C + { + rmt_BeginD3D11Sample(UnscopedSample); + // ... D3D code ... + rmt_EndD3D11Sample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedD3D11Sample(ScopedSample); + // ... D3D code ... + } + +Subsequent sampling calls from the same thread will use that device/context combination. When you shutdown your D3D11 device and context, ensure you notify Remotery before shutting down Remotery itself: + + rmt_UnbindD3D11(); + + +Sampling OpenGL GPU activity +---------------------------- + +Remotery allows sampling of GPU activity on your main OpenGL context. After initialising Remotery, you need +to bind it to OpenGL with the single call: + + rmt_BindOpenGL(); + +Sampling is then a simple case of: + + // Explicit begin/end for C + { + rmt_BeginOpenGLSample(UnscopedSample); + // ... OpenGL code ... + rmt_EndOpenGLSample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedOpenGLSample(ScopedSample); + // ... OpenGL code ... + } + +Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown +your OpenGL device and context, ensure you notify Remotery before shutting down Remotery itself: + + rmt_UnbindOpenGL(); + + +Sampling Metal GPU activity +--------------------------- + +Remotery can sample Metal command buffers issued to the GPU from multiple threads. As the Metal API does not +support finer grained profiling, samples will return only the timing of the bound command buffer, irrespective +of how many you issue. As such, make sure you bind and sample the command buffer for each call site: + + rmt_BindMetal(mtl_command_buffer); + rmt_ScopedMetalSample(command_buffer_name); + +The C API supports begin/end also: + + rmt_BindMetal(mtl_command_buffer); + rmt_BeginMetalSample(command_buffer_name); + ... + rmt_EndMetalSample(); + + +Applying Configuration Settings +------------------------------- + +Before creating your Remotery instance, you can configure its behaviour by retrieving its settings object: + + rmtSettings* settings = rmt_Settings(); + +Some important settings are: + + // Redirect any Remotery allocations to your own malloc/free, with an additional context pointer + // that gets passed to your callbacks. + settings->malloc; + settings->free; + settings->mm_context; + + // Specify an input pipelineStage that receives text input from the Remotery console, with an additional + // context pointer that gets passed to your callback. + // The pipelineStage will be called from the Remotery thread so synchronization with a mutex or atomics + // might be needed to avoid race conditions with your threads. + settings->input_handler; + settings->input_handler_context; diff --git a/profiler/sample/dump.c b/profiler/sample/dump.c new file mode 100644 index 0000000..3681b5c --- /dev/null +++ b/profiler/sample/dump.c @@ -0,0 +1,184 @@ +#include +#include +#include +#include +#include +#include "../lib/Remotery.h" + +#include + +rmt_PropertyDefine_Group(Game, "Game Properties"); +rmt_PropertyDefine_Bool(WasUpdated, RMT_FALSE, FrameReset, "Was the game loop executed this frame?", &Game); +rmt_PropertyDefine_U32(RecursiveDepth, 0, FrameReset, "How deep did we go in recursiveFunction?", &Game); +rmt_PropertyDefine_F32(Accumulated, 0, FrameReset, "What was the latest value?", &Game); +rmt_PropertyDefine_U32(FrameCounter, 0, NoFlags, "What is the current frame number?", &Game); + + +void aggregateFunction() { + rmt_BeginCPUSample(aggregate, RMTSF_Aggregate); + rmt_EndCPUSample(); +} +void recursiveFunction(int depth) { + rmt_PropertySet_U32(RecursiveDepth, depth); + rmt_BeginCPUSample(recursive, RMTSF_Recursive); + if (depth < 5) { + recursiveFunction(depth + 1); + } + rmt_EndCPUSample(); +} + +double delay() { + int i, end; + double j = 0; + + rmt_BeginCPUSample(delay, 0); + for( i = 0, end = rand()/100; i < end; ++i ) { + double v = sin(i); + j += v; + + rmt_PropertyAdd_F32(Accumulated, v); + } + recursiveFunction(0); + aggregateFunction(); + aggregateFunction(); + aggregateFunction(); + rmt_EndCPUSample(); + return j; +} + +void printIndent(int indent) +{ + int i; + for (i = 0; i < indent; ++i) { + printf(" "); + } +} + +void printSample(rmtSample* sample, int indent) +{ + const char* name = rmt_SampleGetName(sample); + rmtU32 callcount = rmt_SampleGetCallCount(sample); + rmtU64 time = rmt_SampleGetTime(sample); + rmtU64 self_time = rmt_SampleGetSelfTime(sample); + rmtSampleType type = rmt_SampleGetType(sample); + rmtU8 r, g, b; + rmt_SampleGetColour(sample, &r, &g, &b); + + printIndent(indent); printf("%s %u time: %llu self: %llu type: %d color: 0x%02x%02x%02x\n", name, callcount, time, self_time, type, r, g, b); +} + +void printTree(rmtSample* sample, int indent) +{ + rmtSampleIterator iter; + + printSample(sample, indent); + + rmt_IterateChildren(&iter, sample); + while (rmt_IterateNext(&iter)) { + printTree(iter.sample, indent+1); + } +} + +void dumpTree(void* ctx, rmtSampleTree* sample_tree) +{ + rmtSample* root = rmt_SampleTreeGetRootSample(sample_tree); + const char* thread_name = rmt_SampleTreeGetThreadName(sample_tree); + if (strcmp("Remotery", thread_name) == 0) + { + return; // to minimize the verbosity in this example + } + + printf("// ******************** DUMP TREE: %s ************************\n", thread_name); + + printTree(root, 0); +} + +void printProperty(rmtProperty* property, int indent) +{ + rmtPropertyIterator iter; + + const char* name = rmt_PropertyGetName(property); + rmtPropertyType type = rmt_PropertyGetType(property); + rmtPropertyValue value = rmt_PropertyGetValue(property); + + printIndent(indent); printf("%s: ", name); + + switch(type) + { + case RMT_PropertyType_rmtBool: printf("%s\n", value.Bool ? "true":"false"); break; + case RMT_PropertyType_rmtS32: printf("%d\n", value.S32); break; + case RMT_PropertyType_rmtU32: printf("%u\n", value.U32); break; + case RMT_PropertyType_rmtF32: printf("%f\n", value.F32); break; + case RMT_PropertyType_rmtS64: printf("%lld\n", value.S64); break; + case RMT_PropertyType_rmtU64: printf("%llu\n", value.U64); break; + case RMT_PropertyType_rmtF64: printf("%g\n", value.F64); break; + case RMT_PropertyType_rmtGroup: printf("\n"); break; + default: break; + }; + + rmt_PropertyIterateChildren(&iter, property); + while (rmt_PropertyIterateNext(&iter)) { + printProperty(iter.property, indent + 1); + } +} + +void dumpProperties(void* ctx, rmtProperty* root) +{ + rmtPropertyIterator iter; + printf("// ******************** DUMP PROPERTIES: ************************\n"); + + rmt_PropertyIterateChildren(&iter, root); + while (rmt_PropertyIterateNext(&iter)) { + printProperty(iter.property, 0); + } +} + +int sig = 0; + +/// Allow to close cleanly with ctrl + c +void sigintHandler(int sig_num) { + sig = sig_num; + printf("Interrupted\n"); +} + +int main() { + Remotery* rmt; + rmtError error; + + signal(SIGINT, sigintHandler); + + rmtSettings* settings = rmt_Settings(); + if (settings) + { + settings->sampletree_handler = dumpTree; + settings->sampletree_context = 0; + + settings->snapshot_callback = dumpProperties; + settings->snapshot_context = 0; + } + + error = rmt_CreateGlobalInstance(&rmt); + + if( RMT_ERROR_NONE != error) { + printf("Error launching Remotery %d\n", error); + return -1; + } + + int max_count = 5; + + while (sig == 0 && --max_count > 0) { + rmt_LogText("start profiling"); + delay(); + rmt_LogText("end profiling"); + + rmt_PropertySet_Bool(WasUpdated, RMT_TRUE); + rmt_PropertyAdd_U32(FrameCounter, 1); + + rmt_PropertySnapshotAll(); + rmt_PropertyFrameResetAll(); + } + + rmt_DestroyGlobalInstance(rmt); + printf("Cleaned up and quit\n"); + return 0; +} diff --git a/profiler/sample/sample.c b/profiler/sample/sample.c new file mode 100644 index 0000000..3f56060 --- /dev/null +++ b/profiler/sample/sample.c @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include "../lib/Remotery.h" + +void aggregateFunction() { + rmt_BeginCPUSample(aggregate, RMTSF_Aggregate); + rmt_EndCPUSample(); +} +void recursiveFunction(int depth) { + rmt_BeginCPUSample(recursive, RMTSF_Recursive); + if (depth < 5) { + recursiveFunction(depth + 1); + } + rmt_EndCPUSample(); +} + +double delay() { + int i, end; + double j = 0; + + rmt_BeginCPUSample(delay, 0); + for( i = 0, end = rand()/100; i < end; ++i ) { + j += sin(i); + } + recursiveFunction(0); + aggregateFunction(); + aggregateFunction(); + aggregateFunction(); + rmt_EndCPUSample(); + return j; +} + +int sig = 0; + +/// Allow to close cleanly with ctrl + c +void sigintHandler(int sig_num) { + sig = sig_num; + printf("Interrupted\n"); +} + +int main( ) { + Remotery *rmt; + rmtError error; + + signal(SIGINT, sigintHandler); + + error = rmt_CreateGlobalInstance(&rmt); + if( RMT_ERROR_NONE != error) { + printf("Error launching Remotery %d\n", error); + return -1; + } + + while (sig == 0) { + rmt_LogText("start profiling"); + delay(); + rmt_LogText("end profiling"); + } + + rmt_DestroyGlobalInstance(rmt); + printf("Cleaned up and quit\n"); + return 0; +} diff --git a/profiler/screenshot.png b/profiler/screenshot.png new file mode 100644 index 0000000..11f4ce4 Binary files /dev/null and b/profiler/screenshot.png differ diff --git a/profiler/vis/Code/Console.js b/profiler/vis/Code/Console.js new file mode 100644 index 0000000..0eb5970 --- /dev/null +++ b/profiler/vis/Code/Console.js @@ -0,0 +1,218 @@ + +Console = (function() +{ + var BORDER = 10; + var HEIGHT = 200; + + + function Console(wm, server) + { + // Create the window and its controls + this.Window = wm.AddWindow("Console", 10, 10, 100, 100); + this.PageContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160)); + DOM.Node.AddClass(this.PageContainer.Node, "ConsoleText"); + this.AppContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160)); + DOM.Node.AddClass(this.AppContainer.Node, "ConsoleText"); + this.UserInput = this.Window.AddControlNew(new WM.EditBox(10, 5, 400, 30, "Input", "")); + this.UserInput.SetChangeHandler(Bind(ProcessInput, this)); + this.Window.ShowNoAnim(); + + // This accumulates log text as fast as is required + this.PageTextBuffer = ""; + this.PageTextUpdatePending = false; + this.AppTextBuffer = ""; + this.AppTextUpdatePending = false; + + // Setup command history control + this.CommandHistory = LocalStore.Get("App", "Global", "CommandHistory", [ ]); + this.CommandIndex = 0; + this.MaxNbCommands = 10000; + DOM.Event.AddHandler(this.UserInput.EditNode, "keydown", Bind(OnKeyPress, this)); + DOM.Event.AddHandler(this.UserInput.EditNode, "focus", Bind(OnFocus, this)); + + // At a much lower frequency this will update the console window + window.setInterval(Bind(UpdateHTML, this), 500); + + // Setup log requests from the server + this.Server = server; + server.SetConsole(this); + server.AddMessageHandler("LOGM", Bind(OnLog, this)); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + } + + + Console.prototype.Log = function(text) + { + this.PageTextBuffer = LogText(this.PageTextBuffer, text); + this.PageTextUpdatePending = true; + } + + + Console.prototype.WindowResized = function(width, height) + { + // Place window + this.Window.SetPosition(BORDER, height - BORDER - 200); + this.Window.SetSize(width - 2 * BORDER, HEIGHT); + + ResizeInternals(this); + } + + + Console.prototype.TriggerUpdate = function() + { + this.AppTextUpdatePending = true; + } + + + function OnLog(self, socket, data_view_reader) + { + var text = data_view_reader.GetString(); + self.AppTextBuffer = LogText(self.AppTextBuffer, text); + + // Don't register text as updating if disconnected as this implies a trace is being loaded, which we want to speed up + if (self.Server.Connected()) + { + self.AppTextUpdatePending = true; + } + } + + + function LogText(existing_text, new_text) + { + // Filter the text a little to make it safer + if (new_text == null) + new_text = "NULL"; + + // Find and convert any HTML entities, ensuring the browser doesn't parse any embedded HTML code + // This also allows the log to contain arbitrary C++ code (e.g. assert comparison operators) + new_text = Convert.string_to_html_entities(new_text); + + // Prefix date and end with new line + var d = new Date(); + new_text = "[" + d.toLocaleTimeString() + "] " + new_text + "
"; + + // Append to local text buffer and ensure clip the oldest text to ensure a max size + existing_text = existing_text + new_text; + var max_len = 100 * 1024; + var len = existing_text.length; + if (len > max_len) + existing_text = existing_text.substr(len - max_len, max_len); + + return existing_text; + } + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + // Place controls + var parent_size = self.Window.Size; + var mid_w = parent_size[0] / 3; + self.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30); + self.UserInput.SetSize(parent_size[0] - 100, 18); + var output_height = self.UserInput.Position[1] - 2 * BORDER; + self.PageContainer.SetPosition(BORDER, BORDER); + self.PageContainer.SetSize(mid_w - 2 * BORDER, output_height); + self.AppContainer.SetPosition(mid_w, BORDER); + self.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height); + } + + + function UpdateHTML(self) + { + // Reset the current text buffer as html + + if (self.PageTextUpdatePending) + { + var page_node = self.PageContainer.Node; + page_node.innerHTML = self.PageTextBuffer; + page_node.scrollTop = page_node.scrollHeight; + self.PageTextUpdatePending = false; + } + + if (self.AppTextUpdatePending) + { + var app_node = self.AppContainer.Node; + app_node.innerHTML = self.AppTextBuffer; + app_node.scrollTop = app_node.scrollHeight; + self.AppTextUpdatePending = false; + } + } + + + function ProcessInput(self, node) + { + // Send the message exactly + var msg = node.value; + self.Server.Send("CONI" + msg); + + // Emit to console and clear + self.Log("> " + msg); + self.UserInput.SetValue(""); + + // Keep track of recently issued commands, with an upper bound + self.CommandHistory.push(msg); + var extra_commands = self.CommandHistory.length - self.MaxNbCommands; + if (extra_commands > 0) + self.CommandHistory.splice(0, extra_commands); + + // Set command history index to the most recent command + self.CommandIndex = self.CommandHistory.length; + + // Backup to local store + LocalStore.Set("App", "Global", "CommandHistory", self.CommandHistory); + + // Keep focus with the edit box + return true; + } + + + function OnKeyPress(self, evt) + { + evt = DOM.Event.Get(evt); + + if (evt.keyCode == Keyboard.Codes.UP) + { + if (self.CommandHistory.length > 0) + { + // Cycle backwards through the command history + self.CommandIndex--; + if (self.CommandIndex < 0) + self.CommandIndex = self.CommandHistory.length - 1; + var command = self.CommandHistory[self.CommandIndex]; + self.UserInput.SetValue(command); + } + + // Stops default behaviour of moving cursor to the beginning + DOM.Event.StopDefaultAction(evt); + } + + else if (evt.keyCode == Keyboard.Codes.DOWN) + { + if (self.CommandHistory.length > 0) + { + // Cycle fowards through the command history + self.CommandIndex = (self.CommandIndex + 1) % self.CommandHistory.length; + var command = self.CommandHistory[self.CommandIndex]; + self.UserInput.SetValue(command); + } + + // Stops default behaviour of moving cursor to the end + DOM.Event.StopDefaultAction(evt); + } + } + + + function OnFocus(self) + { + // Reset command index on focus + self.CommandIndex = self.CommandHistory.length; + } + + + return Console; +})(); diff --git a/profiler/vis/Code/DataViewReader.js b/profiler/vis/Code/DataViewReader.js new file mode 100644 index 0000000..e2752b4 --- /dev/null +++ b/profiler/vis/Code/DataViewReader.js @@ -0,0 +1,94 @@ + +// +// Simple wrapper around DataView that auto-advances the read offset and provides +// a few common data type conversions specific to this app +// +DataViewReader = (function () +{ + function DataViewReader(data_view, offset) + { + this.DataView = data_view; + this.Offset = offset; + } + + DataViewReader.prototype.AtEnd = function() + { + return this.Offset >= this.DataView.byteLength; + } + + DataViewReader.prototype.GetBool = function () + { + let v = this.DataView.getUint8(this.Offset); + this.Offset++; + return v; + } + + DataViewReader.prototype.GetUInt8 = function () + { + let v = this.DataView.getUint8(this.Offset); + this.Offset++; + return v; + } + + DataViewReader.prototype.GetInt32 = function () + { + let v = this.DataView.getInt32(this.Offset, true); + this.Offset += 4; + return v; + } + + DataViewReader.prototype.GetUInt32 = function () + { + var v = this.DataView.getUint32(this.Offset, true); + this.Offset += 4; + return v; + } + + DataViewReader.prototype.GetFloat32 = function () + { + const v = this.DataView.getFloat32(this.Offset, true); + this.Offset += 4; + return v; + } + + DataViewReader.prototype.GetInt64 = function () + { + var v = this.DataView.getFloat64(this.Offset, true); + this.Offset += 8; + return v; + } + + DataViewReader.prototype.GetUInt64 = function () + { + var v = this.DataView.getFloat64(this.Offset, true); + this.Offset += 8; + return v; + } + + DataViewReader.prototype.GetFloat64 = function () + { + var v = this.DataView.getFloat64(this.Offset, true); + this.Offset += 8; + return v; + } + + DataViewReader.prototype.GetStringOfLength = function (string_length) + { + var string = ""; + for (var i = 0; i < string_length; i++) + { + string += String.fromCharCode(this.DataView.getInt8(this.Offset)); + this.Offset++; + } + + return string; + } + + DataViewReader.prototype.GetString = function () + { + var string_length = this.GetUInt32(); + return this.GetStringOfLength(string_length); + } + + return DataViewReader; +})(); diff --git a/profiler/vis/Code/GLCanvas.js b/profiler/vis/Code/GLCanvas.js new file mode 100644 index 0000000..23ce7db --- /dev/null +++ b/profiler/vis/Code/GLCanvas.js @@ -0,0 +1,123 @@ + +class GLCanvas +{ + constructor(width, height) + { + this.width = width; + this.height = height; + + // Create a WebGL 2 canvas without premultiplied alpha + this.glCanvas = document.createElement("canvas"); + this.glCanvas.width = width; + this.glCanvas.height = height; + this.gl = this.glCanvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false }); + + // Overlay the canvas on top of everything and make sure mouse events click-through + this.glCanvas.style.position = "fixed"; + this.glCanvas.style.pointerEvents = "none"; + this.glCanvas.style.zIndex = 1000; + document.body.appendChild(this.glCanvas); + + // Hook up resize event handler + DOM.Event.AddHandler(window, "resize", () => this.OnResizeWindow()); + this.OnResizeWindow(); + + // Compile needed shaders + this.timelineProgram = glCreateProgramFromSource(this.gl, "TimelineVShader", TimelineVShader, "TimelineFShader", TimelineFShader); + this.timelineHighlightProgram = glCreateProgramFromSource(this.gl, "TimelineHighlightVShader", TimelineHighlightVShader, "TimelineHighlightFShader", TimelineHighlightFShader); + this.timelineGpuToCpuProgram = glCreateProgramFromSource(this.gl, "TimelineGpuToCpuVShader", TimelineGpuToCpuVShader, "TimelineGpuToCpuFShader", TimelineGpuToCpuFShader); + this.timelineBackgroundProgram = glCreateProgramFromSource(this.gl, "TimelineBackgroundVShader", TimelineBackgroundVShader, "TimelineBackgroundFShader", TimelineBackgroundFShader); + this.gridProgram = glCreateProgramFromSource(this.gl, "GridVShader", GridVShader, "GridFShader", GridFShader); + this.gridNumberProgram = glCreateProgramFromSource(this.gl, "GridNumberVShader", GridNumberVShader, "GridNumberFShader", GridNumberFShader); + this.windowProgram = glCreateProgramFromSource(this.gl, "WindowVShader", WindowVShader, "WindowFShader", WindowFShader); + + // Create the shader font resources + this.font = new glFont(this.gl); + this.textBuffer = new glTextBuffer(this.gl, this.font); + this.nameMap = new NameMap(this.textBuffer); + + // Kick off the rendering refresh + this.OnDrawHandler = null; + this.Draw(performance.now()); + } + + SetOnDraw(handler) + { + this.OnDrawHandler = handler; + } + + ClearTextResources() + { + this.nameMap = new NameMap(this.textBuffer); + } + + OnResizeWindow() + { + // Resize to match the window + this.width = window.innerWidth; + this.height = window.innerHeight; + this.glCanvas.width = window.innerWidth; + this.glCanvas.height = window.innerHeight; + } + + SetFontUniforms(program) + { + // Font texture may not be loaded yet + if (this.font.atlasTexture != null) + { + const gl = this.gl; + glSetUniform(gl, program, "inFontAtlasTexture", this.font.atlasTexture, 0); + glSetUniform(gl, program, "inTextBufferDesc.fontWidth", this.font.fontWidth); + glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight); + } + } + + SetTextUniforms(program) + { + const gl = this.gl; + this.SetFontUniforms(program); + this.textBuffer.SetAsUniform(gl, program, "inTextBuffer", 1); + } + + SetContainerUniforms(program, container) + { + const gl = this.gl; + const container_rect = container.getBoundingClientRect(); + glSetUniform(gl, program, "inContainer.x0", container_rect.left); + glSetUniform(gl, program, "inContainer.y0", container_rect.top); + glSetUniform(gl, program, "inContainer.x1", container_rect.left + container_rect.width); + glSetUniform(gl, program, "inContainer.y1", container_rect.top + container_rect.height); + } + + EnableBlendPremulAlpha() + { + const gl = this.gl; + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + } + + DisableBlend() + { + const gl = this.gl; + gl.disable(gl.BLEND); + } + + Draw(timestamp) + { + // Setup the viewport and clear the screen + const gl = this.gl; + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Chain to the Draw handler + const seconds = timestamp / 1000.0; + if (this.OnDrawHandler != null) + { + this.OnDrawHandler(gl, seconds); + } + + // Reschedule + window.requestAnimationFrame((timestamp) => this.Draw(timestamp)); + } +}; diff --git a/profiler/vis/Code/GridWindow.js b/profiler/vis/Code/GridWindow.js new file mode 100644 index 0000000..82ac2ec --- /dev/null +++ b/profiler/vis/Code/GridWindow.js @@ -0,0 +1,291 @@ + +class GridConfigSamples +{ + constructor() + { + this.nbFloatsPerSample = g_nbFloatsPerSample; + + this.columns = []; + this.columns.push(new GridColumn("Sample Name", 196)); + this.columns.push(new GridColumn("Time (ms)", 56, g_sampleOffsetFloats_Length, 4)); + this.columns.push(new GridColumn("Self (ms)", 56, g_sampleOffsetFloats_Self, 4)); + this.columns.push(new GridColumn("Calls", 34, g_sampleOffsetFloats_Calls, 0)); + this.columns.push(new GridColumn("Depth", 34, g_sampleOffsetFloats_Recurse, 0)); + } +} + +class GridConfigProperties +{ + constructor() + { + this.nbFloatsPerSample = 10; + + this.columns = []; + this.columns.push(new GridColumn("Property Name", 196)); + this.columns.push(new GridColumn("Value", 90, 4, 4)); + this.columns.push(new GridColumn("Prev Value", 90, 6, 4)); + } +} + +class GridColumn +{ + static ColumnTemplate = `
`; + + constructor(name, width, number_offset, nb_float_chars) + { + // Description + this.name = name; + this.width = width; + this.numberOffset = number_offset; + this.nbFloatChars = nb_float_chars; + + // Constants + this.rowHeight = 15; + } + + Attach(parent_node) + { + // Generate HTML for the header and parent it + const column = DOM.Node.CreateHTML(GridColumn.ColumnTemplate); + column.innerHTML = this.name; + column.style.width = (this.width - 4) + "px"; + this.headerNode = parent_node.appendChild(column); + } + + Draw(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample) + { + // If a number offset in the data stream is provided, we're rendering numbers and not names + if (this.numberOffset !== undefined) + { + this._DrawNumbers(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample); + } + else + { + this._DrawNames(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample); + } + } + + _DrawNames(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample) + { + const gl = gl_canvas.gl; + const program = gl_canvas.gridProgram; + + gl.useProgram(program); + gl_canvas.SetTextUniforms(program); + + this._DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample); + } + + _DrawNumbers(gl_canvas, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample) + { + const gl = gl_canvas.gl; + const program = gl_canvas.gridNumberProgram; + + gl.useProgram(program); + gl_canvas.SetFontUniforms(program); + glSetUniform(gl, program, "inNumberOffset", this.numberOffset); + glSetUniform(gl, program, "inNbFloatChars", this.nbFloatChars); + + this._DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample); + } + + _DrawAny(gl, program, x, buffer, scroll_pos, clip, nb_entries, nb_floats_per_sample) + { + const clip_min_x = clip[0]; + const clip_min_y = clip[1]; + const clip_max_x = clip[2]; + const clip_max_y = clip[3]; + + // Scrolled position of the grid + const pos_x = clip_min_x + scroll_pos[0] + x; + const pos_y = clip_min_y + scroll_pos[1]; + + // Clip column to the window + const min_x = Math.min(Math.max(clip_min_x, pos_x), clip_max_x); + const min_y = Math.min(Math.max(clip_min_y, pos_y), clip_max_y); + const max_x = Math.max(Math.min(clip_max_x, pos_x + this.width), clip_min_x); + const max_y = Math.max(Math.min(clip_max_y, pos_y + nb_entries * this.rowHeight), clip_min_y); + + // Don't render if outside the bounds of the main window + if (min_x > gl.canvas.width || max_x < 0 || min_y > gl.canvas.height || max_y < 0) + { + return; + } + + const pixel_offset_x = Math.max(min_x - pos_x, 0); + const pixel_offset_y = Math.max(min_y - pos_y, 0); + + // Viewport constants + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + // Grid constants + glSetUniform(gl, program, "inGrid.minX", min_x); + glSetUniform(gl, program, "inGrid.minY", min_y); + glSetUniform(gl, program, "inGrid.maxX", max_x); + glSetUniform(gl, program, "inGrid.maxY", max_y); + glSetUniform(gl, program, "inGrid.pixelOffsetX", pixel_offset_x); + glSetUniform(gl, program, "inGrid.pixelOffsetY", pixel_offset_y); + + // Source data set buffers + glSetUniform(gl, program, "inSamples", buffer.texture, 2); + glSetUniform(gl, program, "inSamplesLength", buffer.nbEntries); + glSetUniform(gl, program, "inFloatsPerSample", nb_floats_per_sample); + glSetUniform(gl, program, "inNbSamples", buffer.nbEntries / nb_floats_per_sample); + + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } +} + +class GridWindow +{ + static GridTemplate = ` +
+
+
+
+ `; + + constructor(wm, name, offset, gl_canvas, config) + { + this.nbEntries = 0; + this.scrollPos = [ 0, 0 ]; + + // Window setup + this.xPos = 10 + offset * 410; + this.window = wm.AddWindow(name, 100, 100, 100, 100, null, this); + this.window.ShowNoAnim(); + this.visible = true; + + // Cache how much internal padding the window has, for clipping + const style = getComputedStyle(this.window.BodyNode); + this.bodyPadding = parseFloat(style.padding); + + // Create the Grid host HTML + const grid_node = DOM.Node.CreateHTML(GridWindow.GridTemplate); + this.gridNode = this.window.BodyNode.appendChild(grid_node); + this.headerNode = this.gridNode.children[0]; + this.contentNode = this.gridNode.children[1]; + + // Build column data + this.nbFloatsPerSample = config.nbFloatsPerSample; + this.columns = config.columns; + for (let column of this.columns) + { + column.Attach(this.headerNode); + } + this._PositionHeaders(); + + // Header nodes have 1 pixel borders so the first column is required to have a width 1 less than everything else + // To counter that, shift the first header node one to the left (it will clip) so that it can have its full width + this.columns[0].headerNode.style.marginLeft = "-1px"; + + // Setup for pan/wheel scrolling + this.mouseInteraction = new MouseInteraction(this.window.BodyNode); + this.mouseInteraction.onMoveHandler = (mouse_state, mx, my) => this._OnMouseMove(mouse_state, mx, my); + this.mouseInteraction.onScrollHandler = (mouse_state) => this._OnMouseScroll(mouse_state); + + const gl = gl_canvas.gl; + this.glCanvas = gl_canvas; + this.sampleBuffer = new glDynamicBuffer(gl, glDynamicBufferType.Texture, gl.FLOAT, 1); + } + + Close() + { + this.window.Close(); + } + + static AnimatedMove(self, top_window, bottom_window, val) + { + self.xPos = val; + self.WindowResized(top_window, bottom_window); + } + + SetXPos(xpos, top_window, bottom_window) + { + Anim.Animate( + Bind(GridWindow.AnimatedMove, this, top_window, bottom_window), + this.xPos, 10 + xpos * 410, 0.25); + } + + SetVisible(visible) + { + if (visible != this.visible) + { + if (visible == true) + this.window.ShowNoAnim(); + else + this.window.HideNoAnim(); + + this.visible = visible; + } + } + + WindowResized(top_window, bottom_window) + { + const top = top_window.Position[1] + top_window.Size[1] + 10; + this.window.SetPosition(this.xPos, top_window.Position[1] + top_window.Size[1] + 10); + this.window.SetSize(400, bottom_window.Position[1] - 10 - top); + } + + UpdateEntries(nb_entries, samples) + { + // This tracks the latest actual entry count + this.nbEntries = nb_entries; + + // Resize buffers to match any new entry count + if (nb_entries * this.nbFloatsPerSample > this.sampleBuffer.nbEntries) + { + this.sampleBuffer.ResizeToFitNextPow2(nb_entries * this.nbFloatsPerSample); + } + + // Copy and upload the entry data + this.sampleBuffer.cpuArray.set(samples); + this.sampleBuffer.UploadData(); + } + + Draw() + { + // Establish content node clipping rectangle + const rect = this.contentNode.getBoundingClientRect(); + const clip = [ + rect.left, + rect.top, + rect.left + rect.width, + rect.top + rect.height, + ]; + + // Draw columns, left-to-right + let x = 0; + for (let column of this.columns) + { + column.Draw(this.glCanvas, x, this.sampleBuffer, this.scrollPos, clip, this.nbEntries, this.nbFloatsPerSample); + x += column.width + 1; + } + } + + _PositionHeaders() + { + let x = this.scrollPos[0]; + for (let i in this.columns) + { + const column = this.columns[i]; + column.headerNode.style.left = x + "px"; + x += column.width; + x += (i >= 1) ? 1 : 0; + } + } + + _OnMouseMove(mouse_state, mouse_offset_x, mouse_offset_y) + { + this.scrollPos[0] = Math.min(0, this.scrollPos[0] + mouse_offset_x); + this.scrollPos[1] = Math.min(0, this.scrollPos[1] + mouse_offset_y); + + this._PositionHeaders(); + } + + _OnMouseScroll(mouse_state) + { + this.scrollPos[1] = Math.min(0, this.scrollPos[1] + mouse_state.WheelDelta * 15); + } +} \ No newline at end of file diff --git a/profiler/vis/Code/MouseInteraction.js b/profiler/vis/Code/MouseInteraction.js new file mode 100644 index 0000000..4c5b3a0 --- /dev/null +++ b/profiler/vis/Code/MouseInteraction.js @@ -0,0 +1,106 @@ +class MouseInteraction +{ + constructor(node) + { + this.node = node; + + // Current interaction state + this.mouseDown = false; + this.lastMouseState = null; + this.mouseMoved = false; + + // Empty user handlers + this.onClickHandler = null; + this.onMoveHandler = null; + this.onHoverHandler = null; + this.onScrollHandler = null; + + // Setup DOM handlers + DOM.Event.AddHandler(this.node, "mousedown", (evt) => this._OnMouseDown(evt)); + DOM.Event.AddHandler(this.node, "mouseup", (evt) => this._OnMouseUp(evt)); + DOM.Event.AddHandler(this.node, "mousemove", (evt) => this._OnMouseMove(evt)); + DOM.Event.AddHandler(this.node, "mouseleave", (evt) => this._OnMouseLeave(evt)); + + // Mouse wheel is a little trickier + const mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.node, mouse_wheel_event, (evt) => this._OnMouseScroll(evt)); + } + + _OnMouseDown(evt) + { + this.mouseDown = true; + this.lastMouseState = new Mouse.State(evt); + this.mouseMoved = false; + DOM.Event.StopDefaultAction(evt); + } + + _OnMouseUp(evt) + { + const mouse_state = new Mouse.State(evt); + + this.mouseDown = false; + + // Chain to on click event when released without movement + if (!this.mouseMoved) + { + if (this.onClickHandler) + { + this.onClickHandler(mouse_state); + } + } + } + + _OnMouseMove(evt) + { + const mouse_state = new Mouse.State(evt); + + if (this.mouseDown) + { + // Has the mouse moved while being held down? + const move_offset_x = mouse_state.Position[0] - this.lastMouseState.Position[0]; + const move_offset_y = mouse_state.Position[1] - this.lastMouseState.Position[1]; + if (move_offset_x != 0 || move_offset_y != 0) + { + this.mouseMoved = true; + + // Chain to move handler + if (this.onMoveHandler) + { + this.onMoveHandler(mouse_state, move_offset_x, move_offset_y); + } + } + } + + // Chain to hover handler + else if (this.onHoverHandler) + { + this.onHoverHandler(mouse_state); + } + + this.lastMouseState = mouse_state; + } + + _OnMouseLeave(evt) + { + // Cancel panning + this.mouseDown = false; + + // Cancel hovering + if (this.onHoverHandler) + { + this.onHoverHandler(null); + } + } + + _OnMouseScroll(evt) + { + const mouse_state = new Mouse.State(evt); + if (this.onScrollHandler) + { + this.onScrollHandler(mouse_state); + + // Prevent vertical scrolling on mouse-wheel + DOM.Event.StopDefaultAction(evt); + } + } +}; diff --git a/profiler/vis/Code/NameMap.js b/profiler/vis/Code/NameMap.js new file mode 100644 index 0000000..689ebdd --- /dev/null +++ b/profiler/vis/Code/NameMap.js @@ -0,0 +1,53 @@ +class NameMap +{ + constructor(text_buffer) + { + this.names = { }; + this.textBuffer = text_buffer; + } + + Get(name_hash) + { + // Return immediately if it's in the hash + let name = this.names[name_hash]; + if (name != undefined) + { + return [ true, name ]; + } + + // Create a temporary name that uses the hash + name = { + string: name_hash.toString(), + hash: name_hash + }; + this.names[name_hash] = name; + + // Add to the text buffer the first time this name is encountered + name.textEntry = this.textBuffer.AddText(name.string); + + return [ false, name ]; + } + + Set(name_hash, name_string) + { + // Create the name on-demand if its hash doesn't exist + let name = this.names[name_hash]; + if (name == undefined) + { + name = { + string: name_string, + hash: name_hash + }; + this.names[name_hash] = name; + } + else + { + name.string = name_string; + } + + // Apply the updated text to the buffer + name.textEntry = this.textBuffer.AddText(name_string); + + return name; + } +} \ No newline at end of file diff --git a/profiler/vis/Code/PixelTimeRange.js b/profiler/vis/Code/PixelTimeRange.js new file mode 100644 index 0000000..8c0ce81 --- /dev/null +++ b/profiler/vis/Code/PixelTimeRange.js @@ -0,0 +1,61 @@ + +class PixelTimeRange +{ + constructor(start_us, span_us, span_px) + { + this.Span_px = span_px; + this.Set(start_us, span_us); + } + + Set(start_us, span_us) + { + this.Start_us = start_us; + this.Span_us = span_us; + this.End_us = this.Start_us + span_us; + this.usPerPixel = this.Span_px / this.Span_us; + } + + SetStart(start_us) + { + this.Start_us = start_us; + this.End_us = start_us + this.Span_us; + } + + SetEnd(end_us) + { + this.End_us = end_us; + this.Start_us = end_us - this.Span_us; + } + + SetPixelSpan(span_px) + { + this.Span_px = span_px; + this.usPerPixel = this.Span_px / this.Span_us; + } + + PixelOffset(time_us) + { + return Math.floor((time_us - this.Start_us) * this.usPerPixel); + } + + PixelSize(time_us) + { + return Math.floor(time_us * this.usPerPixel); + } + + TimeAtPosition(position) + { + return this.Start_us + position / this.usPerPixel; + } + + Clone() + { + return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px); + } + + SetAsUniform(gl, program) + { + glSetUniform(gl, program, "inTimeRange.msStart", this.Start_us / 1000.0); + glSetUniform(gl, program, "inTimeRange.msPerPixel", this.usPerPixel * 1000.0); + } +} diff --git a/profiler/vis/Code/Remotery.js b/profiler/vis/Code/Remotery.js new file mode 100644 index 0000000..b0490b7 --- /dev/null +++ b/profiler/vis/Code/Remotery.js @@ -0,0 +1,756 @@ + +// +// TODO: Window resizing needs finer-grain control +// TODO: Take into account where user has moved the windows +// TODO: Controls need automatic resizing within their parent windows +// + + +Settings = (function() +{ + function Settings() + { + this.IsPaused = false; + this.SyncTimelines = true; + } + + return Settings; + +})(); + + +Remotery = (function() +{ + // crack the url and get the parameter we want + var getUrlParameter = function getUrlParameter( search_param) + { + var page_url = decodeURIComponent( window.location.search.substring(1) ), + url_vars = page_url.split('&'), + param_name, + i; + + for (i = 0; i < url_vars.length; i++) + { + param_name = url_vars[i].split('='); + + if (param_name[0] === search_param) + { + return param_name[1] === undefined ? true : param_name[1]; + } + } + }; + + function Remotery() + { + this.WindowManager = new WM.WindowManager(); + this.Settings = new Settings(); + + // "addr" param is ip:port and will override the local store version if passed in the URL + var addr = getUrlParameter( "addr" ); + if ( addr != null ) + this.ConnectionAddress = "ws://" + addr + "/rmt"; + else + this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt"); + + this.Server = new WebSocketConnection(); + this.Server.AddConnectHandler(Bind(OnConnect, this)); + this.Server.AddDisconnectHandler(Bind(OnDisconnect, this)); + + this.glCanvas = new GLCanvas(100, 100); + this.glCanvas.SetOnDraw((gl, seconds) => this.OnGLCanvasDraw(gl, seconds)); + + // Create the console up front as everything reports to it + this.Console = new Console(this.WindowManager, this.Server); + + // Create required windows + this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress); + this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this)); + this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this), this.glCanvas); + this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this)); + this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this)); + this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null, this.glCanvas); + + this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this)); + this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this)); + + this.TraceDrop = new TraceDrop(this); + + this.nbGridWindows = 0; + this.gridWindows = { }; + this.FrameHistory = { }; + this.ProcessorFrameHistory = { }; + this.PropertyFrameHistory = [ ]; + this.SelectedFrames = { }; + + this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this)); + this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this)); + this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this)); + this.Server.AddMessageHandler("PSNP", Bind(OnPropertySnapshots, this)); + + // Kick-off the auto-connect loop + AutoConnect(this); + + // Hook up resize event handler + DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this)); + OnResizeWindow(this); + } + + + Remotery.prototype.Clear = function() + { + // Clear timelines + this.SampleTimelineWindow.Clear(); + this.ProcessorTimelineWindow.Clear(); + + // Close and clear all sample windows + for (var i in this.gridWindows) + { + const grid_window = this.gridWindows[i]; + grid_window.Close(); + } + this.nbGridWindows = 0; + this.gridWindows = { }; + + this.propertyGridWindow = this.AddGridWindow("__rmt__global__properties__", "Global Properties", new GridConfigProperties()); + + // Clear runtime data + this.FrameHistory = { }; + this.ProcessorFrameHistory = { }; + this.PropertyFrameHistory = [ ]; + this.SelectedFrames = { }; + this.glCanvas.ClearTextResources(); + + // Resize everything to fit new layout + OnResizeWindow(this); + } + + function DrawWindowMask(gl, program, window_node) + { + gl.useProgram(program); + + // Using depth as a mask + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + + // Viewport constants + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + // Window dimensions + const rect = window_node.getBoundingClientRect(); + glSetUniform(gl, program, "minX", rect.left); + glSetUniform(gl, program, "minY", rect.top); + glSetUniform(gl, program, "maxX", rect.left + rect.width); + glSetUniform(gl, program, "maxY", rect.top + rect.height); + + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + Remotery.prototype.OnGLCanvasDraw = function(gl, seconds) + { + this.glCanvas.textBuffer.UploadData(); + + // Draw windows in their z-order, front-to-back + // Depth test is enabled, rejecting equal z, and windows render transparent + // Draw window content first, then draw the invisible window mask + // Any windows that come after another window will not draw where the previous window already masked out depth + for (let window of this.WindowManager.Windows) + { + // Some windows might not have WebGL drawing on them; they need to occlude as well + DrawWindowMask(gl, this.glCanvas.windowProgram, window.Node); + + if (window.userData != null) + { + window.userData.Draw(); + } + } + } + + function AutoConnect(self) + { + // Only attempt to connect if there isn't already a connection or an attempt to connect + if (!self.Server.Connected() && !self.Server.Connecting()) + { + self.Server.Connect(self.ConnectionAddress); + } + + // Always schedule another check + window.setTimeout(Bind(AutoConnect, self), 2000); + } + + + function OnConnect(self) + { + // Connection address has been validated + LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress); + + self.Clear(); + + // Ensure the viewer is ready for realtime updates + self.TitleWindow.Unpause(); + } + + function OnDisconnect(self) + { + // Pause so the user can inspect the trace + self.TitleWindow.Pause(); + } + + + function OnAddressChanged(self, node) + { + // Update and disconnect, relying on auto-connect to reconnect + self.ConnectionAddress = node.value; + self.Server.Disconnect(); + + // Give input focus away + return false; + } + + + Remotery.prototype.AddGridWindow = function(name, display_name, config) + { + const grid_window = new GridWindow(this.WindowManager, display_name, this.nbGridWindows, this.glCanvas, config); + this.gridWindows[name] = grid_window; + this.gridWindows[name].WindowResized(this.SampleTimelineWindow.Window, this.Console.Window); + this.nbGridWindows++; + MoveGridWindows(this); + return grid_window; + } + + + function DecodeSampleHeader(self, data_view_reader, length) + { + // Message-specific header + let message = { }; + message.messageStart = data_view_reader.Offset; + message.thread_name = data_view_reader.GetString(); + message.nb_samples = data_view_reader.GetUInt32(); + message.partial_tree = data_view_reader.GetUInt32(); + + // Align sample reading to 32-bit boundary + const align = ((4 - (data_view_reader.Offset & 3)) & 3); + data_view_reader.Offset += align; + message.samplesStart = data_view_reader.Offset; + message.samplesLength = length - (message.samplesStart - message.messageStart); + + return message; + } + + + function SetNanosecondsAsMilliseconds(samples_view, offset) + { + samples_view.setFloat32(offset, samples_view.getFloat64(offset, true) / 1000.0, true); + } + + + function SetUint32AsFloat32(samples_view, offset) + { + samples_view.setFloat32(offset, samples_view.getUint32(offset, true), true); + } + + + function ProcessSampleTree(self, sample_data, message) + { + const empty_text_entry = { + offset: 0, + length: 1, + }; + + const samples_length = message.nb_samples * g_nbBytesPerSample; + const samples_view = new DataView(sample_data, message.samplesStart, samples_length); + message.sampleDataView = samples_view; + + for (let offset = 0; offset < samples_length; offset += g_nbBytesPerSample) + { + // Get name hash and lookup in name map + const name_hash = samples_view.getUint32(offset, true); + const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash); + + // If the name doesn't exist in the map yet, request it from the server + if (!name_exists) + { + if (self.Server.Connected()) + { + self.Server.Send("GSMP" + name_hash); + } + } + + // Add sample name text buffer location + const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry; + samples_view.setFloat32(offset + g_sampleOffsetBytes_NameOffset, text_entry.offset, true); + samples_view.setFloat32(offset + g_sampleOffsetBytes_NameLength, text_entry.length, true); + + // Time in milliseconds + SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Start); + SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Length); + SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Self); + SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_GpuToCpu); + + // Convert call count/recursion integers to float + SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Calls); + SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Recurse); + } + + // Convert to floats for GPU + message.sampleFloats = new Float32Array(sample_data, message.samplesStart, message.nb_samples * g_nbFloatsPerSample); + } + + function OnSamples(self, socket, data_view_reader, length) + { + // Discard any new samples while paused and connected + // Otherwise this stops a paused Remotery from loading new samples from disk + if (self.Settings.IsPaused && self.Server.Connected()) + return; + + // Binary decode incoming sample data + var message = DecodeSampleHeader(self, data_view_reader, length); + if (message.nb_samples == 0) + { + return; + } + var name = message.thread_name; + ProcessSampleTree(self, data_view_reader.DataView.buffer, message); + + // Add to frame history for this thread + var thread_frame = new ThreadFrame(message); + if (!(name in self.FrameHistory)) + { + self.FrameHistory[name] = [ ]; + } + var frame_history = self.FrameHistory[name]; + if (frame_history.length > 0 && frame_history[frame_history.length - 1].PartialTree) + { + // Always overwrite partial trees with new information + frame_history[frame_history.length - 1] = thread_frame; + } + else + { + frame_history.push(thread_frame); + } + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + frame_history.splice(0, extra_frames); + + // Create sample windows on-demand + if (!(name in self.gridWindows)) + { + self.AddGridWindow(name, name, new GridConfigSamples()); + } + + // Set on the window and timeline if connected as this implies a trace is being loaded, which we want to speed up + if (self.Server.Connected()) + { + self.gridWindows[name].UpdateEntries(message.nb_samples, message.sampleFloats); + + self.SampleTimelineWindow.OnSamples(name, frame_history); + } + } + + + function OnSampleName(self, socket, data_view_reader) + { + // Add any names sent by the server to the local map + let name_hash = data_view_reader.GetUInt32(); + let name_string = data_view_reader.GetString(); + self.glCanvas.nameMap.Set(name_hash, name_string); + } + + + function OnProcessorThreads(self, socket, data_view_reader) + { + // Discard any new samples while paused and connected + // Otherwise this stops a paused Remotery from loading new samples from disk + if (self.Settings.IsPaused && self.Server.Connected()) + return; + + let nb_processors = data_view_reader.GetUInt32(); + let message_index = data_view_reader.GetUInt64(); + + const empty_text_entry = { + offset: 0, + length: 1, + }; + + // Decode each processor + for (let i = 0; i < nb_processors; i++) + { + let thread_id = data_view_reader.GetUInt32(); + let thread_name_hash = data_view_reader.GetUInt32(); + let sample_time = data_view_reader.GetUInt64(); + + // Add frame history for this processor + let processor_name = "Processor " + i.toString(); + if (!(processor_name in self.ProcessorFrameHistory)) + { + self.ProcessorFrameHistory[processor_name] = [ ]; + } + let frame_history = self.ProcessorFrameHistory[processor_name]; + + if (thread_id == 0xFFFFFFFF) + { + continue; + } + + // Try to merge this frame's samples with the previous frame if the are the same thread + if (frame_history.length > 0) + { + let last_thread_frame = frame_history[frame_history.length - 1]; + if (last_thread_frame.threadId == thread_id && last_thread_frame.messageIndex == message_index - 1) + { + // Update last frame message index so that the next frame can check for continuity + last_thread_frame.messageIndex = message_index; + + // Sum time elapsed on the previous frame + const us_length = sample_time - last_thread_frame.usLastStart; + last_thread_frame.usLastStart = sample_time; + last_thread_frame.EndTime_us += us_length; + const last_length = last_thread_frame.sampleDataView.getFloat32(g_sampleOffsetBytes_Length, true); + last_thread_frame.sampleDataView.setFloat32(g_sampleOffsetBytes_Length, last_length + us_length / 1000.0, true); + + continue; + } + } + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + { + frame_history.splice(0, extra_frames); + } + + // Lookup the thread name + let [ name_exists, thread_name ] = self.glCanvas.nameMap.Get(thread_name_hash); + + // If the name doesn't exist in the map yet, request it from the server + if (!name_exists) + { + if (self.Server.Connected()) + { + self.Server.Send("GSMP" + thread_name_hash); + } + } + + // We are co-opting the sample rendering functionality of the timeline window to display processor threads as + // thread samples. Fabricate a thread frame message, packing the processor info into one root sample. + // TODO(don): Abstract the timeline window for pure range display as this is quite inefficient. + let thread_message = { }; + thread_message.nb_samples = 1; + thread_message.sampleData = new ArrayBuffer(g_nbBytesPerSample); + thread_message.sampleDataView = new DataView(thread_message.sampleData); + const sample_data_view = thread_message.sampleDataView; + + // Set the name + const text_entry = thread_name.textEntry != null ? thread_name.textEntry : empty_text_entry; + sample_data_view.setFloat32(g_sampleOffsetBytes_NameOffset, text_entry.offset, true); + sample_data_view.setFloat32(g_sampleOffsetBytes_NameLength, text_entry.length, true); + + // Make a pastel-y colour from the thread name hash + const hash = thread_name.hash; + sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 0, 127 + (hash & 255) / 2); + sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 1, 127 + ((hash >> 4) & 255) / 2); + sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 2, 127 + ((hash >> 8) & 255) / 2); + + // Set the time + sample_data_view.setFloat32(g_sampleOffsetBytes_Start, sample_time / 1000.0, true); + sample_data_view.setFloat32(g_sampleOffsetBytes_Length, 0.25, true); + + thread_message.sampleFloats = new Float32Array(thread_message.sampleData, 0, thread_message.nb_samples * g_nbFloatsPerSample); + + // Create a thread frame and annotate with data required to merge processor samples + let thread_frame = new ThreadFrame(thread_message); + thread_frame.threadId = thread_id; + thread_frame.messageIndex = message_index; + thread_frame.usLastStart = sample_time; + frame_history.push(thread_frame); + + if (self.Server.Connected()) + { + self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history); + } + } + } + + + function UInt64ToFloat32(view, offset) + { + // Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value + const a = view.getUint32(offset + 0, true); + const b = view.getUint32(offset + 4, true); + + // Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math + const v = a + (b * Math.pow(2, 32)); + + // TODO(don): Potentially massive data loss! + snapshots_view.setFloat32(offset, v); + } + + + function SInt64ToFloat32(view, offset) + { + // Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value + const a = view.getUint32(offset + 0, true); + const b = view.getUint32(offset + 4, true); + + // Is this negative? + if (b & 0x80000000) + { + // Can only convert from twos-complement with 32-bit arithmetic so shave off the upper 32-bits + // TODO(don): Crazy data loss here + const v = -(~(a - 1)); + } + else + { + // Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math + const v = a + (b * Math.pow(2, 32)); + } + + // TODO(don): Potentially massive data loss! + snapshots_view.setFloat32(offset, v); + } + + + function DecodeSnapshotHeader(self, data_view_reader, length) + { + // Message-specific header + let message = { }; + message.messageStart = data_view_reader.Offset; + message.nbSnapshots = data_view_reader.GetUInt32(); + message.propertyFrame = data_view_reader.GetUInt32(); + message.snapshotsStart = data_view_reader.Offset; + message.snapshotsLength = length - (message.snapshotsStart - message.messageStart); + return message; + } + + + function ProcessSnapshots(self, snapshot_data, message) + { + if (self.Settings.IsPaused) + { + return null; + } + + const empty_text_entry = { + offset: 0, + length: 1, + }; + + const snapshots_length = message.nbSnapshots * g_nbBytesPerSnapshot; + const snapshots_view = new DataView(snapshot_data, message.snapshotsStart, snapshots_length); + + for (let offset = 0; offset < snapshots_length; offset += g_nbBytesPerSnapshot) + { + // Get name hash and lookup in name map + const name_hash = snapshots_view.getUint32(offset, true); + const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash); + + // If the name doesn't exist in the map yet, request it from the server + if (!name_exists) + { + if (self.Server.Connected()) + { + self.Server.Send("GSMP" + name_hash); + } + } + + // Add snapshot name text buffer location + const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry; + snapshots_view.setFloat32(offset + 0, text_entry.offset, true); + snapshots_view.setFloat32(offset + 4, text_entry.length, true); + + // Heat colour style falloff to quickly identify modified properties + let r = 255, g = 255, b = 255; + const prev_value_frame = snapshots_view.getUint32(offset + 32, true); + const frame_delta = message.propertyFrame - prev_value_frame; + if (frame_delta < 64) + { + g = Math.min(Math.min(frame_delta, 32) * 8, 255); + b = Math.min(frame_delta * 4, 255); + } + snapshots_view.setUint8(offset + 8, r); + snapshots_view.setUint8(offset + 9, g); + snapshots_view.setUint8(offset + 10, b); + + const snapshot_type = snapshots_view.getUint32(offset + 12, true); + switch (snapshot_type) + { + case 1: + case 2: + case 3: + case 4: + case 7: + snapshots_view.setFloat32(offset + 16, snapshots_view.getFloat64(offset + 16, true), true); + snapshots_view.setFloat32(offset + 24, snapshots_view.getFloat64(offset + 24, true), true); + break; + + // Unpack 64-bit integers stored full precision in the logs and view them to the best of our current abilities + case 5: + SInt64ToFloat32(snapshots_view, offset + 16); + SInt64ToFloat32(snapshots_view, offset + 24); + case 6: + UInt64ToFloat32(snapshots_view, offset + 16); + UInt64ToFloat32(snapshots_view, offset + 24); + break; + } + } + + // Convert to floats for GPU + return new Float32Array(snapshot_data, message.snapshotsStart, message.nbSnapshots * g_nbFloatsPerSnapshot); + } + + + function OnPropertySnapshots(self, socket, data_view_reader, length) + { + // Discard any new snapshots while paused and connected + // Otherwise this stops a paused Remotery from loading new samples from disk + if (self.Settings.IsPaused && self.Server.Connected()) + return; + + // Binary decode incoming snapshot data + const message = DecodeSnapshotHeader(self, data_view_reader, length); + message.snapshotFloats = ProcessSnapshots(self, data_view_reader.DataView.buffer, message); + + // Add to frame history + const thread_frame = new PropertySnapshotFrame(message); + const frame_history = self.PropertyFrameHistory; + frame_history.push(thread_frame); + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + frame_history.splice(0, extra_frames); + + // Set on the window if connected as this implies a trace is being loaded, which we want to speed up + if (self.Server.Connected()) + { + self.propertyGridWindow.UpdateEntries(message.nbSnapshots, message.snapshotFloats); + } + } + + function OnTimelineCheck(self, name, evt) + { + // Show/hide the equivalent sample window and move all the others to occupy any left-over space + var target = DOM.Event.GetNode(evt); + self.gridWindows[name].SetVisible(target.checked); + MoveGridWindows(self); + } + + + function MoveGridWindows(self) + { + // Stack all windows next to each other + let xpos = 0; + for (let i in self.gridWindows) + { + const grid_window = self.gridWindows[i]; + if (grid_window.visible) + { + grid_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window); + } + } + } + + + function OnSampleHover(self, thread_name, hover) + { + if (!self.Settings.IsPaused) + { + return; + } + + // Search for the grid window for the thread being hovered over + for (let window_thread_name in self.gridWindows) + { + if (window_thread_name == thread_name) + { + const grid_window = self.gridWindows[thread_name]; + + // Populate with the sample under hover + if (hover != null) + { + const frame = hover[0]; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + } + + // When there's no hover, go back to the selected frame + else if (self.SelectedFrames[thread_name]) + { + const frame = self.SelectedFrames[thread_name]; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + } + + // Otherwise display the last sample in the frame + else + { + const frames = self.FrameHistory[thread_name]; + const frame = frames[frames.length - 1]; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + } + + break; + } + } + } + + + function OnSampleSelected(self, thread_name, select) + { + // Lookup sample window + if (thread_name in self.gridWindows) + { + const grid_window = self.gridWindows[thread_name]; + + // Set the grid window to the selected frame if valid + if (select) + { + const frame = select[0]; + self.SelectedFrames[thread_name] = frame; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + } + + // Otherwise deselect + else + { + const frames = self.FrameHistory[thread_name]; + const frame = frames[frames.length - 1]; + self.SelectedFrames[thread_name] = null; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + self.SampleTimelineWindow.Deselect(thread_name); + } + } + } + + + function OnResizeWindow(self) + { + var w = window.innerWidth; + var h = window.innerHeight; + + // Resize windows + self.Console.WindowResized(w, h); + self.TitleWindow.WindowResized(w, h); + self.SampleTimelineWindow.WindowResized(10, w / 2 - 5, self.TitleWindow.Window); + self.ProcessorTimelineWindow.WindowResized(w / 2 + 5, w / 2 - 5, self.TitleWindow.Window); + for (var i in self.gridWindows) + { + self.gridWindows[i].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window); + } + } + + + function OnTimelineMoved(self, timeline) + { + if (self.Settings.SyncTimelines) + { + let other_timeline = timeline == self.ProcessorTimelineWindow ? self.SampleTimelineWindow : self.ProcessorTimelineWindow; + other_timeline.SetTimeRange(timeline.TimeRange.Start_us, timeline.TimeRange.Span_us); + } + } + + return Remotery; +})(); \ No newline at end of file diff --git a/profiler/vis/Code/SampleGlobals.js b/profiler/vis/Code/SampleGlobals.js new file mode 100644 index 0000000..d74c88b --- /dev/null +++ b/profiler/vis/Code/SampleGlobals.js @@ -0,0 +1,28 @@ + +// Sample properties when viewed as an array of floats +const g_nbFloatsPerSample = 14; +const g_sampleOffsetFloats_NameOffset = 0; +const g_sampleOffsetFloats_NameLength = 1; +const g_sampleOffsetFloats_Start = 3; +const g_sampleOffsetFloats_Length = 5; +const g_sampleOffsetFloats_Self = 7; +const g_sampleOffsetFloats_GpuToCpu = 9; +const g_sampleOffsetFloats_Calls = 11; +const g_sampleOffsetFloats_Recurse = 12; + +// Sample properties when viewed as bytes +const g_nbBytesPerSample = g_nbFloatsPerSample * 4; +const g_sampleOffsetBytes_NameOffset = g_sampleOffsetFloats_NameOffset * 4; +const g_sampleOffsetBytes_NameLength = g_sampleOffsetFloats_NameLength * 4; +const g_sampleOffsetBytes_Colour = 8; +const g_sampleOffsetBytes_Depth = 11; +const g_sampleOffsetBytes_Start = g_sampleOffsetFloats_Start * 4; +const g_sampleOffsetBytes_Length = g_sampleOffsetFloats_Length * 4; +const g_sampleOffsetBytes_Self = g_sampleOffsetFloats_Self * 4; +const g_sampleOffsetBytes_GpuToCpu = g_sampleOffsetFloats_GpuToCpu * 4; +const g_sampleOffsetBytes_Calls = g_sampleOffsetFloats_Calls * 4; +const g_sampleOffsetBytes_Recurse = g_sampleOffsetFloats_Recurse * 4; + +// Snapshot properties +const g_nbFloatsPerSnapshot = 10; +const g_nbBytesPerSnapshot = g_nbFloatsPerSnapshot * 4; \ No newline at end of file diff --git a/profiler/vis/Code/Shaders/Grid.glsl b/profiler/vis/Code/Shaders/Grid.glsl new file mode 100644 index 0000000..eb1e1ff --- /dev/null +++ b/profiler/vis/Code/Shaders/Grid.glsl @@ -0,0 +1,162 @@ +const GridShaderShared = ShaderShared + ` + +#define RowHeight 15.0 + +struct Grid +{ + float minX; + float minY; + float maxX; + float maxY; + float pixelOffsetX; + float pixelOffsetY; +}; + +uniform Viewport inViewport; +uniform Grid inGrid; + +float Row(vec2 pixel_position) +{ + return floor(pixel_position.y / RowHeight); +} + +vec3 RowColour(float row) +{ + float row_grey = (int(row) & 1) == 0 ? 0.25 : 0.23; + return vec3(row_grey); +} + +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Vertex Shader +// ------------------------------------------------------------------------------------------------------------------------------- + +const GridVShader = GridShaderShared + ` + +out vec2 varPixelPosition; + +void main() +{ + vec2 position = QuadPosition(gl_VertexID, inGrid.minX, inGrid.minY, inGrid.maxX, inGrid.maxY); + vec4 ndc_pos = UVToNDC(inViewport, position); + + gl_Position = ndc_pos; + varPixelPosition = position - vec2(inGrid.minX, inGrid.minY) + vec2(inGrid.pixelOffsetX, inGrid.pixelOffsetY); +} +`; + +const GridNumberVShader = GridShaderShared + ` + +out vec2 varPixelPosition; + +void main() +{ + vec2 position = QuadPosition(gl_VertexID, inGrid.minX, inGrid.minY, inGrid.maxX, inGrid.maxY); + vec4 ndc_pos = UVToNDC(inViewport, position); + + gl_Position = ndc_pos; + varPixelPosition = position - vec2(inGrid.minX, inGrid.minY) + vec2(inGrid.pixelOffsetX, inGrid.pixelOffsetY); +} +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Fragment Shader +// ------------------------------------------------------------------------------------------------------------------------------- + +const GridFShader = GridShaderShared + ` + +// Array of samples +uniform sampler2D inSamples; +uniform float inSamplesLength; +uniform float inFloatsPerSample; +uniform float inNbSamples; + +in vec2 varPixelPosition; + +out vec4 outColour; + +void main() +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + float text_buffer_length = inTextBufferDesc.textBufferLength; + + // Which row are we on? + float row = Row(varPixelPosition); + vec3 row_colour = RowColour(row); + + float text_weight = 0.0; + vec3 text_colour = vec3(0.0); + if (row < inNbSamples) + { + // Unpack colour and depth + int colour_depth = floatBitsToInt(TextureBufferLookup(inSamples, row * inFloatsPerSample + 2.0, inSamplesLength).r); + text_colour.r = float(colour_depth & 255) / 255.0; + text_colour.g = float((colour_depth >> 8) & 255) / 255.0; + text_colour.b = float((colour_depth >> 16) & 255) / 255.0; + float depth = float(colour_depth >> 24); + + float text_buffer_offset = TextureBufferLookup(inSamples, row * inFloatsPerSample + 0.0, inSamplesLength).r; + float text_length_chars = TextureBufferLookup(inSamples, row * inFloatsPerSample + 1.0, inSamplesLength).r; + float text_length_px = text_length_chars * font_width_px; + + // Pixel position within the row + vec2 pos_in_box_px; + pos_in_box_px.x = varPixelPosition.x; + pos_in_box_px.y = varPixelPosition.y - row * RowHeight; + + // Get text at this position + vec2 text_start_px = vec2(4.0 + depth * 10.0, 3.0); + text_weight = LookupText(pos_in_box_px - text_start_px, text_buffer_offset, text_length_chars); + } + + outColour = vec4(mix(row_colour, text_colour, text_weight), 1.0); +} +`; + +const GridNumberFShader = GridShaderShared + ` + +// Array of samples +uniform sampler2D inSamples; +uniform float inSamplesLength; +uniform float inFloatsPerSample; +uniform float inNbSamples; + +// Offset within the sample +uniform float inNumberOffset; + +uniform float inNbFloatChars; + +in vec2 varPixelPosition; + +out vec4 outColour; + +void main() +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + float text_buffer_length = inTextBufferDesc.textBufferLength; + + // Which row are we on? + float row = Row(varPixelPosition); + vec3 row_colour = RowColour(row); + float text_weight = 0.0; + if (row < inNbSamples) + { + // Pixel position within the row + vec2 pos_in_box_px; + pos_in_box_px.x = varPixelPosition.x; + pos_in_box_px.y = varPixelPosition.y - row * RowHeight; + + // Get the number at this pixel + const vec2 text_start_px = vec2(4.0, 3.0); + float number = TextureBufferLookup(inSamples, row * inFloatsPerSample + inNumberOffset, inSamplesLength).r; + text_weight = LookupNumber(pos_in_box_px - text_start_px, number, inNbFloatChars); + } + + outColour = vec4(mix(row_colour, vec3(1.0), text_weight), 1.0); +} +`; diff --git a/profiler/vis/Code/Shaders/Shared.glsl b/profiler/vis/Code/Shaders/Shared.glsl new file mode 100644 index 0000000..94daf96 --- /dev/null +++ b/profiler/vis/Code/Shaders/Shared.glsl @@ -0,0 +1,154 @@ +const ShaderShared = `#version 300 es + +precision mediump float; + +struct Viewport +{ + float width; + float height; +}; + +vec2 QuadPosition(int vertex_id, float min_x, float min_y, float max_x, float max_y) +{ + // Quad indices are: + // + // 2 3 + // +----+ + // | | + // +----+ + // 0 1 + // + vec2 position; + position.x = (vertex_id & 1) == 0 ? min_x : max_x; + position.y = (vertex_id & 2) == 0 ? min_y : max_y; + return position; +} + +vec4 UVToNDC(Viewport viewport, vec2 uv) +{ + // + // NDC is: + // -1 to 1, left to right + // -1 to 1, bottom to top + // + vec4 ndc_pos; + ndc_pos.x = (uv.x / viewport.width) * 2.0 - 1.0; + ndc_pos.y = 1.0 - (uv.y / viewport.height) * 2.0; + ndc_pos.z = 0.0; + ndc_pos.w = 1.0; + return ndc_pos; +} + +vec4 TextureBufferLookup(sampler2D sampler, float index, float length) +{ + vec2 uv = vec2((index + 0.5) / length, 0.5); + return texture(sampler, uv); +} + +struct TextBufferDesc +{ + float fontWidth; + float fontHeight; + float textBufferLength; +}; + +uniform sampler2D inFontAtlasTexture; +uniform sampler2D inTextBuffer; +uniform TextBufferDesc inTextBufferDesc; + +float LookupCharacter(float char_ascii, float pos_x, float pos_y, float font_width_px, float font_height_px) +{ + // 2D index of the ASCII character in the font atlas + float char_index_y = floor(char_ascii / 16.0); + float char_index_x = char_ascii - char_index_y * 16.0; + + // Start UV of the character in the font atlas + float char_base_uv_x = char_index_x / 16.0; + float char_base_uv_y = char_index_y / 16.0; + + // UV within the character itself, scaled to the font atlas + float char_uv_x = pos_x / (font_width_px * 16.0); + float char_uv_y = pos_y / (font_height_px * 16.0); + + vec2 uv; + uv.x = char_base_uv_x + char_uv_x; + uv.y = char_base_uv_y + char_uv_y; + + // Strip colour and return alpha only + return texture(inFontAtlasTexture, uv).a; +} + +float LookupText(vec2 render_pos_px, float text_buffer_offset, float text_length_chars) +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + float text_buffer_length = inTextBufferDesc.textBufferLength; + float text_length_px = text_length_chars * font_width_px; + + // Text pixel position clamped to the bounds of the full word, allowing leakage to neighbouring NULL characters to pad zeroes + vec2 text_pixel_pos; + text_pixel_pos.x = max(min(render_pos_px.x, text_length_px), -1.0); + text_pixel_pos.y = max(min(render_pos_px.y, font_height_px - 1.0), 0.0); + + // Index of the current character in the text buffer + float text_index = text_buffer_offset + floor(text_pixel_pos.x / font_width_px); + + // Sample the 1D text buffer to get the ASCII character index + float char_ascii = TextureBufferLookup(inTextBuffer, text_index, text_buffer_length).a * 255.0; + + return LookupCharacter(char_ascii, + text_pixel_pos.x - (text_index - text_buffer_offset) * font_width_px, + text_pixel_pos.y, + font_width_px, font_height_px); +} + +float NbIntegerCharsForNumber(float number) +{ + float number_int = floor(number); + return number_int == 0.0 ? 1.0 : floor(log(number_int) / 2.302585092994046) + 1.0; +} + +// Base-10 lookup table for shifting digits of a float to the range 0-9 where they can be rendered +const float g_Multipliers[14] = float[14]( + // Decimal part multipliers + 1000.0, 100.0, 10.0, + // Zero entry for maintaining the ASCII "." base when rendering the period + 0.0, + // Integer part multipliers + 1.0, 0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001, 0.00000001, 0.000000001 ); + +float LookupNumber(vec2 render_pos_px, float number, float nb_float_chars) +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + float text_buffer_length = inTextBufferDesc.textBufferLength; + + float number_integer_chars = NbIntegerCharsForNumber(number); + + // Clip + render_pos_px.y = max(min(render_pos_px.y, font_height_px - 1.0), 0.0); + + float number_index = floor(render_pos_px.x / font_width_px); + + if (number_index >= 0.0 && number_index < number_integer_chars + nb_float_chars) + { + // When we are indexing the period separating integer and decimal, set the base to ASCII "." + // The lookup table stores zero for this entry, multipying with the addend to produce no shift from this base + float base = (number_index == number_integer_chars) ? 46.0 : 48.0; + + // Calculate digit using the current number index base-10 shift + float multiplier = g_Multipliers[int(number_integer_chars - number_index) + 3]; + float number_shifted_int = floor(number * multiplier); + float number_digit = floor(mod(number_shifted_int, 10.0)); + + return LookupCharacter(base + number_digit, + render_pos_px.x - number_index * font_width_px, + render_pos_px.y, + font_width_px, font_height_px); + } + + return 0.0; +} +`; diff --git a/profiler/vis/Code/Shaders/Timeline.glsl b/profiler/vis/Code/Shaders/Timeline.glsl new file mode 100644 index 0000000..afb464f --- /dev/null +++ b/profiler/vis/Code/Shaders/Timeline.glsl @@ -0,0 +1,337 @@ +const TimelineShaderShared = ShaderShared + ` + +#define SAMPLE_HEIGHT 16.0 +#define SAMPLE_BORDER 2.0 +#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0) + +#define PIXEL_ROUNDED_OFFSETS + +struct Container +{ + float x0; + float y0; + float x1; + float y1; +}; + +struct TimeRange +{ + float msStart; + float msPerPixel; +}; + +struct Row +{ + float yOffset; +}; + +uniform Viewport inViewport; +uniform TimeRange inTimeRange; +uniform Container inContainer; +uniform Row inRow; + +float PixelOffset(float time_ms) +{ + float offset = (time_ms - inTimeRange.msStart) * inTimeRange.msPerPixel; + #ifdef PIXEL_ROUNDED_OFFSETS + return floor(offset); + #else + return offset; + #endif +} + +float PixelSize(float time_ms) +{ + float size = time_ms * inTimeRange.msPerPixel; + #ifdef PIXEL_ROUNDED_OFFSETS + return floor(size); + #else + return size; + #endif +} + +vec4 SampleQuad(int vertex_id, vec4 in_sample_textoffset, float padding, out vec4 out_quad_pos_size_px) +{ + // Unpack input data + float ms_start = in_sample_textoffset.x; + float ms_length = in_sample_textoffset.y; + float depth = in_sample_textoffset.z; + + // Determine pixel range of the sample + float x0 = PixelOffset(ms_start); + float x1 = x0 + PixelSize(ms_length); + + // Calculate box to render + // Ensure no sample is less than one pixel in length and so is always visible + float offset_x = inContainer.x0 + x0 - padding; + float offset_y = inRow.yOffset + (depth - 1.0) * SAMPLE_Y_SPACING + SAMPLE_BORDER - padding; + float size_x = max(x1 - x0, 1.0) + padding * 2.0; + float size_y = SAMPLE_HEIGHT + padding * 2.0; + + // Box range clipped to container bounds + float min_x = min(max(offset_x, inContainer.x0), inContainer.x1); + float min_y = min(max(offset_y, inContainer.y0), inContainer.y1); + float max_x = min(max(offset_x + size_x, inContainer.x0), inContainer.x1); + float max_y = min(max(offset_y + size_y, inContainer.y0), inContainer.y1); + + // Box quad position in NDC + vec2 position = QuadPosition(vertex_id, min_x, min_y, max_x, max_y); + vec4 ndc_pos = UVToNDC(inViewport, position); + + out_quad_pos_size_px.xy = vec2(position.x - offset_x, position.y - offset_y); + out_quad_pos_size_px.zw = vec2(max_x - min_x, max_y - min_y); + + return ndc_pos; +} + +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Sample Rendering +// ------------------------------------------------------------------------------------------------------------------------------- + +const TimelineVShader = TimelineShaderShared + ` + +in vec4 inSample_TextOffset; +in vec4 inColour_TextLength; + +out vec4 varColour_TimeMs; +out vec4 varPosInBoxPx_TextEntry; +out float varTimeChars; + +void main() +{ + // Unpack input data + float ms_length = inSample_TextOffset.y; + float text_buffer_offset = inSample_TextOffset.w; + vec3 box_colour = inColour_TextLength.rgb; + float text_length_chars = inColour_TextLength.w; + + // Calculate number of characters required to display the millisecond time + float time_chars = NbIntegerCharsForNumber(ms_length); + + // Calculate sample quad vertex positions + vec4 quad_pos_size_px; + gl_Position = SampleQuad(gl_VertexID, inSample_TextOffset, 0.0, quad_pos_size_px); + + // Pack for fragment shader + varColour_TimeMs = vec4(box_colour / 255.0, ms_length); + varPosInBoxPx_TextEntry = vec4(quad_pos_size_px.x, quad_pos_size_px.y, text_buffer_offset, text_length_chars); + varTimeChars = time_chars; +} +`; + +const TimelineFShader = TimelineShaderShared + ` + +in vec4 varColour_TimeMs; +in vec4 varPosInBoxPx_TextEntry; +in float varTimeChars; + +out vec4 outColour; + +void main() +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + + // Text range in the text buffer + vec2 pos_in_box_px = varPosInBoxPx_TextEntry.xy; + float text_buffer_offset = varPosInBoxPx_TextEntry.z; + float text_length_chars = varPosInBoxPx_TextEntry.w; + float text_length_px = text_length_chars * font_width_px; + + // Text placement offset within the box + const vec2 text_start_px = vec2(10.0, 3.0); + + vec3 box_colour = varColour_TimeMs.rgb; + + // Add a subtle border to the box so that you can visually separate samples when they are next to each other + vec2 top_left = min(pos_in_box_px.xy, 2.0); + float both = min(top_left.x, top_left.y); + box_colour *= (0.8 + both * 0.1); + + float text_weight = 0.0; + + // Are we over the time number or the text? + float text_end_px = text_start_px.x + text_length_px; + float number_start_px = text_end_px + font_width_px * 2.0; + if (pos_in_box_px.x > number_start_px) + { + vec2 time_pixel_pos; + time_pixel_pos.x = pos_in_box_px.x - number_start_px; + time_pixel_pos.y = max(min(pos_in_box_px.y - text_start_px.y, font_height_px - 1.0), 0.0); + + // Time number + float time_ms = varColour_TimeMs.w; + float time_index = floor(time_pixel_pos.x / font_width_px); + if (time_index < varTimeChars + 4.0) + { + text_weight = LookupNumber(time_pixel_pos, time_ms, 4.0); + } + + // " ms" label at the end of the time + else if (time_index < varTimeChars + 7.0) + { + const float ms[3] = float[3] ( 32.0, 109.0, 115.0 ); + float char = ms[int(time_index - (varTimeChars + 4.0))]; + text_weight = LookupCharacter(char, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px); + } + } + else + { + text_weight = LookupText(pos_in_box_px - text_start_px, text_buffer_offset, text_length_chars); + } + + // Blend text onto the box + vec3 text_colour = vec3(0.0, 0.0, 0.0); + outColour = vec4(mix(box_colour, text_colour, text_weight), 1.0); +} +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Sample Highlights +// ------------------------------------------------------------------------------------------------------------------------------- + +const TimelineHighlightVShader = TimelineShaderShared + ` + +uniform float inStartMs; +uniform float inLengthMs; +uniform float inDepth; + +out vec4 varPosSize; + +void main() +{ + // Calculate sample quad vertex positions + gl_Position = SampleQuad(gl_VertexID, vec4(inStartMs, inLengthMs, inDepth, 0.0), 1.0, varPosSize); +} +`; + +const TimelineHighlightFShader = TimelineShaderShared + ` + +// TODO(don): Vector uniforms, please! +uniform float inColourR; +uniform float inColourG; +uniform float inColourB; + +in vec4 varPosSize; + +out vec4 outColour; + +void main() +{ + // Rounded pixel co-ordinates interpolating across the sample + vec2 pos = floor(varPosSize.xy); + + // Sample size in pixel co-ordinates + vec2 size = floor(varPosSize.zw); + + // Highlight thickness + float t = 2.0; + + // Distance along axes to highlight edges + vec2 dmin = abs(pos - 0.0); + vec2 dmax = abs(pos - (size - 1.0)); + + // Take the closest distance + float dx = min(dmin.x, dmax.x); + float dy = min(dmin.y, dmax.y); + float d = min(dx, dy); + + // Invert the distance and clamp to thickness + d = (t + 1.0) - min(d, t + 1.0); + + // Scale with thickness for uniform intensity + d = d / (t + 1.0); + outColour = vec4(inColourR * d, inColourG * d, inColourB * d, d); +} +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// GPU->CPU Sample Sources +// ------------------------------------------------------------------------------------------------------------------------------- + +const TimelineGpuToCpuVShader = TimelineShaderShared + ` + +uniform float inStartMs; +uniform float inLengthMs; +uniform float inDepth; + +out vec4 varPosSize; + +void main() +{ + // Calculate sample quad vertex positions + gl_Position = SampleQuad(gl_VertexID, vec4(inStartMs, inLengthMs, inDepth, 0.0), 1.0, varPosSize); +} +`; + + +const TimelineGpuToCpuFShader = TimelineShaderShared + ` + +in vec4 varPosSize; + +out vec4 outColour; + +void main() +{ + // Rounded pixel co-ordinates interpolating across the sample + vec2 pos = floor(varPosSize.xy); + + // Sample size in pixel co-ordinates + vec2 size = floor(varPosSize.zw); + + // Distance to centre line, bumped out every period to create a dash + float dc = abs(pos.y - size.y / 2.0); + dc += (int(pos.x / 3.0) & 1) == 0 ? 100.0 : 0.0; + + // Min with the start line + float ds = abs(pos.x - 0.0); + float d = min(dc, ds); + + // Invert the distance for highlight + d = 1.0 - min(d, 1.0); + + outColour = vec4(d, d, d, d); +} + +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Background +// ------------------------------------------------------------------------------------------------------------------------------- + +const TimelineBackgroundVShader = TimelineShaderShared + ` + +uniform float inYOffset; + +out vec2 varPosition; + +void main() +{ + + // Container quad position in NDC + vec2 position = QuadPosition(gl_VertexID, inContainer.x0, inContainer.y0, inContainer.x1, inContainer.y1); + gl_Position = UVToNDC(inViewport, position); + + // Offset Y with scroll position + varPosition = vec2(position.x, position.y - inYOffset); +} + +`; + +const TimelineBackgroundFShader = TimelineShaderShared + ` + +in vec2 varPosition; + +out vec4 outColour; + +void main() +{ + vec2 pos = floor(varPosition); + float f = round(fract(pos.y / SAMPLE_Y_SPACING) * SAMPLE_Y_SPACING); + float g = f >= 1.0 && f <= (SAMPLE_Y_SPACING - 2.0) ? 0.30 : 0.23; + outColour = vec4(g, g, g, 1.0); +} +`; \ No newline at end of file diff --git a/profiler/vis/Code/Shaders/Window.glsl b/profiler/vis/Code/Shaders/Window.glsl new file mode 100644 index 0000000..362772e --- /dev/null +++ b/profiler/vis/Code/Shaders/Window.glsl @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------------------------------------- +// Vertex Shader +// ------------------------------------------------------------------------------------------------------------------------------- + +const WindowVShader = ShaderShared + ` + +uniform Viewport inViewport; + +uniform float minX; +uniform float minY; +uniform float maxX; +uniform float maxY; + +void main() +{ + vec2 position = QuadPosition(gl_VertexID, minX, minY, maxX, maxY); + gl_Position = UVToNDC(inViewport, position); +} +`; + +// ------------------------------------------------------------------------------------------------------------------------------- +// Fragment Shader +// ------------------------------------------------------------------------------------------------------------------------------- + +const WindowFShader = ShaderShared + ` + +out vec4 outColour; + +void main() +{ + outColour = vec4(1.0, 1.0, 1.0, 0.0); +} +`; diff --git a/profiler/vis/Code/ThreadFrame.js b/profiler/vis/Code/ThreadFrame.js new file mode 100644 index 0000000..3826205 --- /dev/null +++ b/profiler/vis/Code/ThreadFrame.js @@ -0,0 +1,34 @@ + + +class ThreadFrame +{ + constructor(message) + { + // Persist the required message data + this.NbSamples = message.nb_samples; + this.sampleDataView = message.sampleDataView; + this.sampleFloats = message.sampleFloats; + this.PartialTree = message.partial_tree > 0 ? true : false; + + // Point to the first/last samples + const first_sample_start_us = this.sampleFloats[g_sampleOffsetFloats_Start] * 1000.0; + const last_sample_offset = (this.NbSamples - 1) * g_nbFloatsPerSample; + const last_sample_start_us = this.sampleFloats[last_sample_offset + g_sampleOffsetFloats_Start] * 1000.0; + const last_sample_length_us = this.sampleFloats[last_sample_offset + g_sampleOffsetFloats_Length] * 1000.0; + + // Calculate the frame start/end times + this.StartTime_us = first_sample_start_us; + this.EndTime_us = last_sample_start_us + last_sample_length_us; + this.Length_us = this.EndTime_us - this.StartTime_us; + } +} + +class PropertySnapshotFrame +{ + constructor(message) + { + this.nbSnapshots = message.nbSnapshots; + this.snapshots = message.snapshots; + this.snapshotFloats = message.snapshotFloats; + } +} \ No newline at end of file diff --git a/profiler/vis/Code/TimelineMarkers.js b/profiler/vis/Code/TimelineMarkers.js new file mode 100644 index 0000000..435f4fb --- /dev/null +++ b/profiler/vis/Code/TimelineMarkers.js @@ -0,0 +1,186 @@ + +function GetTimeText(seconds) +{ + if (seconds < 0) + { + return ""; + } + + var text = ""; + + // Add any contributing hours + var h = Math.floor(seconds / 3600); + seconds -= h * 3600; + if (h) + { + text += h + "h "; + } + + // Add any contributing minutes + var m = Math.floor(seconds / 60); + seconds -= m * 60; + if (m) + { + text += m + "m "; + } + + // Add any contributing seconds or always add seconds when hours or minutes have no contribution + // This ensures the 0s marker displays + var s = Math.floor(seconds); + seconds -= s; + if (s || text == "") + { + text += s + "s "; + } + + // Add remaining milliseconds + var ms = Math.floor(seconds * 1000); + if (ms) + { + text += ms + "ms"; + } + + return text; +} + + +class TimelineMarkers +{ + constructor(timeline) + { + this.timeline = timeline; + + // Need a 2D drawing context + this.markerContainer = timeline.Window.AddControlNew(new WM.Container(10, 10, 10, 10)); + this.markerCanvas = document.createElement("canvas"); + this.markerContainer.Node.appendChild(this.markerCanvas); + this.markerContext = this.markerCanvas.getContext("2d"); + } + + Draw(time_range) + { + let ctx = this.markerContext; + + ctx.clearRect(0, 0, this.markerCanvas.width, this.markerCanvas.height); + + // Setup render state for the time line markers + ctx.strokeStyle = "#BBB"; + ctx.fillStyle = "#BBB"; + ctx.lineWidth = 1; + ctx.font = "9px LocalFiraCode"; + + // A list of all supported units of time (measured in seconds) that require markers + let units = [ 0.001, 0.01, 0.1, 1, 10, 60, 60 * 5, 60 * 60, 60 * 60 * 24 ]; + + // Given the current pixel size of a second, calculate the spacing for each unit marker + let second_pixel_size = time_range.PixelSize(1000 * 1000); + let sizeof_units = [ ]; + for (let unit of units) + { + sizeof_units.push(unit * second_pixel_size); + } + + // Calculate whether each unit marker is visible at the current zoom level + var show_unit = [ ]; + for (let sizeof_unit of sizeof_units) + { + show_unit.push(Math.max(Math.min((sizeof_unit - 4) * 0.25, 1), 0)); + } + + // Find the first visible unit + for (let i = 0; i < units.length; i++) + { + if (show_unit[i] > 0) + { + // Cut out unit information for the first set of units not visible + units = units.slice(i); + sizeof_units = sizeof_units.slice(i); + show_unit = show_unit.slice(i); + break; + } + } + + let timeline_end = this.markerCanvas.width; + for (let i = 0; i < 3; i++) + { + // Round the start time up to the next visible unit + let time_start = time_range.Start_us / (1000 * 1000); + let unit_time_start = Math.ceil(time_start / units[i]) * units[i]; + + // Calculate the canvas offset required to step to the first visible unit + let pre_step_x = time_range.PixelOffset(unit_time_start * (1000 * 1000)); + + // Draw lines for every unit at this level, keeping tracking of the seconds + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + // For the first two units, don't draw the units above it to prevent + // overdraw and the visual errors that causes + // The last unit always draws + if (i > 1 || (seconds % units[i + 1])) + { + // Only the first two units scale with unit visibility + // The last unit maintains its size + let height = Math.min(i * 4 + 4 * show_unit[i], 16); + + // Draw the line on an integer boundary, shifted by 0.5 to get an un-anti-aliased 1px line + let ix = Math.floor(x); + ctx.beginPath(); + ctx.moveTo(ix + 0.5, 1); + ctx.lineTo(ix + 0.5, 1 + height); + ctx.stroke(); + } + + seconds += units[i]; + } + + if (i == 1) + { + // Draw text labels for the second unit, fading them out as they slowly + // become the first unit + ctx.globalAlpha = show_unit[0]; + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + if (seconds % units[2]) + { + this.DrawTimeText(seconds, x, 16); + } + seconds += units[i]; + } + + // Restore alpha + ctx.globalAlpha = 1; + } + + else if (i == 2) + { + // Draw text labels for the third unit with no fade + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + this.DrawTimeText(seconds, x, 16); + seconds += units[i]; + } + } + } + } + + DrawTimeText(seconds, x, y) + { + // Use text measuring to centre the text horizontally on the input x + var text = GetTimeText(seconds); + var width = this.markerContext.measureText(text).width; + this.markerContext.fillText(text, Math.floor(x) - width / 2, y); + } + + Resize(x, y, w, h) + { + this.markerContainer.SetPosition(x, y); + this.markerContainer.SetSize(w, h); + + // Match canvas size to container + this.markerCanvas.width = this.markerContainer.Node.clientWidth; + this.markerCanvas.height = this.markerContainer.Node.clientHeight; + } +} \ No newline at end of file diff --git a/profiler/vis/Code/TimelineRow.js b/profiler/vis/Code/TimelineRow.js new file mode 100644 index 0000000..60a54a7 --- /dev/null +++ b/profiler/vis/Code/TimelineRow.js @@ -0,0 +1,400 @@ + + +TimelineRow = (function() +{ + const RowLabelTemplate = ` +
+
+ +
+
+
+
+
+
+
-
+
+
+
+
` + + + var SAMPLE_HEIGHT = 16; + var SAMPLE_BORDER = 2; + var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2; + + + function TimelineRow(gl, name, timeline, frame_history, check_handler) + { + this.Name = name; + this.timeline = timeline; + + // Create the row HTML and add to the parent + this.LabelContainerNode = DOM.Node.CreateHTML(RowLabelTemplate); + const label_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowLabel"); + label_node.innerHTML = name; + timeline.TimelineLabels.Node.appendChild(this.LabelContainerNode); + + // All sample view windows visible by default + const checkbox_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowCheckbox"); + checkbox_node.checked = true; + checkbox_node.addEventListener("change", (e) => check_handler(name, e)); + + // Manually hook-up events to simulate div:active + // I can't get the equivalent CSS to work in Firefox, so... + const expand_node_0 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 0); + const expand_node_1 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 1); + const inc_node = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton"); + const dec_node = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton"); + inc_node.addEventListener("mousedown", ExpandButtonDown); + inc_node.addEventListener("mouseup", ExpandButtonUp); + inc_node.addEventListener("mouseleave", ExpandButtonUp); + dec_node.addEventListener("mousedown", ExpandButtonDown); + dec_node.addEventListener("mouseup", ExpandButtonUp); + dec_node.addEventListener("mouseleave", ExpandButtonUp); + + // Pressing +/i increases/decreases depth + inc_node.addEventListener("click", () => this.IncDepth()); + dec_node.addEventListener("click", () => this.DecDepth()); + + // Frame index to start at when looking for first visible sample + this.StartFrameIndex = 0; + + this.FrameHistory = frame_history; + this.VisibleFrames = [ ]; + this.VisibleTimeRange = null; + this.Depth = 1; + + // Currently selected sample + this.SelectSampleInfo = null; + + // Create WebGL sample buffers + this.sampleBuffer = new glDynamicBuffer(gl, glDynamicBufferType.Buffer, gl.FLOAT, 4); + this.colourBuffer = new glDynamicBuffer(gl, glDynamicBufferType.Buffer, gl.UNSIGNED_BYTE, 4); + + // An initial SetSize call to restore containers to their original size after traces were loaded prior to this + this.SetSize(); + } + + + TimelineRow.prototype.SetSize = function() + { + this.LabelContainerNode.style.height = SAMPLE_Y_SPACING * this.Depth; + } + + + TimelineRow.prototype.SetVisibleFrames = function(time_range) + { + // Clear previous visible list + this.VisibleFrames = [ ]; + if (this.FrameHistory.length == 0) + return; + + // Store a copy of the visible time range rather than referencing it + // This prevents external modifications to the time range from affecting rendering/selection + time_range = time_range.Clone(); + this.VisibleTimeRange = time_range; + + // The frame history can be reset outside this class + // This also catches the overflow to the end of the frame list below when a thread stops sending samples + var max_frame = Math.max(this.FrameHistory.length - 1, 0); + var start_frame_index = Math.min(this.StartFrameIndex, max_frame); + + // First do a back-track in case the time range moves negatively + while (start_frame_index > 0) + { + var frame = this.FrameHistory[start_frame_index]; + if (time_range.Start_us > frame.StartTime_us) + break; + start_frame_index--; + } + + // Then search from this point for the first visible frame + while (start_frame_index < this.FrameHistory.length) + { + var frame = this.FrameHistory[start_frame_index]; + if (frame.EndTime_us > time_range.Start_us) + break; + start_frame_index++; + } + + // Gather all frames up to the end point + this.StartFrameIndex = start_frame_index; + for (var i = start_frame_index; i < this.FrameHistory.length; i++) + { + var frame = this.FrameHistory[i]; + if (frame.StartTime_us > time_range.End_us) + break; + this.VisibleFrames.push(frame); + } + } + + + TimelineRow.prototype.DrawSampleHighlight = function(gl_canvas, container, frame, offset, depth, selected) + { + if (depth <= this.Depth) + { + const gl = gl_canvas.gl; + const program = gl_canvas.timelineHighlightProgram; + + gl_canvas.SetContainerUniforms(program, container); + + // Set row parameters + const row_rect = this.LabelContainerNode.getBoundingClientRect(); + glSetUniform(gl, program, "inRow.yOffset", row_rect.top); + + // Set sample parameters + const float_offset = offset / 4; + glSetUniform(gl, program, "inStartMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start]); + glSetUniform(gl, program, "inLengthMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Length]); + glSetUniform(gl, program, "inDepth", depth); + + // Set colour + glSetUniform(gl, program, "inColourR", 1.0); + glSetUniform(gl, program, "inColourG", selected ? 0.0 : 1.0); + glSetUniform(gl, program, "inColourB", selected ? 0.0 : 1.0); + + gl_canvas.EnableBlendPremulAlpha(); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + gl_canvas.DisableBlend(); + } + } + + + TimelineRow.prototype.DrawSampleGpuToCpu = function(gl_canvas, container, frame, offset, depth) + { + // Is this a GPU sample? + const float_offset = offset / 4; + const start_ms = frame.sampleFloats[float_offset + g_sampleOffsetFloats_GpuToCpu]; + if (start_ms > 0) + { + const gl = gl_canvas.gl; + const program = gl_canvas.timelineGpuToCpuProgram; + + gl_canvas.SetContainerUniforms(program, container); + + // Set row parameters + const row_rect = this.LabelContainerNode.getBoundingClientRect(); + glSetUniform(gl, program, "inRow.yOffset", row_rect.top); + + // Set sample parameters + const length_ms = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start] - start_ms; + glSetUniform(gl, program, "inStartMs", start_ms); + glSetUniform(gl, program, "inLengthMs", length_ms); + glSetUniform(gl, program, "inDepth", depth); + + gl_canvas.EnableBlendPremulAlpha(); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + gl_canvas.DisableBlend(); + } + } + + + TimelineRow.prototype.DisplayHeight = function() + { + return this.LabelContainerNode.clientHeight; + } + + + TimelineRow.prototype.YOffset = function() + { + return this.LabelContainerNode.offsetTop; + } + + + function GatherSamples(self, frame, samples_per_depth) + { + const sample_data_view = frame.sampleDataView; + + for (let offset = 0; offset < sample_data_view.byteLength; offset += g_nbBytesPerSample) + { + depth = sample_data_view.getUint8(offset + g_sampleOffsetBytes_Depth) + 1; + if (depth > self.Depth) + { + continue; + } + + // Ensure there's enough entries for each depth + while (depth >= samples_per_depth.length) + { + samples_per_depth.push([]); + } + + let samples_this_depth = samples_per_depth[depth]; + samples_this_depth.push([frame, offset]); + } + } + + + TimelineRow.prototype.Draw = function(gl_canvas, container) + { + let samples_per_depth = []; + + // Gather all sample data in the visible frame set + for (var i in this.VisibleFrames) + { + var frame = this.VisibleFrames[i]; + GatherSamples(this, frame, samples_per_depth); + } + + // Count number of samples required + let nb_samples = 0; + for (const samples_this_depth of samples_per_depth) + { + nb_samples += samples_this_depth.length; + } + + // Resize buffers to match any new count of samples + const gl = gl_canvas.gl; + const program = gl_canvas.timelineProgram; + if (nb_samples > this.sampleBuffer.nbEntries) + { + this.sampleBuffer.ResizeToFitNextPow2(nb_samples); + this.colourBuffer.ResizeToFitNextPow2(nb_samples); + + // Have to create a new VAO for these buffers + this.vertexArrayObject = gl.createVertexArray(); + gl.bindVertexArray(this.vertexArrayObject); + this.sampleBuffer.BindAsInstanceAttribute(program, "inSample_TextOffset"); + this.colourBuffer.BindAsInstanceAttribute(program, "inColour_TextLength"); + } + + // CPU write destination for samples + let cpu_samples = this.sampleBuffer.cpuArray; + let cpu_colours = this.colourBuffer.cpuArray; + let sample_pos = 0; + + // TODO(don): Pack offsets into the sample buffer, instead? + // Puts all samples together into one growing buffer (will need ring buffer management). + // Offset points into that. + // Remains to be seen how much of this can be done given the limitations of WebGL2... + + // Copy samples to the CPU buffer + // TODO(don): Use a ring buffer instead and take advantage of timeline scrolling adding new samples at the beginning/end + for (let depth = 0; depth < samples_per_depth.length; depth++) + { + let samples_this_depth = samples_per_depth[depth]; + for (const [frame, offset] of samples_this_depth) + { + const float_offset = offset / 4; + + cpu_samples[sample_pos + 0] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start]; + cpu_samples[sample_pos + 1] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Length]; + cpu_samples[sample_pos + 2] = depth; + cpu_samples[sample_pos + 3] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_NameOffset]; + + cpu_colours[sample_pos + 0] = frame.sampleDataView.getUint8(offset + g_sampleOffsetBytes_Colour + 0); + cpu_colours[sample_pos + 1] = frame.sampleDataView.getUint8(offset + g_sampleOffsetBytes_Colour + 1); + cpu_colours[sample_pos + 2] = frame.sampleDataView.getUint8(offset + g_sampleOffsetBytes_Colour + 2); + cpu_colours[sample_pos + 3] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_NameLength]; + + sample_pos += 4; + } + } + + // Upload to GPU + this.sampleBuffer.UploadData(); + this.colourBuffer.UploadData(); + + gl_canvas.SetContainerUniforms(program, container); + + // Set row parameters + const row_rect = this.LabelContainerNode.getBoundingClientRect(); + glSetUniform(gl, program, "inRow.yOffset", row_rect.top); + + gl.bindVertexArray(this.vertexArrayObject); + gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, nb_samples); + } + + + TimelineRow.prototype.SetSelectSample = function(sample_info) + { + this.SelectSampleInfo = sample_info; + } + + + function ExpandButtonDown(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.AddClass(node, "TimelineRowExpandButtonActive"); + } + + + function ExpandButtonUp(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.RemoveClass(node, "TimelineRowExpandButtonActive"); + } + + + TimelineRow.prototype.IncDepth = function() + { + this.Depth++; + this.SetSize(); + } + + + TimelineRow.prototype.DecDepth = function() + { + if (this.Depth > 1) + { + this.Depth--; + this.SetSize(); + + // Trigger scroll handling to ensure reducing the depth reduces the display height + this.timeline.MoveVertically(0); + } + } + + + TimelineRow.prototype.GetSampleAtPosition = function(time_us, mouse_y) + { + // Calculate depth of the mouse cursor + const depth = Math.min(Math.floor(mouse_y / SAMPLE_Y_SPACING) + 1, this.Depth); + + // Search for the first frame to intersect this time + for (let i in this.VisibleFrames) + { + // Use the sample's closed interval to detect hits. + // Rendering of samples ensures a sample is never smaller than one pixel so that all samples always draw, irrespective + // of zoom level. If a half-open interval is used then some visible samples will be unselectable due to them being + // smaller than a pixel. This feels pretty odd and the closed interval fixes this feeling well. + // TODO(don): There are still inconsistencies, need to shift to pixel range checking to match exactly. + const frame = this.VisibleFrames[i]; + if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us) + { + const found_sample = FindSample(this, frame, time_us, depth, 1); + if (found_sample != null) + { + return [ frame, found_sample[0], found_sample[1], this ]; + } + } + } + + return null; + } + + + function FindSample(self, frame, time_us, target_depth, depth) + { + // Search entire frame of samples looking for a depth and time range that contains the input time + const sample_data_view = frame.sampleDataView; + for (let offset = 0; offset < sample_data_view.byteLength; offset += g_nbBytesPerSample) + { + depth = sample_data_view.getUint8(offset + g_sampleOffsetBytes_Depth) + 1; + if (depth == target_depth) + { + const us_start = sample_data_view.getFloat32(offset + g_sampleOffsetBytes_Start, true) * 1000.0; + const us_length = sample_data_view.getFloat32(offset + g_sampleOffsetBytes_Length, true) * 1000.0; + if (time_us >= us_start && time_us < us_start + us_length) + { + return [ offset, depth ]; + } + } + } + + return null; + } + + + return TimelineRow; +})(); diff --git a/profiler/vis/Code/TimelineWindow.js b/profiler/vis/Code/TimelineWindow.js new file mode 100644 index 0000000..7a15f6b --- /dev/null +++ b/profiler/vis/Code/TimelineWindow.js @@ -0,0 +1,496 @@ + +// TODO(don): Separate all knowledge of threads from this timeline + +TimelineWindow = (function() +{ + var BORDER = 10; + + function TimelineWindow(wm, name, settings, check_handler, gl_canvas) + { + this.Settings = settings; + this.glCanvas = gl_canvas; + + // Create timeline window + this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100, null, this); + this.Window.SetTitle(name); + this.Window.ShowNoAnim(); + + this.timelineMarkers = new TimelineMarkers(this); + + // DO THESE need to be containers... can they just be divs? + // divs need a retrieval function + this.TimelineLabelScrollClipper = this.Window.AddControlNew(new WM.Container(10, 10, 10, 10)); + DOM.Node.AddClass(this.TimelineLabelScrollClipper.Node, "TimelineLabelScrollClipper"); + this.TimelineLabels = this.TimelineLabelScrollClipper.AddControlNew(new WM.Container(0, 0, 10, 10)); + DOM.Node.AddClass(this.TimelineLabels.Node, "TimelineLabels"); + + // Ordered list of thread rows on the timeline + this.ThreadRows = [ ]; + + // Create timeline container + this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160)); + DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer"); + + // Setup mouse interaction + this.mouseInteraction = new MouseInteraction(this.TimelineContainer.Node); + this.mouseInteraction.onClickHandler = (mouse_state) => OnMouseClick(this, mouse_state); + this.mouseInteraction.onMoveHandler = (mouse_state, mx, my) => OnMouseMove(this, mouse_state, mx, my); + this.mouseInteraction.onHoverHandler = (mouse_state) => OnMouseHover(this, mouse_state); + this.mouseInteraction.onScrollHandler = (mouse_state) => OnMouseScroll(this, mouse_state); + + // Allow user to click on the thread name to deselect any threads as finding empty space may be difficult + DOM.Event.AddHandler(this.TimelineLabels.Node, "mousedown", (evt) => OnLabelMouseDown(this, evt)); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + + this.Clear(); + + this.OnHoverHandler = null; + this.OnSelectedHandler = null; + this.OnMovedHandler = null; + this.CheckHandler = check_handler; + + this.yScrollOffset = 0; + + this.HoverSampleInfo = null; + this.lastHoverThreadName = null; + } + + + TimelineWindow.prototype.Clear = function() + { + // Clear out labels + this.TimelineLabels.ClearControls(); + + this.ThreadRows = [ ]; + this.TimeRange = new PixelTimeRange(0, 200 * 1000, this.TimelineContainer.Node.clientWidth); + } + + + TimelineWindow.prototype.SetOnHover = function(handler) + { + this.OnHoverHandler = handler; + } + + + TimelineWindow.prototype.SetOnSelected = function(handler) + { + this.OnSelectedHandler = handler; + } + + + TimelineWindow.prototype.SetOnMoved = function(handler) + { + this.OnMovedHandler = handler; + } + + + TimelineWindow.prototype.WindowResized = function(x, width, top_window) + { + // Resize window + var top = top_window.Position[1] + top_window.Size[1] + 10; + this.Window.SetPosition(x, top); + this.Window.SetSize(width - 2 * 10, 260); + + ResizeInternals(this); + } + + + TimelineWindow.prototype.OnSamples = function(thread_name, frame_history) + { + // Shift the timeline to the last entry on this thread + var last_frame = frame_history[frame_history.length - 1]; + this.TimeRange.SetEnd(last_frame.EndTime_us); + + // Search for the index of this thread + var thread_index = -1; + for (var i in this.ThreadRows) + { + if (this.ThreadRows[i].Name == thread_name) + { + thread_index = i; + break; + } + } + + // If this thread has not been seen before, add a new row to the list + if (thread_index == -1) + { + var row = new TimelineRow(this.glCanvas.gl, thread_name, this, frame_history, this.CheckHandler); + this.ThreadRows.push(row); + + // Sort thread rows in the collection by name + this.ThreadRows.sort((a, b) => a.Name.localeCompare(b.Name)); + + // Resort the view by removing timeline row nodes from their DOM parents and re-adding + const thread_rows = new Array(); + for (let thread_row of this.ThreadRows) + { + this.TimelineLabels.Node.removeChild(thread_row.LabelContainerNode); + thread_rows.push(thread_row); + } + for (let thread_row of thread_rows) + { + this.TimelineLabels.Node.appendChild(thread_row.LabelContainerNode); + } + } + } + + + TimelineWindow.prototype.DrawBackground = function() + { + const gl = this.glCanvas.gl; + const program = this.glCanvas.timelineBackgroundProgram; + gl.useProgram(program); + + // Set viewport parameters + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + this.glCanvas.SetContainerUniforms(program, this.TimelineContainer.Node); + + // Set row parameters + const row_rect = this.TimelineLabels.Node.getBoundingClientRect(); + glSetUniform(gl, program, "inYOffset", row_rect.top); + + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + this.timelineMarkers.Draw(this.TimeRange); + } + + + TimelineWindow.prototype.Deselect = function(thread_name) + { + for (let thread_row of this.ThreadRows) + { + if (thread_name == thread_row.Name) + { + thread_row.SelectSampleInfo = null; + } + } + } + + + TimelineWindow.prototype.DrawSampleHighlights = function() + { + const gl = this.glCanvas.gl; + const program = this.glCanvas.timelineHighlightProgram; + gl.useProgram(program); + + // Set viewport parameters + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + // Set time range parameters + const time_range = this.TimeRange; + time_range.SetAsUniform(gl, program); + + for (let thread_row of this.ThreadRows) + { + // Draw highlight for hover row + if (this.HoverSampleInfo != null && this.HoverSampleInfo[3] == thread_row) + { + const frame = this.HoverSampleInfo[0]; + const offset = this.HoverSampleInfo[1]; + const depth = this.HoverSampleInfo[2]; + thread_row.DrawSampleHighlight(this.glCanvas, this.TimelineContainer.Node, frame, offset, depth, false); + } + + // Draw highlight for any samples selected on this row + if (thread_row.SelectSampleInfo != null) + { + const frame = thread_row.SelectSampleInfo[0]; + const offset = thread_row.SelectSampleInfo[1]; + const depth = thread_row.SelectSampleInfo[2]; + thread_row.DrawSampleHighlight(this.glCanvas, this.TimelineContainer.Node, frame, offset, depth, true); + } + } + } + + + TimelineWindow.prototype.DrawSampleGpuToCpu = function() + { + const gl = this.glCanvas.gl; + const program = this.glCanvas.timelineGpuToCpuProgram; + gl.useProgram(program); + + // Set viewport parameters + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + // Set time range parameters + const time_range = this.TimeRange; + time_range.SetAsUniform(gl, program); + + // Draw pointer for hover rows + for (let thread_row of this.ThreadRows) + { + if (this.HoverSampleInfo != null && this.HoverSampleInfo[3] == thread_row) + { + const frame = this.HoverSampleInfo[0]; + const offset = this.HoverSampleInfo[1]; + const depth = this.HoverSampleInfo[2]; + thread_row.DrawSampleGpuToCpu(this.glCanvas, this.TimelineContainer.Node, frame, offset, depth); + } + } + } + + + TimelineWindow.prototype.Draw = function() + { + this.DrawBackground(); + + const gl = this.glCanvas.gl; + const program = this.glCanvas.timelineProgram; + gl.useProgram(program); + + // Set viewport parameters + glSetUniform(gl, program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, program, "inViewport.height", gl.canvas.height); + + // Set time range parameters + const time_range = this.TimeRange; + time_range.SetAsUniform(gl, program); + + this.glCanvas.SetTextUniforms(program); + + for (let i in this.ThreadRows) + { + var thread_row = this.ThreadRows[i]; + thread_row.SetVisibleFrames(time_range); + thread_row.Draw(this.glCanvas, this.TimelineContainer.Node); + } + + this.DrawSampleHighlights(); + this.DrawSampleGpuToCpu(); + } + + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + // .TimelineRowLabel + // .TimelineRowExpand + // .TimelineRowExpand + // .TimelineRowCheck + // Window padding + let offset_x = 145+19+19+19+10; + + let MarkersHeight = 18; + + var parent_size = self.Window.Size; + + self.timelineMarkers.Resize(BORDER + offset_x, 10, parent_size[0] - 2* BORDER - offset_x, MarkersHeight); + + // Resize controls + self.TimelineContainer.SetPosition(BORDER + offset_x, 10 + MarkersHeight); + self.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER - offset_x, parent_size[1] - MarkersHeight - 40); + + self.TimelineLabelScrollClipper.SetPosition(10, 10 + MarkersHeight); + self.TimelineLabelScrollClipper.SetSize(offset_x, parent_size[1] - MarkersHeight - 40); + self.TimelineLabels.SetSize(offset_x, parent_size[1] - MarkersHeight - 40); + + // Adjust time range to new width + const width = self.TimelineContainer.Node.clientWidth; + self.TimeRange.SetPixelSpan(width); + } + + + function OnMouseScroll(self, mouse_state) + { + let scale = 1.11; + if (mouse_state.WheelDelta > 0) + scale = 1 / scale; + + // What time is the mouse hovering over? + let mouse_pos = self.TimelineMousePosition(mouse_state); + let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + + // Calculate start time relative to the mouse hover position + var time_start_us = self.TimeRange.Start_us - time_us; + + // Scale and offset back to the hover time + self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale); + + if (self.OnMovedHandler) + { + self.OnMovedHandler(self); + } + } + + + TimelineWindow.prototype.SetTimeRange = function(start_us, span_us) + { + this.TimeRange.Set(start_us, span_us); + } + + + TimelineWindow.prototype.DisplayHeight = function() + { + // Sum height of each thread row + let height = 0; + for (thread_row of this.ThreadRows) + { + height += thread_row.DisplayHeight(); + } + + return height; + } + + + TimelineWindow.prototype.MoveVertically = function(y_scroll) + { + // Calculate the minimum negative value the position of the labels can be to account for scrolling to the bottom + // of the label/depth list + let display_height = this.DisplayHeight(); + let container_height = this.TimelineLabelScrollClipper.Node.clientHeight; + let minimum_y = Math.min(container_height - display_height, 0.0); + + // Resize the label container to match the display height + this.TimelineLabels.Node.style.height = Math.max(display_height, container_height); + + // Increment the y-scroll using just-calculated limits + let old_y_scroll_offset = this.yScrollOffset; + this.yScrollOffset = Math.min(Math.max(this.yScrollOffset + y_scroll, minimum_y), 0); + + // Calculate how much the labels should actually scroll after limiting and apply + let y_scroll_px = this.yScrollOffset - old_y_scroll_offset; + this.TimelineLabels.Node.style.top = this.TimelineLabels.Node.offsetTop + y_scroll_px; + } + + + TimelineWindow.prototype.TimelineMousePosition = function(mouse_state) + { + // Position of the mouse relative to the timeline container + let node_offset = DOM.Node.GetPosition(this.TimelineContainer.Node); + let mouse_x = mouse_state.Position[0] - node_offset[0]; + let mouse_y = mouse_state.Position[1] - node_offset[1]; + + // Offset by the amount of scroll + mouse_y -= this.yScrollOffset; + + return [ mouse_x, mouse_y ]; + } + + + TimelineWindow.prototype.GetHoverThreadRow = function(mouse_pos) + { + // Search for the thread row the mouse intersects + let height = 0; + for (let thread_row of this.ThreadRows) + { + let row_height = thread_row.DisplayHeight(); + if (mouse_pos[1] >= height && mouse_pos[1] < height + row_height) + { + // Mouse y relative to row start + mouse_pos[1] -= height; + return thread_row; + } + height += row_height; + } + + return null; + } + + + function OnMouseClick(self, mouse_state) + { + // Are we hovering over a thread row? + const mouse_pos = self.TimelineMousePosition(mouse_state); + const hover_thread_row = self.GetHoverThreadRow(mouse_pos); + if (hover_thread_row != null) + { + // Are we hovering over a sample? + const time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + const sample_info = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]); + if (sample_info != null) + { + // Toggle deselect if this sample is already selected + if (hover_thread_row.SelectSampleInfo != null && + sample_info[0] == hover_thread_row.SelectSampleInfo[0] && sample_info[1] == hover_thread_row.SelectSampleInfo[1] && + sample_info[2] == hover_thread_row.SelectSampleInfo[2] && sample_info[3] == hover_thread_row.SelectSampleInfo[3]) + { + hover_thread_row.SetSelectSample(null); + self.OnSelectedHandler?.(hover_thread_row.Name, null); + } + + // Otherwise select + else + { + hover_thread_row.SetSelectSample(sample_info); + self.OnSelectedHandler?.(hover_thread_row.Name, sample_info); + } + } + + // Deselect if not hovering over a sample + else + { + self.OnSelectedHandler?.(hover_thread_row.Name, null); + } + } + } + + + function OnLabelMouseDown(self, evt) + { + // Deselect sample on this thread + const mouse_state = new Mouse.State(evt); + let mouse_pos = self.TimelineMousePosition(mouse_state); + const thread_row = self.GetHoverThreadRow(mouse_pos); + self.OnSelectedHandler?.(thread_row.Name, null); + } + + + function OnMouseMove(self, mouse_state, move_offset_x, move_offset_y) + { + // Shift the visible time range with mouse movement + const time_offset_us = move_offset_x / self.TimeRange.usPerPixel; + self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us); + + // Control vertical movement + self.MoveVertically(move_offset_y); + + // Notify + self.OnMovedHandler?.(self); + } + + + function OnMouseHover(self, mouse_state) + { + // Check for hover ending + if (mouse_state == null) + { + self.OnHoverHandler?.(self.lastHoverThreadName, null); + return; + } + + // Are we hovering over a thread row? + const mouse_pos = self.TimelineMousePosition(mouse_state); + const hover_thread_row = self.GetHoverThreadRow(mouse_pos); + if (hover_thread_row != null) + { + // Are we hovering over a sample? + const time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + self.HoverSampleInfo = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]); + + // Exit hover for the last hover row + self.OnHoverHandler?.(self.lastHoverThreadName, null); + self.lastHoverThreadName = hover_thread_row.Name; + + // Tell listeners which sample we're hovering over + self.OnHoverHandler?.(hover_thread_row.Name, self.HoverSampleInfo); + } + else + { + self.HoverSampleInfo = null; + } + } + + + return TimelineWindow; +})(); + diff --git a/profiler/vis/Code/TitleWindow.js b/profiler/vis/Code/TitleWindow.js new file mode 100644 index 0000000..01ac721 --- /dev/null +++ b/profiler/vis/Code/TitleWindow.js @@ -0,0 +1,105 @@ + +TitleWindow = (function() +{ + function TitleWindow(wm, settings, server, connection_address) + { + this.Settings = settings; + + this.Window = wm.AddWindow("     Remotery", 10, 10, 100, 100); + this.Window.ShowNoAnim(); + + this.PingContainer = this.Window.AddControlNew(new WM.Container(4, -13, 10, 10)); + DOM.Node.AddClass(this.PingContainer.Node, "PingContainer"); + + this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address", connection_address)); + + // Setup pause button + this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true })); + this.PauseButton.SetOnClick(Bind(OnPausePressed, this)); + + this.SyncButton = this.Window.AddControlNew(new WM.Button("Sync Timelines", 5, 5, { toggle: true})); + this.SyncButton.SetOnClick(Bind(OnSyncPressed, this)); + this.SyncButton.SetState(this.Settings.SyncTimelines); + + server.AddMessageHandler("PING", Bind(OnPing, this)); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + } + + + TitleWindow.prototype.SetConnectionAddressChanged = function(handler) + { + this.EditBox.SetChangeHandler(handler); + } + + + TitleWindow.prototype.WindowResized = function(width, height) + { + this.Window.SetSize(width - 2 * 10, 50); + ResizeInternals(this); + } + + TitleWindow.prototype.Pause = function() + { + if (!this.Settings.IsPaused) + { + this.PauseButton.SetText("Paused"); + this.PauseButton.SetState(true); + this.Settings.IsPaused = true; + } + } + + TitleWindow.prototype.Unpause = function() + { + if (this.Settings.IsPaused) + { + this.PauseButton.SetText("Pause"); + this.PauseButton.SetState(false); + this.Settings.IsPaused = false; + } + } + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + self.PauseButton.SetPosition(self.Window.Size[0] - 60, 5); + self.SyncButton.SetPosition(self.Window.Size[0] - 155, 5); + } + + + function OnPausePressed(self) + { + if (self.PauseButton.IsPressed()) + { + self.Pause(); + } + else + { + self.Unpause(); + } + } + + + function OnSyncPressed(self) + { + self.Settings.SyncTimelines = self.SyncButton.IsPressed(); + } + + + function OnPing(self, server) + { + // Set the ping container as active and take it off half a second later + DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive"); + window.setTimeout(Bind(function(self) + { + DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive"); + }, self), 500); + } + + + return TitleWindow; +})(); \ No newline at end of file diff --git a/profiler/vis/Code/TraceDrop.js b/profiler/vis/Code/TraceDrop.js new file mode 100644 index 0000000..d33c323 --- /dev/null +++ b/profiler/vis/Code/TraceDrop.js @@ -0,0 +1,147 @@ + +class TraceDrop +{ + constructor(remotery) + { + this.Remotery = remotery; + + // Create a full-page overlay div for dropping files onto + this.DropNode = DOM.Node.CreateHTML("
Load Remotery Trace
"); + document.body.appendChild(this.DropNode); + + // Attach drop handlers + window.addEventListener("dragenter", () => this.ShowDropZone()); + this.DropNode.addEventListener("dragenter", (e) => this.AllowDrag(e)); + this.DropNode.addEventListener("dragover", (e) => this.AllowDrag(e)); + this.DropNode.addEventListener("dragleave", () => this.HideDropZone()); + this.DropNode.addEventListener("drop", (e) => this.OnDrop(e)); + } + + ShowDropZone() + { + this.DropNode.style.display = "flex"; + } + + HideDropZone() + { + this.DropNode.style.display = "none"; + } + + AllowDrag(evt) + { + // Prevent the default drag handler kicking in + evt.preventDefault(); + + evt.dataTransfer.dropEffect = "copy"; + } + + OnDrop(evt) + { + // Prevent the default drop handler kicking in + evt.preventDefault(); + + this.HideDropZone(evt); + + // Get the file that was dropped + let files = DOM.Event.GetDropFiles(evt); + if (files.length == 0) + { + alert("No files dropped"); + return; + } + if (files.length > 1) + { + alert("Too many files dropped"); + return; + } + + // Check file type + let file = files[0]; + if (!file.name.endsWith(".rbin")) + { + alert("Not the correct .rbin file type"); + return; + } + + // Background-load the file + var remotery = this.Remotery; + let file_reader = new FileReader(); + file_reader.onload = function() + { + // Create the data reader and verify the header + let data_view = new DataView(this.result); + let data_view_reader = new DataViewReader(data_view, 0); + let header = data_view_reader.GetStringOfLength(8); + if (header != "RMTBLOGF") + { + alert("Not a valid Remotery Log File"); + return; + } + + remotery.Clear(); + + try + { + // Forward all recorded events to message handlers + while (!data_view_reader.AtEnd()) + { + const start_offset = data_view_reader.Offset; + const [id, length ] = remotery.Server.CallMessageHandlers(data_view_reader, this.Result); + data_view_reader.Offset = start_offset + length; + } + } + catch (e) + { + // The last message may be partially written due to process exit + // Catch this safely as it's a valid state for the file to be in + if (e instanceof RangeError) + { + console.log("Aborted reading last message"); + } + } + + // After loading completes, populate the UI which wasn't updated during loading + + remotery.Console.TriggerUpdate(); + + // Set frame history for each timeline thread + for (let name in remotery.FrameHistory) + { + let frame_history = remotery.FrameHistory[name]; + remotery.SampleTimelineWindow.OnSamples(name, frame_history); + } + + // Set frame history for each processor + for (let name in remotery.ProcessorFrameHistory) + { + let frame_history = remotery.ProcessorFrameHistory[name]; + remotery.ProcessorTimelineWindow.OnSamples(name, frame_history); + } + + // Set the last frame values for each grid window + for (let name in remotery.gridWindows) + { + const grid_window = remotery.gridWindows[name]; + + const frame_history = remotery.FrameHistory[name]; + if (frame_history) + { + // This is a sample window + const frame = frame_history[frame_history.length - 1]; + grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats); + } + else + { + // This is a property window + const frame_history = remotery.PropertyFrameHistory; + const frame = frame_history[frame_history.length - 1]; + grid_window.UpdateEntries(frame.nbSnapshots, frame.snapshotFloats); + } + } + + // Pause for viewing + remotery.TitleWindow.Pause(); + }; + file_reader.readAsArrayBuffer(file); + } +} \ No newline at end of file diff --git a/profiler/vis/Code/WebGL.js b/profiler/vis/Code/WebGL.js new file mode 100644 index 0000000..1d90b6f --- /dev/null +++ b/profiler/vis/Code/WebGL.js @@ -0,0 +1,252 @@ + +function assert(condition, message) +{ + if (!condition) + { + throw new Error(message || "Assertion failed"); + } +} + +function glCompileShader(gl, type, name, source) +{ + console.log("Compiling " + name); + + // Compile the shader + let shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + // Report any errors + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) + { + console.log("Error compiling " + name); + console.log(gl.getShaderInfoLog(shader)); + console.trace(); + } + + return shader; +} + +function glCreateProgram(gl, vshader, fshader) +{ + // Attach shaders and link + let program = gl.createProgram(); + gl.attachShader(program, vshader); + gl.attachShader(program, fshader); + gl.linkProgram(program); + + // Report any errors + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) + { + console.log("Failed to link program"); + console.trace(); + } + + return program; +} + +function glCreateProgramFromSource(gl, vshader_name, vshader_source, fshader_name, fshader_source) +{ + const vshader = glCompileShader(gl, gl.VERTEX_SHADER, vshader_name, vshader_source); + const fshader = glCompileShader(gl, gl.FRAGMENT_SHADER, fshader_name, fshader_source); + return glCreateProgram(gl, vshader, fshader); +} + +function glSetUniform(gl, program, name, value, index) +{ + // Get location + const location = gl.getUniformLocation(program, name); + assert(location != null, "Can't find uniform " + name); + + // Dispatch to uniform function by type + assert(value != null, "Value is null"); + const type = Object.prototype.toString.call(value).slice(8, -1); + switch (type) + { + case "Number": + gl.uniform1f(location, value); + break; + + case "WebGLTexture": + gl.activeTexture(gl.TEXTURE0 + index); + gl.bindTexture(gl.TEXTURE_2D, value); + gl.uniform1i(location, index); + break; + + default: + assert(false, "Unhandled type " + type); + break; + } +} + +function glCreateTexture(gl, width, height, data) +{ + const texture = gl.createTexture(); + + // Set filtering/wrapping to nearest/clamp + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); + + return texture; +} + +const glDynamicBufferType = Object.freeze({ + Buffer: 1, + Texture: 2 +}); + +class glDynamicBuffer +{ + constructor(gl, buffer_type, element_type, nb_elements, nb_entries = 1) + { + this.gl = gl; + this.elementType = element_type; + this.nbElements = nb_elements; + this.bufferType = buffer_type; + this.dirty = false; + + this.Resize(nb_entries); + } + + BindAsInstanceAttribute(program, attrib_name) + { + assert(this.bufferType == glDynamicBufferType.Buffer, "Can only call BindAsInstanceAttribute with Buffer types"); + + let gl = this.gl; + + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + + // The attribute referenced in the program + const attrib_location = gl.getAttribLocation(program, attrib_name); + + gl.enableVertexAttribArray(attrib_location); + gl.vertexAttribPointer(attrib_location, this.nbElements, this.elementType, false, 0, 0); + + // One per instance + gl.vertexAttribDivisor(attrib_location, 1); + } + + UploadData() + { + let gl = this.gl; + + switch (this.bufferType) + { + case glDynamicBufferType.Buffer: + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.cpuArray); + break; + + case glDynamicBufferType.Texture: + assert(this.elementType == gl.UNSIGNED_BYTE || this.elementType == gl.FLOAT); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + + // Very limited map from internal type to texture type + let internal_format, format, type; + if (this.elementType == gl.UNSIGNED_BYTE) + { + internal_format = this.nbElements == 1 ? gl.ALPHA : gl.RGBA8; + format = this.nbElements == 1 ? gl.ALPHA : gl.RGBA; + type = gl.UNSIGNED_BYTE; + } + else if (this.elementType == gl.FLOAT) + { + internal_format = this.nbElements == 1 ? gl.R32F : RGBA32F; + format = this.nbElements == 1 ? gl.RED : gl.RGBA; + type = gl.FLOAT; + } + + gl.texImage2D(gl.TEXTURE_2D, 0, internal_format, this.nbEntries, 1, 0, format, type, this.cpuArray); + break; + } + } + + UploadDirtyData() + { + if (this.dirty) + { + this.UploadData(); + this.dirty = false; + } + } + + ResizeToFitNextPow2(target_count) + { + let nb_entries = this.nbEntries; + while (target_count > nb_entries) + { + nb_entries <<= 1; + } + + if (nb_entries > this.nbEntries) + { + this.Resize(nb_entries); + } + } + + Resize(nb_entries) + { + this.nbEntries = nb_entries; + + let gl = this.gl; + + // Create the CPU array + const old_array = this.cpuArray; + switch (this.elementType) + { + case gl.FLOAT: + this.nbElementBytes = 4; + this.cpuArray = new Float32Array(this.nbElements * this.nbEntries); + break; + + case gl.UNSIGNED_BYTE: + this.nbElementBytes = 1; + this.cpuArray = new Uint8Array(this.nbElements * this.nbEntries); + break; + + default: + assert(false, "Unsupported dynamic buffer element type"); + } + + // Calculate byte size of the buffer + this.nbBytes = this.nbElementBytes * this.nbElements * this.nbEntries; + + if (old_array != undefined) + { + // Copy the values of the previous array over + this.cpuArray.set(old_array); + } + + // Create the GPU buffer + switch (this.bufferType) + { + case glDynamicBufferType.Buffer: + this.buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, this.nbBytes, gl.DYNAMIC_DRAW); + break; + + case glDynamicBufferType.Texture: + this.texture = gl.createTexture(); + + // Point sampling with clamp for indexing + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + break; + + default: + assert(false, "Unsupported dynamic buffer type"); + } + + this.UploadData(); + } +}; + diff --git a/profiler/vis/Code/WebGLFont.js b/profiler/vis/Code/WebGLFont.js new file mode 100644 index 0000000..be4b897 --- /dev/null +++ b/profiler/vis/Code/WebGLFont.js @@ -0,0 +1,125 @@ + +class glFont +{ + constructor(gl) + { + // Offscreen canvas for rendering individual characters + this.charCanvas = document.createElement("canvas"); + this.charContext = this.charCanvas.getContext("2d"); + + // Describe the font + const font_size = 9; + this.fontWidth = 5; + this.fontHeight = 13; + const font_face = "LocalFiraCode"; + const font_desc = font_size + "px " + font_face; + + // Ensure the CSS font is loaded before we do any work with it + const self = this; + document.fonts.load(font_desc).then(function (){ + + // Create a canvas atlas for all characters in the font + const atlas_canvas = document.createElement("canvas"); + const atlas_context = atlas_canvas.getContext("2d"); + atlas_canvas.width = 16 * self.fontWidth; + atlas_canvas.height = 16 * self.fontHeight; + + // Add each character to the atlas + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-+=[]{};\'~#,./<>?!\"£$%%^&*()"; + for (let char of chars) + { + // Render this character to the canvas on its own + self.RenderTextToCanvas(char, font_desc, self.fontWidth, self.fontHeight); + + // Calculate a location for it in the atlas using its ASCII code + const ascii_code = char.charCodeAt(0); + assert(ascii_code < 256); + const y_index = Math.floor(ascii_code / 16); + const x_index = ascii_code - y_index * 16 + assert(x_index < 16); + assert(y_index < 16); + + // Copy into the atlas + atlas_context.drawImage(self.charCanvas, x_index * self.fontWidth, y_index * self.fontHeight); + } + + // Create the atlas texture and store it in the destination object + self.atlasTexture = glCreateTexture(gl, atlas_canvas.width, atlas_canvas.height, atlas_canvas); + }); + } + + RenderTextToCanvas(text, font, width, height) + { + // Resize canvas to match + this.charCanvas.width = width; + this.charCanvas.height = height; + + // Clear the background + this.charContext.fillStyle = "black"; + this.charContext.clearRect(0, 0, width, height); + + // TODO(don): I don't know why this results in the crispest text! + // Every pattern I've checked so far has thrown up no ideas... but it works, so it will do for now + let offset = 0.25; + if ("AFILMTWijmw4+{};\'#,.?!\"£*()".includes(text)) + { + offset = 0.0; + } + + // Render the text + this.charContext.font = font; + this.charContext.textAlign = "left"; + this.charContext.textBaseline = "top"; + this.charContext.fillText(text, offset, 2.5); + } +} + +class glTextBuffer +{ + constructor(gl, font) + { + this.font = font; + this.textMap = {}; + this.textBuffer = new glDynamicBuffer(gl, glDynamicBufferType.Texture, gl.UNSIGNED_BYTE, 1, 8); + this.textBufferPos = 0; + this.textEncoder = new TextEncoder(); + } + + AddText(text) + { + // Return if it already exists + const existing_entry = this.textMap[text]; + if (existing_entry != undefined) + { + return existing_entry; + } + + // Add to the map + // Note we're leaving an extra NULL character before every piece of text so that the shader can sample into it on text + // boundaries and sample a zero colour for clamp. + let entry = { + offset: this.textBufferPos + 1, + length: text.length, + }; + this.textMap[text] = entry; + + // Ensure there's always enough space in the text buffer before adding + this.textBuffer.ResizeToFitNextPow2(entry.offset + entry.length + 1); + this.textBuffer.cpuArray.set(this.textEncoder.encode(text), entry.offset, entry.length); + this.textBuffer.dirty = true; + this.textBufferPos = entry.offset + entry.length; + + return entry; + } + + UploadData() + { + this.textBuffer.UploadDirtyData(); + } + + SetAsUniform(gl, program, name, index) + { + glSetUniform(gl, program, name, this.textBuffer.texture, index); + glSetUniform(gl, program, "inTextBufferDesc.textBufferLength", this.textBuffer.nbEntries); + } +} \ No newline at end of file diff --git a/profiler/vis/Code/WebSocketConnection.js b/profiler/vis/Code/WebSocketConnection.js new file mode 100644 index 0000000..261fb17 --- /dev/null +++ b/profiler/vis/Code/WebSocketConnection.js @@ -0,0 +1,149 @@ + +WebSocketConnection = (function() +{ + function WebSocketConnection() + { + this.MessageHandlers = { }; + this.Socket = null; + this.Console = null; + } + + + WebSocketConnection.prototype.SetConsole = function(console) + { + this.Console = console; + } + + + WebSocketConnection.prototype.Connecting = function() + { + return this.Socket != null && this.Socket.readyState == WebSocket.CONNECTING; + } + + WebSocketConnection.prototype.Connected = function() + { + return this.Socket != null && this.Socket.readyState == WebSocket.OPEN; + } + + + WebSocketConnection.prototype.AddConnectHandler = function(handler) + { + this.AddMessageHandler("__OnConnect__", handler); + } + + + WebSocketConnection.prototype.AddDisconnectHandler = function(handler) + { + this.AddMessageHandler("__OnDisconnect__", handler); + } + + + WebSocketConnection.prototype.AddMessageHandler = function(message_name, handler) + { + // Create the message handler array on-demand + if (!(message_name in this.MessageHandlers)) + this.MessageHandlers[message_name] = [ ]; + this.MessageHandlers[message_name].push(handler); + } + + + WebSocketConnection.prototype.Connect = function(address) + { + // Abandon previous connection attempt + this.Disconnect(); + + Log(this, "Connecting to " + address); + + this.Socket = new WebSocket(address); + this.Socket.binaryType = "arraybuffer"; + this.Socket.onopen = Bind(OnOpen, this); + this.Socket.onmessage = Bind(OnMessage, this); + this.Socket.onclose = Bind(OnClose, this); + this.Socket.onerror = Bind(OnError, this); + } + + + WebSocketConnection.prototype.Disconnect = function() + { + Log(this, "Disconnecting"); + if (this.Socket != null) + { + this.Socket.close(); + this.Socket = null; + } + } + + + WebSocketConnection.prototype.Send = function(msg) + { + if (this.Connected()) + this.Socket.send(msg); + } + + + function Log(self, message) + { + self.Console.Log(message); + } + + + function CallMessageHandlers(self, message_name, data_view, length) + { + if (message_name in self.MessageHandlers) + { + var handlers = self.MessageHandlers[message_name]; + for (var i in handlers) + handlers[i](self, data_view, length); + } + } + + + function OnOpen(self, event) + { + Log(self, "Connected"); + CallMessageHandlers(self, "__OnConnect__"); + } + + + function OnClose(self, event) + { + // Clear all references + self.Socket.onopen = null; + self.Socket.onmessage = null; + self.Socket.onclose = null; + self.Socket.onerror = null; + self.Socket = null; + + Log(self, "Disconnected"); + CallMessageHandlers(self, "__OnDisconnect__"); + } + + + function OnError(self, event) + { + Log(self, "Connection Error "); + } + + + function OnMessage(self, event) + { + let data_view = new DataView(event.data); + let data_view_reader = new DataViewReader(data_view, 0); + self.CallMessageHandlers(data_view_reader); + } + + WebSocketConnection.prototype.CallMessageHandlers = function(data_view_reader) + { + // Decode standard message header + const id = data_view_reader.GetStringOfLength(4); + const length = data_view_reader.GetUInt32(); + + // Pass the length of the message left to parse + CallMessageHandlers(this, id, data_view_reader, length - 8); + + return [ id, length ]; + } + + + return WebSocketConnection; +})(); diff --git a/profiler/vis/Code/tsconfig.json b/profiler/vis/Code/tsconfig.json new file mode 100644 index 0000000..029304c --- /dev/null +++ b/profiler/vis/Code/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outFile": "out.js", + "allowJs": true, + "sourceMap": true + }, + "include": ["ThreadFrame.js"] +} \ No newline at end of file diff --git a/profiler/vis/Styles/Fonts/FiraCode/LICENSE b/profiler/vis/Styles/Fonts/FiraCode/LICENSE new file mode 100644 index 0000000..8e9c277 --- /dev/null +++ b/profiler/vis/Styles/Fonts/FiraCode/LICENSE @@ -0,0 +1,93 @@ +Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/profiler/vis/Styles/Remotery.css b/profiler/vis/Styles/Remotery.css new file mode 100644 index 0000000..79df692 --- /dev/null +++ b/profiler/vis/Styles/Remotery.css @@ -0,0 +1,237 @@ + +body +{ + /* Take up the full page */ + width: 100%; + height: 100%; + margin: 0px; + + background-color: #999; + + touch-action: none; +} + + +/* Override default container style to remove 3D effect */ +.Container +{ + border: none; + box-shadow: none; +} + + +/* Override default edit box style to remove 3D effect */ +.EditBox +{ + border: none; + box-shadow: none; + width:200; +} + + +@font-face +{ + font-family: "LocalFiraCode"; + src:url("Fonts/FiraCode/FiraCode-Regular.ttf"); +} + +.ConsoleText +{ + overflow:auto; + color: #BBB; + font: 10px LocalFiraCode; + margin: 3px; + white-space: pre; + line-height:14px; +} + + +.PingContainer +{ + background-color: #F55; + border-radius: 2px; + + /* Transition from green is gradual */ + transition: background-color 0.25s ease-in; +} + + +.PingContainerActive +{ + background-color: #5F5; + + /* Transition to green is instant */ + transition: none; +} + + +.GridNameHeader +{ + position: absolute; + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-align: center; + + background:rgb(48, 48, 48); + + color: #BBB; + font: 9px Verdana; + + padding: 1px 1px 1px 2px; + border: 1px solid; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; +} + +.TimelineBox +{ + /* Following style generally copies GridRowCell.GridGroup from BrowserLib */ + + padding: 1px 1px 1px 2px; + margin: 1px; + + border: 1px solid; + border-radius: 2px; + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; + + font: 9px Verdana; + color: #BBB; +} +.TimelineRow +{ + width: 100%; +} +.TimelineRowCheckbox +{ + width: 12px; + height: 12px; + margin: 0px; +} +.TimelineRowCheck +{ + /* Pull .TimelineRowExpand to the right of the checkbox */ + float:left; + + width: 14px; + height: 14px; +} +.TimelineRowExpand +{ + /* Pull .TimelineRowLabel to the right of +/- buttons */ + float:left; + + width: 14px; + height: 14px; +} +.TimelineRowExpandButton +{ + width: 11px; + height: 12px; + + color: #333; + + border: 1px solid; + + border-top-color:#F4F4F4; + border-left-color:#F4F4F4; + border-bottom-color:#8E8F8F; + border-right-color:#8E8F8F; + + /* Top-right to bottom-left grey background gradient */ + background: #f6f6f6; /* Old browsers */ + background: -moz-linear-gradient(-45deg, #f6f6f6 0%, #abaeb2 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,#f6f6f6), color-stop(100%,#abaeb2)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* IE10+ */ + background: linear-gradient(135deg, #f6f6f6 0%,#abaeb2 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f6f6f6', endColorstr='#abaeb2',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ + + text-align: center; + vertical-align: center; +} +.TimelineRowExpandButton:hover +{ + border-top-color:#79C6F9; + border-left-color:#79C6F9; + border-bottom-color:#385D72; + border-right-color:#385D72; + + /* Top-right to bottom-left blue background gradient, matching border */ + background: #f3f3f3; /* Old browsers */ + background: -moz-linear-gradient(-45deg, #f3f3f3 0%, #79c6f9 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,#f3f3f3), color-stop(100%,#79c6f9)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* IE10+ */ + background: linear-gradient(135deg, #f3f3f3 0%,#79c6f9 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f3f3f3', endColorstr='#79c6f9',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ +} +.TimelineRowExpandButtonActive +{ + /* Simple means of shifting text within a div to the bottom-right */ + padding-left:1px; + padding-top:1px; + width:10px; + height:11px; +} +.TimelineRowLabel +{ + float:left; + + width: 140px; + height: 14px; +} + +.TimelineContainer +{ +} +.TimelineLabels +{ + padding: 0; + margin: 0; + border: 0; + overflow-y: hidden; +} +.TimelineLabelScrollClipper +{ + padding: 0; + margin: 0; + border: 0; + overflow-y: hidden; +} + +.DropZone +{ + /* Covers the whole page, initially hidden */ + box-sizing: border-box; + display: none; + position: fixed; + width: 100%; + height: 100%; + left: 0; + top: 0; + + /* On top of everything possible */ + z-index: 99999; + + /* Styling for when visible */ + background: rgba(32, 4, 136, 0.25); + border: 3px dashed white; + + /* Styling for text when visible */ + color: white; + font-family: Arial, Helvetica, sans-serif; + font-size: xx-large; + align-items: center; + justify-content: center; +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Animation.js b/profiler/vis/extern/BrowserLib/Core/Code/Animation.js new file mode 100644 index 0000000..67e9332 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Animation.js @@ -0,0 +1,65 @@ + +// +// Very basic linear value animation system, for now. +// + + +namespace("Anim"); + + +Anim.Animation = (function() +{ + var anim_hz = 60; + + + function Animation(anim_func, start_value, end_value, time, end_callback) + { + // Setup initial parameters + this.StartValue = start_value; + this.EndValue = end_value; + this.ValueInc = (end_value - start_value) / (time * anim_hz); + this.Value = start_value; + this.Complete = false; + this.EndCallback = end_callback; + + // Cache the update function to prevent recreating the closure + var self = this; + this.AnimFunc = anim_func; + this.AnimUpdate = function() { Update(self); } + + // Call for the start value + this.AnimUpdate(); + } + + + function Update(self) + { + // Queue up the next frame immediately + var id = window.setTimeout(self.AnimUpdate, 1000 / anim_hz); + + // Linear step the value and check for completion + self.Value += self.ValueInc; + if (Math.abs(self.Value - self.EndValue) < 0.01) + { + self.Value = self.EndValue; + self.Complete = true; + + if (self.EndCallback) + self.EndCallback(); + + window.clearTimeout(id); + } + + // Pass to the animation function + self.AnimFunc(self.Value); + } + + + return Animation; +})(); + + +Anim.Animate = function(anim_func, start_value, end_value, time, end_callback) +{ + return new Anim.Animation(anim_func, start_value, end_value, time, end_callback); +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Bind.js b/profiler/vis/extern/BrowserLib/Core/Code/Bind.js new file mode 100644 index 0000000..23c15e9 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Bind.js @@ -0,0 +1,92 @@ +// +// This will generate a closure for the given function and optionally bind an arbitrary number of +// its initial arguments to specific values. +// +// Parameters: +// +// 0: Either the function scope or the function. +// 1: If 0 is the function scope, this is the function. +// Otherwise it's the start of the optional bound argument list. +// 2: Start of the optional bound argument list if 1 is the function. +// +// Examples: +// +// function GlobalFunction(p0, p1, p2) { } +// function ThisFunction(p0, p1, p2) { } +// +// var a = Bind("GlobalFunction"); +// var b = Bind(this, "ThisFunction"); +// var c = Bind("GlobalFunction", BoundParam0, BoundParam1); +// var d = Bind(this, "ThisFunction", BoundParam0, BoundParam1); +// var e = Bind(GlobalFunction); +// var f = Bind(this, ThisFunction); +// var g = Bind(GlobalFunction, BoundParam0, BoundParam1); +// var h = Bind(this, ThisFunction, BoundParam0, BoundParam1); +// +// a(0, 1, 2); +// b(0, 1, 2); +// c(2); +// d(2); +// e(0, 1, 2); +// f(0, 1, 2); +// g(2); +// h(2); +// +function Bind() +{ + // No closure to define? + if (arguments.length == 0) + return null; + + // Figure out which of the 4 call types is being used to bind + // Locate scope, function and bound parameter start index + + if (typeof(arguments[0]) == "string") + { + var scope = window; + var func = window[arguments[0]]; + var start = 1; + } + + else if (typeof(arguments[0]) == "function") + { + var scope = window; + var func = arguments[0]; + var start = 1; + } + + else if (typeof(arguments[1]) == "string") + { + var scope = arguments[0]; + var func = scope[arguments[1]]; + var start = 2; + } + + else if (typeof(arguments[1]) == "function") + { + var scope = arguments[0]; + var func = arguments[1]; + var start = 2; + } + + else + { + // unknown + console.log("Bind() ERROR: Unknown bind parameter configuration"); + return; + } + + // Convert the arguments list to an array + var arg_array = Array.prototype.slice.call(arguments, start); + start = arg_array.length; + + return function() + { + // Concatenate incoming arguments + for (var i = 0; i < arguments.length; i++) + arg_array[start + i] = arguments[i]; + + // Call the function in the given scope with the new arguments + return func.apply(scope, arg_array); + } +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Convert.js b/profiler/vis/extern/BrowserLib/Core/Code/Convert.js new file mode 100644 index 0000000..0fa7ff4 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Convert.js @@ -0,0 +1,218 @@ + +namespace("Convert"); + + +// +// Convert between utf8 and b64 without raising character out of range exceptions with unicode strings +// Technique described here: http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html +// +Convert.utf8string_to_b64string = function(str) +{ + return btoa(unescape(encodeURIComponent(str))); +} +Convert.b64string_to_utf8string = function(str) +{ + return decodeURIComponent(escape(atob(str))); +} + + +// +// More general approach, converting between byte arrays and b64 +// Info here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding +// +Convert.b64string_to_Uint8Array = function(sBase64, nBlocksSize) +{ + function b64ToUint6 (nChr) + { + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + } + + var + sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), + nInLen = sB64Enc.length, + nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, + taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) + { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) + { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + nUint24 = 0; + } + } + + return taBytes; +} +Convert.Uint8Array_to_b64string = function(aBytes) +{ + function uint6ToB64 (nUint6) + { + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; + } + + var nMod3, sB64Enc = ""; + + for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) + { + nMod3 = nIdx % 3; + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) + sB64Enc += "\r\n"; + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) + { + sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + + return sB64Enc.replace(/A(?=A$|$)/g, "="); +} + + +// +// Unicode and arbitrary value safe conversion between strings and Uint8Arrays +// +Convert.Uint8Array_to_string = function(aBytes) +{ + var sView = ""; + + for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) + { + nPart = aBytes[nIdx]; + sView += String.fromCharCode( + nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */ + /* (nPart - 252 << 32) is not possible in ECMAScript! So...: */ + (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */ + (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */ + (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */ + (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */ + (nPart - 192 << 6) + aBytes[++nIdx] - 128 + : /* nPart < 127 ? */ /* one byte */ + nPart + ); + } + + return sView; +} +Convert.string_to_Uint8Array = function(sDOMStr) +{ + var aBytes, nChr, nStrLen = sDOMStr.length, nArrLen = 0; + + /* mapping... */ + + for (var nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) + { + nChr = sDOMStr.charCodeAt(nMapIdx); + nArrLen += nChr < 0x80 ? 1 : nChr < 0x800 ? 2 : nChr < 0x10000 ? 3 : nChr < 0x200000 ? 4 : nChr < 0x4000000 ? 5 : 6; + } + + aBytes = new Uint8Array(nArrLen); + + /* transcription... */ + + for (var nIdx = 0, nChrIdx = 0; nIdx < nArrLen; nChrIdx++) + { + nChr = sDOMStr.charCodeAt(nChrIdx); + if (nChr < 128) + { + /* one byte */ + aBytes[nIdx++] = nChr; + } + else if (nChr < 0x800) + { + /* two bytes */ + aBytes[nIdx++] = 192 + (nChr >>> 6); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x10000) + { + /* three bytes */ + aBytes[nIdx++] = 224 + (nChr >>> 12); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x200000) + { + /* four bytes */ + aBytes[nIdx++] = 240 + (nChr >>> 18); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x4000000) + { + /* five bytes */ + aBytes[nIdx++] = 248 + (nChr >>> 24); + aBytes[nIdx++] = 128 + (nChr >>> 18 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else /* if (nChr <= 0x7fffffff) */ + { + /* six bytes */ + aBytes[nIdx++] = 252 + /* (nChr >>> 32) is not possible in ECMAScript! So...: */ (nChr / 1073741824); + aBytes[nIdx++] = 128 + (nChr >>> 24 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 18 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + } + + return aBytes; +} + + +// +// Converts all characters in a string that have equivalent entities to their ampersand/entity names. +// Based on https://gist.github.com/jonathantneal/6093551 +// +Convert.string_to_html_entities = (function() +{ + 'use strict'; + + var data = '34quot38amp39apos60lt62gt160nbsp161iexcl162cent163pound164curren165yen166brvbar167sect168uml169copy170ordf171laquo172not173shy174reg175macr176deg177plusmn178sup2179sup3180acute181micro182para183middot184cedil185sup1186ordm187raquo188frac14189frac12190frac34191iquest192Agrave193Aacute194Acirc195Atilde196Auml197Aring198AElig199Ccedil200Egrave201Eacute202Ecirc203Euml204Igrave205Iacute206Icirc207Iuml208ETH209Ntilde210Ograve211Oacute212Ocirc213Otilde214Ouml215times216Oslash217Ugrave218Uacute219Ucirc220Uuml221Yacute222THORN223szlig224agrave225aacute226acirc227atilde228auml229aring230aelig231ccedil232egrave233eacute234ecirc235euml236igrave237iacute238icirc239iuml240eth241ntilde242ograve243oacute244ocirc245otilde246ouml247divide248oslash249ugrave250uacute251ucirc252uuml253yacute254thorn255yuml402fnof913Alpha914Beta915Gamma916Delta917Epsilon918Zeta919Eta920Theta921Iota922Kappa923Lambda924Mu925Nu926Xi927Omicron928Pi929Rho931Sigma932Tau933Upsilon934Phi935Chi936Psi937Omega945alpha946beta947gamma948delta949epsilon950zeta951eta952theta953iota954kappa955lambda956mu957nu958xi959omicron960pi961rho962sigmaf963sigma964tau965upsilon966phi967chi968psi969omega977thetasym978upsih982piv8226bull8230hellip8242prime8243Prime8254oline8260frasl8472weierp8465image8476real8482trade8501alefsym8592larr8593uarr8594rarr8595darr8596harr8629crarr8656lArr8657uArr8658rArr8659dArr8660hArr8704forall8706part8707exist8709empty8711nabla8712isin8713notin8715ni8719prod8721sum8722minus8727lowast8730radic8733prop8734infin8736ang8743and8744or8745cap8746cup8747int8756there48764sim8773cong8776asymp8800ne8801equiv8804le8805ge8834sub8835sup8836nsub8838sube8839supe8853oplus8855otimes8869perp8901sdot8968lceil8969rceil8970lfloor8971rfloor9001lang9002rang9674loz9824spades9827clubs9829hearts9830diams338OElig339oelig352Scaron353scaron376Yuml710circ732tilde8194ensp8195emsp8201thinsp8204zwnj8205zwj8206lrm8207rlm8211ndash8212mdash8216lsquo8217rsquo8218sbquo8220ldquo8221rdquo8222bdquo8224dagger8225Dagger8240permil8249lsaquo8250rsaquo8364euro'; + var charCodes = data.split(/[A-z]+/); + var entities = data.split(/\d+/).slice(1); + + return function encodeHTMLEntities(text) + { + return text.replace(/[\u00A0-\u2666<>"'&]/g, function (match) + { + var charCode = String(match.charCodeAt(0)); + var index = charCodes.indexOf(charCode); + return '&' + (entities[index] ? entities[index] : '#' + charCode) + ';'; + }); + }; +})(); diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Core.js b/profiler/vis/extern/BrowserLib/Core/Code/Core.js new file mode 100644 index 0000000..4ebcb0d --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Core.js @@ -0,0 +1,26 @@ + +// TODO: requires function for checking existence of dependencies + + +function namespace(name) +{ + // Ensure all nested namespaces are created only once + + var ns_list = name.split("."); + var parent_ns = window; + + for (var i in ns_list) + { + var ns_name = ns_list[i]; + if (!(ns_name in parent_ns)) + parent_ns[ns_name] = { }; + + parent_ns = parent_ns[ns_name]; + } +} + + +function multiline(fn) +{ + return fn.toString().split(/\n/).slice(1, -1).join("\n"); +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/DOM.js b/profiler/vis/extern/BrowserLib/Core/Code/DOM.js new file mode 100644 index 0000000..963ce3f --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/DOM.js @@ -0,0 +1,526 @@ + +namespace("DOM.Node"); +namespace("DOM.Event"); +namespace("DOM.Applet"); + + + +// +// ===================================================================================================================== +// ----- DOCUMENT NODE/ELEMENT EXTENSIONS ------------------------------------------------------------------------------ +// ===================================================================================================================== +// + + + +DOM.Node.Get = function(id) +{ + return document.getElementById(id); +} + + +// +// Set node position +// +DOM.Node.SetPosition = function(node, position) +{ + node.style.left = position[0]; + node.style.top = position[1]; +} +DOM.Node.SetX = function(node, x) +{ + node.style.left = x; +} +DOM.Node.SetY = function(node, y) +{ + node.style.top = y; +} + + +// +// Get the absolute position of a HTML element on the page +// +DOM.Node.GetPosition = function(element, account_for_scroll) +{ + // Recurse up through parents, summing offsets from their parent + var x = 0, y = 0; + for (var node = element; node != null; node = node.offsetParent) + { + x += node.offsetLeft; + y += node.offsetTop; + } + + if (account_for_scroll) + { + // Walk up the hierarchy subtracting away any scrolling + for (var node = element; node != document.body; node = node.parentNode) + { + x -= node.scrollLeft; + y -= node.scrollTop; + } + } + + return [x, y]; +} + + +// +// Set node size +// +DOM.Node.SetSize = function(node, size) +{ + node.style.width = size[0]; + node.style.height = size[1]; +} +DOM.Node.SetWidth = function(node, width) +{ + node.style.width = width; +} +DOM.Node.SetHeight = function(node, height) +{ + node.style.height = height; +} + + +// +// Get node OFFSET size: +// clientX includes padding +// offsetX includes padding and borders +// scrollX includes padding, borders and size of contained node +// +DOM.Node.GetSize = function(node) +{ + return [ node.offsetWidth, node.offsetHeight ]; +} +DOM.Node.GetWidth = function(node) +{ + return node.offsetWidth; +} +DOM.Node.GetHeight = function(node) +{ + return node.offsetHeight; +} + + +// +// Set node opacity +// +DOM.Node.SetOpacity = function(node, value) +{ + node.style.opacity = value; +} + + +DOM.Node.SetColour = function(node, colour) +{ + node.style.color = colour; +} + + +// +// Hide a node by completely disabling its rendering (it no longer contributes to document layout) +// +DOM.Node.Hide = function(node) +{ + node.style.display = "none"; +} + + +// +// Show a node by restoring its influcen in document layout +// +DOM.Node.Show = function(node) +{ + node.style.display = "block"; +} + + +// +// Add a CSS class to a HTML element, specified last +// +DOM.Node.AddClass = function(node, class_name) +{ + // Ensure the class hasn't already been added + DOM.Node.RemoveClass(node, class_name); + node.className += " " + class_name; +} + + +// +// Remove a CSS class from a HTML element +// +DOM.Node.RemoveClass = function(node, class_name) +{ + // Remove all variations of where the class name can be in the string list + var regexp = new RegExp("\\b" + class_name + "\\b"); + node.className = node.className.replace(regexp, ""); +} + + + +// +// Check to see if a HTML element contains a class +// +DOM.Node.HasClass = function(node, class_name) +{ + var regexp = new RegExp("\\b" + class_name + "\\b"); + return regexp.test(node.className); +} + + +// +// Recursively search for a node with the given class name +// +DOM.Node.FindWithClass = function(parent_node, class_name, index) +{ + // Search the children looking for a node with the given class name + for (var i in parent_node.childNodes) + { + var node = parent_node.childNodes[i]; + if (DOM.Node.HasClass(node, class_name)) + { + if (index === undefined || index-- == 0) + return node; + } + + // Recurse into children + node = DOM.Node.FindWithClass(node, class_name); + if (node != null) + return node; + } + + return null; +} + + +// +// Check to see if one node logically contains another +// +DOM.Node.Contains = function(node, container_node) +{ + while (node != null && node != container_node) + node = node.parentNode; + return node != null; +} + + +// +// Create the HTML nodes specified in the text passed in +// Assumes there is only one root node in the text +// +DOM.Node.CreateHTML = function(html) +{ + var div = document.createElement("div"); + div.innerHTML = html; + + // First child may be a text node, followed by the created HTML + var child = div.firstChild; + if (child != null && child.nodeType == 3) + child = child.nextSibling; + return child; +} + + +// +// Make a copy of a HTML element, making it visible and clearing its ID to ensure it's not a duplicate +// +DOM.Node.Clone = function(name) +{ + // Get the template element and clone it, making sure it's renderable + var node = DOM.Node.Get(name); + node = node.cloneNode(true); + node.id = null; + node.style.display = "block"; + return node; +} + + +// +// Append an arbitrary block of HTML to an existing node +// +DOM.Node.AppendHTML = function(node, html) +{ + var child = DOM.Node.CreateHTML(html); + node.appendChild(child); + return child; +} + + +// +// Append a div that clears the float style +// +DOM.Node.AppendClearFloat = function(node) +{ + var div = document.createElement("div"); + div.style.clear = "both"; + node.appendChild(div); +} + + +// +// Check to see that the object passed in is an instance of a DOM node +// +DOM.Node.IsNode = function(object) +{ + return object instanceof Element; +} + + +// +// Create an "iframe shim" so that elements within it render over a Java Applet +// http://web.archive.org/web/20110707212850/http://www.oratransplant.nl/2007/10/26/using-iframe-shim-to-partly-cover-a-java-applet/ +// +DOM.Node.CreateShim = function(parent) +{ + var shimmer = document.createElement("iframe"); + + // Position the shimmer so that it's the same location/size as its parent + shimmer.style.position = "fixed"; + shimmer.style.left = parent.style.left; + shimmer.style.top = parent.style.top; + shimmer.style.width = parent.offsetWidth; + shimmer.style.height = parent.offsetHeight; + + // We want the shimmer to be one level below its contents + shimmer.style.zIndex = parent.style.zIndex - 1; + + // Ensure its empty + shimmer.setAttribute("frameborder", "0"); + shimmer.setAttribute("src", ""); + + // Add to the document and the parent + document.body.appendChild(shimmer); + parent.Shimmer = shimmer; + return shimmer; +} + + + +// +// ===================================================================================================================== +// ----- EVENT HANDLING EXTENSIONS ------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Retrieves the event from the first parameter passed into an HTML event +// +DOM.Event.Get = function(evt) +{ + // Internet explorer doesn't pass the event + return window.event || evt; +} + + +// +// Retrieves the element that triggered an event from the event object +// +DOM.Event.GetNode = function(evt) +{ + evt = DOM.Event.Get(evt); + + // Get the target element + var element; + if (evt.target) + element = evt.target; + else if (e.srcElement) + element = evt.srcElement; + + // Default Safari bug + if (element.nodeType == 3) + element = element.parentNode; + + return element; +} + + +// +// Stop default action for an event +// +DOM.Event.StopDefaultAction = function(evt) +{ + if (evt && evt.preventDefault) + evt.preventDefault(); + else if (window.event && window.event.returnValue) + window.event.returnValue = false; +} + + +// +// Stops events bubbling up to parent event handlers +// +DOM.Event.StopPropagation = function(evt) +{ + evt = DOM.Event.Get(evt); + if (evt) + { + evt.cancelBubble = true; + if (evt.stopPropagation) + evt.stopPropagation(); + } +} + + +// +// Stop both event default action and propagation +// +DOM.Event.StopAll = function(evt) +{ + DOM.Event.StopDefaultAction(evt); + DOM.Event.StopPropagation(evt); +} + + +// +// Adds an event handler to an event +// +DOM.Event.AddHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.addEventListener) + obj.addEventListener(evt, func, false); + else if (obj.attachEvent) + obj.attachEvent("on" + evt, func); + } +} + + +// +// Removes an event handler from an event +// +DOM.Event.RemoveHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.removeEventListener) + obj.removeEventListener(evt, func, false); + else if (obj.detachEvent) + obj.detachEvent("on" + evt, func); + } +} + + +// +// Get the position of the mouse cursor, page relative +// +DOM.Event.GetMousePosition = function(evt) +{ + evt = DOM.Event.Get(evt); + + var px = 0; + var py = 0; + if (evt.pageX || evt.pageY) + { + px = evt.pageX; + py = evt.pageY; + } + else if (evt.clientX || evt.clientY) + { + px = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + py = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return [px, py]; +} + + +// +// Get the list of files attached to a drop event +// +DOM.Event.GetDropFiles = function(evt) +{ + let files = []; + if (evt.dataTransfer.items) + { + for (let i = 0; i < evt.dataTransfer.items.length; i++) + { + if (evt.dataTransfer.items[i].kind === 'file') + { + files.push(evt.dataTransfer.items[i].getAsFile()); + } + } + } + else + { + for (let i = 0; i < evt.dataTransfer.files.length; i++) + { + files.push(evt.dataTransfer.files[i]); + } + } + return files; +} + + + +// +// ===================================================================================================================== +// ----- JAVA APPLET EXTENSIONS ---------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Create an applet element for loading a Java applet, attaching it to the specified node +// +DOM.Applet.Load = function(dest_id, id, code, archive) +{ + // Lookup the applet destination + var dest = DOM.Node.Get(dest_id); + if (!dest) + return; + + // Construct the applet element and add it to the destination + Debug.Log("Injecting applet DOM code"); + var applet = ""; + applet += ""; + dest.innerHTML = applet; +} + + +// +// Moves and resizes a named applet so that it fits in the destination div element. +// The applet must be contained by a div element itself. This container div is moved along +// with the applet. +// +DOM.Applet.Move = function(dest_div, applet, z_index, hide) +{ + if (!applet || !dest_div) + return; + + // Before modifying any location information, hide the applet so that it doesn't render over + // any newly visible elements that appear while the location information is being modified. + if (hide) + applet.style.visibility = "hidden"; + + // Get its view rect + var pos = DOM.Node.GetPosition(dest_div); + var w = dest_div.offsetWidth; + var h = dest_div.offsetHeight; + + // It needs to be embedded in a
for correct scale/position adjustment + var container = applet.parentNode; + if (!container || container.localName != "div") + { + Debug.Log("ERROR: Couldn't find source applet's div container"); + return; + } + + // Reposition and resize the containing div element + container.style.left = pos[0]; + container.style.top = pos[1]; + container.style.width = w; + container.style.height = h; + container.style.zIndex = z_index; + + // Resize the applet itself + applet.style.width = w; + applet.style.height = h; + + // Everything modified, safe to show + applet.style.visibility = "visible"; +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Keyboard.js b/profiler/vis/extern/BrowserLib/Core/Code/Keyboard.js new file mode 100644 index 0000000..b233ffb --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Keyboard.js @@ -0,0 +1,149 @@ + +namespace("Keyboard") + + +// ===================================================================================================================== +// Key codes copied from closure-library +// https://code.google.com/p/closure-library/source/browse/closure/goog/events/keycodes.js +// --------------------------------------------------------------------------------------------------------------------- +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +Keyboard.Codes = { + WIN_KEY_FF_LINUX : 0, + MAC_ENTER : 3, + BACKSPACE : 8, + TAB : 9, + NUM_CENTER : 12, // NUMLOCK on FF/Safari Mac + ENTER : 13, + SHIFT : 16, + CTRL : 17, + ALT : 18, + PAUSE : 19, + CAPS_LOCK : 20, + ESC : 27, + SPACE : 32, + PAGE_UP : 33, // also NUM_NORTH_EAST + PAGE_DOWN : 34, // also NUM_SOUTH_EAST + END : 35, // also NUM_SOUTH_WEST + HOME : 36, // also NUM_NORTH_WEST + LEFT : 37, // also NUM_WEST + UP : 38, // also NUM_NORTH + RIGHT : 39, // also NUM_EAST + DOWN : 40, // also NUM_SOUTH + PRINT_SCREEN : 44, + INSERT : 45, // also NUM_INSERT + DELETE : 46, // also NUM_DELETE + ZERO : 48, + ONE : 49, + TWO : 50, + THREE : 51, + FOUR : 52, + FIVE : 53, + SIX : 54, + SEVEN : 55, + EIGHT : 56, + NINE : 57, + FF_SEMICOLON : 59, // Firefox (Gecko) fires this for semicolon instead of 186 + FF_EQUALS : 61, // Firefox (Gecko) fires this for equals instead of 187 + FF_DASH : 173, // Firefox (Gecko) fires this for dash instead of 189 + QUESTION_MARK : 63, // needs localization + A : 65, + B : 66, + C : 67, + D : 68, + E : 69, + F : 70, + G : 71, + H : 72, + I : 73, + J : 74, + K : 75, + L : 76, + M : 77, + N : 78, + O : 79, + P : 80, + Q : 81, + R : 82, + S : 83, + T : 84, + U : 85, + V : 86, + W : 87, + X : 88, + Y : 89, + Z : 90, + META : 91, // WIN_KEY_LEFT + WIN_KEY_RIGHT : 92, + CONTEXT_MENU : 93, + NUM_ZERO : 96, + NUM_ONE : 97, + NUM_TWO : 98, + NUM_THREE : 99, + NUM_FOUR : 100, + NUM_FIVE : 101, + NUM_SIX : 102, + NUM_SEVEN : 103, + NUM_EIGHT : 104, + NUM_NINE : 105, + NUM_MULTIPLY : 106, + NUM_PLUS : 107, + NUM_MINUS : 109, + NUM_PERIOD : 110, + NUM_DIVISION : 111, + F1 : 112, + F2 : 113, + F3 : 114, + F4 : 115, + F5 : 116, + F6 : 117, + F7 : 118, + F8 : 119, + F9 : 120, + F10 : 121, + F11 : 122, + F12 : 123, + NUMLOCK : 144, + SCROLL_LOCK : 145, + + // OS-specific media keys like volume controls and browser controls. + FIRST_MEDIA_KEY : 166, + LAST_MEDIA_KEY : 183, + + SEMICOLON : 186, // needs localization + DASH : 189, // needs localization + EQUALS : 187, // needs localization + COMMA : 188, // needs localization + PERIOD : 190, // needs localization + SLASH : 191, // needs localization + APOSTROPHE : 192, // needs localization + TILDE : 192, // needs localization + SINGLE_QUOTE : 222, // needs localization + OPEN_SQUARE_BRACKET : 219, // needs localization + BACKSLASH : 220, // needs localization + CLOSE_SQUARE_BRACKET: 221, // needs localization + WIN_KEY : 224, + MAC_FF_META : 224, // Firefox (Gecko) fires this for the meta key instead of 91 + MAC_WK_CMD_LEFT : 91, // WebKit Left Command key fired, same as META + MAC_WK_CMD_RIGHT : 93, // WebKit Right Command key fired, different from META + WIN_IME : 229, + + // We've seen users whose machines fire this keycode at regular one + // second intervals. The common thread among these users is that + // they're all using Dell Inspiron laptops, so we suspect that this + // indicates a hardware/bios problem. + // http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx + PHANTOM : 255 +}; +// ===================================================================================================================== diff --git a/profiler/vis/extern/BrowserLib/Core/Code/LocalStore.js b/profiler/vis/extern/BrowserLib/Core/Code/LocalStore.js new file mode 100644 index 0000000..2ca5664 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/LocalStore.js @@ -0,0 +1,40 @@ + +namespace("LocalStore"); + + +LocalStore.Set = function(class_name, class_id, variable_id, data) +{ + try + { + if (typeof(Storage) != "undefined") + { + var name = class_name + "_" + class_id + "_" + variable_id; + localStorage[name] = JSON.stringify(data); + } + } + catch (e) + { + console.log("Local Storage Set Error: " + e.message); + } +} + + +LocalStore.Get = function(class_name, class_id, variable_id, default_data) +{ + try + { + if (typeof(Storage) != "undefined") + { + var name = class_name + "_" + class_id + "_" + variable_id; + var data = localStorage[name] + if (data) + return JSON.parse(data); + } + } + catch (e) + { + console.log("Local Storage Get Error: " + e.message); + } + + return default_data; +} \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/Core/Code/Mouse.js b/profiler/vis/extern/BrowserLib/Core/Code/Mouse.js new file mode 100644 index 0000000..45699cc --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/Mouse.js @@ -0,0 +1,83 @@ + +namespace("Mouse"); + + +Mouse.State =(function() +{ + function State(event) + { + // Get button press states + if (typeof event.buttons != "undefined") + { + // Firefox + this.Left = (event.buttons & 1) != 0; + this.Right = (event.buttons & 2) != 0; + this.Middle = (event.buttons & 4) != 0; + } + else + { + // Chrome + this.Left = (event.button == 0); + this.Middle = (event.button == 1); + this.Right = (event.button == 2); + } + + // Get page-relative mouse position + this.Position = DOM.Event.GetMousePosition(event); + + // Get wheel delta + var delta = 0; + if (event.wheelDelta) + delta = event.wheelDelta / 120; // IE/Opera + else if (event.detail) + delta = -event.detail / 3; // Mozilla + this.WheelDelta = delta; + + // Get the mouse position delta + // Requires Pointer Lock API support + this.PositionDelta = [ + event.movementX || event.mozMovementX || event.webkitMovementX || 0, + event.movementY || event.mozMovementY || event.webkitMovementY || 0 + ]; + } + + return State; +})(); + + +// +// Basic Pointer Lock API support +// https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock +// http://www.chromium.org/developers/design-documents/mouse-lock +// +// Note that API has not been standardised yet so browsers can implement functions with prefixes +// + + +Mouse.PointerLockSupported = function() +{ + return 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document; +} + + +Mouse.RequestPointerLock = function(element) +{ + element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; + if (element.requestPointerLock) + element.requestPointerLock(); +} + + +Mouse.ExitPointerLock = function() +{ + document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; + if (document.exitPointerLock) + document.exitPointerLock(); +} + + +// Can use this element to detect whether pointer lock is enabled (returns non-null) +Mouse.PointerLockElement = function() +{ + return document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement; +} diff --git a/profiler/vis/extern/BrowserLib/Core/Code/MurmurHash3.js b/profiler/vis/extern/BrowserLib/Core/Code/MurmurHash3.js new file mode 100644 index 0000000..deafe7a --- /dev/null +++ b/profiler/vis/extern/BrowserLib/Core/Code/MurmurHash3.js @@ -0,0 +1,68 @@ + +namespace("Hash"); + +/** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} key ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + +Hash.Murmur3 = function(key, seed) +{ + var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; + + remainder = key.length & 3; // key.length % 4 + bytes = key.length - remainder; + h1 = seed; + c1 = 0xcc9e2d51; + c2 = 0x1b873593; + i = 0; + + while (i < bytes) { + k1 = + ((key.charCodeAt(i) & 0xff)) | + ((key.charCodeAt(++i) & 0xff) << 8) | + ((key.charCodeAt(++i) & 0xff) << 16) | + ((key.charCodeAt(++i) & 0xff) << 24); + ++i; + + k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); + h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; + h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); + } + + k1 = 0; + + switch (remainder) { + case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + case 1: k1 ^= (key.charCodeAt(i) & 0xff); + + k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + h1 ^= k1; + } + + h1 ^= key.length; + + h1 ^= h1 >>> 16; + h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 13; + h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; + h1 ^= h1 >>> 16; + + return h1 >>> 0; +} \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Button.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Button.js new file mode 100644 index 0000000..2cbc510 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Button.js @@ -0,0 +1,131 @@ + +namespace("WM"); + + +WM.Button = (function() +{ + var template_html = "
"; + + + function Button(text, x, y, opts) + { + this.OnClick = null; + this.Toggle = opts && opts.toggle; + + this.Node = DOM.Node.CreateHTML(template_html); + + // Set node dimensions + this.SetPosition(x, y); + if (opts && opts.w && opts.h) + this.SetSize(opts.w, opts.h); + + // Override the default class name + if (opts && opts.class) + this.Node.className = opts.class; + + this.SetText(text); + + // Create the mouse press event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + this.OnMouseOutDelegate = Bind(OnMouseUp, this, false); + this.OnMouseUpDelegate = Bind(OnMouseUp, this, true); + } + + + Button.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Button.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Button.prototype.SetText = function(text) + { + this.Node.innerHTML = text; + } + + + Button.prototype.SetOnClick = function(on_click) + { + this.OnClick = on_click; + } + + + Button.prototype.SetState = function(pressed) + { + if (pressed) + DOM.Node.AddClass(this.Node, "ButtonHeld"); + else + DOM.Node.RemoveClass(this.Node, "ButtonHeld"); + } + + + Button.prototype.ToggleState = function() + { + if (DOM.Node.HasClass(this.Node, "ButtonHeld")) + this.SetState(false); + else + this.SetState(true); + } + + + Button.prototype.IsPressed = function() + { + return DOM.Node.HasClass(this.Node, "ButtonHeld"); + } + + + function OnMouseDown(self, evt) + { + // Decide how to set the button state + if (self.Toggle) + self.ToggleState(); + else + self.SetState(true); + + // Activate release handlers + DOM.Event.AddHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.AddHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + DOM.Event.StopAll(evt); + } + + + function OnMouseUp(self, confirm, evt) + { + if (confirm) + { + // Only release for non-toggles + if (!self.Toggle) + self.SetState(false); + } + else + { + // Decide how to set the button state + if (self.Toggle) + self.ToggleState(); + else + self.SetState(false); + } + + // Remove release handlers + DOM.Event.RemoveHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.RemoveHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + // Call the click handler if this is a button press + if (confirm && self.OnClick) + self.OnClick(self); + + DOM.Event.StopAll(evt); + } + + + return Button; +})(); \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js new file mode 100644 index 0000000..9af0ca1 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js @@ -0,0 +1,237 @@ + +namespace("WM"); + + +WM.ComboBoxPopup = (function() +{ + var body_template_html = "
"; + + var item_template_html = " \ +
\ +
\ +
\ +
\ +
"; + + + function ComboBoxPopup(combo_box) + { + this.ComboBox = combo_box; + this.ParentNode = combo_box.Node; + this.ValueNodes = [ ]; + + // Create the template node + this.Node = DOM.Node.CreateHTML(body_template_html); + + DOM.Event.AddHandler(this.Node, "mousedown", Bind(SelectItem, this)); + this.CancelDelegate = Bind(this, "Cancel"); + } + + + ComboBoxPopup.prototype.SetValues = function(values) + { + // Clear existing values + this.Node.innerHTML = ""; + + // Generate HTML nodes for each value + this.ValueNodes = [ ]; + for (var i in values) + { + var item_node = DOM.Node.CreateHTML(item_template_html); + var text_node = DOM.Node.FindWithClass(item_node, "ComboBoxPopupItemText"); + + item_node.Value = values[i]; + text_node.innerHTML = values[i]; + + this.Node.appendChild(item_node); + this.ValueNodes.push(item_node); + } + } + + + ComboBoxPopup.prototype.Show = function(selection_index) + { + // Initially match the position of the parent node + var pos = DOM.Node.GetPosition(this.ParentNode); + DOM.Node.SetPosition(this.Node, pos); + + // Take the width/z-index from the parent node + this.Node.style.width = this.ParentNode.offsetWidth; + this.Node.style.zIndex = this.ParentNode.style.zIndex + 1; + + // Setup event handlers + DOM.Event.AddHandler(document.body, "mousedown", this.CancelDelegate); + + // Show the popup so that the HTML layout engine kicks in before + // the layout info is used below + this.ParentNode.appendChild(this.Node); + + // Show/hide the tick image based on which node is selected + for (var i in this.ValueNodes) + { + var node = this.ValueNodes[i]; + var icon_node = DOM.Node.FindWithClass(node, "ComboBoxPopupItemIcon"); + + if (i == selection_index) + { + icon_node.style.display = "block"; + + // Also, shift the popup up so that the mouse is over the selected item and is highlighted + var item_pos = DOM.Node.GetPosition(this.ValueNodes[selection_index]); + var diff_pos = [ item_pos[0] - pos[0], item_pos[1] - pos[1] ]; + pos = [ pos[0] - diff_pos[0], pos[1] - diff_pos[1] ]; + } + else + { + icon_node.style.display = "none"; + } + } + + DOM.Node.SetPosition(this.Node, pos); + } + + + ComboBoxPopup.prototype.Hide = function() + { + DOM.Event.RemoveHandler(document.body, "mousedown", this.CancelDelegate); + this.ParentNode.removeChild(this.Node); + } + + + function SelectItem(self, evt) + { + // Search for which item node is being clicked on + var node = DOM.Event.GetNode(evt); + for (var i in self.ValueNodes) + { + var value_node = self.ValueNodes[i]; + if (DOM.Node.Contains(node, value_node)) + { + // Set the value on the combo box + self.ComboBox.SetValue(value_node.Value); + self.Hide(); + break; + } + } + } + + + function Cancel(self, evt) + { + // Don't cancel if the mouse up is anywhere on the popup or combo box + var node = DOM.Event.GetNode(evt); + if (!DOM.Node.Contains(node, self.Node) && + !DOM.Node.Contains(node, self.ParentNode)) + { + self.Hide(); + } + + + DOM.Event.StopAll(evt); + } + + + return ComboBoxPopup; +})(); + + +WM.ComboBox = (function() +{ + var template_html = " \ +
\ +
\ +
\ +
\ +
"; + + + function ComboBox() + { + this.OnChange = null; + + // Create the template node and locate key nodes + this.Node = DOM.Node.CreateHTML(template_html); + this.TextNode = DOM.Node.FindWithClass(this.Node, "ComboBoxText"); + + // Create a reusable popup + this.Popup = new WM.ComboBoxPopup(this); + + // Set an empty set of values + this.SetValues([]); + this.SetValue("<empty>"); + + // Create the mouse press event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + this.OnMouseOutDelegate = Bind(OnMouseUp, this, false); + this.OnMouseUpDelegate = Bind(OnMouseUp, this, true); + } + + + ComboBox.prototype.SetOnChange = function(on_change) + { + this.OnChange = on_change; + } + + + ComboBox.prototype.SetValues = function(values) + { + this.Values = values; + this.Popup.SetValues(values); + } + + + ComboBox.prototype.SetValue = function(value) + { + // Set the value and its HTML rep + var old_value = this.Value; + this.Value = value; + this.TextNode.innerHTML = value; + + // Call change handler + if (this.OnChange) + this.OnChange(value, old_value); + } + + + ComboBox.prototype.GetValue = function() + { + return this.Value; + } + + + function OnMouseDown(self, evt) + { + // If this check isn't made, the click will trigger from the popup, too + var node = DOM.Event.GetNode(evt); + if (DOM.Node.Contains(node, self.Node)) + { + // Add the depression class and activate release handlers + DOM.Node.AddClass(self.Node, "ComboBoxPressed"); + DOM.Event.AddHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.AddHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + DOM.Event.StopAll(evt); + } + } + + + function OnMouseUp(self, confirm, evt) + { + // Remove depression class and remove release handlers + DOM.Node.RemoveClass(self.Node, "ComboBoxPressed"); + DOM.Event.RemoveHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.RemoveHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + // If this is a confirmed press and there are some values in the list, show the popup + if (confirm && self.Values.length > 0) + { + var selection_index = self.Values.indexOf(self.Value); + self.Popup.Show(selection_index); + } + + DOM.Event.StopAll(evt); + } + + + return ComboBox; +})(); diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Container.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Container.js new file mode 100644 index 0000000..9a4598c --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Container.js @@ -0,0 +1,48 @@ + +namespace("WM"); + + +WM.Container = (function() +{ + var template_html = "
"; + + + function Container(x, y, w, h) + { + // Create a simple container node + this.Node = DOM.Node.CreateHTML(template_html); + this.SetPosition(x, y); + this.SetSize(w, h); + } + + + Container.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Container.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Container.prototype.AddControlNew = function(control) + { + control.ParentNode = this.Node; + this.Node.appendChild(control.Node); + return control; + } + + + Container.prototype.ClearControls = function() + { + this.Node.innerHTML = ""; + } + + + return Container; +})(); \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/EditBox.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/EditBox.js new file mode 100644 index 0000000..111898c --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/EditBox.js @@ -0,0 +1,119 @@ + +namespace("WM"); + + +WM.EditBox = (function() +{ + var template_html = " \ +
\ +
Label
\ + \ +
"; + + + function EditBox(x, y, w, h, label, text) + { + this.ChangeHandler = null; + + // Create node and locate its internal nodes + this.Node = DOM.Node.CreateHTML(template_html); + this.LabelNode = DOM.Node.FindWithClass(this.Node, "EditBoxLabel"); + this.EditNode = DOM.Node.FindWithClass(this.Node, "EditBox"); + + // Set label and value + this.LabelNode.innerHTML = label; + this.SetValue(text); + + this.SetPosition(x, y); + this.SetSize(w, h); + + this.PreviousValue = ""; + + // Hook up the event handlers + DOM.Event.AddHandler(this.EditNode, "focus", Bind(OnFocus, this)); + DOM.Event.AddHandler(this.EditNode, "keypress", Bind(OnKeyPress, this)); + DOM.Event.AddHandler(this.EditNode, "keydown", Bind(OnKeyDown, this)); + } + + + EditBox.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + EditBox.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.EditNode, this.Size); + } + + + EditBox.prototype.SetChangeHandler = function(handler) + { + this.ChangeHandler = handler; + } + + + EditBox.prototype.SetValue = function(value) + { + if (this.EditNode) + this.EditNode.value = value; + } + + + EditBox.prototype.GetValue = function() + { + if (this.EditNode) + return this.EditNode.value; + + return null; + } + + + EditBox.prototype.LoseFocus = function() + { + if (this.EditNode) + this.EditNode.blur(); + } + + + function OnFocus(self, evt) + { + // Backup on focus + self.PreviousValue = self.EditNode.value; + } + + + function OnKeyPress(self, evt) + { + // Allow enter to confirm the text only when there's data + if (evt.keyCode == 13 && self.EditNode.value != "" && self.ChangeHandler) + { + var focus = self.ChangeHandler(self.EditNode); + if (!focus) + self.EditNode.blur(); + self.PreviousValue = ""; + } + } + + + function OnKeyDown(self, evt) + { + // Allow escape to cancel any text changes + if (evt.keyCode == 27) + { + // On initial edit of the input, escape should NOT replace with the empty string + if (self.PreviousValue != "") + { + self.EditNode.value = self.PreviousValue; + } + + self.EditNode.blur(); + } + } + + + return EditBox; +})(); diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Grid.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Grid.js new file mode 100644 index 0000000..1c4ef2e --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Grid.js @@ -0,0 +1,248 @@ + +namespace("WM"); + + +WM.GridRows = (function() +{ + function GridRows(parent_object) + { + this.ParentObject = parent_object; + + // Array of rows in the order they were added + this.Rows = [ ]; + + // Collection of custom row indexes for fast lookup + this.Indexes = { }; + } + + + GridRows.prototype.AddIndex = function(cell_field_name) + { + var index = { }; + + // Go through existing rows and add to the index + for (var i in this.Rows) + { + var row = this.Rows[i]; + if (cell_field_name in row.CellData) + { + var cell_field = row.CellData[cell_field_name]; + index[cell_field] = row; + } + } + + this.Indexes[cell_field_name] = index; + } + + + GridRows.prototype.ClearIndex = function(index_name) + { + this.Indexes[index_name] = { }; + } + + GridRows.prototype.AddRowToIndex = function(index_name, cell_data, row) + { + this.Indexes[index_name][cell_data] = row; + } + + + GridRows.prototype.Add = function(cell_data, row_classes, cell_classes) + { + var row = new WM.GridRow(this.ParentObject, cell_data, row_classes, cell_classes); + this.Rows.push(row); + return row; + } + + + GridRows.prototype.GetBy = function(cell_field_name, cell_data) + { + var index = this.Indexes[cell_field_name]; + return index[cell_data]; + } + + + GridRows.prototype.Clear = function() + { + // Remove all node references from the parent + for (var i in this.Rows) + { + var row = this.Rows[i]; + row.Parent.BodyNode.removeChild(row.Node); + } + + // Clear all indexes + for (var i in this.Indexes) + this.Indexes[i] = { }; + + this.Rows = [ ]; + } + + + return GridRows; +})(); + + +WM.GridRow = (function() +{ + var template_html = "
"; + + + // + // 'cell_data' is an object with a variable number of fields. + // Any fields prefixed with an underscore are hidden. + // + function GridRow(parent, cell_data, row_classes, cell_classes) + { + // Setup data + this.Parent = parent; + this.IsOpen = true; + this.AnimHandle = null; + this.Rows = new WM.GridRows(this); + this.CellData = cell_data; + this.CellNodes = { } + + // Create the main row node + this.Node = DOM.Node.CreateHTML(template_html); + if (row_classes) + DOM.Node.AddClass(this.Node, row_classes); + + // Embed a pointer to the row in the root node so that it can be clicked + this.Node.GridRow = this; + + // Create nodes for each required cell + for (var attr in this.CellData) + { + if (this.CellData.hasOwnProperty(attr)) + { + var data = this.CellData[attr]; + + // Update any grid row index references + if (attr in parent.Rows.Indexes) + parent.Rows.AddRowToIndex(attr, data, this); + + // Hide any cells with underscore prefixes + if (attr[0] == "_") + continue; + + // Create a node for the cell and add any custom classes + var node = DOM.Node.AppendHTML(this.Node, "
"); + if (cell_classes && attr in cell_classes) + DOM.Node.AddClass(node, cell_classes[attr]); + this.CellNodes[attr] = node; + + // If this is a Window Control, add its node to the cell + if (data instanceof Object && "Node" in data && DOM.Node.IsNode(data.Node)) + { + data.ParentNode = node; + node.appendChild(data.Node); + } + + else + { + // Otherwise just assign the data as text + node.innerHTML = data; + } + } + } + + // Add the body node for any children + if (!this.Parent.BodyNode) + this.Parent.BodyNode = DOM.Node.AppendHTML(this.Parent.Node, "
"); + + // Add the row to the parent + this.Parent.BodyNode.appendChild(this.Node); + } + + + GridRow.prototype.Open = function() + { + // Don't allow open while animating + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + this.IsOpen = true; + + // Kick off open animation + var node = this.BodyNode; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(node, val) }, + 0, this.Height, 0.2); + } + } + + + GridRow.prototype.Close = function() + { + // Don't allow close while animating + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + this.IsOpen = false; + + // Record height for the next open request + this.Height = this.BodyNode.offsetHeight; + + // Kick off close animation + var node = this.BodyNode; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(node, val) }, + this.Height, 0, 0.2); + } + } + + + GridRow.prototype.Toggle = function() + { + if (this.IsOpen) + this.Close(); + else + this.Open(); + } + + + return GridRow; +})(); + + +WM.Grid = (function() +{ + var template_html = " \ +
\ +
\ +
"; + + + function Grid() + { + this.Rows = new WM.GridRows(this); + + this.Node = DOM.Node.CreateHTML(template_html); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "GridBody"); + + DOM.Event.AddHandler(this.Node, "dblclick", OnDblClick); + + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + } + + function OnDblClick(evt) + { + // Clicked on a header? + var node = DOM.Event.GetNode(evt); + if (DOM.Node.HasClass(node, "GridRowName")) + { + // Toggle rows open/close + var row = node.parentNode.GridRow; + if (row) + row.Toggle(); + } + } + + + function OnMouseScroll(self, evt) + { + var mouse_state = new Mouse.State(evt); + self.Node.scrollTop -= mouse_state.WheelDelta * 20; + } + + + return Grid; +})(); diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Label.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Label.js new file mode 100644 index 0000000..dd2d74f --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Label.js @@ -0,0 +1,31 @@ + +namespace("WM"); + + +WM.Label = (function() +{ + var template_html = "
"; + + + function Label(x, y, text) + { + // Create the node + this.Node = DOM.Node.CreateHTML(template_html); + + // Allow position to be optional + if (x != null && y != null) + DOM.Node.SetPosition(this.Node, [x, y]); + + this.SetText(text); + } + + + Label.prototype.SetText = function(text) + { + if (text != null) + this.Node.innerHTML = text; + } + + + return Label; +})(); \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Treeview.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Treeview.js new file mode 100644 index 0000000..539d080 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Treeview.js @@ -0,0 +1,352 @@ + +namespace("WM"); + + +WM.Treeview = (function() +{ + var Margin = 10; + + + var tree_template_html = " \ +
\ +
\ +
\ +
\ +
\ +
\ +
"; + + + var item_template_html = " \ +
\ + \ +
\ +
\ +
\ +
\ +
"; + + + // TODO: Remove parent_node (required for stuff that doesn't use the WM yet) + function Treeview(x, y, width, height, parent_node) + { + // Cache initialisation options + this.ParentNode = parent_node; + this.Position = [ x, y ]; + this.Size = [ width, height ]; + + this.Node = null; + this.ScrollbarNode = null; + this.SelectedItem = null; + this.ContentsNode = null; + + // Setup options + this.HighlightOnHover = false; + this.EnableScrollbar = true; + this.HorizontalLayoutDepth = 1; + + // Generate an empty tree + this.Clear(); + } + + + Treeview.prototype.SetHighlightOnHover = function(highlight) + { + this.HighlightOnHover = highlight; + } + + + Treeview.prototype.SetEnableScrollbar = function(enable) + { + this.EnableScrollbar = enable; + } + + + Treeview.prototype.SetHorizontalLayoutDepth = function(depth) + { + this.HorizontalLayoutDepth = depth; + } + + + Treeview.prototype.SetNodeSelectedHandler = function(handler) + { + this.NodeSelectedHandler = handler; + } + + + Treeview.prototype.Clear = function() + { + this.RootItem = new WM.TreeviewItem(this, null, null, null, null); + this.GenerateHTML(); + } + + + Treeview.prototype.Root = function() + { + return this.RootItem; + } + + + Treeview.prototype.ClearSelection = function() + { + if (this.SelectedItem != null) + { + DOM.Node.RemoveClass(this.SelectedItem.Node, "TreeviewItemSelected"); + this.SelectedItem = null; + } + } + + + Treeview.prototype.SelectItem = function(item, mouse_pos) + { + // Notify the select handler + if (this.NodeSelectedHandler) + this.NodeSelectedHandler(item.Node, this.SelectedItem, item, mouse_pos); + + // Remove highlight from the old selection + this.ClearSelection(); + + // Swap in new selection and apply highlight + this.SelectedItem = item; + DOM.Node.AddClass(this.SelectedItem.Node, "TreeviewItemSelected"); + } + + + Treeview.prototype.GenerateHTML = function() + { + // Clone the template and locate important nodes + var old_node = this.Node; + this.Node = DOM.Node.CreateHTML(tree_template_html); + this.ChildrenNode = DOM.Node.FindWithClass(this.Node, "TreeviewItemChildren"); + this.ScrollbarNode = DOM.Node.FindWithClass(this.Node, "TreeviewScrollbar"); + + DOM.Node.SetPosition(this.Node, this.Position); + DOM.Node.SetSize(this.Node, this.Size); + + // Generate the contents of the treeview + GenerateTree(this, this.ChildrenNode, this.RootItem.Children, 0); + + // Cross-browser (?) means of adding a mouse wheel handler + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + + DOM.Event.AddHandler(this.Node, "dblclick", Bind(OnMouseDoubleClick, this)); + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + DOM.Event.AddHandler(this.Node, "mouseup", OnMouseUp); + + // Swap in the newly generated control node if it's already been attached to a parent + if (old_node && old_node.parentNode) + { + old_node.parentNode.removeChild(old_node); + this.ParentNode.appendChild(this.Node); + } + + if (this.EnableScrollbar) + { + this.UpdateScrollbar(); + DOM.Event.AddHandler(this.ScrollbarNode, "mousedown", Bind(OnMouseDown_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mouseup", Bind(OnMouseUp_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mouseout", Bind(OnMouseUp_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mousemove", Bind(OnMouseMove_Scrollbar, this)); + } + + else + { + DOM.Node.Hide(DOM.Node.FindWithClass(this.Node, "TreeviewScrollbarInset")); + } + } + + + Treeview.prototype.UpdateScrollbar = function() + { + if (!this.EnableScrollbar) + return; + + var scrollbar_scale = Math.min((this.Node.offsetHeight - Margin * 2) / this.ChildrenNode.offsetHeight, 1); + this.ScrollbarNode.style.height = parseInt(scrollbar_scale * 100) + "%"; + + // Shift the scrollbar container along with the parent window + this.ScrollbarNode.parentNode.style.top = this.Node.scrollTop; + + var scroll_fraction = this.Node.scrollTop / (this.Node.scrollHeight - this.Node.offsetHeight); + var max_height = this.Node.offsetHeight - Margin; + var max_scrollbar_offset = max_height - this.ScrollbarNode.offsetHeight; + var scrollbar_offset = scroll_fraction * max_scrollbar_offset; + this.ScrollbarNode.style.top = scrollbar_offset; + } + + + function GenerateTree(self, parent_node, items, depth) + { + if (items.length == 0) + return null; + + for (var i in items) + { + var item = items[i]; + + // Create the node for this item and locate important nodes + var node = DOM.Node.CreateHTML(item_template_html); + var img = DOM.Node.FindWithClass(node, "TreeviewItemImage"); + var text = DOM.Node.FindWithClass(node, "TreeviewItemText"); + var children = DOM.Node.FindWithClass(node, "TreeviewItemChildren"); + + // Attach the item to the node + node.TreeviewItem = item; + item.Node = node; + + // Add the class which highlights selection on hover + if (self.HighlightOnHover) + DOM.Node.AddClass(node, "TreeviewItemHover"); + + // Instruct the children to wrap around + if (depth >= self.HorizontalLayoutDepth) + node.style.cssFloat = "left"; + + if (item.OpenImage == null || item.CloseImage == null) + { + // If there no images, remove the image node + node.removeChild(img); + } + else + { + // Set the image source to open + img.src = item.OpenImage.src; + img.style.width = item.OpenImage.width; + img.style.height = item.OpenImage.height; + item.ImageNode = img; + } + + // Setup the text to display + text.innerHTML = item.Label; + + // Add the div to the parent and recurse into children + parent_node.appendChild(node); + GenerateTree(self, children, item.Children, depth + 1); + item.ChildrenNode = children; + } + + // Clear the wrap-around + if (depth >= self.HorizontalLayoutDepth) + DOM.Node.AppendClearFloat(parent_node.parentNode); + } + + + function OnMouseScroll(self, evt) + { + // Get mouse wheel movement + var delta = evt.detail ? evt.detail * -1 : evt.wheelDelta; + delta *= 8; + + // Scroll the main window with wheel movement and clamp + self.Node.scrollTop -= delta; + self.Node.scrollTop = Math.min(self.Node.scrollTop, (self.ChildrenNode.offsetHeight - self.Node.offsetHeight) + Margin * 2); + + self.UpdateScrollbar(); + } + + + function OnMouseDoubleClick(self, evt) + { + DOM.Event.StopDefaultAction(evt); + + // Get the tree view item being clicked, if any + var node = DOM.Event.GetNode(evt); + var tvitem = GetTreeviewItemFromNode(self, node); + if (tvitem == null) + return; + + if (tvitem.Children.length) + tvitem.Toggle(); + } + + + function OnMouseDown(self, evt) + { + DOM.Event.StopDefaultAction(evt); + + // Get the tree view item being clicked, if any + var node = DOM.Event.GetNode(evt); + var tvitem = GetTreeviewItemFromNode(self, node); + if (tvitem == null) + return; + + // If clicking on the image, expand any children + if (node.tagName == "IMG" && tvitem.Children.length) + { + tvitem.Toggle(); + } + + else + { + var mouse_pos = DOM.Event.GetMousePosition(evt); + self.SelectItem(tvitem, mouse_pos); + } + } + + + function OnMouseUp(evt) + { + // Event handler used merely to stop events bubbling up to containers + DOM.Event.StopPropagation(evt); + } + + + function OnMouseDown_Scrollbar(self, evt) + { + self.ScrollbarHeld = true; + + // Cache the mouse height relative to the scrollbar + self.LastY = evt.clientY; + self.ScrollY = self.Node.scrollTop; + + DOM.Node.AddClass(self.ScrollbarNode, "TreeviewScrollbarHeld"); + DOM.Event.StopDefaultAction(evt); + } + + + function OnMouseUp_Scrollbar(self, evt) + { + self.ScrollbarHeld = false; + DOM.Node.RemoveClass(self.ScrollbarNode, "TreeviewScrollbarHeld"); + } + + + function OnMouseMove_Scrollbar(self, evt) + { + if (self.ScrollbarHeld) + { + var delta_y = evt.clientY - self.LastY; + self.LastY = evt.clientY; + + var max_height = self.Node.offsetHeight - Margin; + var max_scrollbar_offset = max_height - self.ScrollbarNode.offsetHeight; + var max_contents_scroll = self.Node.scrollHeight - self.Node.offsetHeight; + var scale = max_contents_scroll / max_scrollbar_offset; + + // Increment the local float variable and assign, as scrollTop is of type int + self.ScrollY += delta_y * scale; + self.Node.scrollTop = self.ScrollY; + self.Node.scrollTop = Math.min(self.Node.scrollTop, (self.ChildrenNode.offsetHeight - self.Node.offsetHeight) + Margin * 2); + + self.UpdateScrollbar(); + } + } + + + function GetTreeviewItemFromNode(self, node) + { + // Walk up toward the tree view node looking for this first item + while (node && node != self.Node) + { + if ("TreeviewItem" in node) + return node.TreeviewItem; + + node = node.parentNode; + } + + return null; + } + + return Treeview; +})(); diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js new file mode 100644 index 0000000..fc04088 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js @@ -0,0 +1,109 @@ + +namespace("WM"); + + +WM.TreeviewItem = (function() +{ + function TreeviewItem(treeview, name, data, open_image, close_image) + { + // Assign members + this.Treeview = treeview; + this.Label = name; + this.Data = data; + this.OpenImage = open_image; + this.CloseImage = close_image; + + this.Children = [ ]; + + // The HTML node wrapping the item and its children + this.Node = null; + + // The HTML node storing the image for the open/close state feedback + this.ImageNode = null; + + // The HTML node storing just the children + this.ChildrenNode = null; + + // Animation handle for opening and closing the child nodes, only used + // if the tree view item as children + this.AnimHandle = null; + + // Open state of the item + this.IsOpen = true; + } + + + TreeviewItem.prototype.AddItem = function(name, data, open_image, close_image) + { + var item = new WM.TreeviewItem(this.Treeview, name, data, open_image, close_image); + this.Children.push(item); + return item; + } + + + TreeviewItem.prototype.Open = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the open state + this.IsOpen = true; + if (this.ImageNode != null && this.OpenImage != null) + this.ImageNode.src = this.OpenImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var end_height = this.StartHeight; + var treeview = this.Treeview; + + // Reveal the children and animate their height to max + this.ChildrenNode.style.display = "block"; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + 0, end_height, 0.2, + function() { treeview.UpdateScrollbar(); }); + + // Fade the children in + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 0, 1, 0.2); + } + } + + + TreeviewItem.prototype.Close = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the close state + this.IsOpen = false; + if (this.ImageNode != null && this.CloseImage != null) + this.ImageNode.src = this.CloseImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var treeview = this.Treeview; + + // Mark the height of the item for reload later + this.StartHeight = child_node.offsetHeight; + + // Shrink the height of the children and hide them upon completion + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + this.ChildrenNode.offsetHeight, 0, 0.2, + function() { child_node.style.display = "none"; treeview.UpdateScrollbar(); }); + + // Fade the children out + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 1, 0, 0.2); + } + } + + + TreeviewItem.prototype.Toggle = function() + { + if (this.IsOpen) + this.Close(); + else + this.Open(); + } + + + return TreeviewItem; +})(); diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/Window.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/Window.js new file mode 100644 index 0000000..3a7ac94 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/Window.js @@ -0,0 +1,318 @@ + +namespace("WM"); + + +WM.Window = (function() +{ + var template_html = multiline(function(){/* \ +
+
+
Window Title Bar
+
+
+
+
+
+
+ */}); + + + function Window(manager, title, x, y, width, height, parent_node, user_data) + { + this.Manager = manager; + this.ParentNode = parent_node || document.body; + this.userData = user_data; + this.OnMove = null; + this.OnResize = null; + this.Visible = false; + this.AnimatedShow = false; + + // Clone the window template and locate key nodes within it + this.Node = DOM.Node.CreateHTML(template_html); + this.TitleBarNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBar"); + this.TitleBarTextNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarText"); + this.TitleBarCloseNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarClose"); + this.ResizeHandleNode = DOM.Node.FindWithClass(this.Node, "WindowResizeHandle"); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "WindowBody"); + + // Setup the position and dimensions of the window + this.SetPosition(x, y); + this.SetSize(width, height); + + // Set the title text + this.TitleBarTextNode.innerHTML = title; + + // Hook up event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(this, "SetTop")); + DOM.Event.AddHandler(this.TitleBarNode, "mousedown", Bind(this, "BeginMove")); + DOM.Event.AddHandler(this.ResizeHandleNode, "mousedown", Bind(this, "BeginResize")); + DOM.Event.AddHandler(this.TitleBarCloseNode, "mouseup", Bind(this, "Hide")); + + // Create delegates for removable handlers + this.MoveDelegate = Bind(this, "Move"); + this.EndMoveDelegate = Bind(this, "EndMove") + this.ResizeDelegate = Bind(this, "Resize"); + this.EndResizeDelegate = Bind(this, "EndResize"); + } + + Window.prototype.SetOnMove = function(on_move) + { + this.OnMove = on_move; + } + + Window.prototype.SetOnResize = function(on_resize) + { + this.OnResize = on_resize; + } + + + Window.prototype.Show = function() + { + if (this.Node.parentNode != this.ParentNode) + { + this.ShowNoAnim(); + Anim.Animate(Bind(this, "OpenAnimation"), 0, 1, 1); + } + } + + + Window.prototype.ShowNoAnim = function() + { + // Add to the document + this.ParentNode.appendChild(this.Node); + this.AnimatedShow = false; + this.Visible = true; + } + + + Window.prototype.Hide = function(evt) + { + if (this.Node.parentNode == this.ParentNode && evt.button == 0) + { + if (this.AnimatedShow) + { + // Trigger animation that ends with removing the window from the document + Anim.Animate( + Bind(this, "CloseAnimation"), + 0, 1, 0.25, + Bind(this, "HideNoAnim")); + } + else + { + this.HideNoAnim(); + } + } + } + + + Window.prototype.HideNoAnim = function() + { + if (this.Node.parentNode == this.ParentNode) + { + // Remove node + this.ParentNode.removeChild(this.Node); + this.Visible = false; + } + } + + + Window.prototype.Close = function() + { + this.HideNoAnim(); + this.Manager.RemoveWindow(this); + } + + + Window.prototype.SetTop = function() + { + this.Manager.SetTopWindow(this); + } + + + + Window.prototype.SetTitle = function(title) + { + this.TitleBarTextNode.innerHTML = title; + } + + + // TODO: Update this + Window.prototype.AddControl = function(control) + { + // Get all arguments to this function and replace the first with this window node + var args = [].slice.call(arguments); + args[0] = this.BodyNode; + + // Create the control and call its Init method with the modified arguments + var instance = new control(); + instance.Init.apply(instance, args); + + return instance; + } + + + Window.prototype.AddControlNew = function(control) + { + control.ParentNode = this.BodyNode; + this.BodyNode.appendChild(control.Node); + return control; + } + + + Window.prototype.RemoveControl = function(control) + { + if (control.ParentNode == this.BodyNode) + { + control.ParentNode.removeChild(control.Node); + } + } + + + Window.prototype.Scale = function(t) + { + // Calculate window bounds centre/extents + var ext_x = this.Size[0] / 2; + var ext_y = this.Size[1] / 2; + var mid_x = this.Position[0] + ext_x; + var mid_y = this.Position[1] + ext_y; + + // Scale from the mid-point + DOM.Node.SetPosition(this.Node, [ mid_x - ext_x * t, mid_y - ext_y * t ]); + DOM.Node.SetSize(this.Node, [ this.Size[0] * t, this.Size[1] * t ]); + } + + + Window.prototype.OpenAnimation = function(val) + { + // Power ease in + var t = 1 - Math.pow(1 - val, 8); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, 1 - Math.pow(1 - val, 8)); + this.AnimatedShow = true; + } + + + Window.prototype.CloseAnimation = function(val) + { + // Power ease out + var t = 1 - Math.pow(val, 4); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, t); + } + + + Window.prototype.NotifyChange = function() + { + if (this.OnMove) + { + var pos = DOM.Node.GetPosition(this.Node); + this.OnMove(this, pos); + } + } + + + Window.prototype.BeginMove = function(evt) + { + // Calculate offset of the window from the mouse down position + var mouse_pos = DOM.Event.GetMousePosition(evt); + this.Offset = [ mouse_pos[0] - this.Position[0], mouse_pos[1] - this.Position[1] ]; + + // Dynamically add handlers for movement and release + DOM.Event.AddHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.AddHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.Move = function(evt) + { + // Use the offset at the beginning of movement to drag the window around + var mouse_pos = DOM.Event.GetMousePosition(evt); + var offset = this.Offset; + var pos = [ mouse_pos[0] - offset[0], mouse_pos[1] - offset[1] ]; + this.SetPosition(pos[0], pos[1]); + + if (this.OnMove) + this.OnMove(this, pos); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.EndMove = function(evt) + { + // Remove handlers added during mouse down + DOM.Event.RemoveHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.RemoveHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.BeginResize = function(evt) + { + // Calculate offset of the window from the mouse down position + var mouse_pos = DOM.Event.GetMousePosition(evt); + this.MousePosBeforeResize = [ mouse_pos[0], mouse_pos[1] ]; + this.SizeBeforeResize = this.Size; + + // Dynamically add handlers for movement and release + DOM.Event.AddHandler(document, "mousemove", this.ResizeDelegate); + DOM.Event.AddHandler(document, "mouseup", this.EndResizeDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.Resize = function(evt) + { + // Use the offset at the beginning of movement to drag the window around + var mouse_pos = DOM.Event.GetMousePosition(evt); + var offset = [ mouse_pos[0] - this.MousePosBeforeResize[0], mouse_pos[1] - this.MousePosBeforeResize[1] ]; + this.SetSize(this.SizeBeforeResize[0] + offset[0], this.SizeBeforeResize[1] + offset[1]); + + if (this.OnResize) + this.OnResize(this, this.Size); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.EndResize = function(evt) + { + // Remove handlers added during mouse down + DOM.Event.RemoveHandler(document, "mousemove", this.ResizeDelegate); + DOM.Event.RemoveHandler(document, "mouseup", this.EndResizeDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Window.prototype.SetSize = function(w, h) + { + w = Math.max(80, w); + h = Math.max(15, h); + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + + if (this.OnResize) + this.OnResize(this, this.Size); + } + + + Window.prototype.GetZIndex = function() + { + return parseInt(this.Node.style.zIndex); + } + + + return Window; +})(); \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js b/profiler/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js new file mode 100644 index 0000000..49da617 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js @@ -0,0 +1,65 @@ + +namespace("WM"); + + +WM.WindowManager = (function() +{ + function WindowManager() + { + // An empty list of windows under window manager control + this.Windows = [ ]; + } + + + WindowManager.prototype.AddWindow = function(title, x, y, width, height, parent_node, user_data) + { + // Create the window and add it to the list of windows + var wnd = new WM.Window(this, title, x, y, width, height, parent_node, user_data); + this.Windows.push(wnd); + + // Always bring to the top on creation + wnd.SetTop(); + + return wnd; + } + + + WindowManager.prototype.RemoveWindow = function(window) + { + // Remove from managed window list + var index = this.Windows.indexOf(window); + if (index != -1) + { + this.Windows.splice(index, 1); + } + } + + + WindowManager.prototype.SetTopWindow = function(top_wnd) + { + // Bring the window to the top of the window list + var top_wnd_index = this.Windows.indexOf(top_wnd); + if (top_wnd_index != -1) + this.Windows.splice(top_wnd_index, 1); + this.Windows.push(top_wnd); + + // Set a CSS z-index for each visible window from the bottom up + for (var i in this.Windows) + { + var wnd = this.Windows[i]; + if (!wnd.Visible) + continue; + + // Ensure there's space between each window for the elements inside to be sorted + var z = (parseInt(i) + 1) * 10; + wnd.Node.style.zIndex = z; + + // Notify window that its z-order has changed + wnd.NotifyChange(); + } + } + + + return WindowManager; + +})(); \ No newline at end of file diff --git a/profiler/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css b/profiler/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css new file mode 100644 index 0000000..b9d71b3 --- /dev/null +++ b/profiler/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css @@ -0,0 +1,652 @@ + + +.notextsel +{ + /* Disable text selection so that it doesn't interfere with button-clicking */ + user-select: none; + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer */ + -khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */ + -webkit-user-select: none; /* Chrome, Safari, and Opera */ + -webkit-touch-callout: none; /* Disable Android and iOS callouts*/ + + /* Stops the text cursor over the label */ + cursor:default; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Window Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + +body +{ + /* Clip contents to browser window without adding scrollbars */ + overflow: hidden; +} + +.Window +{ + position:absolute; + + /* Clip all contents to the window border */ + overflow: hidden; + + background: #555; + + /*padding: 0px !important;*/ + + border-radius: 3px; + -moz-border-radius: 5px; + + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +/*:root +{ + --SideBarSize: 5px; +} + +.WindowBodyDebug +{ + color: #BBB; + font: 9px Verdana; + white-space: nowrap; +} + +.WindowSizeLeft +{ + position: absolute; + left: 0px; + top: 0px; + width: var(--SideBarSize); + height: 100%; +} +.WindowSizeRight +{ + position: absolute; + left: calc(100% - var(--SideBarSize)); + top:0px; + width: var(--SideBarSize); + height:100%; +} +.WindowSizeTop +{ + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: var(--SideBarSize); +} +.WindowSizeBottom +{ + position: absolute; + left: 0px; + top: calc(100% - var(--SideBarSize)); + width: 100%; + height: var(--SideBarSize); +}*/ + + +.Window_Transparent +{ + /* Set transparency changes to fade in/out */ + opacity: 0.5; + transition: opacity 0.5s ease-out; + -moz-transition: opacity 0.5s ease-out; + -webkit-transition: opacity 0.5s ease-out; +} + +.Window_Transparent:hover +{ + opacity: 1; +} + +.WindowTitleBar +{ + height: 17px; + cursor: move; + /*overflow: hidden;*/ + + border-bottom: 1px solid #303030; + border-radius: 5px; +} + +.WindowTitleBarText +{ + color: #BBB; + font: 9px Verdana; + /*white-space: nowrap;*/ + + padding: 3px; + cursor: move; +} + +.WindowTitleBarClose +{ + color: #999999; + font: 9px Verdana; + + padding: 3px; + cursor: default; +} + +.WindowTitleBarClose:hover { + color: #bbb; +} + +.WindowResizeHandle +{ + color: #999999; + font: 17px Verdana; + padding: 3px; + cursor: se-resize; + position: absolute; + bottom: -7px; + right: -3px; +} + +.WindowBody { + position: absolute; + /* overflow: hidden; */ + display: block; + padding: 10px; + border-top: 1px solid #606060; + top: 18px; + left: 0; + right: 0; + bottom: 0; + height: auto; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Container Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Container +{ + /* Position relative to the parent window */ + position: absolute; + + /* Clip contents */ + /*overflow: hidden;*/ + + background:#2C2C2C; + + border: 1px black solid; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +/*.Panel +{*/ + /* Position relative to the parent window */ + /*position: absolute;*/ + + /* Clip contents */ + /*overflow: hidden; + + background:#2C2C2C; + + border: 1px black solid;*/ + + /* Two inset box shadows to simulate depressing */ + /*-webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset;*/ +/*}*/ + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Ruler Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +/*.Ruler +{ + position: absolute; + + border: dashed 1px; + + opacity: 0.35; +}*/ + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Treeview Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Treeview +{ + position: absolute; + + background:#2C2C2C; + border: 1px solid black; + overflow:hidden; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewItem +{ + margin:1px; + padding:2px; + border:solid 1px #2C2C2C; + background-color:#2C2C2C; +} + +.TreeviewItemImage +{ + float: left; +} + +.TreeviewItemText +{ + float: left; + margin-left:4px; +} + +.TreeviewItemChildren +{ + overflow: hidden; +} + +.TreeviewItemSelected +{ + background-color:#444; + border-color:#FFF; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +/* Used to populate treeviews that want highlight on hover behaviour */ +.TreeviewItemHover +{ +} + +.TreeviewItemHover:hover +{ + background-color:#111; + border-color:#444; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +.TreeviewScrollbarInset +{ + float: right; + + position:relative; + + height: 100%; + + /* CRAZINESS PART A: Trying to get the inset and scrollbar to have 100% height match its container */ + margin: -8px -8px 0 0; + padding: 0 1px 14px 1px; + + width:20px; + background:#2C2C2C; + border: 1px solid black; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewScrollbar +{ + position:relative; + + background:#2C2C2C; + border: 1px solid black; + + /* CRAZINESS PART B: Trying to get the inset and scrollbar to have 100% height match its container */ + padding: 0 0 10px 0; + margin: 1px 0 0 0; + + width: 18px; + height: 100%; + + border-radius:6px; + border-color:#000; + border-width:1px; + border-style:solid; + + /* The gradient for the button background */ + background-color:#666; + background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#383838)); + background: -moz-linear-gradient(top, #666, #383838); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#666666', endColorstr='#383838'); + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.TreeviewScrollbarHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Edit Box Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.EditBoxContainer +{ + position: absolute; + padding:2px 10px 2px 10px; +} + +.EditBoxLabel +{ + float:left; + padding: 3px 4px 4px 4px; + font: 9px Verdana; +} + +.EditBox +{ + float:left; + + background:#666; + border: 1px solid; + border-radius: 6px; + padding: 3px 4px 3px 4px; + height: 20px; + + box-shadow: 1px 1px 1px #222 inset; + + transition: all 0.3s ease-in-out; +} + +.EditBox:focus +{ + background:#FFF; + outline:0; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Label Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Label +{ + /* Position relative to the parent window */ + position:absolute; + + color: #BBB; + font: 9px Verdana; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Combo Box Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.ComboBox +{ + position:absolute; + + /* TEMP! */ + width:90px; + + /* Height is fixed to match the font */ + height:14px; + + /* Align the text within the combo box */ + padding: 1px 0 0 5px; + + /* Solid, rounded border */ + border: 1px solid #111; + border-radius: 5px; + + /* http://www.colorzilla.com/gradient-editor/#e3e3e3+0,c6c6c6+22,b7b7b7+33,afafaf+50,a7a7a7+67,797979+82,414141+100;Custom */ + background: #e3e3e3; + background: -moz-linear-gradient(top, #e3e3e3 0%, #c6c6c6 22%, #b7b7b7 33%, #afafaf 50%, #a7a7a7 67%, #797979 82%, #414141 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e3e3e3), color-stop(22%,#c6c6c6), color-stop(33%,#b7b7b7), color-stop(50%,#afafaf), color-stop(67%,#a7a7a7), color-stop(82%,#797979), color-stop(100%,#414141)); + background: -webkit-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -o-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -ms-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e3e3e3', endColorstr='#414141',GradientType=0 ); +} + +.ComboBoxPressed +{ + /* The reverse of the default background, simulating depression */ + background: #414141; + background: -moz-linear-gradient(top, #414141 0%, #797979 18%, #a7a7a7 33%, #afafaf 50%, #b7b7b7 67%, #c6c6c6 78%, #e3e3e3 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#414141), color-stop(18%,#797979), color-stop(33%,#a7a7a7), color-stop(50%,#afafaf), color-stop(67%,#b7b7b7), color-stop(78%,#c6c6c6), color-stop(100%,#e3e3e3)); + background: -webkit-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -o-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -ms-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#414141', endColorstr='#e3e3e3',GradientType=0 ); +} + +.ComboBoxText +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + float:left; +} + +.ComboBoxIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopup +{ + position: fixed; + + background: #CCC; + + border-radius: 5px; + + padding: 1px 0 1px 0; +} + +.ComboBoxPopupItem +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + padding: 1px 1px 1px 5px; + + border-bottom: 1px solid #AAA; + border-top: 1px solid #FFF; +} + +.ComboBoxPopupItemText +{ + float:left; +} + +.ComboBoxPopupItemIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopupItem:first-child +{ + border-top: 0px; +} + +.ComboBoxPopupItem:last-child +{ + border-bottom: 0px; +} + +.ComboBoxPopupItem:hover +{ + color:#FFF; + background: #2036E1; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Grid Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + +.Grid { + overflow: auto; + background: #333; + height: 100%; + border-radius: 2px; +} + +.GridBody +{ + overflow-x: auto; + overflow-y: auto; + height: inherit; +} + +.GridRow +{ + display: inline-block; + white-space: nowrap; + + background:rgb(48, 48, 48); + + color: #BBB; + font: 9px Verdana; + + padding: 2px; +} + +.GridRow.GridGroup +{ + padding: 0px; +} + +.GridRow:nth-child(odd) +{ + background:#333; +} + +.GridRowCell +{ + display: inline-block; +} +.GridRowCell.GridGroup +{ + color: #BBB; + + /* Override default from name */ + width: 100%; + + padding: 1px 1px 1px 2px; + border: 1px solid; + border-radius: 2px; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; +} + +.GridRowBody +{ + /* Clip all contents for show/hide group*/ + overflow: hidden; + + /* Crazy CSS rules: controls for properties don't clip if this isn't set on this parent */ + position: relative; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Button Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Button +{ + /* Position relative to the parent window */ + position:absolute; + + border-radius:4px; + + /* Padding at the top includes 2px for the text drop-shadow */ + padding: 2px 5px 3px 5px; + + color: #BBB; + font: 9px Verdana; + text-shadow: 1px 1px 1px black; + text-align: center; + + background-color:#555; + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.Button:hover { + background-color: #616161; +} + +.Button.ButtonHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} diff --git a/profiler/vis/index.html b/profiler/vis/index.html new file mode 100644 index 0000000..fa8c324 --- /dev/null +++ b/profiler/vis/index.html @@ -0,0 +1,69 @@ + + + + + + Remotery Viewer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/human.blend b/res/human.blend new file mode 100644 index 0000000..261a5b3 Binary files /dev/null and b/res/human.blend differ diff --git a/res/human_rigged.mtl b/res/human_rigged.mtl new file mode 100644 index 0000000..4115ceb --- /dev/null +++ b/res/human_rigged.mtl @@ -0,0 +1,13 @@ +# Blender MTL File: 'human.blend' +# Material Count: 1 + +newmtl Material.001 +Ns 225.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.800000 0.800000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 +map_Kd uv.png diff --git a/res/human_rigged.obj b/res/human_rigged.obj new file mode 100644 index 0000000..9bc63dd --- /dev/null +++ b/res/human_rigged.obj @@ -0,0 +1,3720 @@ +# Blender v2.82 (sub 7) OBJ File: 'human.blend' +# www.blender.org +mtllib human_rigged.mtl +o Player_Cube +v 0.037076 1.858509 -0.084616 +v 0.000000 1.825845 -0.100783 +v 0.000000 1.867147 -0.094439 +v 0.039519 1.890449 -0.067669 +v 0.061775 1.877816 -0.054881 +v 0.066135 1.850885 -0.061687 +v 0.033565 1.818892 -0.089486 +v 0.085912 1.855764 0.039931 +v 0.097455 1.817449 0.002158 +v 0.094648 1.865329 0.001769 +v 0.070192 1.893718 0.043494 +v 0.058441 1.875653 0.070268 +v 0.065973 1.844247 0.075376 +v 0.088766 1.806105 0.039347 +v 0.034800 1.666445 0.068535 +v 0.000000 1.675269 0.017918 +v 0.038151 1.684552 0.017918 +v 0.063939 1.698741 0.048145 +v 0.052652 1.702676 0.073004 +v 0.030845 1.675051 0.092854 +v 0.000000 1.659269 0.071886 +v 0.042763 1.927662 0.002906 +v 0.038718 1.916177 0.049178 +v 0.000000 1.936975 0.002960 +v 0.000000 1.923371 0.052538 +v 0.035902 1.887032 0.086918 +v 0.042479 1.916110 -0.037283 +v 0.000000 1.899221 -0.074408 +v 0.000000 1.925530 -0.040662 +v 0.076652 1.903402 0.002615 +v 0.072991 1.893374 -0.032625 +v 0.033948 1.673998 -0.063213 +v 0.000000 1.718847 -0.064986 +v 0.032826 1.720687 -0.058351 +v 0.048323 1.695466 -0.032039 +v 0.053536 1.733205 -0.052279 +v 0.063281 1.719973 -0.028153 +v 0.070087 1.710983 0.008846 +v 0.032382 1.735482 0.102615 +v 0.060916 1.736689 0.076695 +v 0.035348 1.798795 0.103635 +v 0.000000 1.737518 0.113529 +v 0.000000 1.672284 0.099771 +v 0.035680 1.848587 0.101735 +v 0.000000 1.854796 0.112649 +v 0.000000 1.804374 0.116655 +v 0.081755 1.748453 0.042592 +v 0.089593 1.761058 0.005372 +v 0.066376 1.795044 0.074543 +v 0.079124 1.764787 -0.029898 +v 0.056970 1.765367 -0.060512 +v 0.062784 1.811619 -0.063988 +v 0.086777 1.814971 -0.032882 +v 0.087015 1.858199 -0.032515 +v 0.029751 1.768525 -0.083765 +v 0.000000 1.770748 -0.093550 +v 0.020370 0.982926 -0.108391 +v 0.000000 1.052011 -0.105345 +v 0.039687 1.061181 -0.111213 +v 0.068480 1.507587 0.091218 +v 0.000000 1.586915 0.038773 +v 0.000000 1.511507 0.091218 +v 0.000000 1.635574 -0.075169 +v 0.050313 1.639147 -0.075169 +v 0.012816 0.947773 -0.012744 +v 0.000000 0.954027 0.083055 +v 0.000000 0.947283 -0.013179 +v 0.045001 1.062460 0.102852 +v 0.000000 1.167043 0.113534 +v 0.000000 1.052924 0.103167 +v 0.048474 1.177487 0.113428 +v 0.000000 1.309757 0.115343 +v 0.067376 1.425237 0.119270 +v 0.000000 1.388329 0.111737 +v 0.065684 1.386033 0.104795 +v 0.056626 1.515658 -0.131944 +v 0.000000 1.439421 -0.124429 +v 0.000000 1.521401 -0.119433 +v 0.043219 1.315791 -0.087856 +v 0.000000 1.390508 -0.094629 +v 0.051329 1.386848 -0.105784 +v 0.038092 1.176026 -0.083757 +v 0.000000 1.309757 -0.075345 +v 0.143853 1.131735 -0.003491 +v 0.130719 1.193312 0.056201 +v 0.130590 1.121384 0.051184 +v 0.141455 1.199363 0.003221 +v 0.146290 1.330464 0.057512 +v 0.171144 1.392860 -0.009314 +v 0.178751 1.438988 0.043976 +v 0.159270 1.397239 0.043546 +v 0.107164 1.189638 -0.071986 +v 0.121445 1.318938 -0.074093 +v 0.132319 1.385987 -0.092787 +v 0.145248 1.425703 -0.121872 +v 0.146352 1.509305 -0.117818 +v 0.133476 1.389902 0.082347 +v 0.109817 1.182256 0.088620 +v 0.051881 1.319843 0.115343 +v 0.099590 1.094447 0.081829 +v 0.180236 0.177917 -0.022872 +v 0.178287 0.083456 -0.054487 +v 0.190416 0.179836 -0.056543 +v 0.090992 1.610316 -0.031982 +v 0.149859 1.581663 0.028624 +v 0.151054 1.589990 -0.032092 +v 0.146758 1.508477 0.071408 +v 0.074576 1.599748 0.038773 +v 0.087355 1.016714 -0.104260 +v 0.095435 1.093395 -0.094011 +v 0.057342 1.662275 -0.034688 +v 0.067174 1.632837 -0.032770 +v 0.000000 1.614985 0.017448 +v 0.040983 1.640008 0.013126 +v 0.000000 1.640008 0.013126 +v 0.172052 0.083346 -0.031918 +v 0.178569 0.042725 -0.046631 +v 0.140246 0.179287 -0.095317 +v 0.166619 0.082795 -0.069996 +v 0.141414 0.080563 -0.074057 +v 0.097427 0.177699 -0.035774 +v 0.120696 0.081839 -0.058637 +v 0.115271 0.082355 -0.033570 +v 0.151023 0.176025 -0.004768 +v 0.128705 0.082744 -0.016578 +v 0.152650 0.081337 -0.019070 +v 0.031565 0.960214 0.084589 +v 0.096315 0.948373 0.079854 +v 0.089856 1.005797 0.075096 +v 0.028274 0.907513 -0.087748 +v 0.012054 0.897561 -0.006707 +v 0.153073 0.986991 -0.072822 +v 0.090137 0.944725 -0.094233 +v 0.134082 1.049541 0.044828 +v 0.162354 1.004200 -0.016891 +v 0.149577 1.066464 -0.016823 +v 0.000000 1.172886 -0.071110 +v 0.060468 0.706027 0.034538 +v 0.120889 0.592339 0.034112 +v 0.116964 0.711516 0.053278 +v 0.041063 0.705382 -0.012297 +v 0.073096 0.592201 -0.054571 +v 0.061484 0.589926 -0.016888 +v 0.108994 0.713292 -0.082379 +v 0.157659 0.595641 -0.055651 +v 0.114564 0.593107 -0.074420 +v 0.178406 0.719523 -0.021374 +v 0.154334 0.594562 0.010947 +v 0.167678 0.595511 -0.025208 +v 0.087104 0.547567 0.020872 +v 0.132982 0.510242 0.022818 +v 0.127677 0.550863 0.036910 +v 0.082833 0.550508 -0.050901 +v 0.081687 0.505476 -0.021846 +v 0.071885 0.547362 -0.014926 +v 0.121244 0.554107 -0.068883 +v 0.162695 0.518306 -0.058489 +v 0.126807 0.514661 -0.076459 +v 0.170287 0.557056 -0.020399 +v 0.160394 0.516265 0.002091 +v 0.171972 0.518815 -0.030709 +v 0.146035 0.991090 0.047219 +v 0.181229 0.830372 -0.016883 +v 0.167895 0.829903 -0.066160 +v 0.100456 0.820757 -0.085342 +v 0.037695 0.812711 -0.061387 +v 0.022504 0.808682 -0.007694 +v 0.034417 0.906084 0.068338 +v 0.109503 0.818100 0.067795 +v 0.142096 0.405704 0.019871 +v 0.104716 0.300794 0.003246 +v 0.150750 0.301437 0.012032 +v 0.082249 0.404416 -0.027454 +v 0.099529 0.307596 -0.075145 +v 0.087724 0.303741 -0.033706 +v 0.178616 0.414121 -0.074864 +v 0.144021 0.308745 -0.105889 +v 0.135341 0.411843 -0.093831 +v 0.189148 0.412937 -0.040933 +v 0.187729 0.303001 -0.008573 +v 0.201640 0.306680 -0.050592 +v 0.157932 0.554853 0.015013 +v 0.093824 0.407858 -0.066977 +v 0.175804 0.409676 -0.001765 +v 0.162980 0.827153 0.039613 +v 0.091993 0.508196 -0.055905 +v 0.054958 0.708650 -0.059670 +v 0.162027 0.717362 0.026658 +v 0.134402 1.127129 -0.060022 +v 0.134012 1.201417 -0.042605 +v 0.106353 0.179409 -0.070948 +v 0.181537 0.050874 -0.001796 +v 0.137268 1.051908 -0.076227 +v 0.199752 1.502164 0.034010 +v 0.206891 1.560636 0.006467 +v 0.210706 1.564382 -0.031973 +v 0.123113 1.320690 0.090250 +v 0.146328 1.431696 0.094041 +v 0.184875 1.444427 -0.077735 +v 0.199752 1.497767 -0.085108 +v 0.150594 1.332191 -0.043932 +v 0.164117 1.400839 -0.058144 +v 0.205874 1.553365 -0.072699 +v 0.153518 0.056105 0.009394 +v 0.077325 0.590231 0.019853 +v 0.160589 0.557302 -0.050460 +v 0.190799 0.309451 -0.087627 +v 0.098696 0.403181 0.008866 +v 0.044093 0.809621 0.045905 +v 0.096111 0.506065 0.010582 +v 0.166298 0.719347 -0.062704 +v 0.114164 0.176421 -0.007255 +v 0.109556 0.042544 -0.031665 +v 0.053293 1.618930 0.017448 +v 0.149666 1.577764 -0.096880 +v 0.178193 0.180888 -0.084046 +v 0.193150 1.429977 -0.018378 +v 0.157828 1.332308 0.003275 +v 0.000000 0.973274 -0.106683 +v 0.075267 1.599556 -0.107606 +v 0.000000 1.600658 -0.091052 +v 0.000000 1.435494 0.131781 +v 0.333171 1.274545 -0.028654 +v 0.337102 1.235137 -0.008053 +v 0.343983 1.227675 -0.027888 +v 0.326539 1.265639 -0.055879 +v 0.337363 1.219528 -0.050658 +v 0.268650 1.241445 0.007365 +v 0.312671 1.225966 0.007033 +v 0.300043 1.261324 0.013689 +v 0.326144 1.280990 -0.004441 +v 0.252759 1.511879 0.006463 +v 0.257921 1.513270 -0.031976 +v 0.250479 1.503346 -0.072703 +v 0.230949 1.441668 -0.085112 +v 0.206557 1.397433 -0.073726 +v 0.202700 1.381067 -0.018382 +v 0.204126 1.393117 0.028953 +v 0.233107 1.445500 0.034006 +v 0.374483 1.108711 -0.005265 +v 0.405592 0.975664 0.055032 +v 0.397119 0.967292 0.042831 +v 0.352413 1.114480 0.053118 +v 0.365745 0.988610 0.091579 +v 0.386505 0.985787 0.086582 +v 0.403681 0.989432 0.072381 +v 0.376614 1.123344 0.032048 +v 0.319205 1.088516 0.023141 +v 0.323471 1.102856 0.049006 +v 0.303963 1.241609 -0.063972 +v 0.316592 1.204550 -0.057371 +v 0.275566 1.226939 -0.058908 +v 0.290516 1.196465 -0.053851 +v 0.280164 1.200581 -0.023771 +v 0.265265 1.227775 -0.022685 +v 0.284213 1.213657 0.000858 +v 0.414212 0.964803 0.055253 +v 0.412026 0.973783 0.079264 +v 0.363237 0.969640 0.105161 +v 0.379487 1.112823 0.011092 +v 0.350402 1.090071 -0.012357 +v 0.373129 0.956933 0.044996 +v 0.360628 0.963643 0.057675 +v 0.325747 1.082409 -0.008639 +v 0.359146 0.976650 0.078521 +v 0.291224 1.164660 -0.008860 +v 0.300772 1.159758 -0.040633 +v 0.327402 1.168020 -0.044350 +v 0.359898 1.193805 -0.013208 +v 0.295497 1.178637 0.017105 +v 0.325201 1.191473 0.023676 +v 0.354016 1.200807 0.006887 +v 0.353533 1.184729 -0.037260 +v 0.229443 1.319787 0.021299 +v 0.237087 1.315535 -0.068834 +v 0.228565 1.306509 -0.020097 +v 0.267891 1.347431 -0.077680 +v 0.292262 1.394527 -0.066767 +v 0.291535 1.406689 0.002761 +v 0.264592 1.358988 0.027066 +v 0.297883 1.404279 -0.030622 +v 0.393159 0.918100 0.065712 +v 0.374491 0.947049 0.036986 +v 0.402215 0.909147 0.036536 +v 0.400452 0.953765 0.038707 +v 0.420338 0.911992 0.036536 +v 0.421809 0.942435 0.114019 +v 0.430139 0.933824 0.089865 +v 0.396517 0.975009 0.099440 +v 0.391403 0.948536 0.116254 +v 0.364768 0.809637 0.134272 +v 0.375102 0.827372 0.155605 +v 0.367934 0.829079 0.155989 +v 0.401980 0.867710 0.139132 +v 0.407131 0.900838 0.125081 +v 0.392369 0.867532 0.139132 +v 0.405938 0.855734 0.114294 +v 0.419184 0.902943 0.125081 +v 0.407506 0.868789 0.047606 +v 0.401281 0.831549 0.064618 +v 0.392668 0.831389 0.064618 +v 0.402109 0.878247 0.076171 +v 0.388098 0.847713 0.094397 +v 0.375877 0.819428 0.078459 +v 0.364515 0.801125 0.107089 +v 0.382301 0.817898 0.078116 +v 0.377876 0.806516 0.133571 +v 0.424148 0.892953 0.100626 +v 0.418307 0.870675 0.047606 +v 0.384687 0.934725 0.091765 +v 0.402109 0.889104 0.100626 +v 0.424148 0.882095 0.076171 +v 0.430139 0.923906 0.065712 +v 0.405938 0.843685 0.089456 +v 0.377623 0.798004 0.106388 +v 0.388364 0.855408 0.114294 +v 0.358609 0.958574 0.083464 +v 0.362291 0.957040 0.055253 +v 0.363997 0.862320 0.152558 +v 0.359513 0.906270 0.123986 +v 0.360572 0.855419 0.140973 +v 0.373037 0.856505 0.140973 +v 0.380948 0.903451 0.123986 +v 0.372626 0.861214 0.152558 +v 0.382013 0.908809 0.139073 +v 0.363866 0.911628 0.139073 +v 0.111999 0.005822 -0.032681 +v 0.116540 0.037087 -0.069658 +v 0.119000 0.007747 -0.074214 +v 0.135313 0.035888 -0.081484 +v 0.158957 0.007702 -0.081761 +v 0.136749 0.008415 -0.083994 +v 0.213258 0.002518 0.053796 +v 0.217888 0.011298 0.102320 +v 0.220810 -0.000024 0.106187 +v 0.176998 -0.000009 0.124057 +v 0.130977 0.011298 0.118907 +v 0.129684 -0.000024 0.123578 +v 0.165254 -0.012866 0.062522 +v 0.216161 -0.010486 0.102100 +v 0.175020 -0.012182 0.113693 +v 0.165040 0.028835 0.061399 +v 0.175162 0.012325 0.114438 +v 0.174760 -0.008299 -0.043327 +v 0.154775 -0.006893 0.007613 +v 0.146137 -0.011830 -0.037824 +v 0.197111 0.009794 -0.001330 +v 0.179697 0.005835 -0.046009 +v 0.122773 0.023879 0.069000 +v 0.112121 0.009794 0.014890 +v 0.117411 0.002518 0.072089 +v 0.159014 0.037380 -0.077995 +v 0.158664 -0.005692 -0.077254 +v 0.138552 -0.007248 -0.075814 +v 0.121149 -0.005844 -0.069945 +v 0.117557 -0.008308 -0.032138 +v 0.118886 -0.003729 0.014190 +v 0.207135 0.023879 0.052900 +v 0.132501 -0.010486 0.118067 +v 0.124667 -0.009489 0.069702 +v 0.190563 -0.003729 0.000510 +v 0.205632 -0.009489 0.054249 +v 0.122019 0.050480 0.010598 +v 0.046760 1.429164 -0.136940 +v -0.037076 1.858509 -0.084616 +v -0.033565 1.818892 -0.089486 +v -0.039519 1.890449 -0.067669 +v -0.061775 1.877816 -0.054881 +v -0.066135 1.850885 -0.061687 +v -0.085912 1.855764 0.039931 +v -0.097455 1.817449 0.002158 +v -0.088766 1.806105 0.039347 +v -0.070192 1.893718 0.043494 +v -0.094648 1.865329 0.001769 +v -0.058441 1.875653 0.070268 +v -0.065973 1.844247 0.075376 +v -0.034800 1.666445 0.068535 +v -0.063939 1.698741 0.048145 +v -0.038151 1.684552 0.017918 +v -0.052652 1.702676 0.073004 +v -0.030845 1.675051 0.092854 +v -0.042763 1.927662 0.002906 +v -0.038718 1.916177 0.049178 +v -0.035902 1.887032 0.086918 +v -0.042479 1.916110 -0.037283 +v -0.072991 1.893374 -0.032625 +v -0.076652 1.903402 0.002615 +v -0.033948 1.673998 -0.063213 +v -0.032826 1.720687 -0.058351 +v -0.048323 1.695466 -0.032039 +v -0.053536 1.733205 -0.052279 +v -0.070087 1.710983 0.008846 +v -0.063281 1.719973 -0.028153 +v -0.057342 1.662275 -0.034688 +v -0.032382 1.735482 0.102615 +v -0.035348 1.798795 0.103635 +v -0.060916 1.736689 0.076695 +v -0.035680 1.848587 0.101735 +v -0.081755 1.748453 0.042592 +v -0.089593 1.761058 0.005372 +v -0.066376 1.795044 0.074543 +v -0.079124 1.764787 -0.029898 +v -0.062784 1.811619 -0.063988 +v -0.056970 1.765367 -0.060512 +v -0.086777 1.814971 -0.032882 +v -0.087015 1.858199 -0.032515 +v -0.029751 1.768525 -0.083765 +v -0.020370 0.982925 -0.108391 +v -0.039687 1.061181 -0.111213 +v -0.068480 1.507587 0.091218 +v -0.074576 1.599748 0.038773 +v -0.050313 1.639147 -0.075169 +v -0.012816 0.947773 -0.012744 +v -0.031565 0.960214 0.084589 +v -0.045001 1.062461 0.102852 +v -0.048474 1.177487 0.113428 +v -0.051881 1.319843 0.115343 +v -0.067376 1.425237 0.119270 +v -0.065685 1.386033 0.104795 +v -0.056626 1.515658 -0.131944 +v -0.046760 1.429164 -0.136940 +v -0.043219 1.315791 -0.087856 +v -0.051329 1.386848 -0.105784 +v -0.038092 1.176026 -0.083757 +v -0.143853 1.131735 -0.003491 +v -0.130719 1.193312 0.056201 +v -0.141455 1.199363 0.003221 +v -0.146290 1.330464 0.057512 +v -0.157828 1.332308 0.003275 +v -0.171144 1.392860 -0.009314 +v -0.178751 1.438988 0.043976 +v -0.193150 1.429977 -0.018378 +v -0.107164 1.189638 -0.071986 +v -0.121445 1.318938 -0.074093 +v -0.132319 1.385987 -0.092787 +v -0.145248 1.425703 -0.121872 +v -0.146352 1.509305 -0.117818 +v -0.133476 1.389902 0.082347 +v -0.146328 1.431696 0.094041 +v -0.109817 1.182256 0.088620 +v -0.123113 1.320690 0.090250 +v -0.099590 1.094447 0.081829 +v -0.178288 0.083456 -0.054487 +v -0.180236 0.177917 -0.022871 +v -0.190416 0.179836 -0.056542 +v -0.149859 1.581663 0.028624 +v -0.090992 1.610316 -0.031982 +v -0.151054 1.589990 -0.032092 +v -0.146758 1.508477 0.071408 +v -0.087355 1.016714 -0.104260 +v -0.095435 1.093394 -0.094011 +v -0.040983 1.640008 0.013126 +v -0.067174 1.632837 -0.032770 +v -0.053293 1.618930 0.017448 +v -0.172052 0.083346 -0.031918 +v -0.178569 0.042725 -0.046631 +v -0.181537 0.050874 -0.001796 +v -0.140246 0.179288 -0.095315 +v -0.166619 0.082796 -0.069996 +v -0.178193 0.180888 -0.084045 +v -0.097428 0.177699 -0.035773 +v -0.120696 0.081839 -0.058636 +v -0.106353 0.179409 -0.070947 +v -0.151023 0.176024 -0.004767 +v -0.128705 0.082744 -0.016578 +v -0.114164 0.176421 -0.007254 +v -0.096315 0.948374 0.079855 +v -0.089856 1.005798 0.075096 +v -0.028274 0.907513 -0.087747 +v -0.153073 0.986990 -0.072822 +v -0.137269 1.051908 -0.076227 +v -0.162354 1.004200 -0.016891 +v -0.134082 1.049541 0.044828 +v -0.149577 1.066464 -0.016824 +v -0.130590 1.121384 0.051184 +v -0.120890 0.592339 0.034116 +v -0.060468 0.706027 0.034541 +v -0.116964 0.711516 0.053281 +v -0.041064 0.705382 -0.012293 +v -0.073097 0.592200 -0.054566 +v -0.054959 0.708650 -0.059666 +v -0.108995 0.713291 -0.082376 +v -0.157660 0.595641 -0.055647 +v -0.166299 0.719346 -0.062701 +v -0.178406 0.719522 -0.021372 +v -0.154335 0.594562 0.010951 +v -0.162027 0.717362 0.026661 +v -0.132983 0.510241 0.022823 +v -0.087105 0.547567 0.020877 +v -0.127678 0.550863 0.036915 +v -0.081688 0.505476 -0.021842 +v -0.082834 0.550508 -0.050896 +v -0.071886 0.547362 -0.014921 +v -0.121245 0.554107 -0.068878 +v -0.162697 0.518306 -0.058484 +v -0.160590 0.557302 -0.050456 +v -0.170289 0.557056 -0.020394 +v -0.160395 0.516264 0.002096 +v -0.157933 0.554852 0.015018 +v -0.181230 0.830372 -0.016881 +v -0.146035 0.991090 0.047219 +v -0.090137 0.944724 -0.094232 +v -0.167895 0.829902 -0.066158 +v -0.012054 0.897561 -0.006706 +v -0.037695 0.812710 -0.061384 +v -0.109503 0.818100 0.067797 +v -0.034417 0.906084 0.068339 +v -0.142096 0.405704 0.019875 +v -0.104717 0.300794 0.003248 +v -0.098696 0.403181 0.008870 +v -0.082250 0.404416 -0.027450 +v -0.099529 0.307597 -0.075143 +v -0.093825 0.407858 -0.066973 +v -0.144022 0.308746 -0.105886 +v -0.178617 0.414121 -0.074860 +v -0.135342 0.411844 -0.093827 +v -0.189149 0.412937 -0.040930 +v -0.187730 0.303000 -0.008571 +v -0.175805 0.409675 -0.001762 +v -0.167679 0.595511 -0.025203 +v -0.114565 0.593106 -0.074416 +v -0.061485 0.589925 -0.016884 +v -0.077326 0.590231 0.019857 +v -0.150750 0.301437 0.012034 +v -0.162980 0.827153 0.039615 +v -0.100457 0.820756 -0.085340 +v -0.091994 0.508196 -0.055900 +v -0.134402 1.127129 -0.060023 +v -0.134012 1.201417 -0.042606 +v -0.141414 0.080564 -0.074057 +v -0.152650 0.081337 -0.019070 +v -0.153518 0.056105 0.009394 +v -0.199752 1.502164 0.034010 +v -0.206891 1.560636 0.006466 +v -0.210706 1.564382 -0.031973 +v -0.159270 1.397239 0.043546 +v -0.184875 1.444427 -0.077735 +v -0.199752 1.497767 -0.085108 +v -0.150594 1.332191 -0.043932 +v -0.164117 1.400839 -0.058144 +v -0.205874 1.553365 -0.072699 +v -0.149666 1.577764 -0.096880 +v -0.122019 0.050480 0.010598 +v -0.201640 0.306680 -0.050590 +v -0.087724 0.303741 -0.033703 +v -0.044093 0.809621 0.045907 +v -0.171973 0.518815 -0.030704 +v -0.096112 0.506065 0.010586 +v -0.115271 0.082355 -0.033570 +v -0.109556 0.042544 -0.031665 +v -0.075267 1.599556 -0.107606 +v -0.333171 1.274545 -0.028654 +v -0.337102 1.235137 -0.008053 +v -0.326144 1.280990 -0.004441 +v -0.326539 1.265639 -0.055879 +v -0.343983 1.227675 -0.027888 +v -0.312671 1.225966 0.007033 +v -0.268650 1.241445 0.007365 +v -0.300043 1.261324 0.013689 +v -0.252759 1.511879 0.006463 +v -0.250479 1.503346 -0.072703 +v -0.230949 1.441668 -0.085112 +v -0.206558 1.397433 -0.073726 +v -0.204126 1.393117 0.028953 +v -0.233107 1.445500 0.034006 +v -0.257921 1.513270 -0.031976 +v -0.374483 1.108711 -0.005265 +v -0.405592 0.975664 0.055032 +v -0.379487 1.112823 0.011092 +v -0.352413 1.114480 0.053118 +v -0.365745 0.988610 0.091579 +v -0.323471 1.102856 0.049007 +v -0.403681 0.989432 0.072381 +v -0.376614 1.123344 0.032048 +v -0.319205 1.088516 0.023141 +v -0.303963 1.241609 -0.063972 +v -0.337364 1.219528 -0.050658 +v -0.275566 1.226939 -0.058908 +v -0.316592 1.204550 -0.057370 +v -0.280164 1.200581 -0.023771 +v -0.265265 1.227775 -0.022685 +v -0.284213 1.213657 0.000858 +v -0.397119 0.967292 0.042831 +v -0.414212 0.964803 0.055253 +v -0.412026 0.973783 0.079264 +v -0.386505 0.985787 0.086582 +v -0.396517 0.975009 0.099440 +v -0.363237 0.969640 0.105161 +v -0.350402 1.090071 -0.012356 +v -0.360628 0.963643 0.057675 +v -0.325747 1.082409 -0.008639 +v -0.291224 1.164660 -0.008860 +v -0.300772 1.159758 -0.040632 +v -0.327402 1.168020 -0.044349 +v -0.353533 1.184729 -0.037260 +v -0.359898 1.193805 -0.013208 +v -0.354016 1.200807 0.006887 +v -0.295497 1.178637 0.017105 +v -0.325202 1.191473 0.023676 +v -0.229443 1.319787 0.021299 +v -0.202700 1.381067 -0.018382 +v -0.237087 1.315535 -0.068834 +v -0.267891 1.347431 -0.077680 +v -0.292263 1.394527 -0.066767 +v -0.291535 1.406689 0.002761 +v -0.264592 1.358988 0.027066 +v -0.297883 1.404279 -0.030622 +v -0.393159 0.918100 0.065712 +v -0.374491 0.947049 0.036986 +v -0.362291 0.957040 0.055253 +v -0.402215 0.909147 0.036536 +v -0.400452 0.953765 0.038707 +v -0.421809 0.942435 0.114019 +v -0.430139 0.933824 0.089865 +v -0.391403 0.948536 0.116254 +v -0.364768 0.809637 0.134272 +v -0.375102 0.827372 0.155605 +v -0.377876 0.806516 0.133571 +v -0.359146 0.976650 0.078521 +v -0.358609 0.958574 0.083464 +v -0.401980 0.867710 0.139132 +v -0.407131 0.900838 0.125081 +v -0.419184 0.902943 0.125081 +v -0.405938 0.855734 0.114294 +v -0.424148 0.892953 0.100626 +v -0.401281 0.831549 0.064618 +v -0.407506 0.868789 0.047606 +v -0.392668 0.831389 0.064618 +v -0.402109 0.878247 0.076171 +v -0.388098 0.847713 0.094397 +v -0.375877 0.819428 0.078459 +v -0.364515 0.801125 0.107089 +v -0.382301 0.817898 0.078116 +v -0.392369 0.867532 0.139132 +v -0.418307 0.870675 0.047606 +v -0.384687 0.934725 0.091765 +v -0.402109 0.889104 0.100626 +v -0.424148 0.882095 0.076171 +v -0.420338 0.911992 0.036536 +v -0.430139 0.923906 0.065712 +v -0.405938 0.843685 0.089456 +v -0.377623 0.798004 0.106388 +v -0.367934 0.829079 0.155989 +v -0.388364 0.855408 0.114294 +v -0.373129 0.956933 0.044996 +v -0.363997 0.862320 0.152558 +v -0.359513 0.906270 0.123986 +v -0.363866 0.911628 0.139073 +v -0.373037 0.856506 0.140973 +v -0.360572 0.855419 0.140973 +v -0.372626 0.861215 0.152558 +v -0.380948 0.903451 0.123986 +v -0.382014 0.908809 0.139073 +v -0.111999 0.005822 -0.032681 +v -0.116540 0.037087 -0.069658 +v -0.158958 0.007702 -0.081762 +v -0.135313 0.035888 -0.081484 +v -0.136749 0.008415 -0.083994 +v -0.213258 0.002518 0.053796 +v -0.217888 0.011298 0.102320 +v -0.207135 0.023879 0.052900 +v -0.176998 -0.000009 0.124057 +v -0.130977 0.011298 0.118907 +v -0.175162 0.012325 0.114438 +v -0.165254 -0.012866 0.062522 +v -0.216162 -0.010486 0.102100 +v -0.205632 -0.009489 0.054249 +v -0.165040 0.028835 0.061399 +v -0.122773 0.023879 0.069000 +v -0.154775 -0.006893 0.007613 +v -0.174760 -0.008299 -0.043328 +v -0.146137 -0.011830 -0.037824 +v -0.197111 0.009794 -0.001330 +v -0.179698 0.005835 -0.046009 +v -0.112121 0.009794 0.014890 +v -0.117411 0.002518 0.072089 +v -0.159015 0.037380 -0.077995 +v -0.158665 -0.005692 -0.077254 +v -0.138552 -0.007248 -0.075814 +v -0.121149 -0.005844 -0.069945 +v -0.117557 -0.008308 -0.032138 +v -0.132501 -0.010486 0.118067 +v -0.124667 -0.009489 0.069702 +v -0.220810 -0.000023 0.106187 +v -0.119000 0.007748 -0.074214 +v -0.118886 -0.003729 0.014190 +v -0.190564 -0.003729 0.000510 +v -0.175020 -0.012182 0.113693 +v -0.129685 -0.000023 0.123578 +v -0.228565 1.306509 -0.020097 +v -0.290516 1.196465 -0.053851 +v -0.126808 0.514661 -0.076455 +v -0.190799 0.309452 -0.087625 +v -0.022505 0.808682 -0.007692 +v 0.000000 1.893042 0.093835 +v 0.000000 1.674877 -0.063213 +vt 0.799479 0.097856 +vt 0.826749 0.078305 +vt 0.822867 0.108025 +vt 0.790997 0.115892 +vt 0.779065 0.107870 +vt 0.779221 0.093609 +vt 0.802765 0.074067 +vt 0.735083 0.116539 +vt 0.733264 0.086246 +vt 0.749099 0.108857 +vt 0.743643 0.128422 +vt 0.733462 0.132472 +vt 0.723756 0.127364 +vt 0.716713 0.104378 +vt 0.643797 0.123791 +vt 0.612869 0.102034 +vt 0.638723 0.091854 +vt 0.660803 0.106265 +vt 0.663419 0.120209 +vt 0.650704 0.134584 +vt 0.627395 0.138130 +vt 0.761560 0.136547 +vt 0.746642 0.140126 +vt 0.763754 0.151703 +vt 0.747093 0.151703 +vt 0.733035 0.141610 +vt 0.777101 0.129454 +vt 0.810755 0.131853 +vt 0.788963 0.151703 +vt 0.756568 0.123074 +vt 0.768121 0.115461 +vt 0.852747 0.454877 +vt 0.827266 0.443658 +vt 0.839914 0.435285 +vt 0.648106 0.056047 +vt 0.673206 0.039541 +vt 0.667835 0.058653 +vt 0.665331 0.083141 +vt 0.857904 0.435314 +vt 0.678464 0.137007 +vt 0.680183 0.119736 +vt 0.704183 0.137826 +vt 0.679106 0.151706 +vt 0.644123 0.151710 +vt 0.721002 0.140441 +vt 0.721456 0.151704 +vt 0.705247 0.151705 +vt 0.687778 0.100975 +vt 0.697194 0.081119 +vt 0.706039 0.121154 +vt 0.749892 0.045041 +vt 0.770688 0.019779 +vt 0.775657 0.042263 +vt 0.778741 0.071577 +vt 0.756021 0.077791 +vt 0.699982 0.057626 +vt 0.762177 0.100020 +vt 0.799453 0.041470 +vt 0.806258 0.000312 +vt 0.821006 0.039360 +vt 0.783727 0.007986 +vt 0.561223 0.495110 +vt 0.546190 0.536601 +vt 0.527004 0.529492 +vt 0.237739 0.632014 +vt 0.287481 0.666945 +vt 0.240042 0.666945 +vt 0.843053 0.479372 +vt 0.867251 0.471136 +vt 0.312783 0.390761 +vt 0.330658 0.439442 +vt 0.306591 0.393739 +vt 0.005244 0.643652 +vt 0.059232 0.666945 +vt 0.000312 0.666945 +vt 0.064597 0.642251 +vt 0.132168 0.666945 +vt 0.193547 0.633571 +vt 0.172805 0.666945 +vt 0.172319 0.632354 +vt 0.577407 0.686986 +vt 0.538456 0.658910 +vt 0.580822 0.658910 +vt 0.469165 0.683356 +vt 0.508353 0.658910 +vt 0.509031 0.686527 +vt 0.392439 0.680349 +vt 0.464852 0.658910 +vt 0.035930 0.559758 +vt 0.070711 0.588184 +vt 0.033353 0.589187 +vt 0.071084 0.560047 +vt 0.141725 0.580366 +vt 0.869036 0.590039 +vt 0.825843 0.584171 +vt 0.852803 0.570196 +vt 0.398713 0.718761 +vt 0.470609 0.727755 +vt 0.509733 0.730544 +vt 0.535502 0.732085 +vt 0.576409 0.730972 +vt 0.174739 0.593373 +vt 0.066495 0.608431 +vt 0.137304 0.640271 +vt 0.020984 0.612784 +vt 0.526508 0.058172 +vt 0.552152 0.017220 +vt 0.542052 0.062734 +vt 0.326547 0.609477 +vt 0.285483 0.587136 +vt 0.316242 0.577890 +vt 0.241155 0.590429 +vt 0.292540 0.627440 +vt 0.514862 0.497942 +vt 0.493965 0.532104 +vt 0.871754 0.415887 +vt 0.871759 0.445855 +vt 0.886700 0.454224 +vt 0.907833 0.400180 +vt 0.894382 0.423412 +vt 0.894939 0.400180 +vt 0.928602 0.125112 +vt 0.923430 0.102825 +vt 0.938505 0.118630 +vt 0.573289 0.066643 +vt 0.561704 0.018469 +vt 0.574681 0.018220 +vt 0.610322 0.064004 +vt 0.587786 0.018029 +vt 0.600585 0.015920 +vt 0.511337 0.052635 +vt 0.518664 0.005858 +vt 0.530121 0.009659 +vt 0.350080 0.435941 +vt 0.391570 0.434191 +vt 0.386689 0.466241 +vt 0.607435 0.457735 +vt 0.563584 0.446183 +vt 0.605226 0.432822 +vt 0.484618 0.470098 +vt 0.525478 0.459564 +vt 0.415492 0.491466 +vt 0.454072 0.472338 +vt 0.447896 0.504533 +vt 0.347882 0.708289 +vt 0.360360 0.497447 +vt 0.408920 0.527900 +vt 0.338510 0.494629 +vt 0.330969 0.679963 +vt 0.391690 0.658911 +vt 0.406101 0.302378 +vt 0.454998 0.253975 +vt 0.433673 0.315237 +vt 0.593172 0.335394 +vt 0.568407 0.275562 +vt 0.588914 0.275510 +vt 0.537320 0.335763 +vt 0.517202 0.267353 +vt 0.543716 0.271546 +vt 0.484398 0.330614 +vt 0.477468 0.259115 +vt 0.499136 0.263593 +vt 0.441678 0.226481 +vt 0.469460 0.210930 +vt 0.462063 0.232902 +vt 0.569971 0.252023 +vt 0.589716 0.231017 +vt 0.590396 0.253217 +vt 0.545306 0.248347 +vt 0.525345 0.221715 +vt 0.548199 0.224444 +vt 0.500636 0.241266 +vt 0.488857 0.216434 +vt 0.508727 0.219920 +vt 0.421156 0.460200 +vt 0.471884 0.385193 +vt 0.497299 0.390023 +vt 0.533888 0.392293 +vt 0.569924 0.392413 +vt 0.598750 0.388605 +vt 0.361848 0.402232 +vt 0.413805 0.366906 +vt 0.480910 0.155983 +vt 0.471677 0.102379 +vt 0.492793 0.108165 +vt 0.603816 0.179727 +vt 0.590840 0.126948 +vt 0.612245 0.127821 +vt 0.536644 0.166309 +vt 0.566338 0.123366 +vt 0.559252 0.170210 +vt 0.519727 0.163537 +vt 0.511213 0.112327 +vt 0.529611 0.116621 +vt 0.480456 0.237396 +vt 0.583032 0.175389 +vt 0.499958 0.159985 +vt 0.442840 0.378071 +vt 0.570833 0.227806 +vt 0.567769 0.337102 +vt 0.459015 0.324610 +vt 0.388700 0.513417 +vt 0.365202 0.733408 +vt 0.404428 0.740580 +vt 0.540851 0.014259 +vt 0.592651 0.066438 +vt 0.923702 0.135679 +vt 0.904194 0.120190 +vt 0.480212 0.504195 +vt 0.464249 0.539624 +vt 0.782342 0.575989 +vt 0.752381 0.540249 +vt 0.792465 0.545539 +vt 0.723511 0.549356 +vt 0.751105 0.570284 +vt 0.735167 0.577365 +vt 0.137710 0.601239 +vt 0.175789 0.567946 +vt 0.199166 0.591078 +vt 0.686809 0.645044 +vt 0.676490 0.593995 +vt 0.703326 0.611925 +vt 0.474838 0.750968 +vt 0.510594 0.755872 +vt 0.717489 0.586130 +vt 0.925286 0.147626 +vt 0.903226 0.138025 +vt 0.431217 0.246193 +vt 0.423991 0.219005 +vt 0.518973 0.244447 +vt 0.545437 0.120065 +vt 0.459483 0.150917 +vt 0.452245 0.096844 +vt 0.330983 0.372837 +vt 0.381561 0.351118 +vt 0.448342 0.204636 +vt 0.506225 0.334475 +vt 0.383519 0.289777 +vt 0.494639 0.046546 +vt 0.509230 0.000312 +vt 0.923096 0.171066 +vt 0.934003 0.154037 +vt 0.907260 0.430019 +vt 0.878207 0.400180 +vt 0.611050 0.732346 +vt 0.652315 0.713858 +vt 0.641324 0.742539 +vt 0.555357 0.065698 +vt 0.658025 0.660817 +vt 0.674644 0.680987 +vt 0.654647 0.683393 +vt 0.469885 0.774898 +vt 0.402036 0.765052 +vt 0.368087 0.761762 +vt 0.573988 0.500143 +vt 0.694774 0.564069 +vt 0.831950 0.545831 +vt 0.619376 0.695362 +vt 0.621893 0.658909 +vt 0.198405 0.666945 +vt 0.795437 0.717507 +vt 0.813866 0.734782 +vt 0.803657 0.742309 +vt 0.782700 0.725732 +vt 0.791684 0.748416 +vt 0.845159 0.706586 +vt 0.831295 0.729095 +vt 0.825471 0.708063 +vt 0.805839 0.709010 +vt 0.762454 0.597489 +vt 0.746814 0.605364 +vt 0.729829 0.614702 +vt 0.717573 0.645535 +vt 0.707762 0.672200 +vt 0.691402 0.697865 +vt 0.859406 0.606886 +vt 0.829971 0.621872 +vt 0.797745 0.608582 +vt 0.044193 0.868114 +vt 0.120717 0.860757 +vt 0.121961 0.869520 +vt 0.049124 0.831513 +vt 0.116876 0.827314 +vt 0.119278 0.838338 +vt 0.116753 0.849907 +vt 0.043808 0.847380 +vt 0.054661 0.800563 +vt 0.051150 0.815638 +vt 0.772492 0.739332 +vt 0.781362 0.756752 +vt 0.759897 0.749145 +vt 0.769354 0.762892 +vt 0.755397 0.770304 +vt 0.744572 0.759340 +vt 0.860759 0.710007 +vt 0.849773 0.723995 +vt 0.762744 0.827461 +vt 0.773142 0.814490 +vt 0.774103 0.829835 +vt 0.761175 0.846517 +vt 0.778249 0.851232 +vt 0.764186 0.867592 +vt 0.749927 0.873951 +vt 0.757490 0.876868 +vt 0.045527 0.859215 +vt 0.049552 0.883160 +vt 0.125435 0.882619 +vt 0.123926 0.892321 +vt 0.051445 0.896457 +vt 0.125435 0.806277 +vt 0.120751 0.818243 +vt 0.010734 0.918406 +vt 0.056113 0.912868 +vt 0.006402 0.901215 +vt 0.004341 0.886711 +vt 0.000899 0.856580 +vt 0.009269 0.794266 +vt 0.006493 0.809721 +vt 0.004683 0.827042 +vt 0.000443 0.844951 +vt 0.000312 0.870472 +vt 0.855480 0.630252 +vt 0.837262 0.663317 +vt 0.733046 0.710031 +vt 0.715798 0.727577 +vt 0.744555 0.691115 +vt 0.753536 0.665911 +vt 0.783695 0.646785 +vt 0.812387 0.656134 +vt 0.769145 0.655318 +vt 0.712131 0.902312 +vt 0.730619 0.927944 +vt 0.698647 0.920020 +vt 0.732825 0.792671 +vt 0.762534 0.809466 +vt 0.731437 0.804501 +vt 0.733954 0.852853 +vt 0.734068 0.838953 +vt 0.754398 0.862137 +vt 0.741129 0.869439 +vt 0.666442 0.848596 +vt 0.672897 0.849933 +vt 0.672937 0.848881 +vt 0.751635 0.880343 +vt 0.687863 0.850693 +vt 0.706694 0.859708 +vt 0.685451 0.853760 +vt 0.686736 0.839075 +vt 0.709818 0.852490 +vt 0.700157 0.797640 +vt 0.674790 0.812774 +vt 0.669853 0.810349 +vt 0.686689 0.887963 +vt 0.655982 0.882384 +vt 0.671441 0.870301 +vt 0.652939 0.867388 +vt 0.655772 0.847682 +vt 0.661811 0.824470 +vt 0.656641 0.824820 +vt 0.667050 0.846088 +vt 0.709634 0.837607 +vt 0.703625 0.804127 +vt 0.674411 0.903879 +vt 0.729391 0.887388 +vt 0.697478 0.874529 +vt 0.707410 0.822903 +vt 0.733347 0.824407 +vt 0.682035 0.827125 +vt 0.658679 0.841704 +vt 0.678581 0.863219 +vt 0.746235 0.883621 +vt 0.747253 0.877363 +vt 0.739617 0.927366 +vt 0.744424 0.918012 +vt 0.749508 0.918311 +vt 0.742158 0.871788 +vt 0.742374 0.871789 +vt 0.742172 0.871782 +vt 0.742179 0.871778 +vt 0.742299 0.871798 +vt 0.742170 0.871776 +vt 0.742068 0.871709 +vt 0.741501 0.873740 +vt 0.778249 0.794067 +vt 0.741913 0.871888 +vt 0.915919 0.188147 +vt 0.938731 0.182100 +vt 0.934891 0.195651 +vt 0.949129 0.083213 +vt 0.931873 0.076757 +vt 0.941358 0.071011 +vt 0.863044 0.105717 +vt 0.841255 0.117732 +vt 0.837341 0.112788 +vt 0.834916 0.139492 +vt 0.842706 0.160788 +vt 0.836380 0.161939 +vt 0.877674 0.017416 +vt 0.844064 0.018770 +vt 0.856577 0.000312 +vt 0.870170 0.138281 +vt 0.842153 0.139247 +vt 0.945304 0.160325 +vt 0.909587 0.063047 +vt 0.900273 0.036075 +vt 0.919246 0.051174 +vt 0.891933 0.098904 +vt 0.913480 0.086023 +vt 0.869388 0.160842 +vt 0.894329 0.176900 +vt 0.865207 0.171460 +vt 0.938588 0.090131 +vt 0.927333 0.071422 +vt 0.934933 0.064115 +vt 0.957400 0.165699 +vt 0.957400 0.105359 +vt 0.946563 0.112828 +vt 0.930830 0.201324 +vt 0.923671 0.220952 +vt 0.913556 0.210000 +vt 0.905980 0.236611 +vt 0.893610 0.222717 +vt 0.867770 0.116029 +vt 0.884968 0.255947 +vt 0.852196 0.256389 +vt 0.871088 0.240181 +vt 0.834871 0.117009 +vt 0.948608 0.187030 +vt 0.943647 0.200104 +vt 0.938030 0.207298 +vt 0.828722 0.118525 +vt 0.827373 0.139745 +vt 0.891818 0.184321 +vt 0.912871 0.195102 +vt 0.909723 0.079182 +vt 0.863877 0.178521 +vt 0.888986 0.091551 +vt 0.830141 0.160837 +vt 0.837868 0.106589 +vt 0.861328 0.098746 +vt 0.840003 0.172114 +vt 0.839101 0.165961 +vt 0.904284 0.156237 +vt 0.888362 0.050995 +vt 0.900601 0.462861 +vt 0.924745 0.436521 +vt 0.924520 0.400180 +vt 0.846664 0.496649 +vt 0.882781 0.495749 +vt 0.857659 0.668437 +vt 0.802466 0.765861 +vt 0.854588 0.745125 +vt 0.839582 0.750189 +vt 0.823681 0.754803 +vt 0.864900 0.723479 +vt 0.814379 0.760545 +vt 0.789428 0.774940 +vt 0.776945 0.781234 +vt 0.762308 0.789119 +vt 0.430490 0.198607 +vt 0.479749 0.039512 +vt 0.356297 0.335500 +vt 0.500348 0.777571 +vt 0.652939 0.625744 +vt 0.535599 0.683699 +vt 0.139670 0.551588 +vt 0.799480 0.205548 +vt 0.826749 0.225097 +vt 0.802766 0.229336 +vt 0.790998 0.187513 +vt 0.822866 0.195379 +vt 0.779066 0.195535 +vt 0.779223 0.209795 +vt 0.735086 0.186867 +vt 0.733267 0.217159 +vt 0.716717 0.199029 +vt 0.743645 0.174984 +vt 0.749102 0.194548 +vt 0.733463 0.170935 +vt 0.723758 0.176043 +vt 0.643804 0.179626 +vt 0.612883 0.201383 +vt 0.627403 0.165292 +vt 0.660810 0.197148 +vt 0.638734 0.211559 +vt 0.663425 0.183204 +vt 0.650708 0.168833 +vt 0.761560 0.166859 +vt 0.746642 0.163280 +vt 0.733036 0.161797 +vt 0.777101 0.173951 +vt 0.768122 0.187944 +vt 0.810755 0.171551 +vt 0.756569 0.180331 +vt 0.827266 0.356703 +vt 0.852747 0.345484 +vt 0.839914 0.365076 +vt 0.648119 0.247361 +vt 0.673218 0.263864 +vt 0.658409 0.272946 +vt 0.665341 0.220269 +vt 0.667846 0.244754 +vt 0.857904 0.365047 +vt 0.871759 0.354506 +vt 0.678466 0.166405 +vt 0.704184 0.165584 +vt 0.680187 0.183675 +vt 0.721003 0.162967 +vt 0.687784 0.202434 +vt 0.697200 0.222288 +vt 0.706042 0.182255 +vt 0.699990 0.245779 +vt 0.749895 0.258362 +vt 0.778743 0.231826 +vt 0.775660 0.261139 +vt 0.756023 0.225613 +vt 0.762179 0.203385 +vt 0.799454 0.261931 +vt 0.806260 0.303088 +vt 0.783731 0.295414 +vt 0.821007 0.264041 +vt 0.770691 0.283622 +vt 0.239913 0.003336 +vt 0.254945 0.044827 +vt 0.220726 0.010444 +vt 0.237739 0.701876 +vt 0.292540 0.706450 +vt 0.843053 0.320989 +vt 0.867251 0.329225 +vt 0.006504 0.149175 +vt 0.024380 0.100495 +vt 0.043802 0.103996 +vt 0.005245 0.690238 +vt 0.064597 0.691639 +vt 0.137304 0.693619 +vt 0.193547 0.700319 +vt 0.172319 0.701536 +vt 0.577407 0.630833 +vt 0.535599 0.634121 +vt 0.469164 0.634464 +vt 0.509031 0.631293 +vt 0.392439 0.637473 +vt 0.035930 0.774132 +vt 0.070711 0.745706 +vt 0.071084 0.773843 +vt 0.141725 0.753524 +vt 0.139670 0.782303 +vt 0.826642 0.494404 +vt 0.782121 0.495953 +vt 0.818555 0.476243 +vt 0.398712 0.599060 +vt 0.470608 0.590065 +vt 0.509732 0.587276 +vt 0.535501 0.585735 +vt 0.576409 0.586847 +vt 0.174739 0.740517 +vt 0.199166 0.742812 +vt 0.066495 0.725459 +vt 0.137710 0.732651 +vt 0.020984 0.721106 +vt 0.245873 0.522717 +vt 0.220229 0.481765 +vt 0.235774 0.477203 +vt 0.285483 0.746754 +vt 0.326547 0.724414 +vt 0.316242 0.756000 +vt 0.241155 0.743461 +vt 0.208584 0.041994 +vt 0.187688 0.007833 +vt 0.871754 0.384474 +vt 0.894382 0.376949 +vt 0.886700 0.346137 +vt 0.907260 0.370342 +vt 0.970889 0.688830 +vt 0.965717 0.711118 +vt 0.946481 0.693752 +vt 0.267011 0.473293 +vt 0.255426 0.521467 +vt 0.249079 0.474239 +vt 0.304043 0.475933 +vt 0.281508 0.521908 +vt 0.286372 0.473499 +vt 0.205058 0.487302 +vt 0.212385 0.534079 +vt 0.188361 0.493391 +vt 0.085292 0.105745 +vt 0.080411 0.073695 +vt 0.301157 0.082201 +vt 0.257306 0.093753 +vt 0.178340 0.069839 +vt 0.173934 0.035742 +vt 0.147794 0.067599 +vt 0.109215 0.048471 +vt 0.141619 0.035404 +vt 0.347880 0.609533 +vt 0.054083 0.042489 +vt 0.082422 0.026520 +vt 0.102642 0.012037 +vt 0.129667 0.003445 +vt 0.032233 0.045308 +vt 0.330968 0.637859 +vt 0.148720 0.285962 +vt 0.099822 0.237559 +vt 0.127395 0.224700 +vt 0.286895 0.204542 +vt 0.262130 0.264375 +vt 0.261491 0.202834 +vt 0.231042 0.204174 +vt 0.210924 0.272584 +vt 0.199947 0.205462 +vt 0.178120 0.209323 +vt 0.171190 0.280822 +vt 0.152737 0.215326 +vt 0.163182 0.329007 +vt 0.135400 0.313455 +vt 0.155784 0.307035 +vt 0.283438 0.308920 +vt 0.263693 0.287913 +vt 0.284118 0.286719 +vt 0.239028 0.291590 +vt 0.219066 0.318222 +vt 0.212695 0.295489 +vt 0.194357 0.298671 +vt 0.182579 0.323503 +vt 0.174177 0.302541 +vt 0.165606 0.154744 +vt 0.114879 0.079737 +vt 0.219200 0.080372 +vt 0.191022 0.149914 +vt 0.298948 0.107114 +vt 0.263646 0.147524 +vt 0.107527 0.173030 +vt 0.055570 0.137704 +vt 0.174631 0.383953 +vt 0.165399 0.437558 +vt 0.153204 0.389020 +vt 0.297538 0.360209 +vt 0.284561 0.412988 +vt 0.276754 0.364548 +vt 0.260059 0.416570 +vt 0.230366 0.373628 +vt 0.252973 0.369726 +vt 0.213448 0.376399 +vt 0.204935 0.427610 +vt 0.193680 0.379952 +vt 0.192858 0.276344 +vt 0.237438 0.268391 +vt 0.282636 0.264427 +vt 0.124939 0.293744 +vt 0.186514 0.431771 +vt 0.136562 0.161866 +vt 0.227610 0.147644 +vt 0.264555 0.312131 +vt 0.365201 0.584414 +vt 0.404427 0.577241 +vt 0.234572 0.525678 +vt 0.268403 0.521716 +vt 0.965989 0.678263 +vt 0.945513 0.675917 +vt 0.157971 0.000312 +vt 0.737182 0.499525 +vt 0.702848 0.532248 +vt 0.705000 0.501745 +vt 0.674718 0.519674 +vt 0.689751 0.492770 +vt 0.033353 0.744703 +vt 0.808034 0.512958 +vt 0.784212 0.535503 +vt 0.632629 0.469010 +vt 0.649288 0.418975 +vt 0.661808 0.454236 +vt 0.474837 0.566852 +vt 0.510593 0.561947 +vt 0.672972 0.481877 +vt 0.647452 0.501402 +vt 0.967573 0.666316 +vt 0.946572 0.657705 +vt 0.117713 0.320931 +vt 0.106332 0.302484 +vt 0.223333 0.423315 +vt 0.145967 0.443092 +vt 0.133463 0.393384 +vt 0.075283 0.188818 +vt 0.024705 0.167099 +vt 0.202448 0.320016 +vt 0.142064 0.335301 +vt 0.077241 0.250159 +vt 0.202952 0.539625 +vt 0.173470 0.500424 +vt 0.965383 0.642876 +vt 0.976290 0.659906 +vt 0.611049 0.585472 +vt 0.652315 0.603960 +vt 0.619376 0.622457 +vt 0.622420 0.399500 +vt 0.641710 0.381375 +vt 0.469884 0.542922 +vt 0.368086 0.556060 +vt 0.267709 0.039794 +vt 0.744046 0.531466 +vt 0.764880 0.361624 +vt 0.783540 0.349534 +vt 0.773637 0.370894 +vt 0.754185 0.351913 +vt 0.776551 0.340678 +vt 0.797693 0.355231 +vt 0.810189 0.374824 +vt 0.791736 0.373267 +vt 0.719515 0.475631 +vt 0.688740 0.454612 +vt 0.680148 0.422281 +vt 0.673705 0.394443 +vt 0.790088 0.458172 +vt 0.756228 0.468345 +vt 0.704694 0.465952 +vt 0.390594 0.827581 +vt 0.467189 0.834666 +vt 0.391970 0.836382 +vt 0.395352 0.863906 +vt 0.463299 0.868359 +vt 0.397091 0.879837 +vt 0.463223 0.845592 +vt 0.390274 0.848098 +vt 0.400293 0.895107 +vt 0.746725 0.337127 +vt 0.767160 0.332120 +vt 0.736038 0.325434 +vt 0.758903 0.321563 +vt 0.734864 0.303712 +vt 0.722386 0.312987 +vt 0.813222 0.358998 +vt 0.825887 0.370014 +vt 0.592679 0.891649 +vt 0.582281 0.878678 +vt 0.593640 0.876305 +vt 0.580712 0.859622 +vt 0.597785 0.854908 +vt 0.583723 0.838547 +vt 0.573934 0.844002 +vt 0.569464 0.832188 +vt 0.577027 0.829271 +vt 0.395671 0.812549 +vt 0.468411 0.825854 +vt 0.470277 0.802861 +vt 0.397301 0.799190 +vt 0.401657 0.782564 +vt 0.355474 0.778465 +vt 0.352373 0.796041 +vt 0.351097 0.810480 +vt 0.348207 0.826190 +vt 0.349262 0.839200 +vt 0.348742 0.850087 +vt 0.354044 0.899751 +vt 0.352469 0.884003 +vt 0.351951 0.867034 +vt 0.800960 0.417002 +vt 0.816799 0.451978 +vt 0.660725 0.366657 +vt 0.703828 0.360154 +vt 0.712649 0.380370 +vt 0.718358 0.406354 +vt 0.746057 0.428673 +vt 0.775512 0.422219 +vt 0.732563 0.418642 +vt 0.531668 0.803827 +vt 0.550155 0.778195 +vt 0.563961 0.788127 +vt 0.552362 0.913468 +vt 0.582071 0.896674 +vt 0.588492 0.916396 +vt 0.553490 0.853287 +vt 0.553605 0.867186 +vt 0.560665 0.836700 +vt 0.485979 0.857543 +vt 0.492433 0.856206 +vt 0.486586 0.860052 +vt 0.571172 0.825796 +vt 0.566790 0.828776 +vt 0.507400 0.855446 +vt 0.526231 0.846431 +vt 0.529354 0.853649 +vt 0.506272 0.867064 +vt 0.529171 0.868532 +vt 0.494326 0.893365 +vt 0.519694 0.908499 +vt 0.489390 0.895790 +vt 0.475519 0.823755 +vt 0.506226 0.818176 +vt 0.490978 0.835838 +vt 0.472476 0.838752 +vt 0.475309 0.858457 +vt 0.481348 0.881669 +vt 0.476178 0.881319 +vt 0.504988 0.852379 +vt 0.523162 0.902012 +vt 0.518183 0.786119 +vt 0.548927 0.818752 +vt 0.517015 0.831610 +vt 0.526946 0.883236 +vt 0.550974 0.901638 +vt 0.552884 0.881733 +vt 0.501572 0.879014 +vt 0.478215 0.864435 +vt 0.492474 0.857258 +vt 0.498118 0.842920 +vt 0.565772 0.822518 +vt 0.564181 0.823099 +vt 0.559154 0.778773 +vt 0.561037 0.832399 +vt 0.561695 0.834351 +vt 0.561911 0.834350 +vt 0.561449 0.834251 +vt 0.561716 0.834361 +vt 0.561708 0.834357 +vt 0.561706 0.834364 +vt 0.561836 0.834342 +vt 0.561605 0.834430 +vt 0.597785 0.912072 +vt 0.958207 0.625795 +vt 0.981019 0.631843 +vt 0.974160 0.737185 +vt 0.991416 0.730731 +vt 0.983645 0.742933 +vt 0.905330 0.708225 +vt 0.883542 0.696209 +vt 0.910057 0.697914 +vt 0.877204 0.674450 +vt 0.884993 0.653153 +vt 0.884440 0.674694 +vt 0.919959 0.796526 +vt 0.886348 0.795170 +vt 0.906603 0.779602 +vt 0.912457 0.675661 +vt 0.911676 0.653100 +vt 0.987591 0.653619 +vt 0.942558 0.777867 +vt 0.951874 0.750895 +vt 0.961532 0.762769 +vt 0.934220 0.715038 +vt 0.955766 0.727920 +vt 0.936617 0.637043 +vt 0.907495 0.642482 +vt 0.980875 0.723812 +vt 0.969619 0.742521 +vt 0.977219 0.749829 +vt 0.999688 0.648245 +vt 0.990896 0.626913 +vt 0.999688 0.708584 +vt 0.988851 0.701115 +vt 0.973118 0.612619 +vt 0.965960 0.592991 +vt 0.980318 0.606645 +vt 0.955844 0.603943 +vt 0.948269 0.577331 +vt 0.980792 0.695313 +vt 0.894485 0.557552 +vt 0.927257 0.557994 +vt 0.913376 0.573761 +vt 0.877158 0.696933 +vt 0.977179 0.618293 +vt 0.871009 0.695417 +vt 0.934105 0.629621 +vt 0.906165 0.635421 +vt 0.931273 0.722391 +vt 0.952009 0.734761 +vt 0.872428 0.653105 +vt 0.869661 0.674197 +vt 0.880155 0.707352 +vt 0.903615 0.715196 +vt 0.955159 0.618841 +vt 0.882290 0.641828 +vt 0.881389 0.647982 +vt 0.935898 0.591225 +vt 0.930647 0.762946 +vt 0.900601 0.337500 +vt 0.924745 0.363839 +vt 0.882781 0.304612 +vt 0.846664 0.303712 +vt 0.822044 0.412967 +vt 0.689143 0.340311 +vt 0.328338 0.838677 +vt 0.327172 0.824903 +vt 0.330502 0.867438 +vt 0.331031 0.884775 +vt 0.327533 0.850948 +vt 0.331952 0.900162 +vt 0.329751 0.810598 +vt 0.330773 0.795620 +vt 0.333310 0.778195 +vt 0.124212 0.341329 +vt 0.241920 0.315493 +vt 0.239159 0.419872 +vt 0.305966 0.412116 +vt 0.050019 0.204437 +vt 0.292472 0.151332 +vt 0.500347 0.540249 +vt 0.612869 0.434116 +vt 0.175789 0.765944 +vt 0.170475 0.793641 +vt 0.733096 0.151704 +vt 0.836790 0.461428 +vt 0.658395 0.030459 +vt 0.435945 0.536491 +vt 0.327172 0.658911 +vt 0.412610 0.237453 +vt 0.439741 0.146552 +vt 0.612245 0.462903 +vt 0.768955 0.789743 +vt 0.744644 0.883041 +vt 0.864318 0.034339 +vt 0.865337 0.273692 +vt 0.869036 0.742885 +vt 0.170475 0.540249 +vt 0.836790 0.338933 +vt 0.000312 0.146197 +vt 0.294306 0.524016 +vt 0.223843 0.530278 +vt 0.641323 0.575279 +vt 0.621931 0.376384 +vt 0.402034 0.552769 +vt 0.305966 0.077033 +vt 0.465747 0.857259 +vt 0.467173 0.877546 +vt 0.747787 0.313256 +vt 0.826642 0.354418 +vt 0.471852 0.812625 +vt 0.471852 0.889661 +vt 0.493948 0.802260 +vt 0.569045 0.787828 +vt 0.879628 0.701153 +vt 0.878668 0.652003 +vt 0.898861 0.813629 +vt 0.907626 0.540249 +vt 0.985935 0.613839 +vn 0.4747 0.2269 -0.8504 +vn -0.0000 0.0107 -0.9999 +vn 0.0000 0.3413 -0.9400 +vn 0.4524 0.5439 -0.7068 +vn 0.6617 0.3862 -0.6426 +vn 0.7224 0.1215 -0.6807 +vn 0.4792 -0.0415 -0.8767 +vn 0.9019 0.1834 0.3910 +vn 0.9986 -0.0415 -0.0312 +vn 0.9688 0.2477 0.0054 +vn 0.7262 0.5406 0.4248 +vn 0.6971 0.3821 0.6067 +vn 0.7601 0.1306 0.6366 +vn 0.9312 -0.0357 0.3627 +vn 0.5254 -0.8470 0.0809 +vn 0.0000 -0.8015 0.5980 +vn 0.7151 -0.6733 0.1879 +vn 0.8383 -0.5280 0.1356 +vn 0.8023 -0.2930 0.5202 +vn 0.5096 -0.5418 0.6685 +vn 0.0000 -0.9970 0.0778 +vn 0.4051 0.9142 0.0055 +vn 0.3995 0.8094 0.4305 +vn 0.0000 1.0000 0.0039 +vn -0.0000 0.8959 0.4443 +vn 0.4573 0.5353 0.7102 +vn 0.4169 0.7932 -0.4438 +vn 0.0000 0.6630 -0.7486 +vn 0.0000 0.8952 -0.4458 +vn 0.7613 0.6483 0.0123 +vn 0.7256 0.5311 -0.4375 +vn 0.4537 0.2744 -0.8479 +vn 0.0000 -0.2140 -0.9768 +vn 0.4462 -0.2587 -0.8567 +vn 0.9337 -0.1592 -0.3207 +vn 0.6586 -0.3533 -0.6644 +vn 0.8386 -0.3857 -0.3847 +vn 0.8352 -0.5404 -0.1021 +vn 0.5029 -0.1158 0.8566 +vn 0.7788 -0.1594 0.6066 +vn 0.5186 -0.0109 0.8550 +vn -0.0000 -0.1169 0.9931 +vn -0.0000 -0.5973 0.8020 +vn 0.4962 0.1732 0.8508 +vn -0.0000 0.2508 0.9680 +vn 0.0000 0.0081 1.0000 +vn 0.9334 -0.2051 0.2945 +vn 0.9691 -0.2348 -0.0756 +vn 0.7766 -0.0327 0.6291 +vn 0.8739 -0.2239 -0.4314 +vn 0.7102 -0.1951 -0.6764 +vn 0.7229 -0.0880 -0.6854 +vn 0.8905 -0.0847 -0.4470 +vn 0.8869 0.1707 -0.4293 +vn 0.4289 -0.2716 -0.8615 +vn 0.0000 -0.2958 -0.9552 +vn -0.0754 -0.2878 -0.9547 +vn 0.0000 0.1213 -0.9926 +vn 0.0016 0.1254 -0.9921 +vn 0.1390 0.4284 0.8928 +vn -0.0000 0.5753 0.8179 +vn -0.0000 0.5233 0.8521 +vn -0.0000 0.3619 -0.9322 +vn 0.4198 0.5897 -0.6899 +vn -0.6832 -0.7283 -0.0540 +vn -0.0000 -0.7833 0.6216 +vn 0.0000 -0.9946 -0.1036 +vn 0.2183 -0.1317 0.9670 +vn -0.0000 -0.0576 0.9983 +vn -0.0000 -0.1440 0.9896 +vn 0.2009 -0.0578 0.9779 +vn -0.0000 0.0126 0.9999 +vn 0.2332 -0.0475 0.9713 +vn 0.0000 -0.1511 0.9885 +vn 0.2234 -0.1227 0.9670 +vn -0.0195 0.1704 -0.9852 +vn -0.0000 -0.2030 -0.9792 +vn -0.0000 0.2065 -0.9784 +vn -0.0449 -0.1421 -0.9888 +vn -0.0000 -0.4004 -0.9164 +vn -0.0322 -0.4397 -0.8976 +vn -0.0889 0.1022 -0.9908 +vn 0.0000 -0.1163 -0.9932 +vn 0.9982 0.0469 0.0383 +vn 0.9249 -0.0620 0.3752 +vn 0.8883 -0.0389 0.4576 +vn 0.9987 -0.0452 0.0226 +vn 0.9114 -0.1011 0.3988 +vn 0.9373 -0.3471 0.0320 +vn 0.5745 -0.2788 0.7696 +vn 0.8910 -0.2733 0.3627 +vn 0.4617 0.0641 -0.8847 +vn 0.4939 -0.1832 -0.8500 +vn 0.4819 -0.4698 -0.7397 +vn 0.4796 -0.3176 -0.8180 +vn 0.3668 0.1091 -0.9239 +vn 0.6188 -0.1969 0.7605 +vn 0.6281 -0.0762 0.7744 +vn 0.1746 0.0318 0.9841 +vn 0.5660 -0.0998 0.8183 +vn 0.7925 -0.1183 0.5983 +vn 0.9746 -0.0824 -0.2084 +vn 0.9904 -0.1132 -0.0788 +vn 0.4838 0.8747 0.0267 +vn 0.3816 0.7429 0.5499 +vn 0.3665 0.9298 -0.0336 +vn 0.4414 0.3082 0.8427 +vn 0.1645 0.7351 0.6577 +vn 0.2815 -0.0204 -0.9593 +vn 0.3563 0.1658 -0.9195 +vn 0.9503 0.3047 -0.0640 +vn 0.8146 0.5719 0.0971 +vn -0.0000 0.4115 0.9114 +vn 0.4923 0.2037 0.8462 +vn 0.0000 0.0180 0.9998 +vn 0.8661 0.1363 0.4809 +vn 0.9407 0.0769 -0.3304 +vn -0.1688 -0.1359 -0.9762 +vn 0.5639 -0.0847 -0.8215 +vn -0.2230 0.0050 -0.9748 +vn -0.9833 -0.1326 0.1247 +vn -0.8516 -0.0015 -0.5242 +vn -0.9740 -0.0255 0.2252 +vn 0.2477 -0.1269 0.9605 +vn -0.4917 0.2162 0.8435 +vn 0.3299 0.3728 0.8673 +vn -0.0941 -0.4041 0.9099 +vn 0.2620 -0.0144 0.9650 +vn 0.3976 -0.0224 0.9173 +vn -0.6590 -0.1918 -0.7273 +vn -0.9962 -0.0684 0.0546 +vn 0.7726 0.0780 -0.6301 +vn 0.1908 -0.1055 -0.9759 +vn 0.8279 0.0543 0.5582 +vn 0.9857 0.1658 0.0297 +vn 0.9919 0.1267 0.0088 +vn 0.0000 0.1081 -0.9941 +vn -0.6611 -0.1851 0.7271 +vn 0.1265 -0.0471 0.9909 +vn 0.1276 -0.1439 0.9813 +vn -0.9827 -0.1790 0.0477 +vn -0.7383 -0.1771 -0.6508 +vn -0.9779 -0.2056 0.0385 +vn -0.0230 -0.0587 -0.9980 +vn 0.7245 -0.0525 -0.6873 +vn -0.0262 -0.1032 -0.9943 +vn 0.9980 -0.0594 0.0219 +vn 0.7950 0.0078 0.6065 +vn 0.9999 -0.0125 0.0040 +vn -0.6457 -0.2193 0.7314 +vn 0.1784 -0.1517 0.9722 +vn 0.1334 -0.1086 0.9851 +vn -0.7488 -0.1654 -0.6418 +vn -0.9915 -0.1212 0.0480 +vn -0.9690 -0.2376 0.0674 +vn -0.0107 0.0004 -0.9999 +vn 0.7256 0.1979 -0.6590 +vn -0.0506 0.1756 -0.9832 +vn 0.9971 0.0527 0.0546 +vn 0.8206 -0.0211 0.5711 +vn 0.9945 0.1046 -0.0061 +vn 0.7769 0.1278 0.6166 +vn 0.9991 0.0314 0.0298 +vn 0.7244 0.0004 -0.6894 +vn -0.0006 -0.0921 -0.9957 +vn -0.7449 -0.1784 -0.6428 +vn -0.9874 -0.1495 0.0521 +vn -0.6553 -0.1786 0.7339 +vn 0.1238 -0.1286 0.9839 +vn 0.1614 -0.0268 0.9865 +vn -0.6146 -0.0947 0.7831 +vn 0.1791 -0.0845 0.9802 +vn -0.9971 -0.0329 0.0689 +vn -0.8044 -0.0254 -0.5935 +vn -0.9955 -0.0663 0.0674 +vn 0.7331 0.1810 -0.6556 +vn -0.0986 0.0240 -0.9948 +vn -0.0890 0.1272 -0.9879 +vn 0.9901 0.1389 0.0192 +vn 0.7765 -0.0383 0.6290 +vn 0.9999 0.0083 0.0079 +vn 0.7860 -0.0005 0.6182 +vn -0.8027 0.0268 -0.5957 +vn 0.7822 0.0583 0.6203 +vn 0.7733 -0.0214 0.6337 +vn -0.7731 0.0023 -0.6343 +vn -0.7279 -0.1319 -0.6729 +vn 0.7777 -0.0967 0.6211 +vn 0.8441 0.1207 -0.5224 +vn 0.9051 -0.0174 -0.4247 +vn -0.8168 -0.1243 -0.5633 +vn 0.7495 0.6364 0.1824 +vn 0.7589 0.1389 -0.6362 +vn 0.4462 0.2237 0.8665 +vn 0.5461 0.6859 0.4809 +vn 0.5730 0.8164 -0.0722 +vn 0.5998 -0.0316 0.7995 +vn 0.5981 -0.0692 0.7984 +vn 0.2919 -0.4113 -0.8635 +vn 0.3254 0.0217 -0.9453 +vn 0.8969 -0.2039 -0.3925 +vn 0.8314 -0.4281 -0.3542 +vn 0.5559 0.5945 -0.5809 +vn 0.1280 0.8109 0.5710 +vn -0.6771 -0.1656 0.7170 +vn 0.7684 0.0719 -0.6359 +vn 0.7403 0.0398 -0.6711 +vn -0.6408 -0.0540 0.7658 +vn -0.6829 -0.1951 0.7040 +vn -0.6525 -0.1789 0.7364 +vn 0.7212 -0.0512 -0.6909 +vn -0.5317 -0.1396 0.8354 +vn -0.9931 0.1155 0.0186 +vn 0.3325 0.6893 0.6436 +vn 0.3929 0.6570 -0.6434 +vn 0.6571 -0.1298 -0.7426 +vn -0.2376 -0.9681 -0.0790 +vn 0.9861 -0.1607 0.0413 +vn 0.0000 -0.6846 -0.7289 +vn 0.1831 0.6059 -0.7742 +vn -0.0000 0.4192 -0.9079 +vn 0.0000 0.0328 0.9995 +vn 0.9716 0.2336 -0.0373 +vn 0.7683 0.3326 0.5469 +vn 0.9453 0.3046 -0.1171 +vn 0.7139 0.1131 -0.6910 +vn 0.6828 0.0606 -0.7281 +vn -0.5402 -0.4027 0.7390 +vn 0.1247 0.1718 0.9772 +vn 0.2504 -0.0715 0.9655 +vn 0.8051 0.1712 0.5679 +vn 0.7215 0.4462 0.5294 +vn 0.8514 0.5202 -0.0675 +vn 0.6368 0.3661 -0.6786 +vn 0.0816 -0.0195 -0.9965 +vn -0.6846 -0.2976 -0.6654 +vn -0.9600 -0.2784 -0.0289 +vn -0.6298 -0.3281 0.7041 +vn 0.3379 0.0727 0.9384 +vn 0.7612 -0.0640 -0.6453 +vn 0.8641 0.3167 -0.3912 +vn 0.4032 0.0743 -0.9121 +vn 0.1698 0.3589 0.9178 +vn -0.4036 0.3437 0.8479 +vn 0.3499 0.5755 0.7391 +vn 0.8377 0.4054 0.3660 +vn 0.8221 0.3530 0.4467 +vn -0.9497 -0.2997 0.0908 +vn -0.6942 0.0220 0.7194 +vn 0.1339 -0.1011 -0.9858 +vn 0.1686 -0.2017 -0.9648 +vn -0.5745 -0.3667 -0.7317 +vn -0.5225 -0.3977 -0.7542 +vn -0.9252 -0.3795 0.0018 +vn -0.8847 -0.4639 0.0455 +vn -0.6643 -0.2220 0.7137 +vn 0.8068 0.3928 -0.4414 +vn 0.7934 0.5829 0.1756 +vn -0.5930 0.4709 0.6532 +vn 0.9748 0.1643 -0.1506 +vn 0.2047 -0.3428 -0.9168 +vn -0.4666 0.1447 -0.8725 +vn -0.9288 -0.2293 -0.2911 +vn -0.5614 -0.5039 -0.6564 +vn -0.9808 -0.0330 0.1922 +vn -0.9532 -0.3006 0.0336 +vn -0.5581 -0.4443 -0.7008 +vn 0.1940 -0.3052 -0.9323 +vn 0.9478 0.3037 -0.0971 +vn -0.7341 0.0470 0.6774 +vn 0.0218 0.3922 0.9196 +vn 0.7003 0.4742 0.5336 +vn 0.7407 -0.0016 -0.6718 +vn -0.6102 -0.3364 0.7173 +vn -0.6229 -0.3085 -0.7189 +vn -0.9193 -0.3923 -0.0306 +vn 0.1123 -0.0572 -0.9920 +vn 0.7262 0.1948 -0.6594 +vn 0.8114 0.2375 0.5341 +vn 0.2750 -0.0046 0.9614 +vn 0.9523 0.3033 -0.0339 +vn -0.8932 -0.4457 -0.0593 +vn -0.5914 -0.0589 -0.8042 +vn -0.4991 -0.2826 -0.8192 +vn 0.3210 0.2610 -0.9104 +vn 0.6148 -0.0050 -0.7887 +vn 0.6495 0.3592 0.6701 +vn 0.9812 0.1688 0.0939 +vn 0.3044 0.7191 0.6247 +vn 0.1401 0.3336 0.9322 +vn -0.8492 -0.4248 0.3138 +vn 0.3561 -0.3160 0.8794 +vn -0.6906 0.0447 0.7219 +vn 0.5833 -0.0010 0.8123 +vn -0.5664 0.2609 0.7818 +vn -0.5874 0.5494 0.5943 +vn 0.8878 -0.3783 0.2622 +vn 0.6324 0.0612 0.7722 +vn -0.5330 -0.2416 -0.8109 +vn 0.5511 -0.6956 -0.4608 +vn -0.5204 -0.2412 -0.8191 +vn -0.9885 0.0548 -0.1407 +vn -0.9055 0.3759 -0.1968 +vn -0.7689 -0.1550 -0.6202 +vn -0.8329 -0.5272 -0.1684 +vn 0.2398 -0.8033 -0.5452 +vn 0.4091 -0.7866 0.4625 +vn 0.9546 -0.2294 0.1899 +vn 0.6710 -0.3770 -0.6385 +vn -0.6571 -0.7241 -0.2095 +vn -0.9977 0.0520 0.0439 +vn 0.9622 -0.2722 0.0016 +vn 0.9723 0.0840 -0.2180 +vn 0.8756 -0.4737 0.0948 +vn 0.3142 -0.9420 -0.1181 +vn -0.9125 0.3941 -0.1094 +vn -0.9603 -0.2712 -0.0660 +vn -0.8787 -0.2919 -0.3776 +vn -0.5008 -0.2905 0.8154 +vn -0.8610 -0.2991 -0.4114 +vn -0.6028 -0.7629 -0.2338 +vn 0.5486 -0.7774 -0.3077 +vn 0.6851 -0.4934 -0.5359 +vn 0.4935 -0.4340 0.7537 +vn 0.7123 0.1393 0.6880 +vn -0.5612 0.3193 0.7636 +vn -0.9743 -0.2174 -0.0588 +vn -0.8283 0.0979 -0.5517 +vn -0.7699 -0.1626 -0.6171 +vn -0.2199 0.1409 -0.9653 +vn 0.5000 -0.1149 -0.8584 +vn -0.1900 -0.1891 -0.9634 +vn 0.9631 -0.1427 -0.2283 +vn 0.6682 0.6469 0.3675 +vn 0.8454 -0.0665 0.5299 +vn 0.1874 -0.0238 0.9820 +vn -0.4859 0.6469 0.5877 +vn -0.5908 -0.0665 0.8040 +vn -0.0107 -0.9984 -0.0559 +vn 0.5760 -0.7700 0.2745 +vn 0.0816 -0.9003 0.4276 +vn 0.0658 0.9398 0.3354 +vn 0.1073 0.8201 0.5621 +vn 0.5659 -0.7972 -0.2103 +vn -0.0013 -1.0000 -0.0071 +vn 0.0001 -1.0000 -0.0008 +vn 0.9547 -0.0700 -0.2891 +vn 0.8957 -0.1733 -0.4095 +vn -0.6441 0.7073 0.2913 +vn -0.9892 -0.1162 0.0890 +vn -0.9795 -0.1427 0.1424 +vn 0.5320 0.0815 -0.8428 +vn 0.3918 -0.6619 -0.6390 +vn -0.1173 -0.7996 -0.5890 +vn -0.5925 -0.6706 -0.4464 +vn -0.6065 -0.7951 0.0054 +vn -0.5853 -0.8106 0.0203 +vn 0.7025 0.7112 0.0237 +vn -0.4345 -0.7699 0.4674 +vn -0.5251 -0.8508 0.0167 +vn 0.5515 -0.8108 -0.1960 +vn 0.4944 -0.8509 -0.1779 +vn -0.6666 0.6045 0.4361 +vn -0.0790 -0.2748 -0.9583 +vn -0.4747 0.2269 -0.8504 +vn -0.4792 -0.0415 -0.8767 +vn -0.4524 0.5439 -0.7068 +vn -0.6617 0.3862 -0.6426 +vn -0.7224 0.1215 -0.6807 +vn -0.9019 0.1834 0.3910 +vn -0.9986 -0.0415 -0.0312 +vn -0.9312 -0.0357 0.3627 +vn -0.7262 0.5406 0.4248 +vn -0.9688 0.2477 0.0054 +vn -0.6971 0.3821 0.6067 +vn -0.7601 0.1306 0.6366 +vn -0.5254 -0.8470 0.0809 +vn -0.8383 -0.5280 0.1356 +vn -0.7151 -0.6733 0.1879 +vn -0.8023 -0.2930 0.5202 +vn -0.5096 -0.5418 0.6685 +vn -0.4051 0.9142 0.0055 +vn -0.3995 0.8094 0.4305 +vn -0.4573 0.5353 0.7102 +vn -0.4169 0.7932 -0.4438 +vn -0.7256 0.5311 -0.4375 +vn -0.7613 0.6483 0.0123 +vn -0.4537 0.2744 -0.8479 +vn -0.4462 -0.2587 -0.8567 +vn -0.9337 -0.1592 -0.3207 +vn -0.6586 -0.3533 -0.6644 +vn -0.8352 -0.5404 -0.1021 +vn -0.8386 -0.3857 -0.3847 +vn -0.9503 0.3047 -0.0640 +vn -0.5029 -0.1158 0.8566 +vn -0.5186 -0.0109 0.8550 +vn -0.7788 -0.1594 0.6066 +vn -0.4962 0.1732 0.8508 +vn -0.9334 -0.2051 0.2945 +vn -0.9691 -0.2348 -0.0756 +vn -0.7766 -0.0327 0.6291 +vn -0.8739 -0.2239 -0.4314 +vn -0.7229 -0.0880 -0.6854 +vn -0.7102 -0.1951 -0.6764 +vn -0.8905 -0.0847 -0.4470 +vn -0.8869 0.1707 -0.4293 +vn -0.4289 -0.2716 -0.8615 +vn 0.0754 -0.2878 -0.9547 +vn -0.0016 0.1254 -0.9921 +vn -0.1390 0.4284 0.8928 +vn -0.1645 0.7351 0.6577 +vn -0.4198 0.5897 -0.6899 +vn 0.6832 -0.7283 -0.0540 +vn 0.0941 -0.4041 0.9099 +vn -0.2183 -0.1317 0.9670 +vn -0.2009 -0.0578 0.9779 +vn -0.1746 0.0318 0.9841 +vn -0.2332 -0.0475 0.9713 +vn -0.2234 -0.1227 0.9670 +vn 0.0195 0.1704 -0.9852 +vn 0.0790 -0.2748 -0.9583 +vn 0.0449 -0.1421 -0.9888 +vn 0.0322 -0.4397 -0.8976 +vn 0.0889 0.1022 -0.9908 +vn -0.9982 0.0469 0.0383 +vn -0.9249 -0.0620 0.3752 +vn -0.9987 -0.0452 0.0226 +vn -0.9114 -0.1011 0.3988 +vn -0.9861 -0.1607 0.0413 +vn -0.9373 -0.3471 0.0320 +vn -0.5745 -0.2788 0.7696 +vn 0.2376 -0.9681 -0.0790 +vn -0.4617 0.0641 -0.8847 +vn -0.4939 -0.1832 -0.8500 +vn -0.4819 -0.4698 -0.7397 +vn -0.4796 -0.3176 -0.8180 +vn -0.3668 0.1091 -0.9239 +vn -0.6188 -0.1969 0.7605 +vn -0.5981 -0.0692 0.7984 +vn -0.6281 -0.0762 0.7744 +vn -0.5998 -0.0316 0.7995 +vn -0.5660 -0.0998 0.8183 +vn -0.9746 -0.0824 -0.2084 +vn -0.7925 -0.1183 0.5983 +vn -0.9904 -0.1132 -0.0788 +vn -0.3816 0.7429 0.5499 +vn -0.4838 0.8747 0.0267 +vn -0.3665 0.9298 -0.0336 +vn -0.4414 0.3082 0.8427 +vn -0.2815 -0.0204 -0.9593 +vn -0.3563 0.1658 -0.9195 +vn -0.4923 0.2037 0.8462 +vn -0.8146 0.5719 0.0971 +vn -0.3325 0.6893 0.6436 +vn -0.8661 0.1363 0.4809 +vn -0.9407 0.0769 -0.3304 +vn -0.7495 0.6364 0.1824 +vn 0.1688 -0.1359 -0.9762 +vn -0.5639 -0.0847 -0.8215 +vn -0.6571 -0.1298 -0.7426 +vn 0.9833 -0.1326 0.1247 +vn 0.8516 -0.0014 -0.5242 +vn 0.8168 -0.1243 -0.5633 +vn -0.2477 -0.1269 0.9605 +vn 0.4918 0.2162 0.8435 +vn 0.5317 -0.1396 0.8354 +vn -0.2620 -0.0144 0.9650 +vn -0.3976 -0.0224 0.9173 +vn 0.6590 -0.1918 -0.7273 +vn -0.7726 0.0780 -0.6301 +vn -0.7589 0.1389 -0.6362 +vn -0.9857 0.1658 0.0297 +vn -0.8279 0.0543 0.5582 +vn -0.9919 0.1267 0.0088 +vn -0.8883 -0.0389 0.4576 +vn -0.1265 -0.0471 0.9909 +vn 0.6611 -0.1851 0.7271 +vn -0.1276 -0.1439 0.9813 +vn 0.9827 -0.1790 0.0477 +vn 0.7383 -0.1771 -0.6508 +vn 0.7279 -0.1320 -0.6729 +vn 0.0231 -0.0587 -0.9980 +vn -0.7245 -0.0525 -0.6873 +vn -0.7212 -0.0512 -0.6909 +vn -0.9980 -0.0594 0.0219 +vn -0.7950 0.0079 0.6065 +vn -0.7777 -0.0967 0.6211 +vn -0.1784 -0.1517 0.9722 +vn 0.6457 -0.2193 0.7314 +vn -0.1334 -0.1086 0.9851 +vn 0.9915 -0.1212 0.0480 +vn 0.7488 -0.1655 -0.6418 +vn 0.9690 -0.2376 0.0675 +vn 0.0107 0.0004 -0.9999 +vn -0.7256 0.1979 -0.6590 +vn -0.7684 0.0719 -0.6359 +vn -0.9971 0.0527 0.0546 +vn -0.8206 -0.0211 0.5711 +vn -0.7860 -0.0005 0.6182 +vn -0.9991 0.0314 0.0298 +vn -0.7769 0.1278 0.6166 +vn -0.1908 -0.1056 -0.9759 +vn -0.7244 0.0004 -0.6894 +vn 0.9962 -0.0684 0.0546 +vn 0.7449 -0.1785 -0.6428 +vn -0.1238 -0.1286 0.9839 +vn 0.6553 -0.1786 0.7339 +vn -0.1614 -0.0268 0.9865 +vn 0.6146 -0.0947 0.7831 +vn 0.6408 -0.0540 0.7658 +vn 0.9971 -0.0329 0.0689 +vn 0.8044 -0.0254 -0.5935 +vn 0.8027 0.0268 -0.5957 +vn 0.0986 0.0241 -0.9948 +vn -0.7331 0.1810 -0.6556 +vn 0.0890 0.1272 -0.9879 +vn -0.9901 0.1389 0.0192 +vn -0.7765 -0.0383 0.6290 +vn -0.7822 0.0583 0.6203 +vn -0.9999 -0.0125 0.0041 +vn 0.0262 -0.1032 -0.9943 +vn 0.9779 -0.2056 0.0385 +vn 0.6771 -0.1656 0.7170 +vn -0.1791 -0.0845 0.9802 +vn -0.7733 -0.0214 0.6337 +vn 0.0006 -0.0921 -0.9957 +vn 0.7731 0.0023 -0.6343 +vn -0.8441 0.1207 -0.5224 +vn -0.9051 -0.0174 -0.4247 +vn 0.2230 0.0050 -0.9748 +vn -0.3299 0.3728 0.8673 +vn -0.1280 0.8109 0.5710 +vn -0.4462 0.2237 0.8665 +vn -0.5462 0.6859 0.4809 +vn -0.5730 0.8164 -0.0722 +vn -0.8910 -0.2733 0.3627 +vn -0.2919 -0.4113 -0.8635 +vn -0.3254 0.0217 -0.9453 +vn -0.8969 -0.2039 -0.3925 +vn -0.8314 -0.4281 -0.3542 +vn -0.5559 0.5945 -0.5809 +vn -0.3929 0.6570 -0.6434 +vn 0.6666 0.6045 0.4361 +vn -0.9999 0.0083 0.0079 +vn 0.9955 -0.0663 0.0674 +vn 0.6829 -0.1951 0.7040 +vn -0.9945 0.1046 -0.0061 +vn 0.6525 -0.1789 0.7364 +vn 0.9740 -0.0255 0.2252 +vn 0.9931 0.1155 0.0186 +vn -0.1831 0.6059 -0.7742 +vn -0.9716 0.2336 -0.0373 +vn -0.7683 0.3326 0.5469 +vn -0.8051 0.1712 0.5679 +vn -0.7139 0.1131 -0.6910 +vn -0.9453 0.3046 -0.1171 +vn -0.1247 0.1718 0.9772 +vn 0.5402 -0.4027 0.7390 +vn -0.2504 -0.0715 0.9655 +vn -0.7215 0.4462 0.5294 +vn -0.6368 0.3661 -0.6786 +vn -0.0816 -0.0195 -0.9965 +vn 0.6846 -0.2976 -0.6654 +vn 0.6298 -0.3281 0.7041 +vn -0.3379 0.0727 0.9384 +vn -0.8514 0.5202 -0.0675 +vn -0.7612 -0.0640 -0.6453 +vn -0.8641 0.3167 -0.3912 +vn -0.9748 0.1643 -0.1506 +vn -0.1698 0.3589 0.9178 +vn 0.4036 0.3437 0.8479 +vn 0.6942 0.0220 0.7194 +vn -0.8377 0.4054 0.3660 +vn -0.8221 0.3530 0.4467 +vn 0.9497 -0.2997 0.0908 +vn -0.1339 -0.1011 -0.9858 +vn -0.6828 0.0606 -0.7281 +vn 0.5745 -0.3667 -0.7317 +vn -0.1686 -0.2017 -0.9648 +vn 0.9252 -0.3795 0.0018 +vn 0.8847 -0.4639 0.0455 +vn 0.6643 -0.2220 0.7137 +vn -0.4032 0.0743 -0.9121 +vn -0.8068 0.3928 -0.4414 +vn -0.7934 0.5829 0.1756 +vn -0.3499 0.5755 0.7391 +vn -0.3044 0.7191 0.6247 +vn 0.5930 0.4709 0.6532 +vn -0.2047 -0.3428 -0.9168 +vn 0.9288 -0.2293 -0.2911 +vn 0.5614 -0.5039 -0.6565 +vn 0.9532 -0.3006 0.0336 +vn 0.5581 -0.4443 -0.7008 +vn -0.1940 -0.3052 -0.9323 +vn -0.7407 -0.0017 -0.6718 +vn -0.9478 0.3037 -0.0971 +vn -0.7003 0.4742 0.5336 +vn 0.7341 0.0470 0.6774 +vn -0.0218 0.3922 0.9196 +vn 0.6102 -0.3364 0.7173 +vn 0.9600 -0.2784 -0.0289 +vn 0.6229 -0.3085 -0.7189 +vn -0.1123 -0.0572 -0.9920 +vn -0.7262 0.1948 -0.6594 +vn -0.8114 0.2375 0.5341 +vn -0.2750 -0.0046 0.9614 +vn -0.9523 0.3033 -0.0338 +vn 0.8932 -0.4457 -0.0593 +vn 0.5914 -0.0589 -0.8042 +vn 0.8787 -0.2919 -0.3776 +vn 0.4991 -0.2826 -0.8192 +vn -0.3210 0.2610 -0.9104 +vn -0.6495 0.3592 0.6701 +vn -0.9812 0.1688 0.0939 +vn -0.1401 0.3336 0.9322 +vn 0.8492 -0.4248 0.3138 +vn -0.3561 -0.3160 0.8794 +vn -0.4091 -0.7866 0.4625 +vn 0.9808 -0.0330 0.1922 +vn 0.9603 -0.2712 -0.0660 +vn -0.5833 -0.0010 0.8123 +vn 0.5664 0.2609 0.7818 +vn -0.6324 0.0612 0.7722 +vn -0.8878 -0.3783 0.2622 +vn -0.9546 -0.2294 0.1899 +vn -0.5511 -0.6956 -0.4608 +vn 0.5330 -0.2416 -0.8109 +vn 0.5204 -0.2412 -0.8191 +vn 0.9885 0.0548 -0.1407 +vn 0.9055 0.3759 -0.1968 +vn 0.7689 -0.1550 -0.6202 +vn 0.8329 -0.5272 -0.1684 +vn -0.2398 -0.8033 -0.5452 +vn 0.5874 0.5494 0.5943 +vn -0.6710 -0.3770 -0.6385 +vn 0.6571 -0.7241 -0.2095 +vn 0.9977 0.0520 0.0439 +vn -0.9622 -0.2722 0.0016 +vn -0.6148 -0.0050 -0.7887 +vn -0.9723 0.0840 -0.2180 +vn -0.8756 -0.4737 0.0948 +vn -0.3142 -0.9420 -0.1181 +vn 0.6906 0.0447 0.7219 +vn 0.9125 0.3941 -0.1094 +vn 0.4666 0.1447 -0.8725 +vn 0.5008 -0.2905 0.8154 +vn 0.8610 -0.2991 -0.4114 +vn 0.5612 0.3193 0.7636 +vn -0.5487 -0.7773 -0.3077 +vn 0.6028 -0.7629 -0.2338 +vn -0.4935 -0.4339 0.7538 +vn -0.6851 -0.4934 -0.5360 +vn -0.7123 0.1393 0.6879 +vn 0.9743 -0.2174 -0.0588 +vn 0.8283 0.0979 -0.5517 +vn -0.5000 -0.1149 -0.8584 +vn 0.2199 0.1409 -0.9653 +vn 0.1900 -0.1891 -0.9634 +vn -0.9631 -0.1427 -0.2283 +vn -0.6682 0.6469 0.3675 +vn -0.7025 0.7112 0.0237 +vn -0.1874 -0.0238 0.9820 +vn 0.4859 0.6469 0.5877 +vn -0.1073 0.8201 0.5621 +vn 0.0107 -0.9984 -0.0559 +vn -0.5761 -0.7699 0.2745 +vn -0.4944 -0.8509 -0.1779 +vn -0.0658 0.9398 0.3354 +vn 0.6441 0.7073 0.2913 +vn 0.0013 -1.0000 -0.0071 +vn -0.5658 -0.7973 -0.2103 +vn -0.0001 -1.0000 -0.0008 +vn -0.9547 -0.0700 -0.2891 +vn -0.8957 -0.1733 -0.4095 +vn 0.9892 -0.1162 0.0890 +vn 0.9795 -0.1427 0.1425 +vn -0.5320 0.0815 -0.8428 +vn -0.3918 -0.6619 -0.6390 +vn 0.1173 -0.7996 -0.5890 +vn 0.5925 -0.6706 -0.4464 +vn 0.6065 -0.7951 0.0054 +vn 0.4345 -0.7699 0.4674 +vn 0.5251 -0.8508 0.0167 +vn -0.8454 -0.0664 0.5300 +vn 0.7699 -0.1626 -0.6171 +vn 0.5853 -0.8106 0.0203 +vn -0.5515 -0.8108 -0.1960 +vn -0.0816 -0.9003 0.4276 +vn 0.5908 -0.0664 0.8041 +vn 0.9193 -0.3923 -0.0306 +vn 0.5225 -0.3977 -0.7542 +vn 0.0506 0.1756 -0.9832 +vn -0.7403 0.0398 -0.6711 +vn 0.9874 -0.1495 0.0521 +vn -0.0000 0.6426 0.7662 +vn 0.0000 0.1244 -0.9922 +usemtl Material.001 +s 1 +f 1/1/1 2/2/2 3/3/3 +f 3/3/3 4/4/4 1/1/1 +f 1/1/1 5/5/5 6/6/6 +f 6/6/6 7/7/7 1/1/1 +f 8/8/8 9/9/9 10/10/10 +f 10/10/10 11/11/11 8/8/8 +f 8/8/8 12/12/12 13/13/13 +f 13/13/13 14/14/14 8/8/8 +f 15/15/15 16/16/16 17/17/17 +f 17/17/17 18/18/18 15/15/15 +f 15/15/15 19/19/19 20/20/20 +f 20/20/20 21/21/21 15/15/15 +f 11/11/11 22/22/22 23/23/23 +f 23/23/23 24/24/24 25/25/25 +f 25/25/25 26/26/26 23/23/23 +f 23/23/23 12/12/12 11/11/11 +f 27/27/27 5/5/5 4/4/4 +f 27/27/27 28/28/28 29/29/29 +f 29/29/29 22/22/22 27/27/27 +f 27/27/27 30/30/30 31/31/31 +f 32/32/32 33/33/33 34/34/34 +f 35/35/35 36/36/36 37/37/37 +f 35/35/35 38/38/38 17/17/17 +f 35/39/35 32/32/32 34/34/34 +f 39/40/39 19/19/19 40/41/40 +f 40/41/40 41/42/41 39/40/39 +f 41/42/41 42/43/42 39/40/39 +f 39/40/39 43/44/43 20/20/20 +f 41/42/41 13/13/13 44/45/44 +f 44/45/44 12/12/12 26/26/26 +f 26/26/26 45/46/45 44/45/44 +f 44/45/44 46/47/46 41/42/41 +f 47/48/47 38/38/38 48/49/48 +f 48/49/48 14/14/14 47/48/47 +f 47/48/47 49/50/49 40/41/40 +f 40/41/40 18/18/18 47/48/47 +f 50/51/50 36/52/36 51/53/51 +f 50/51/50 52/54/52 53/55/53 +f 50/56/50 9/9/9 48/49/48 +f 48/49/48 37/37/37 50/56/50 +f 53/55/53 6/6/6 54/57/54 +f 54/57/54 5/5/5 31/31/31 +f 31/31/31 10/10/10 54/57/54 +f 54/57/54 9/9/9 53/55/53 +f 55/58/55 33/59/33 56/60/56 +f 56/60/56 7/7/7 55/58/55 +f 55/58/55 52/54/52 51/53/51 +f 55/58/55 36/52/36 34/61/34 +f 57/62/57 58/63/58 59/64/59 +f 60/65/60 61/66/61 62/67/62 +f 63/68/63 32/32/32 64/69/64 +f 65/70/65 66/71/66 67/72/67 +f 68/73/68 69/74/69 70/75/70 +f 71/76/71 72/77/72 69/74/69 +f 73/78/73 74/79/74 75/80/75 +f 76/81/76 77/82/77 78/83/78 +f 79/84/79 80/85/80 81/86/81 +f 82/87/82 83/88/83 79/84/79 +f 84/89/84 85/90/85 86/91/86 +f 87/92/87 88/93/88 85/90/85 +f 89/94/89 90/95/90 91/96/91 +f 92/97/92 79/84/79 93/98/93 +f 93/98/93 81/86/81 94/99/94 +f 95/100/95 76/81/76 96/101/96 +f 97/102/97 73/78/73 75/80/75 +f 98/103/98 99/104/99 71/76/71 +f 100/105/100 71/76/71 68/73/68 +f 101/106/101 102/107/102 103/108/103 +f 104/109/104 105/110/105 106/111/106 +f 107/112/107 108/113/108 60/65/60 +f 109/114/109 59/64/59 110/115/110 +f 17/116/17 111/117/111 35/39/35 +f 64/69/64 111/117/111 112/118/112 +f 113/119/113 114/120/114 115/121/115 +f 116/122/116 117/123/117 102/124/102 +f 118/125/118 119/126/119 120/127/120 +f 121/128/121 122/129/122 123/130/123 +f 124/131/124 125/132/125 126/133/126 +f 127/134/127 128/135/128 129/136/129 +f 65/137/65 130/138/130 131/139/131 +f 109/114/109 132/140/132 133/141/133 +f 134/142/134 135/143/135 136/144/136 +f 110/145/110 82/87/82 92/97/92 +f 129/136/129 68/146/68 127/134/127 +f 136/144/136 86/147/86 134/142/134 +f 127/134/127 70/148/70 66/71/66 +f 59/149/59 137/150/137 82/87/82 +f 138/151/138 139/152/139 140/153/140 +f 141/154/141 142/155/142 143/156/143 +f 144/157/144 145/158/145 146/159/146 +f 147/160/147 148/161/148 149/162/149 +f 150/163/150 151/164/151 152/165/152 +f 153/166/153 154/167/154 155/168/155 +f 156/169/156 157/170/157 158/171/158 +f 159/172/159 160/173/160 161/174/161 +f 162/175/162 163/176/163 135/143/135 +f 133/141/133 164/177/164 165/178/165 +f 131/139/131 166/179/166 167/180/167 +f 168/181/168 169/182/169 128/135/128 +f 170/183/170 171/184/171 172/185/172 +f 173/186/173 174/187/174 175/188/175 +f 176/189/176 177/190/177 178/191/178 +f 179/192/179 180/193/180 181/194/181 +f 148/161/148 159/172/159 149/162/149 +f 145/158/145 156/169/156 146/159/146 +f 143/156/143 153/166/153 155/168/155 +f 139/152/139 150/163/150 152/165/152 +f 139/152/139 182/195/182 148/161/148 +f 146/159/146 153/166/153 142/155/142 +f 178/191/178 174/187/174 183/196/183 +f 184/197/184 172/185/172 180/193/180 +f 128/135/128 185/198/185 162/175/162 +f 130/138/130 165/178/165 166/179/166 +f 156/169/156 186/199/186 153/166/153 +f 182/195/182 151/164/151 160/173/160 +f 144/157/144 142/155/142 187/200/187 +f 140/153/140 148/161/148 188/201/188 +f 134/142/134 100/202/100 129/136/129 +f 189/203/189 92/97/92 190/204/190 +f 57/62/57 133/141/133 130/138/130 +f 129/136/129 162/175/162 134/142/134 +f 124/131/124 116/205/116 101/106/101 +f 191/206/191 120/127/120 122/129/122 +f 126/207/126 192/208/192 116/122/116 +f 193/209/193 110/115/110 189/210/189 +f 194/211/194 105/212/105 107/213/107 +f 106/214/106 195/215/195 196/216/196 +f 86/91/86 98/103/98 100/105/100 +f 85/90/85 197/217/197 98/103/98 +f 91/218/91 198/219/198 97/102/97 +f 199/220/199 96/221/96 200/222/200 +f 201/223/201 94/99/94 202/224/202 +f 190/204/190 93/98/93 201/223/201 +f 203/225/203 106/214/106 196/216/196 +f 125/226/125 204/227/204 126/207/126 +f 205/228/205 155/229/155 150/163/150 +f 149/162/149 206/230/206 145/158/145 +f 176/189/176 181/194/181 207/231/207 +f 208/232/208 175/233/175 171/184/171 +f 131/234/131 209/235/209 168/181/168 +f 132/140/132 163/176/163 164/177/164 +f 206/230/206 161/174/161 157/170/157 +f 155/229/155 210/236/210 150/163/150 +f 211/237/211 149/162/149 145/158/145 +f 141/238/141 205/228/205 138/151/138 +f 189/210/189 136/144/136 193/209/193 +f 193/209/193 135/143/135 132/140/132 +f 65/70/65 168/181/168 127/134/127 +f 212/239/212 123/240/123 125/132/125 +f 213/241/213 125/226/125 123/242/123 +f 112/118/112 114/120/114 214/243/214 +f 16/244/16 114/120/114 17/116/17 +f 215/245/215 104/246/104 106/247/106 +f 103/108/103 119/126/119 216/248/216 +f 202/249/202 217/250/217 89/251/89 +f 190/204/190 218/252/218 87/253/87 +f 190/204/190 84/254/84 189/203/189 +f 65/137/65 219/255/219 57/62/57 +f 203/225/203 96/221/96 215/256/215 +f 90/95/90 107/213/107 198/257/198 +f 198/219/198 60/65/60 73/78/73 +f 215/245/215 76/81/76 220/258/220 +f 220/258/220 78/83/78 221/259/221 +f 60/65/60 222/260/222 73/78/73 +f 223/261/223 224/262/224 225/263/225 +f 226/264/226 225/263/225 227/265/227 +f 228/266/228 229/267/229 230/268/230 +f 230/268/230 224/262/224 231/269/231 +f 196/216/196 232/270/232 233/271/233 +f 200/222/200 234/272/234 235/273/235 +f 199/220/199 235/273/235 236/274/236 +f 217/250/217 236/274/236 237/275/237 +f 217/276/217 238/277/238 90/95/90 +f 194/211/194 232/270/232 195/215/195 +f 90/95/90 239/278/239 194/211/194 +f 203/225/203 233/271/233 234/272/234 +f 240/279/240 241/280/241 242/281/242 +f 243/282/243 244/283/244 245/284/245 +f 243/282/243 246/285/246 247/286/247 +f 248/287/248 244/283/244 249/288/249 +f 250/289/250 227/265/227 251/290/251 +f 252/291/252 251/290/251 253/292/253 +f 252/291/252 254/293/254 255/294/255 +f 255/295/255 256/296/256 228/266/228 +f 257/297/257 242/298/242 241/299/241 +f 258/300/258 241/299/241 246/301/246 +f 245/302/245 258/300/258 246/301/246 +f 259/303/259 245/302/245 244/304/244 +f 260/305/260 246/285/246 241/280/241 +f 261/306/261 242/281/242 262/307/262 +f 261/306/261 263/308/263 264/309/264 +f 248/287/248 263/310/263 265/311/265 +f 266/312/266 264/309/264 248/313/248 +f 267/314/267 261/306/261 264/309/264 +f 268/315/268 240/279/240 261/306/261 +f 269/316/269 247/286/247 260/305/260 +f 266/317/266 249/288/249 270/318/270 +f 271/319/271 247/286/247 272/320/272 +f 271/319/271 249/288/249 243/282/243 +f 269/316/269 240/279/240 273/321/273 +f 237/322/237 274/323/274 238/277/238 +f 237/275/237 275/324/275 276/325/276 +f 236/274/236 277/326/277 275/324/275 +f 235/273/235 278/327/278 277/326/277 +f 239/278/239 279/328/279 232/270/232 +f 238/277/238 280/329/280 239/278/239 +f 234/272/234 281/330/281 278/327/278 +f 233/271/233 279/328/279 281/330/281 +f 282/331/282 283/332/283 284/333/284 +f 284/334/284 285/335/285 286/336/286 +f 258/300/258 287/337/287 288/338/288 +f 289/339/289 290/340/290 287/337/287 +f 291/341/291 292/342/292 293/343/293 +f 265/344/265 259/303/259 244/304/244 +f 294/345/294 295/346/295 296/347/296 +f 297/348/297 298/349/298 294/345/294 +f 299/350/299 300/351/300 301/352/301 +f 302/353/302 301/354/301 303/355/303 +f 303/355/303 304/356/304 305/357/305 +f 301/352/301 306/358/306 304/359/304 +f 297/348/297 292/342/292 307/360/307 +f 292/342/292 296/347/296 293/343/293 +f 287/337/287 295/346/295 298/349/298 +f 288/338/288 298/349/298 308/361/308 +f 284/334/284 309/362/309 299/350/299 +f 302/353/302 284/333/284 299/363/299 +f 295/346/295 310/364/310 311/365/311 +f 311/365/311 282/331/282 302/353/302 +f 286/336/286 312/366/312 309/362/309 +f 313/367/313 308/361/308 312/366/312 +f 306/358/306 314/368/314 315/369/315 +f 314/368/314 307/360/307 315/369/315 +f 293/343/293 316/370/316 291/341/291 +f 291/341/291 303/355/303 305/357/305 +f 296/347/296 311/365/311 316/370/316 +f 311/365/311 303/355/303 316/370/316 +f 309/362/309 314/368/314 300/351/300 +f 312/366/312 297/348/297 314/368/314 +f 263/371/263 317/372/317 265/344/265 +f 262/373/262 318/374/318 263/375/263 +f 304/359/304 315/369/315 305/357/305 +f 305/357/305 307/360/307 291/341/291 +f 285/335/285 313/367/313 286/336/286 +f 257/297/257 288/338/288 313/367/313 +f 310/364/310 318/374/318 282/331/282 +f 319/376/319 320/377/320 321/378/321 +f 320/377/320 322/379/322 321/378/321 +f 323/380/323 324/381/324 322/379/322 +f 325/382/325 319/376/319 324/381/324 +f 322/379/322 319/376/319 321/378/321 +f 320/377/320 310/383/310 323/380/323 +f 285/335/285 262/384/262 242/298/242 +f 323/380/323 290/340/290 325/382/325 +f 320/377/320 259/303/259 317/372/317 +f 326/385/326 290/340/290 259/303/259 +f 327/386/327 328/387/328 329/388/329 +f 330/389/330 331/390/331 332/391/332 +f 333/392/333 334/393/334 335/394/335 +f 336/395/336 337/396/337 338/397/338 +f 339/398/339 340/399/340 341/400/341 +f 342/401/342 337/396/337 343/402/343 +f 122/403/122 213/241/213 123/242/123 +f 344/404/344 345/405/345 346/406/346 +f 117/123/117 347/407/347 348/408/348 +f 349/409/349 350/410/350 351/411/351 +f 352/412/352 348/408/348 331/390/331 +f 353/413/353 346/406/346 354/414/354 +f 120/415/120 328/387/328 122/403/122 +f 350/410/350 213/241/213 327/386/327 +f 352/412/352 120/416/120 119/417/119 +f 355/418/355 346/419/346 356/420/356 +f 356/420/356 345/421/345 357/422/357 +f 117/123/117 119/417/119 102/124/102 +f 342/401/342 334/393/334 358/423/358 +f 339/424/339 359/425/359 360/426/360 +f 334/393/334 336/395/336 335/427/335 +f 329/388/329 330/428/330 332/429/332 +f 329/388/329 354/430/354 355/418/355 +f 340/431/340 336/395/336 341/432/341 +f 357/433/357 327/386/327 356/434/356 +f 353/413/353 348/408/348 344/435/344 +f 351/411/351 357/433/357 360/436/360 +f 348/408/348 361/437/361 344/435/344 +f 336/395/336 359/438/359 341/432/341 +f 333/392/333 340/439/340 362/440/362 +f 354/414/354 331/390/331 353/413/353 +f 327/386/327 355/418/355 356/434/356 +f 361/437/361 333/392/333 362/440/362 +f 359/441/359 351/411/351 360/436/360 +f 357/422/357 339/424/339 360/426/360 +f 192/208/192 342/401/342 358/423/358 +f 337/396/337 351/411/351 338/442/338 +f 363/443/363 342/401/342 204/227/204 +f 361/444/361 339/398/339 345/405/345 +f 347/407/347 358/423/358 333/392/333 +f 104/445/104 214/243/214 108/446/108 +f 61/447/61 214/243/214 113/119/113 +f 104/445/104 64/69/64 112/118/112 +f 221/448/221 64/69/64 220/449/220 +f 276/450/276 228/266/228 274/323/274 +f 276/325/276 252/291/252 255/294/255 +f 275/324/275 250/289/250 252/291/252 +f 277/326/277 226/264/226 250/289/250 +f 280/329/280 231/269/231 279/328/279 +f 274/323/274 230/268/230 280/329/280 +f 278/327/278 223/261/223 226/264/226 +f 281/330/281 231/269/231 223/261/223 +f 225/263/225 273/451/273 227/265/227 +f 229/267/229 270/452/270 271/453/271 +f 229/267/229 272/454/272 224/262/224 +f 254/455/254 270/452/270 256/296/256 +f 225/263/225 272/454/272 269/456/269 +f 251/290/251 273/451/273 268/457/268 +f 253/292/253 268/457/268 267/458/267 +f 254/293/254 267/458/267 266/459/266 +f 154/460/154 208/232/208 210/236/210 +f 157/170/157 179/192/179 176/189/176 +f 151/164/151 184/197/184 160/173/160 +f 186/199/186 178/191/178 183/196/183 +f 161/174/161 184/197/184 179/192/179 +f 158/171/158 176/189/176 178/191/178 +f 186/199/186 173/186/173 154/167/154 +f 210/236/210 170/183/170 151/164/151 +f 181/194/181 216/248/216 207/231/207 +f 171/184/171 121/461/121 212/239/212 +f 174/187/174 118/125/118 191/206/191 +f 172/185/172 101/106/101 180/193/180 +f 172/185/172 212/239/212 124/131/124 +f 175/188/175 191/206/191 121/128/121 +f 177/190/177 216/248/216 118/125/118 +f 180/193/180 103/108/103 181/194/181 +f 167/462/167 138/151/138 209/235/209 +f 164/177/164 147/160/147 211/237/211 +f 169/182/169 188/201/188 185/198/185 +f 165/178/165 187/200/187 166/179/166 +f 163/176/163 188/201/188 147/160/147 +f 165/178/165 211/237/211 144/157/144 +f 167/180/167 187/200/187 141/154/141 +f 209/235/209 140/153/140 169/182/169 +f 201/223/201 89/463/89 218/252/218 +f 202/249/202 95/464/95 199/220/199 +f 88/93/88 97/102/97 197/217/197 +f 197/217/197 75/80/75 99/104/99 +f 95/100/95 81/86/81 364/465/364 +f 218/466/218 91/218/91 88/93/88 +f 364/465/364 80/85/80 77/82/77 +f 99/104/99 74/79/74 72/77/72 +f 365/467/365 2/468/2 366/469/366 +f 367/470/367 3/471/3 365/467/365 +f 365/467/365 368/472/368 367/470/367 +f 366/469/366 369/473/369 365/467/365 +f 370/474/370 371/475/371 372/476/372 +f 373/477/373 374/478/374 370/474/370 +f 370/474/370 375/479/375 373/477/373 +f 372/476/372 376/480/376 370/474/370 +f 377/481/377 16/482/16 21/483/21 +f 378/484/378 379/485/379 377/481/377 +f 377/481/377 380/486/380 378/484/378 +f 21/483/21 381/487/381 377/481/377 +f 382/488/382 373/477/373 383/489/383 +f 383/489/383 24/24/24 382/488/382 +f 384/490/384 25/25/25 383/489/383 +f 383/489/383 375/479/375 384/490/384 +f 385/491/385 368/472/368 386/492/386 +f 385/491/385 28/493/28 367/470/367 +f 382/488/382 29/29/29 385/491/385 +f 385/491/385 387/494/387 382/488/382 +f 33/495/33 388/496/388 389/497/389 +f 390/498/390 391/499/391 389/500/389 +f 390/498/390 392/501/392 393/502/393 +f 390/503/390 388/496/388 394/504/394 +f 395/505/395 380/486/380 381/487/381 +f 396/506/396 397/507/397 395/505/395 +f 42/43/42 396/506/396 395/505/395 +f 395/505/395 43/44/43 42/43/42 +f 376/480/376 396/506/396 398/508/398 +f 398/508/398 375/479/375 376/480/376 +f 45/46/45 384/490/384 398/508/398 +f 398/508/398 46/47/46 45/46/45 +f 399/509/399 392/501/392 378/484/378 +f 372/476/372 400/510/400 399/509/399 +f 399/509/399 401/511/401 372/476/372 +f 378/484/378 397/507/397 399/509/399 +f 402/512/402 391/499/391 393/502/393 +f 402/513/402 403/514/403 404/515/404 +f 402/513/402 371/475/371 405/516/405 +f 393/502/393 400/510/400 402/512/402 +f 369/473/369 405/516/405 406/517/406 +f 406/517/406 368/472/368 369/473/369 +f 374/478/374 386/492/386 406/517/406 +f 406/517/406 371/475/371 374/478/374 +f 407/518/407 33/519/33 389/520/389 +f 366/469/366 56/521/56 407/518/407 +f 407/518/407 403/514/403 366/469/366 +f 407/518/407 391/522/391 404/515/404 +f 58/523/58 408/524/408 409/525/409 +f 410/526/410 61/66/61 411/527/411 +f 388/496/388 63/528/63 412/529/412 +f 413/530/413 66/531/66 414/532/414 +f 415/533/415 69/74/69 416/534/416 +f 416/534/416 72/77/72 417/535/417 +f 74/79/74 418/536/418 419/537/419 +f 420/538/420 77/82/77 421/539/421 +f 80/85/80 422/540/422 423/541/423 +f 83/88/83 424/542/424 422/540/422 +f 425/543/425 426/544/426 427/545/427 +f 427/545/427 428/546/428 429/547/429 +f 430/548/430 431/549/431 432/550/432 +f 422/540/422 433/551/433 434/552/434 +f 423/541/423 434/552/434 435/553/435 +f 420/538/420 436/554/436 437/555/437 +f 438/556/438 418/536/418 439/557/439 +f 440/558/440 417/535/417 441/559/441 +f 442/560/442 416/534/416 440/558/440 +f 443/561/443 444/562/444 445/563/445 +f 446/564/446 447/565/447 448/566/448 +f 449/567/449 411/527/411 446/564/446 +f 409/525/409 450/568/450 451/569/451 +f 379/570/379 394/504/394 452/571/452 +f 394/504/394 412/529/412 453/572/453 +f 113/119/113 452/571/452 454/573/454 +f 455/574/455 456/575/456 457/576/457 +f 458/577/458 459/578/459 460/579/460 +f 461/580/461 462/581/462 463/582/463 +f 464/583/464 465/584/465 466/585/466 +f 467/586/467 414/532/414 468/587/468 +f 413/588/413 469/589/469 408/524/408 +f 450/568/450 470/590/470 471/591/471 +f 472/592/472 473/593/473 474/594/474 +f 424/542/424 451/595/451 433/551/433 +f 468/587/468 415/596/415 442/597/442 +f 474/594/474 475/598/475 425/599/425 +f 414/532/414 70/600/70 415/596/415 +f 137/150/137 409/601/409 424/542/424 +f 476/602/476 477/603/477 478/604/478 +f 479/605/479 480/606/480 481/607/481 +f 482/608/482 483/609/483 484/610/484 +f 485/611/485 486/612/486 487/613/487 +f 488/614/488 489/615/489 490/616/490 +f 491/617/491 492/618/492 493/619/493 +f 494/620/494 495/621/495 496/622/496 +f 497/623/497 498/624/498 499/625/499 +f 500/626/500 501/627/501 472/592/472 +f 502/628/502 503/629/503 470/590/470 +f 504/630/504 505/631/505 469/589/469 +f 506/632/506 507/633/507 467/586/467 +f 508/634/508 509/635/509 510/636/510 +f 511/637/511 512/638/512 513/639/513 +f 514/640/514 515/641/515 516/642/516 +f 517/643/517 518/644/518 519/645/519 +f 497/623/497 486/612/486 520/646/520 +f 494/620/494 483/609/483 521/647/521 +f 522/648/522 492/618/492 480/606/480 +f 476/602/476 489/615/489 523/649/523 +f 499/625/499 476/602/476 486/612/486 +f 492/618/492 521/647/521 480/606/480 +f 512/638/512 516/642/516 513/639/513 +f 519/645/519 524/650/524 508/634/508 +f 525/651/525 467/586/467 501/627/501 +f 469/589/469 526/652/526 502/628/502 +f 527/653/527 494/620/494 492/618/492 +f 499/625/499 488/614/488 490/616/490 +f 480/606/480 482/608/482 481/607/481 +f 486/612/486 478/604/478 487/613/487 +f 473/593/473 442/597/442 475/598/475 +f 433/551/433 528/654/528 529/655/529 +f 408/524/408 502/628/502 450/568/450 +f 501/627/501 468/587/468 473/593/473 +f 455/656/455 464/583/464 444/562/444 +f 463/582/463 530/657/530 458/577/458 +f 531/658/531 457/576/457 532/659/532 +f 451/569/451 471/591/471 528/660/528 +f 533/661/533 446/662/446 534/663/534 +f 534/663/534 448/664/448 535/665/535 +f 475/666/475 440/558/440 426/544/426 +f 426/544/426 441/559/441 428/546/428 +f 536/667/536 439/668/439 431/549/431 +f 437/669/437 537/670/537 538/671/538 +f 435/553/435 539/672/539 540/673/540 +f 434/552/434 529/655/529 539/672/539 +f 541/674/541 448/664/448 542/675/542 +f 465/676/465 532/659/532 543/677/543 +f 523/649/523 493/678/493 522/679/522 +f 496/622/496 520/646/520 483/609/483 +f 515/641/515 544/680/544 517/643/517 +f 510/636/510 545/681/545 511/682/511 +f 546/683/546 504/684/504 507/633/507 +f 470/590/470 500/626/500 472/592/472 +f 496/622/496 547/685/547 497/623/497 +f 548/686/548 493/678/493 489/615/489 +f 484/610/484 520/646/520 485/611/485 +f 523/649/523 479/687/479 477/603/477 +f 474/594/474 528/660/528 471/591/471 +f 471/591/471 472/592/472 474/594/474 +f 507/633/507 413/530/413 414/532/414 +f 466/585/466 549/688/549 461/689/461 +f 465/676/465 550/690/550 549/691/549 +f 452/571/452 453/572/453 454/573/454 +f 16/244/16 452/571/452 115/121/115 +f 542/692/542 447/693/447 551/694/551 +f 459/578/459 445/563/445 460/579/460 +f 540/695/540 432/696/432 537/670/537 +f 529/655/529 429/697/429 539/672/539 +f 425/698/425 529/655/529 528/654/528 +f 219/699/219 413/588/413 408/524/408 +f 541/674/541 437/669/437 538/671/538 +f 431/549/431 449/700/449 533/661/533 +f 439/557/439 410/526/410 449/567/449 +f 542/692/542 420/538/420 437/555/437 +f 551/694/551 78/83/78 420/538/420 +f 222/260/222 410/526/410 418/536/418 +f 552/701/552 553/702/553 554/703/554 +f 555/704/555 556/705/556 552/701/552 +f 557/706/557 558/707/558 559/708/559 +f 553/702/553 559/708/559 554/703/554 +f 535/665/535 560/709/560 534/663/534 +f 538/671/538 561/710/561 541/674/541 +f 537/670/537 562/711/562 538/671/538 +f 432/696/432 563/712/563 537/670/537 +f 564/713/564 432/550/432 431/549/431 +f 560/709/560 533/661/533 534/663/534 +f 565/714/565 431/549/431 533/661/533 +f 541/674/541 566/715/566 535/665/535 +f 567/716/567 568/717/568 569/718/569 +f 570/719/570 571/720/571 572/721/572 +f 573/722/573 570/719/570 574/723/574 +f 571/720/571 575/724/575 572/721/572 +f 576/725/576 577/726/577 555/704/555 +f 578/727/578 579/728/579 576/725/576 +f 580/729/580 578/727/578 581/730/581 +f 582/731/582 581/732/581 558/707/558 +f 583/733/583 584/734/584 568/735/568 +f 568/735/568 585/736/585 573/737/573 +f 586/738/586 585/736/585 587/739/587 +f 586/738/586 588/740/588 571/741/571 +f 569/718/569 573/722/573 574/723/574 +f 589/742/589 583/743/583 567/716/567 +f 590/744/590 589/742/589 591/745/591 +f 575/746/575 590/744/590 591/745/591 +f 592/747/592 591/745/591 593/748/593 +f 593/748/593 589/742/589 594/749/594 +f 594/749/594 567/716/567 595/750/595 +f 596/751/596 574/723/574 597/752/597 +f 572/721/572 592/753/592 598/754/598 +f 574/723/574 599/755/599 597/752/597 +f 599/755/599 572/721/572 598/754/598 +f 567/716/567 596/751/596 595/750/595 +f 600/756/600 601/757/601 564/713/564 +f 601/758/601 602/759/602 563/712/563 +f 563/712/563 603/760/603 562/711/562 +f 562/711/562 604/761/604 561/710/561 +f 605/762/605 565/714/565 560/709/560 +f 606/763/606 564/713/564 565/714/565 +f 561/710/561 607/764/607 566/715/566 +f 566/715/566 605/762/605 560/709/560 +f 608/765/608 609/766/609 610/767/610 +f 611/768/611 612/769/612 609/770/609 +f 613/771/613 585/736/585 614/772/614 +f 615/773/615 587/739/587 613/771/613 +f 616/774/616 617/775/617 618/776/618 +f 619/777/619 588/740/588 620/778/620 +f 621/779/621 622/780/622 623/781/623 +f 624/782/624 623/781/623 625/783/625 +f 626/784/626 627/785/627 628/786/628 +f 628/787/628 629/788/629 630/789/630 +f 631/790/631 630/789/630 632/791/632 +f 633/792/633 628/786/628 631/793/631 +f 617/775/617 624/782/624 618/776/618 +f 617/775/617 634/794/634 621/779/621 +f 622/780/622 613/771/613 623/781/623 +f 623/781/623 614/772/614 625/783/625 +f 635/795/635 611/768/611 627/785/627 +f 629/788/629 611/796/611 608/765/608 +f 622/780/622 636/797/636 615/773/615 +f 637/798/637 608/765/608 636/797/636 +f 638/799/638 639/800/639 635/795/635 +f 625/783/625 640/801/640 638/799/638 +f 633/792/633 641/802/641 626/784/626 +f 618/776/618 641/802/641 642/803/642 +f 643/804/643 644/805/644 634/794/634 +f 616/774/616 630/789/630 644/805/644 +f 634/794/634 637/798/637 622/780/622 +f 630/789/630 637/798/637 644/805/644 +f 641/802/641 635/795/635 626/784/626 +f 624/782/624 638/799/638 641/802/641 +f 590/806/590 620/778/620 610/807/610 +f 645/808/645 610/767/610 609/766/609 +f 631/793/631 642/803/642 633/792/633 +f 632/791/632 618/776/618 642/803/642 +f 640/801/640 612/769/612 639/800/639 +f 614/772/614 584/734/584 640/801/640 +f 636/809/636 610/807/610 620/778/620 +f 646/810/646 647/811/647 648/812/648 +f 649/813/649 647/811/647 650/814/650 +f 651/815/651 652/816/652 649/813/649 +f 646/810/646 653/817/653 651/815/651 +f 646/810/646 649/813/649 650/814/650 +f 636/809/636 647/811/647 652/816/652 +f 645/818/645 612/769/612 583/733/583 +f 615/773/615 652/816/652 653/817/653 +f 647/811/647 588/740/588 648/812/648 +f 648/812/648 615/773/615 653/817/653 +f 654/819/654 655/820/655 550/690/550 +f 656/821/656 657/822/657 658/823/658 +f 659/824/659 660/825/660 661/826/661 +f 662/827/662 663/828/663 664/829/664 +f 665/830/665 666/831/666 667/832/667 +f 668/833/668 663/828/663 669/834/669 +f 462/835/462 550/690/550 655/820/655 +f 670/836/670 671/837/671 672/838/672 +f 673/839/673 456/575/456 674/840/674 +f 675/841/675 669/834/669 676/842/676 +f 674/840/674 677/843/677 656/821/656 +f 672/838/672 678/844/678 679/845/679 +f 530/846/530 655/820/655 657/847/657 +f 675/841/675 550/690/550 543/677/543 +f 530/848/530 677/843/677 459/849/459 +f 680/850/680 672/851/672 679/852/679 +f 681/853/681 670/854/670 672/851/672 +f 459/849/459 456/575/456 443/855/443 +f 660/825/660 668/833/668 661/826/661 +f 682/856/682 665/857/665 683/858/683 +f 662/827/662 660/825/660 684/859/684 +f 685/860/685 657/847/657 655/820/655 +f 679/852/679 685/860/685 680/850/680 +f 666/861/666 662/827/662 684/859/684 +f 686/862/686 654/819/654 675/841/675 +f 678/844/678 674/840/674 656/821/656 +f 686/862/686 676/842/676 683/863/683 +f 687/864/687 674/840/674 671/865/671 +f 682/866/682 662/827/662 688/867/688 +f 666/868/666 659/824/659 667/869/667 +f 679/845/679 656/821/656 658/823/658 +f 680/850/680 654/819/654 681/870/681 +f 687/864/687 659/824/659 673/839/673 +f 682/871/682 676/842/676 689/872/689 +f 686/873/686 665/857/665 670/854/670 +f 457/576/457 668/833/668 532/659/532 +f 676/842/676 663/828/663 689/872/689 +f 668/833/668 543/677/543 532/659/532 +f 665/830/665 687/874/687 670/836/670 +f 673/839/673 661/826/661 457/576/457 +f 454/573/454 447/875/447 411/876/411 +f 61/447/61 454/573/454 411/876/411 +f 447/875/447 412/529/412 551/877/551 +f 412/529/412 221/878/221 551/877/551 +f 558/707/558 690/879/690 600/756/600 +f 690/880/690 578/727/578 602/759/602 +f 602/759/602 576/725/576 603/760/603 +f 603/760/603 555/704/555 604/761/604 +f 554/703/554 606/763/606 605/762/605 +f 559/708/559 600/756/600 606/763/606 +f 604/761/604 552/701/552 607/764/607 +f 607/764/607 554/703/554 605/762/605 +f 595/750/595 556/881/556 577/882/577 +f 557/883/557 598/754/598 582/884/582 +f 597/752/597 557/883/557 553/885/553 +f 598/754/598 580/886/580 582/884/582 +f 556/881/556 597/752/597 553/885/553 +f 579/887/579 595/750/595 577/882/577 +f 691/888/691 594/749/594 579/887/579 +f 580/889/580 593/748/593 691/888/691 +f 510/636/510 491/890/491 548/686/548 +f 495/621/495 517/643/517 547/685/547 +f 519/645/519 488/614/488 498/624/498 +f 527/653/527 516/642/516 692/891/692 +f 547/685/547 519/645/519 498/624/498 +f 692/891/692 515/641/515 495/621/495 +f 511/637/511 527/653/527 491/617/491 +f 508/634/508 548/686/548 488/614/488 +f 460/579/460 544/680/544 693/892/693 +f 509/635/509 461/689/461 545/681/545 +f 512/638/512 458/577/458 514/640/514 +f 444/562/444 524/650/524 518/644/518 +f 524/650/524 466/585/466 509/635/509 +f 545/893/545 463/582/463 512/638/512 +f 514/640/514 460/579/460 693/892/693 +f 445/563/445 518/644/518 544/680/544 +f 477/603/477 694/894/694 546/683/546 +f 503/629/503 485/611/485 500/626/500 +f 487/613/487 506/632/506 525/651/525 +f 481/607/481 526/652/526 505/631/505 +f 500/626/500 487/613/487 525/651/525 +f 526/652/526 484/610/484 503/629/503 +f 694/895/694 481/607/481 505/631/505 +f 478/604/478 546/683/546 506/632/506 +f 539/672/539 430/896/430 540/673/540 +f 436/897/436 540/695/540 537/670/537 +f 428/546/428 438/556/438 536/898/536 +f 441/559/441 419/537/419 438/556/438 +f 436/554/436 423/541/423 435/553/435 +f 429/547/429 536/898/536 430/899/430 +f 421/539/421 80/85/80 423/541/423 +f 417/535/417 74/79/74 419/537/419 +f 1/1/1 7/7/7 2/2/2 +f 3/3/3 28/28/28 4/4/4 +f 1/1/1 4/4/4 5/5/5 +f 6/6/6 52/54/52 7/7/7 +f 8/8/8 14/14/14 9/9/9 +f 10/10/10 30/30/30 11/11/11 +f 8/8/8 11/11/11 12/12/12 +f 13/13/13 49/50/49 14/14/14 +f 15/15/15 21/21/21 16/16/16 +f 17/17/17 38/38/38 18/18/18 +f 15/15/15 18/18/18 19/19/19 +f 20/20/20 43/44/43 21/21/21 +f 11/11/11 30/30/30 22/22/22 +f 23/23/23 22/22/22 24/24/24 +f 25/25/25 695/900/695 26/26/26 +f 23/23/23 26/26/26 12/12/12 +f 27/27/27 31/31/31 5/5/5 +f 27/27/27 4/4/4 28/28/28 +f 29/29/29 24/24/24 22/22/22 +f 27/27/27 22/22/22 30/30/30 +f 32/32/32 696/901/696 33/33/33 +f 35/35/35 34/902/34 36/36/36 +f 35/35/35 37/37/37 38/38/38 +f 35/39/35 111/117/111 32/32/32 +f 39/40/39 20/20/20 19/19/19 +f 40/41/40 49/50/49 41/42/41 +f 41/42/41 46/47/46 42/43/42 +f 39/40/39 42/43/42 43/44/43 +f 41/42/41 49/50/49 13/13/13 +f 44/45/44 13/13/13 12/12/12 +f 26/26/26 695/900/695 45/46/45 +f 44/45/44 45/46/45 46/47/46 +f 47/48/47 18/18/18 38/38/38 +f 48/49/48 9/9/9 14/14/14 +f 47/48/47 14/14/14 49/50/49 +f 40/41/40 19/19/19 18/18/18 +f 50/56/50 37/37/37 36/36/36 +f 50/51/50 51/53/51 52/54/52 +f 50/51/50 53/55/53 9/9/9 +f 48/49/48 38/38/38 37/37/37 +f 53/55/53 52/54/52 6/6/6 +f 54/57/54 6/6/6 5/5/5 +f 31/31/31 30/30/30 10/10/10 +f 54/57/54 10/10/10 9/9/9 +f 55/58/55 34/61/34 33/59/33 +f 56/60/56 2/2/2 7/7/7 +f 55/58/55 7/7/7 52/54/52 +f 55/58/55 51/53/51 36/52/36 +f 57/62/57 219/255/219 58/63/58 +f 60/65/60 108/113/108 61/66/61 +f 63/68/63 696/901/696 32/32/32 +f 65/70/65 127/134/127 66/71/66 +f 68/73/68 71/76/71 69/74/69 +f 71/76/71 99/104/99 72/77/72 +f 73/78/73 222/260/222 74/79/74 +f 76/81/76 364/465/364 77/82/77 +f 79/84/79 83/88/83 80/85/80 +f 82/87/82 137/150/137 83/88/83 +f 84/89/84 87/92/87 85/90/85 +f 87/92/87 218/466/218 88/93/88 +f 89/94/89 217/276/217 90/95/90 +f 92/97/92 82/87/82 79/84/79 +f 93/98/93 79/84/79 81/86/81 +f 95/100/95 364/465/364 76/81/76 +f 97/102/97 198/219/198 73/78/73 +f 98/103/98 197/217/197 99/104/99 +f 100/105/100 98/103/98 71/76/71 +f 101/106/101 116/205/116 102/107/102 +f 104/109/104 108/113/108 105/110/105 +f 107/112/107 105/110/105 108/113/108 +f 109/114/109 57/62/57 59/64/59 +f 17/116/17 114/120/114 111/117/111 +f 64/69/64 32/32/32 111/117/111 +f 113/119/113 214/243/214 114/120/114 +f 116/122/116 192/208/192 117/123/117 +f 118/125/118 216/248/216 119/126/119 +f 121/128/121 191/206/191 122/129/122 +f 124/131/124 212/239/212 125/132/125 +f 127/134/127 168/181/168 128/135/128 +f 65/137/65 57/62/57 130/138/130 +f 109/114/109 193/209/193 132/140/132 +f 134/142/134 162/175/162 135/143/135 +f 110/145/110 59/149/59 82/87/82 +f 129/136/129 100/202/100 68/146/68 +f 136/144/136 84/903/84 86/147/86 +f 127/134/127 68/146/68 70/148/70 +f 59/149/59 58/904/58 137/150/137 +f 138/151/138 205/228/205 139/152/139 +f 141/154/141 187/200/187 142/155/142 +f 144/157/144 211/237/211 145/158/145 +f 147/160/147 188/201/188 148/161/148 +f 150/163/150 210/236/210 151/164/151 +f 153/166/153 186/199/186 154/167/154 +f 156/169/156 206/230/206 157/170/157 +f 159/172/159 182/195/182 160/173/160 +f 162/175/162 185/198/185 163/176/163 +f 133/141/133 132/140/132 164/177/164 +f 131/139/131 130/138/130 166/179/166 +f 168/181/168 209/235/209 169/182/169 +f 170/183/170 208/232/208 171/184/171 +f 173/186/173 183/196/183 174/187/174 +f 176/189/176 207/231/207 177/190/177 +f 179/192/179 184/197/184 180/193/180 +f 148/161/148 182/195/182 159/172/159 +f 145/158/145 206/230/206 156/169/156 +f 143/156/143 142/155/142 153/166/153 +f 139/152/139 205/228/205 150/163/150 +f 139/152/139 152/165/152 182/195/182 +f 146/159/146 156/169/156 153/166/153 +f 178/191/178 177/190/177 174/187/174 +f 184/197/184 170/183/170 172/185/172 +f 128/135/128 169/182/169 185/198/185 +f 130/138/130 133/141/133 165/178/165 +f 156/169/156 158/171/158 186/199/186 +f 182/195/182 152/165/152 151/164/151 +f 144/157/144 146/159/146 142/155/142 +f 140/153/140 139/152/139 148/161/148 +f 134/142/134 86/147/86 100/202/100 +f 189/203/189 110/145/110 92/97/92 +f 57/62/57 109/114/109 133/141/133 +f 129/136/129 128/135/128 162/175/162 +f 124/131/124 126/133/126 116/205/116 +f 191/206/191 118/125/118 120/127/120 +f 126/207/126 204/227/204 192/208/192 +f 193/209/193 109/114/109 110/115/110 +f 194/211/194 195/215/195 105/212/105 +f 106/214/106 105/212/105 195/215/195 +f 86/91/86 85/90/85 98/103/98 +f 85/90/85 88/93/88 197/217/197 +f 91/96/91 90/95/90 198/257/198 +f 199/220/199 95/464/95 96/221/96 +f 201/223/201 93/98/93 94/99/94 +f 190/204/190 92/97/92 93/98/93 +f 203/225/203 215/256/215 106/214/106 +f 125/226/125 363/443/363 204/227/204 +f 205/228/205 143/905/143 155/229/155 +f 149/162/149 159/172/159 206/230/206 +f 176/189/176 179/192/179 181/194/181 +f 208/232/208 173/906/173 175/233/175 +f 131/234/131 167/462/167 209/235/209 +f 132/140/132 135/143/135 163/176/163 +f 206/230/206 159/172/159 161/174/161 +f 155/229/155 154/460/154 210/236/210 +f 211/237/211 147/160/147 149/162/149 +f 141/238/141 143/905/143 205/228/205 +f 189/210/189 84/903/84 136/144/136 +f 193/209/193 136/144/136 135/143/135 +f 65/70/65 131/234/131 168/181/168 +f 212/239/212 121/461/121 123/240/123 +f 213/241/213 363/443/363 125/226/125 +f 112/118/112 111/117/111 114/120/114 +f 16/244/16 115/121/115 114/120/114 +f 215/245/215 220/258/220 104/246/104 +f 103/108/103 102/107/102 119/126/119 +f 202/249/202 199/220/199 217/250/217 +f 190/204/190 201/223/201 218/252/218 +f 190/204/190 87/253/87 84/254/84 +f 65/137/65 67/907/67 219/255/219 +f 203/225/203 200/222/200 96/221/96 +f 90/95/90 194/211/194 107/213/107 +f 198/219/198 107/112/107 60/65/60 +f 215/245/215 96/101/96 76/81/76 +f 220/258/220 76/81/76 78/83/78 +f 60/65/60 62/67/62 222/260/222 +f 223/261/223 231/269/231 224/262/224 +f 226/264/226 223/261/223 225/263/225 +f 228/266/228 256/296/256 229/267/229 +f 230/268/230 229/267/229 224/262/224 +f 196/216/196 195/215/195 232/270/232 +f 200/222/200 203/225/203 234/272/234 +f 199/220/199 200/222/200 235/273/235 +f 217/250/217 199/220/199 236/274/236 +f 217/276/217 237/322/237 238/277/238 +f 194/211/194 239/278/239 232/270/232 +f 90/95/90 238/277/238 239/278/239 +f 203/225/203 196/216/196 233/271/233 +f 240/279/240 260/305/260 241/280/241 +f 243/282/243 249/288/249 244/283/244 +f 243/282/243 245/284/245 246/285/246 +f 248/287/248 265/311/265 244/283/244 +f 250/289/250 226/264/226 227/265/227 +f 252/291/252 250/289/250 251/290/251 +f 252/291/252 253/292/253 254/293/254 +f 255/295/255 254/455/254 256/296/256 +f 257/297/257 285/335/285 242/298/242 +f 258/300/258 257/297/257 241/299/241 +f 245/302/245 289/339/289 258/300/258 +f 259/303/259 289/339/289 245/302/245 +f 260/305/260 247/286/247 246/285/246 +f 261/306/261 240/279/240 242/281/242 +f 261/306/261 262/307/262 263/308/263 +f 248/313/248 264/309/264 263/308/263 +f 266/312/266 267/314/267 264/309/264 +f 267/314/267 268/315/268 261/306/261 +f 268/315/268 273/321/273 240/279/240 +f 269/316/269 272/320/272 247/286/247 +f 266/317/266 248/287/248 249/288/249 +f 271/319/271 243/282/243 247/286/247 +f 271/319/271 270/318/270 249/288/249 +f 269/316/269 260/305/260 240/279/240 +f 237/322/237 276/450/276 274/323/274 +f 237/275/237 236/274/236 275/324/275 +f 236/274/236 235/273/235 277/326/277 +f 235/273/235 234/272/234 278/327/278 +f 239/278/239 280/329/280 279/328/279 +f 238/277/238 274/323/274 280/329/280 +f 234/272/234 233/271/233 281/330/281 +f 233/271/233 232/270/232 279/328/279 +f 282/331/282 318/374/318 283/332/283 +f 284/334/284 283/908/283 285/335/285 +f 258/300/258 289/339/289 287/337/287 +f 289/339/289 259/303/259 290/340/290 +f 291/341/291 307/360/307 292/342/292 +f 265/344/265 317/372/317 259/303/259 +f 294/345/294 298/349/298 295/346/295 +f 297/348/297 308/361/308 298/349/298 +f 299/350/299 309/362/309 300/351/300 +f 302/353/302 299/363/299 301/354/301 +f 303/355/303 301/354/301 304/356/304 +f 301/352/301 300/351/300 306/358/306 +f 297/348/297 294/345/294 292/342/292 +f 292/342/292 294/345/294 296/347/296 +f 287/337/287 290/340/290 295/346/295 +f 288/338/288 287/337/287 298/349/298 +f 284/334/284 286/336/286 309/362/309 +f 302/353/302 282/331/282 284/333/284 +f 295/346/295 290/340/290 310/364/310 +f 311/365/311 310/364/310 282/331/282 +f 286/336/286 313/367/313 312/366/312 +f 313/367/313 288/338/288 308/361/308 +f 306/358/306 300/351/300 314/368/314 +f 314/368/314 297/348/297 307/360/307 +f 293/343/293 296/347/296 316/370/316 +f 291/341/291 316/370/316 303/355/303 +f 296/347/296 295/346/295 311/365/311 +f 311/365/311 302/353/302 303/355/303 +f 309/362/309 312/366/312 314/368/314 +f 312/366/312 308/361/308 297/348/297 +f 263/371/263 318/909/318 317/372/317 +f 262/373/262 283/332/283 318/374/318 +f 304/359/304 306/358/306 315/369/315 +f 305/357/305 315/369/315 307/360/307 +f 285/335/285 257/297/257 313/367/313 +f 257/297/257 258/300/258 288/338/288 +f 310/383/310 317/372/317 318/909/318 +f 319/376/319 326/385/326 320/377/320 +f 320/377/320 323/380/323 322/379/322 +f 323/380/323 325/382/325 324/381/324 +f 325/382/325 326/385/326 319/376/319 +f 322/379/322 324/381/324 319/376/319 +f 320/377/320 317/372/317 310/383/310 +f 285/335/285 283/908/283 262/384/262 +f 323/380/323 310/383/310 290/340/290 +f 320/377/320 326/385/326 259/303/259 +f 326/385/326 325/382/325 290/340/290 +f 327/386/327 213/241/213 328/387/328 +f 330/389/330 352/412/352 331/390/331 +f 333/392/333 358/423/358 334/393/334 +f 336/395/336 343/402/343 337/396/337 +f 339/398/339 362/910/362 340/399/340 +f 342/401/342 349/409/349 337/396/337 +f 122/403/122 328/387/328 213/241/213 +f 344/404/344 361/444/361 345/405/345 +f 117/123/117 192/208/192 347/407/347 +f 349/409/349 363/443/363 350/410/350 +f 352/412/352 117/123/117 348/408/348 +f 353/413/353 344/404/344 346/406/346 +f 120/415/120 330/428/330 328/387/328 +f 350/410/350 363/443/363 213/241/213 +f 352/412/352 330/389/330 120/416/120 +f 355/418/355 354/430/354 346/419/346 +f 356/420/356 346/419/346 345/421/345 +f 117/123/117 352/412/352 119/417/119 +f 342/401/342 343/402/343 334/393/334 +f 339/424/339 341/911/341 359/425/359 +f 334/393/334 343/402/343 336/395/336 +f 329/388/329 328/387/328 330/428/330 +f 329/388/329 332/429/332 354/430/354 +f 340/431/340 335/427/335 336/395/336 +f 357/433/357 350/410/350 327/386/327 +f 353/413/353 331/390/331 348/408/348 +f 351/411/351 350/410/350 357/433/357 +f 348/408/348 347/407/347 361/437/361 +f 336/395/336 338/397/338 359/438/359 +f 333/392/333 335/394/335 340/439/340 +f 354/414/354 332/391/332 331/390/331 +f 327/386/327 329/388/329 355/418/355 +f 361/437/361 347/407/347 333/392/333 +f 359/441/359 338/442/338 351/411/351 +f 357/422/357 345/421/345 339/424/339 +f 192/208/192 204/227/204 342/401/342 +f 337/396/337 349/409/349 351/411/351 +f 363/443/363 349/409/349 342/401/342 +f 361/444/361 362/910/362 339/398/339 +f 347/407/347 192/208/192 358/423/358 +f 104/445/104 112/118/112 214/243/214 +f 61/447/61 108/446/108 214/243/214 +f 104/445/104 220/449/220 64/69/64 +f 221/448/221 63/68/63 64/69/64 +f 276/450/276 255/295/255 228/266/228 +f 276/325/276 275/324/275 252/291/252 +f 275/324/275 277/326/277 250/289/250 +f 277/326/277 278/327/278 226/264/226 +f 280/329/280 230/268/230 231/269/231 +f 274/323/274 228/266/228 230/268/230 +f 278/327/278 281/330/281 223/261/223 +f 281/330/281 279/328/279 231/269/231 +f 225/263/225 269/456/269 273/451/273 +f 229/267/229 256/296/256 270/452/270 +f 229/267/229 271/453/271 272/454/272 +f 254/455/254 266/912/266 270/452/270 +f 225/263/225 224/262/224 272/454/272 +f 251/290/251 227/265/227 273/451/273 +f 253/292/253 251/290/251 268/457/268 +f 254/293/254 253/292/253 267/458/267 +f 154/460/154 173/906/173 208/232/208 +f 157/170/157 161/174/161 179/192/179 +f 151/164/151 170/183/170 184/197/184 +f 186/199/186 158/171/158 178/191/178 +f 161/174/161 160/173/160 184/197/184 +f 158/171/158 157/170/157 176/189/176 +f 186/199/186 183/196/183 173/186/173 +f 210/236/210 208/232/208 170/183/170 +f 181/194/181 103/108/103 216/248/216 +f 171/184/171 175/233/175 121/461/121 +f 174/187/174 177/190/177 118/125/118 +f 172/185/172 124/131/124 101/106/101 +f 172/185/172 171/184/171 212/239/212 +f 175/188/175 174/187/174 191/206/191 +f 177/190/177 207/231/207 216/248/216 +f 180/193/180 101/106/101 103/108/103 +f 167/462/167 141/238/141 138/151/138 +f 164/177/164 163/176/163 147/160/147 +f 169/182/169 140/153/140 188/201/188 +f 165/178/165 144/157/144 187/200/187 +f 163/176/163 185/198/185 188/201/188 +f 165/178/165 164/177/164 211/237/211 +f 167/180/167 166/179/166 187/200/187 +f 209/235/209 138/151/138 140/153/140 +f 201/223/201 202/224/202 89/463/89 +f 202/224/202 94/99/94 95/100/95 +f 88/93/88 91/218/91 97/102/97 +f 197/217/197 97/102/97 75/80/75 +f 95/100/95 94/99/94 81/86/81 +f 218/466/218 89/913/89 91/218/91 +f 364/465/364 81/86/81 80/85/80 +f 99/104/99 75/80/75 74/79/74 +f 365/467/365 3/471/3 2/468/2 +f 367/470/367 28/493/28 3/471/3 +f 365/467/365 369/473/369 368/472/368 +f 366/469/366 403/514/403 369/473/369 +f 370/474/370 374/478/374 371/475/371 +f 373/477/373 387/494/387 374/478/374 +f 370/474/370 376/480/376 375/479/375 +f 372/476/372 401/511/401 376/480/376 +f 377/481/377 379/485/379 16/482/16 +f 378/484/378 392/501/392 379/485/379 +f 377/481/377 381/487/381 380/486/380 +f 21/483/21 43/44/43 381/487/381 +f 382/488/382 387/494/387 373/477/373 +f 383/489/383 25/25/25 24/24/24 +f 384/490/384 695/900/695 25/25/25 +f 383/489/383 373/477/373 375/479/375 +f 385/491/385 367/470/367 368/472/368 +f 385/491/385 29/29/29 28/493/28 +f 382/488/382 24/24/24 29/29/29 +f 385/491/385 386/492/386 387/494/387 +f 33/495/33 696/914/696 388/496/388 +f 390/498/390 393/502/393 391/499/391 +f 390/498/390 379/485/379 392/501/392 +f 390/503/390 389/497/389 388/496/388 +f 395/505/395 397/507/397 380/486/380 +f 396/506/396 401/511/401 397/507/397 +f 42/43/42 46/47/46 396/506/396 +f 395/505/395 381/487/381 43/44/43 +f 376/480/376 401/511/401 396/506/396 +f 398/508/398 384/490/384 375/479/375 +f 45/46/45 695/900/695 384/490/384 +f 398/508/398 396/506/396 46/47/46 +f 399/509/399 400/510/400 392/501/392 +f 372/476/372 371/475/371 400/510/400 +f 399/509/399 397/507/397 401/511/401 +f 378/484/378 380/486/380 397/507/397 +f 402/513/402 404/515/404 391/522/391 +f 402/513/402 405/516/405 403/514/403 +f 402/512/402 400/510/400 371/475/371 +f 393/502/393 392/501/392 400/510/400 +f 369/473/369 403/514/403 405/516/405 +f 406/517/406 386/492/386 368/472/368 +f 374/478/374 387/494/387 386/492/386 +f 406/517/406 405/516/405 371/475/371 +f 407/518/407 56/521/56 33/519/33 +f 366/469/366 2/468/2 56/521/56 +f 407/518/407 404/515/404 403/514/403 +f 407/518/407 389/520/389 391/522/391 +f 58/523/58 219/699/219 408/524/408 +f 410/526/410 62/67/62 61/66/61 +f 388/496/388 696/914/696 63/528/63 +f 413/530/413 67/915/67 66/531/66 +f 415/533/415 70/75/70 69/74/69 +f 416/534/416 69/74/69 72/77/72 +f 74/79/74 222/260/222 418/536/418 +f 420/538/420 78/83/78 77/82/77 +f 80/85/80 83/88/83 422/540/422 +f 83/88/83 137/150/137 424/542/424 +f 425/543/425 475/666/475 426/544/426 +f 427/545/427 426/544/426 428/546/428 +f 430/548/430 536/667/536 431/549/431 +f 422/540/422 424/542/424 433/551/433 +f 423/541/423 422/540/422 434/552/434 +f 420/538/420 421/539/421 436/554/436 +f 438/556/438 419/537/419 418/536/418 +f 440/558/440 416/534/416 417/535/417 +f 442/560/442 415/533/415 416/534/416 +f 443/561/443 455/656/455 444/562/444 +f 446/564/446 411/527/411 447/565/447 +f 449/567/449 410/526/410 411/527/411 +f 409/525/409 408/524/408 450/568/450 +f 379/570/379 390/503/390 394/504/394 +f 394/504/394 388/496/388 412/529/412 +f 113/119/113 115/121/115 452/571/452 +f 455/574/455 443/855/443 456/575/456 +f 458/577/458 530/657/530 459/578/459 +f 461/580/461 549/916/549 462/581/462 +f 464/583/464 531/917/531 465/584/465 +f 467/586/467 507/633/507 414/532/414 +f 413/588/413 504/630/504 469/589/469 +f 450/568/450 502/628/502 470/590/470 +f 472/592/472 501/627/501 473/593/473 +f 424/542/424 409/601/409 451/595/451 +f 468/587/468 414/532/414 415/596/415 +f 474/594/474 473/593/473 475/598/475 +f 414/532/414 66/531/66 70/600/70 +f 137/150/137 58/904/58 409/601/409 +f 476/602/476 523/649/523 477/603/477 +f 479/605/479 522/648/522 480/606/480 +f 482/608/482 521/647/521 483/609/483 +f 485/611/485 520/646/520 486/612/486 +f 488/614/488 548/686/548 489/615/489 +f 491/617/491 527/653/527 492/618/492 +f 494/620/494 692/891/692 495/621/495 +f 497/623/497 547/685/547 498/624/498 +f 500/626/500 525/651/525 501/627/501 +f 502/628/502 526/652/526 503/629/503 +f 504/630/504 694/895/694 505/631/505 +f 506/632/506 546/683/546 507/633/507 +f 508/634/508 524/650/524 509/635/509 +f 511/637/511 545/893/545 512/638/512 +f 514/640/514 693/892/693 515/641/515 +f 517/643/517 544/680/544 518/644/518 +f 497/623/497 499/625/499 486/612/486 +f 494/620/494 496/622/496 483/609/483 +f 522/648/522 493/619/493 492/618/492 +f 476/602/476 490/616/490 489/615/489 +f 499/625/499 490/616/490 476/602/476 +f 492/618/492 494/620/494 521/647/521 +f 512/638/512 514/640/514 516/642/516 +f 519/645/519 518/644/518 524/650/524 +f 525/651/525 506/632/506 467/586/467 +f 469/589/469 505/631/505 526/652/526 +f 527/653/527 692/891/692 494/620/494 +f 499/625/499 498/624/498 488/614/488 +f 480/606/480 521/647/521 482/608/482 +f 486/612/486 476/602/476 478/604/478 +f 473/593/473 468/587/468 442/597/442 +f 433/551/433 451/595/451 528/654/528 +f 408/524/408 469/589/469 502/628/502 +f 501/627/501 467/586/467 468/587/468 +f 455/656/455 531/917/531 464/583/464 +f 463/582/463 462/581/462 530/657/530 +f 531/658/531 455/574/455 457/576/457 +f 451/569/451 450/568/450 471/591/471 +f 533/661/533 449/700/449 446/662/446 +f 534/663/534 446/662/446 448/664/448 +f 475/666/475 442/560/442 440/558/440 +f 426/544/426 440/558/440 441/559/441 +f 536/898/536 438/556/438 439/557/439 +f 437/669/437 436/897/436 537/670/537 +f 435/553/435 434/552/434 539/672/539 +f 434/552/434 433/551/433 529/655/529 +f 541/674/541 535/665/535 448/664/448 +f 465/676/465 531/658/531 532/659/532 +f 523/649/523 489/615/489 493/678/493 +f 496/622/496 497/623/497 520/646/520 +f 515/641/515 693/892/693 544/680/544 +f 510/636/510 509/635/509 545/681/545 +f 546/683/546 694/894/694 504/684/504 +f 470/590/470 503/629/503 500/626/500 +f 496/622/496 495/621/495 547/685/547 +f 548/686/548 491/890/491 493/678/493 +f 484/610/484 483/609/483 520/646/520 +f 523/649/523 522/679/522 479/687/479 +f 474/594/474 425/599/425 528/660/528 +f 471/591/471 470/590/470 472/592/472 +f 507/633/507 504/684/504 413/530/413 +f 466/585/466 465/584/465 549/688/549 +f 465/676/465 543/677/543 550/690/550 +f 452/571/452 394/504/394 453/572/453 +f 16/244/16 379/570/379 452/571/452 +f 542/692/542 448/918/448 447/693/447 +f 459/578/459 443/561/443 445/563/445 +f 540/695/540 430/919/430 432/696/432 +f 529/655/529 427/920/427 429/697/429 +f 425/698/425 427/920/427 529/655/529 +f 219/699/219 67/921/67 413/588/413 +f 541/674/541 542/675/542 437/669/437 +f 431/549/431 439/668/439 449/700/449 +f 439/557/439 418/536/418 410/526/410 +f 542/692/542 551/694/551 420/538/420 +f 551/694/551 221/259/221 78/83/78 +f 222/260/222 62/67/62 410/526/410 +f 552/701/552 556/705/556 553/702/553 +f 555/704/555 577/726/577 556/705/556 +f 557/706/557 582/731/582 558/707/558 +f 553/702/553 557/706/557 559/708/559 +f 535/665/535 566/715/566 560/709/560 +f 538/671/538 562/711/562 561/710/561 +f 537/670/537 563/712/563 562/711/562 +f 432/696/432 601/758/601 563/712/563 +f 564/713/564 601/757/601 432/550/432 +f 560/709/560 565/714/565 533/661/533 +f 565/714/565 564/713/564 431/549/431 +f 541/674/541 561/710/561 566/715/566 +f 567/716/567 583/743/583 568/717/568 +f 570/719/570 586/922/586 571/720/571 +f 573/722/573 586/922/586 570/719/570 +f 571/720/571 619/923/619 575/724/575 +f 576/725/576 579/728/579 577/726/577 +f 578/727/578 691/924/691 579/728/579 +f 580/729/580 691/924/691 578/727/578 +f 582/731/582 580/925/580 581/732/581 +f 583/733/583 612/769/612 584/734/584 +f 568/735/568 584/734/584 585/736/585 +f 586/738/586 573/737/573 585/736/585 +f 586/738/586 587/739/587 588/740/588 +f 569/718/569 568/717/568 573/722/573 +f 589/742/589 645/926/645 583/743/583 +f 590/744/590 645/926/645 589/742/589 +f 575/724/575 619/923/619 590/927/590 +f 592/747/592 575/746/575 591/745/591 +f 593/748/593 591/745/591 589/742/589 +f 594/749/594 589/742/589 567/716/567 +f 596/751/596 569/718/569 574/723/574 +f 572/721/572 575/724/575 592/753/592 +f 574/723/574 570/719/570 599/755/599 +f 599/755/599 570/719/570 572/721/572 +f 567/716/567 569/718/569 596/751/596 +f 600/756/600 690/879/690 601/757/601 +f 601/758/601 690/880/690 602/759/602 +f 563/712/563 602/759/602 603/760/603 +f 562/711/562 603/760/603 604/761/604 +f 605/762/605 606/763/606 565/714/565 +f 606/763/606 600/756/600 564/713/564 +f 561/710/561 604/761/604 607/764/607 +f 566/715/566 607/764/607 605/762/605 +f 608/765/608 611/796/611 609/766/609 +f 611/768/611 639/800/639 612/769/612 +f 613/771/613 587/739/587 585/736/585 +f 615/773/615 588/740/588 587/739/587 +f 616/774/616 643/804/643 617/775/617 +f 619/777/619 571/741/571 588/740/588 +f 621/779/621 634/794/634 622/780/622 +f 624/782/624 621/779/621 623/781/623 +f 626/784/626 635/795/635 627/785/627 +f 628/787/628 627/928/627 629/788/629 +f 631/790/631 628/787/628 630/789/630 +f 633/792/633 626/784/626 628/786/628 +f 617/775/617 621/779/621 624/782/624 +f 617/775/617 643/804/643 634/794/634 +f 622/780/622 615/773/615 613/771/613 +f 623/781/623 613/771/613 614/772/614 +f 635/795/635 639/800/639 611/768/611 +f 629/788/629 627/928/627 611/796/611 +f 622/780/622 637/798/637 636/797/636 +f 637/798/637 629/788/629 608/765/608 +f 638/799/638 640/801/640 639/800/639 +f 625/783/625 614/772/614 640/801/640 +f 633/792/633 642/803/642 641/802/641 +f 618/776/618 624/782/624 641/802/641 +f 643/804/643 616/774/616 644/805/644 +f 616/774/616 632/791/632 630/789/630 +f 634/794/634 644/805/644 637/798/637 +f 630/789/630 629/788/629 637/798/637 +f 641/802/641 638/799/638 635/795/635 +f 624/782/624 625/783/625 638/799/638 +f 590/806/590 619/777/619 620/778/620 +f 645/808/645 590/929/590 610/767/610 +f 631/793/631 632/791/632 642/803/642 +f 632/791/632 616/774/616 618/776/618 +f 640/801/640 584/734/584 612/769/612 +f 614/772/614 585/736/585 584/734/584 +f 636/797/636 608/765/608 610/767/610 +f 646/810/646 650/814/650 647/811/647 +f 649/813/649 652/816/652 647/811/647 +f 651/815/651 653/817/653 652/816/652 +f 646/810/646 648/812/648 653/817/653 +f 646/810/646 651/815/651 649/813/649 +f 636/809/636 620/778/620 647/811/647 +f 645/818/645 609/770/609 612/769/612 +f 615/773/615 636/809/636 652/816/652 +f 647/811/647 620/778/620 588/740/588 +f 648/812/648 588/740/588 615/773/615 +f 654/819/654 685/860/685 655/820/655 +f 656/821/656 677/843/677 657/822/657 +f 659/824/659 684/930/684 660/825/660 +f 662/827/662 689/931/689 663/828/663 +f 665/830/665 688/932/688 666/831/666 +f 668/833/668 664/829/664 663/828/663 +f 462/835/462 549/691/549 550/690/550 +f 670/836/670 687/874/687 671/837/671 +f 673/839/673 457/576/457 456/575/456 +f 675/841/675 543/677/543 669/834/669 +f 674/840/674 456/575/456 677/843/677 +f 672/838/672 671/837/671 678/844/678 +f 530/846/530 462/835/462 655/820/655 +f 675/841/675 654/819/654 550/690/550 +f 530/848/530 657/822/657 677/843/677 +f 680/850/680 681/853/681 672/851/672 +f 681/853/681 686/873/686 670/854/670 +f 459/849/459 677/843/677 456/575/456 +f 660/825/660 664/829/664 668/833/668 +f 682/856/682 688/933/688 665/857/665 +f 662/827/662 664/829/664 660/825/660 +f 685/860/685 658/934/658 657/847/657 +f 679/852/679 658/934/658 685/860/685 +f 666/861/666 688/867/688 662/827/662 +f 686/862/686 681/870/681 654/819/654 +f 678/844/678 671/865/671 674/840/674 +f 686/862/686 675/841/675 676/842/676 +f 687/864/687 673/839/673 674/840/674 +f 682/866/682 689/931/689 662/827/662 +f 666/868/666 684/930/684 659/824/659 +f 679/845/679 678/844/678 656/821/656 +f 680/850/680 685/860/685 654/819/654 +f 687/864/687 667/869/667 659/824/659 +f 682/871/682 683/863/683 676/842/676 +f 686/873/686 683/858/683 665/857/665 +f 457/576/457 661/826/661 668/833/668 +f 676/842/676 669/834/669 663/828/663 +f 668/833/668 669/834/669 543/677/543 +f 665/830/665 667/832/667 687/874/687 +f 673/839/673 659/824/659 661/826/661 +f 454/573/454 453/572/453 447/875/447 +f 61/447/61 113/119/113 454/573/454 +f 447/875/447 453/572/453 412/529/412 +f 412/529/412 63/528/63 221/878/221 +f 558/707/558 581/732/581 690/879/690 +f 690/880/690 581/730/581 578/727/578 +f 602/759/602 578/727/578 576/725/576 +f 603/760/603 576/725/576 555/704/555 +f 554/703/554 559/708/559 606/763/606 +f 559/708/559 558/707/558 600/756/600 +f 604/761/604 555/704/555 552/701/552 +f 607/764/607 552/701/552 554/703/554 +f 595/750/595 596/751/596 556/881/556 +f 557/883/557 599/755/599 598/754/598 +f 597/752/597 599/755/599 557/883/557 +f 598/754/598 592/753/592 580/886/580 +f 556/881/556 596/751/596 597/752/597 +f 579/887/579 594/749/594 595/750/595 +f 691/888/691 593/748/593 594/749/594 +f 580/889/580 592/747/592 593/748/593 +f 510/636/510 511/682/511 491/890/491 +f 495/621/495 515/641/515 517/643/517 +f 519/645/519 508/634/508 488/614/488 +f 527/653/527 513/639/513 516/642/516 +f 547/685/547 517/643/517 519/645/519 +f 692/891/692 516/642/516 515/641/515 +f 511/637/511 513/639/513 527/653/527 +f 508/634/508 510/636/510 548/686/548 +f 460/579/460 445/563/445 544/680/544 +f 509/635/509 466/585/466 461/689/461 +f 512/638/512 463/582/463 458/577/458 +f 444/562/444 464/583/464 524/650/524 +f 524/650/524 464/583/464 466/585/466 +f 545/893/545 461/580/461 463/582/463 +f 514/640/514 458/577/458 460/579/460 +f 445/563/445 444/562/444 518/644/518 +f 477/603/477 479/687/479 694/894/694 +f 503/629/503 484/610/484 485/611/485 +f 487/613/487 478/604/478 506/632/506 +f 481/607/481 482/608/482 526/652/526 +f 500/626/500 485/611/485 487/613/487 +f 526/652/526 482/608/482 484/610/484 +f 694/895/694 479/605/479 481/607/481 +f 478/604/478 477/603/477 546/683/546 +f 539/672/539 429/697/429 430/896/430 +f 436/554/436 435/553/435 540/673/540 +f 428/546/428 441/559/441 438/556/438 +f 441/559/441 417/535/417 419/537/419 +f 436/554/436 421/539/421 423/541/423 +f 429/547/429 428/546/428 536/898/536 +f 421/539/421 77/82/77 80/85/80 +f 417/535/417 72/77/72 74/79/74 diff --git a/res/shader/fs/default.glsl b/res/shader/fs/default.glsl new file mode 100644 index 0000000..a10d077 --- /dev/null +++ b/res/shader/fs/default.glsl @@ -0,0 +1,29 @@ +#version 330 core + +in vec2 passTextureCoords; +in vec3 passNormals; + +uniform vec4 color; +uniform sampler2D albedoTexture; +uniform bool useAlbedoTexture; + +out vec4 fragmentColor; + +void main() { + if (useAlbedoTexture) { + vec4 textureColor = texture(albedoTexture, passTextureCoords); + vec3 lightDirection = normalize(vec3(0.5, 0.5, 1.0)); // Direction of the light source + + // Calculate diffuse lighting + float diffuseFactor = max(dot(normalize(passNormals), lightDirection), 0.0); + vec3 diffuseLight = vec3(1.0) * diffuseFactor; + + // Combine diffuse lighting and texture color + vec3 finalColor = (diffuseLight + color.xyz) * textureColor.rgb; + + fragmentColor = vec4(finalColor, textureColor.a); + } else { + // If no texture, use the solid color + fragmentColor = color; + } +} \ No newline at end of file diff --git a/res/shader/fs/testFS.glsl b/res/shader/fs/testFS.glsl new file mode 100644 index 0000000..bbc2ed3 --- /dev/null +++ b/res/shader/fs/testFS.glsl @@ -0,0 +1,9 @@ +#version 400 core +in vec4 vertexColor; + +out vec4 fragColor; + +void main() +{ + fragColor = vertexColor; +} \ No newline at end of file diff --git a/res/shader/vs/default.glsl b/res/shader/vs/default.glsl new file mode 100644 index 0000000..de862c4 --- /dev/null +++ b/res/shader/vs/default.glsl @@ -0,0 +1,20 @@ +#version 330 core + +layout (location = 0) in vec3 vertexPositions; +layout (location = 1) in vec2 textureCoords; +layout (location = 2) in vec3 normals; + +uniform mat4 transformationMatrix; +uniform mat4 viewMatrix; +uniform mat4 projectionMatrix; + +out vec2 passTextureCoords; +out vec3 passNormals; + +void main() { + vec4 worldPosition = transformationMatrix * vec4(vertexPositions, 1.0); + gl_Position = projectionMatrix * (viewMatrix * worldPosition); + + passTextureCoords = textureCoords; + passNormals = mat3(transpose(inverse(transformationMatrix))) * normals; +} diff --git a/res/shader/vs/testVS.glsl b/res/shader/vs/testVS.glsl new file mode 100644 index 0000000..bd996ed --- /dev/null +++ b/res/shader/vs/testVS.glsl @@ -0,0 +1,9 @@ +#version 400 core +layout (location = 0) in vec3 position; + +out vec4 vertexColor; + +void main(){ + gl_Position = vec4(position, 1.0); + vertexColor = vec4(1.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/res/uv.png b/res/uv.png new file mode 100644 index 0000000..cfa969a Binary files /dev/null and b/res/uv.png differ diff --git a/src/dev/euph/engine/datastructs/Archetype.java b/src/dev/euph/engine/datastructs/Archetype.java new file mode 100644 index 0000000..9be2495 --- /dev/null +++ b/src/dev/euph/engine/datastructs/Archetype.java @@ -0,0 +1,56 @@ +package dev.euph.engine.datastructs; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Archetype { + //region Fields + private final Set> componentClasses; + //endregion + + //region Constructor + @SafeVarargs + public Archetype(Class... componentClasses){ + this.componentClasses = new HashSet<>(); + this.componentClasses.addAll(Arrays.asList(componentClasses)); + } + public Archetype(List> componentClasses){ + this.componentClasses = new HashSet<>(); + this.componentClasses.addAll(componentClasses); + } + //endregion + + + public boolean contains(Class componentClass) { + for(Class _componentClass : componentClasses){ + if(componentClass.isAssignableFrom(_componentClass)){ + return true; + } + } + return false; + } + + //region Overrides Comparison + @Override + public int hashCode() { + return componentClasses.hashCode(); + } + @Override + public boolean equals(Object obj) { + if(this == obj)return true; + if(obj == null || obj.getClass() != this.getClass()) return false; + + Archetype other = (Archetype) obj; + return other.hashCode() == this.hashCode(); + } + //endregion + + //region Getter + public Set> getComponentClasses() { + return componentClasses; + } + //endregion + +} diff --git a/src/dev/euph/engine/datastructs/octree/Octree.java b/src/dev/euph/engine/datastructs/octree/Octree.java new file mode 100644 index 0000000..803463a --- /dev/null +++ b/src/dev/euph/engine/datastructs/octree/Octree.java @@ -0,0 +1,22 @@ +package dev.euph.engine.datastructs.octree; + +import org.joml.Vector3f; + +import java.util.List; + +public class Octree { + private final OctreeNode root; + + public Octree(int maxCapacity, Vector3f center, float halfSize) { + root = new OctreeNode(maxCapacity, center, halfSize); + } + + public void insert(Vector3f position, T data) { + root.insert(position, data); + } + + public List query(Vector3f position, float radius) { + return root.query(position, radius); + } +} + diff --git a/src/dev/euph/engine/datastructs/octree/OctreeNode.java b/src/dev/euph/engine/datastructs/octree/OctreeNode.java new file mode 100644 index 0000000..a79c7da --- /dev/null +++ b/src/dev/euph/engine/datastructs/octree/OctreeNode.java @@ -0,0 +1,153 @@ +package dev.euph.engine.datastructs.octree; + +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.Math.max; +import static java.lang.String.join; + +public class OctreeNode { + private final int maxCapacity; + private final Vector3f center; + private final float halfSize; + private final List positions; + private final Map dataMap; + private OctreeNode[] children; + + public OctreeNode(int maxCapacity, Vector3f center, float halfSize) { + this.maxCapacity = maxCapacity; + this.center = center; + this.halfSize = halfSize; + this.positions = new ArrayList<>(); + this.dataMap = new HashMap<>(); + this.children = null; + } + + public void insert(Vector3f position, T data) { + // Check if the item fits within this node's region + if (isOutOfBounds(position)) { + return; + } + + // If the node is not subdivided, add the item to its data list and map + if (children == null) { + positions.add(position); + dataMap.put(position, data); + + // Subdivide if the capacity is exceeded + if (positions.size() > maxCapacity) { + subdivide(); + } + } else { + // Otherwise, insert the item into the appropriate child node + for (OctreeNode child : children) { + child.insert(position, data); + } + } + } + + public List query(Vector3f position, float radius) { + List result = new ArrayList<>(); + + if (isOutOfBounds(position)) { + return result; + } + + // Add data items from this node if their positions are within the query radius + for (Vector3f itemPosition : positions) { + if (itemPosition.distance(position) <= radius) { + result.add(dataMap.get(itemPosition)); + } + } + + // Recursively query child nodes + if (children != null) { + for (OctreeNode child : children) { + result.addAll(child.query(position, radius)); + } + } + + return result; + } + + private boolean isOutOfBounds(Vector3f position) { + float minX = center.x - halfSize; + float maxX = center.x + halfSize; + float minY = center.y - halfSize; + float maxY = center.y + halfSize; + float minZ = center.z - halfSize; + float maxZ = center.z + halfSize; + + return ( + position.x < minX || + position.x > maxX || + position.y < minY || + position.y > maxY || + position.z < minZ || + position.z > maxZ + ); + } + + + private void subdivide() { + OctreeNode[] childNodes = new OctreeNode[8]; + + float quarterSize = halfSize / 2.0f; + + // Calculate centers for each child node + Vector3f[] childCenters = new Vector3f[]{ + new Vector3f(center.x - quarterSize, center.y - quarterSize, center.z - quarterSize), + new Vector3f(center.x + quarterSize, center.y - quarterSize, center.z - quarterSize), + new Vector3f(center.x - quarterSize, center.y + quarterSize, center.z - quarterSize), + new Vector3f(center.x + quarterSize, center.y + quarterSize, center.z - quarterSize), + new Vector3f(center.x - quarterSize, center.y - quarterSize, center.z + quarterSize), + new Vector3f(center.x + quarterSize, center.y - quarterSize, center.z + quarterSize), + new Vector3f(center.x - quarterSize, center.y + quarterSize, center.z + quarterSize), + new Vector3f(center.x + quarterSize, center.y + quarterSize, center.z + quarterSize) + }; + + // Create and initialize child nodes + for (int i = 0; i < 8; i++) { + childNodes[i] = new OctreeNode<>(maxCapacity, childCenters[i], quarterSize); + } + children = childNodes; // Assign the initialized children array to the class field + + // Redistribute positions and data from this node to children + for (int i = positions.size() - 1; i >= 0; i--) { + Vector3f position = positions.get(i); + T data = dataMap.remove(position); // Remove data from the current node + + // Find the child that contains the position and add data to it + for (OctreeNode child : children) { + if (!child.isOutOfBounds(position)) { + child.insert(position, data); + break; + } + } + + // Remove the position from the current node + positions.remove(i); + } + } + + public List getPositions() { + List allPositions = new ArrayList<>(positions); + + if (children != null) { + for (OctreeNode child : children) { + allPositions.addAll(child.getPositions()); + } + } + + return allPositions; + } + + public OctreeNode[] getChildren() { + return children; + } + +} diff --git a/src/dev/euph/engine/datastructs/pipeline/Pipeline.java b/src/dev/euph/engine/datastructs/pipeline/Pipeline.java new file mode 100644 index 0000000..d769578 --- /dev/null +++ b/src/dev/euph/engine/datastructs/pipeline/Pipeline.java @@ -0,0 +1,42 @@ +package dev.euph.engine.datastructs.pipeline; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +public class Pipeline { + private final Collection> pipelineStages; + + public Pipeline() { + this.pipelineStages = new ArrayList<>(); + } + public Pipeline(PipelineStage pipelineStage) { + pipelineStages = Collections.singletonList(pipelineStage); + } + private Pipeline(Collection> pipelineStages) { + this.pipelineStages = new ArrayList<>(pipelineStages); + } + public Pipeline addStage(PipelineStage pipelineStage){ + final ArrayList> newPipelineStages = new ArrayList<>(pipelineStages); + newPipelineStages.add(pipelineStage); + return new Pipeline<>(newPipelineStages); + } + + /** + * @param input the input of the pipeline + * @return the Output of the pipelines execution + * @throws PipelineRuntimeException if there is an error durring execution + */ + @SuppressWarnings("all") + public Output execute(Input input) { + try { + Object output = input; + for (final PipelineStage pipelineStage : pipelineStages){ + output = pipelineStage.execute(output); + } + return (Output) output; + } catch (Exception exception){ + throw new PipelineRuntimeException(exception); + } + } +} diff --git a/src/dev/euph/engine/datastructs/pipeline/PipelineRuntimeException.java b/src/dev/euph/engine/datastructs/pipeline/PipelineRuntimeException.java new file mode 100644 index 0000000..1644e0c --- /dev/null +++ b/src/dev/euph/engine/datastructs/pipeline/PipelineRuntimeException.java @@ -0,0 +1,16 @@ +package dev.euph.engine.datastructs.pipeline; + +public class PipelineRuntimeException extends RuntimeException{ + public PipelineRuntimeException(String message) { + super("Pipeline Runtime Exception: " + message); + } + public PipelineRuntimeException(String message, Throwable cause) { + super("Pipeline Runtime Exception: " + message, cause); + } + public PipelineRuntimeException(Throwable cause) { + super("Pipeline Runtime Exception: " + cause); + } + protected PipelineRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super("Pipeline Runtime Exception: " + message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/dev/euph/engine/datastructs/pipeline/PipelineStage.java b/src/dev/euph/engine/datastructs/pipeline/PipelineStage.java new file mode 100644 index 0000000..7903d74 --- /dev/null +++ b/src/dev/euph/engine/datastructs/pipeline/PipelineStage.java @@ -0,0 +1,5 @@ +package dev.euph.engine.datastructs.pipeline; + +public interface PipelineStage { + Output execute(Input input); +} diff --git a/src/dev/euph/engine/ecs/Component.java b/src/dev/euph/engine/ecs/Component.java new file mode 100644 index 0000000..af67448 --- /dev/null +++ b/src/dev/euph/engine/ecs/Component.java @@ -0,0 +1,37 @@ +package dev.euph.engine.ecs; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Component { + + //region Fields + protected Entity entity = null; + protected List> requiredComponents = new ArrayList<>(); + //endregion + + public void OnReady(){} + public void OnUpdate(float deltaTime){} + public void OnDestroy(){} + + //region Component Management + protected void requireComponent(Class componentClass) { + requiredComponents.add(componentClass); + } + protected boolean hasRequiredComponents(Entity entity) { + + for (Class requiredClass : requiredComponents) { + if (entity.getComponent(requiredClass) == null) { + return false; + } + } + return true; + } + //endregion + + //region Getter + public Entity getEntity() { + return entity; + } + //endregion +} diff --git a/src/dev/euph/engine/ecs/Entity.java b/src/dev/euph/engine/ecs/Entity.java new file mode 100644 index 0000000..12e321d --- /dev/null +++ b/src/dev/euph/engine/ecs/Entity.java @@ -0,0 +1,101 @@ +package dev.euph.engine.ecs; + +import java.util.ArrayList; +import java.util.List; + +public class Entity { + //region Fields + private boolean destroyPending = false; + private boolean componentsChanged = false; + private String name; + private final List components; + //endregion + + //region Constructor + public Entity(String name){ + this.name = name; + components = new ArrayList<>(); + } + //endregion + + //region Component Management + public T addComponent(T component) { + + if (!component.hasRequiredComponents(this)) { + throw new IllegalArgumentException("Cannot add Component. Missing required Components."); + } + + this.components.add(component); + this.componentsChanged = true; + + if(component.entity != null){ + component.entity.removeComponent(component.getClass()); + } + component.entity = this; + + return component; + } + + public T getComponent(Class componentClass){ + for(Component c : components){ + if(componentClass.isAssignableFrom(c.getClass())){ + try { + return componentClass.cast(c); + }catch (ClassCastException e){ + e.printStackTrace(); + assert false : "Error: Casting component."; + } + } + } + return null; + } + public void removeComponent(Class componentClass){ + for(int i=0; i < components.size(); i++){ + if(componentClass.isAssignableFrom(components.get(i).getClass())){ + components.remove(i); + this.componentsChanged = true; + return; + } + } + } + //endregion + + public void ready(){ + for (Component component : components) { + component.OnReady(); + } + } + public void update(float deltaTime){ + for (Component component : components) { + component.OnUpdate(deltaTime); + } + } + public void destroy(){ + destroyPending = true; + for (Component component : components) { + component.OnDestroy(); + } + } + + //region Getter/Setter + public boolean isDestroyPending(){ + return destroyPending; + } + public List getComponents() { + return components; + } + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + protected boolean areComponentsChanged() { + return componentsChanged; + } + protected void setComponentsChanged(boolean componentsChanged) { + this.componentsChanged = componentsChanged; + } + //endregion +} diff --git a/src/dev/euph/engine/ecs/Scene.java b/src/dev/euph/engine/ecs/Scene.java new file mode 100644 index 0000000..ddc7bc1 --- /dev/null +++ b/src/dev/euph/engine/ecs/Scene.java @@ -0,0 +1,186 @@ +package dev.euph.engine.ecs; + +import dev.euph.engine.datastructs.Archetype; + +import java.util.*; + +public class Scene { + + //region Fields + private boolean isRunning = false; + private final List entities; + private final Map, List> entitiesByArchetype; + private final Map, Set>> subclassesByComponent; + private final Map, List> componentInstancesByType; + //endregion + + //region Constructor + public Scene(){ + entities = new ArrayList<>(); + entitiesByArchetype = new HashMap<>(); + subclassesByComponent = new HashMap<>(); + componentInstancesByType = new HashMap<>(); + } + //endregion + + public void start(){ + if(isRunning)return; + isRunning = true; + + for(Entity entity : entities){ + entity.ready(); + } + } + public void update(float deltaTime) { + List entitiesToRemove = new ArrayList<>(); + + for (Entity entity : entities) { + entity.update(deltaTime); + + if (entity.isDestroyPending()) { + removeEntityFromArchetype(entity); + entitiesToRemove.add(entity); + + // Remove the components of the entity from the componentInstancesByType map + for (Component component : entity.getComponents()) { + Class componentClass = component.getClass(); + List componentInstances = componentInstancesByType.get(componentClass); + if (componentInstances != null) { + componentInstances.remove(component); + } + } + } else if (entity.areComponentsChanged()) { + removeEntityFromArchetype(entity); + + List> components = new ArrayList<>(); + for (Component component : entity.getComponents()) { + components.add(component.getClass()); + } + Archetype newArchetype = new Archetype<>(components); + List archetypeEntities = entitiesByArchetype.computeIfAbsent(newArchetype, key -> new ArrayList<>()); + archetypeEntities.add(entity); + + for (Component component : entity.getComponents()) { + Class componentClass = component.getClass(); + List componentInstances = componentInstancesByType.computeIfAbsent(componentClass, key -> new ArrayList<>()); + componentInstances.add(component); + } + + entity.setComponentsChanged(false); + } + } + + entities.removeAll(entitiesToRemove); + } + + public void addEntityToScene(Entity entity) { + if (entities.contains(entity)) { + return; + } + entities.add(entity); + + List> components = new ArrayList<>(); + for (Component component : entity.getComponents()) { + components.add(component.getClass()); + } + Archetype newArchetype = new Archetype<>(components); + + for (Class componentClass : components) { + // Update subclasses mapping + updateSubclassesMapping(componentClass); + + // Add component instance to the componentInstancesByType map + List componentInstances = componentInstancesByType.computeIfAbsent(componentClass, key -> new ArrayList<>()); + componentInstances.add(entity.getComponent(componentClass)); // added + + // Handle subclasses: If the componentClass has a superclass, add the component instance to its list too + Class superClass = componentClass.getSuperclass(); + while (Component.class.isAssignableFrom(superClass)) { + if (Component.class.isAssignableFrom(superClass)) { + List superClassInstances = componentInstancesByType.computeIfAbsent(superClass.asSubclass(Component.class), key -> new ArrayList<>()); + superClassInstances.add(entity.getComponent(componentClass)); + } + superClass = superClass.getSuperclass(); + } + } + + List archetypeEntities = entitiesByArchetype.computeIfAbsent(newArchetype, key -> new ArrayList<>()); + archetypeEntities.add(entity); + + if (!isRunning) { + return; + } + entity.ready(); + } + @SafeVarargs + public final List getEntitiesWithComponents(boolean exactMatch, Class... componentClasses) { + List filteredEntities = new ArrayList<>(); + + if (exactMatch) { + Archetype requestedArchetype = new Archetype<>(componentClasses); + List matchingEntities = entitiesByArchetype.get(requestedArchetype); + if (matchingEntities != null) { + filteredEntities.addAll(matchingEntities); + } + } else { + Set> componentSet = new HashSet<>(); + for (Class componentClass : componentClasses) { + componentSet.add(componentClass); + componentSet.addAll(subclassesByComponent.getOrDefault(componentClass, Collections.emptySet())); + } + + for (Class componentClass : componentSet) { + List componentInstances = componentInstancesByType.get(componentClass); + if (componentInstances != null) { + filteredEntities.addAll(componentInstances.stream() + .map(Component::getEntity) + .toList()); + } + } + + } + + return filteredEntities; + } + public List getComponentInstances(Class componentClass){ + return componentInstancesByType.get(componentClass); + } + + //region ECS Utility Functions + private void removeEntityFromArchetype(Entity entity) { + for (Map.Entry, List> entry : entitiesByArchetype.entrySet()) { + Archetype archetype = entry.getKey(); + List archetypeEntities = entry.getValue(); + if (archetypeEntities.remove(entity)) { + if (archetypeEntities.isEmpty()) { + entitiesByArchetype.remove(archetype); + } + break; + } + } + } + private void updateSubclassesMapping(Class componentClass) { + Set> subclasses = getSubclasses(componentClass); + subclassesByComponent.put(componentClass, subclasses); + } + private Set> getSubclasses(Class componentClass) { + Set> subclasses = new HashSet<>(); + for (Class clazz : subclassesByComponent.keySet()) { + if (componentClass.isAssignableFrom(clazz)) { + subclasses.add(clazz); + } + } + return subclasses; + } + //endregion + + //region Getter + public boolean isRunning() { + return isRunning; + } + public List getEntities() { + return entities; + } + //endregion + +} diff --git a/src/dev/euph/engine/ecs/components/Camera.java b/src/dev/euph/engine/ecs/components/Camera.java new file mode 100644 index 0000000..e820fcb --- /dev/null +++ b/src/dev/euph/engine/ecs/components/Camera.java @@ -0,0 +1,38 @@ +package dev.euph.engine.ecs.components; + +import dev.euph.engine.ecs.Component; +import dev.euph.engine.managers.Window; + +import java.awt.*; + + +public class Camera extends Component { + + //region Fields + public float fov = 90f; + public float nearPlane = 0.1f; + public float farPlane = 500f; + public Color backgroundColor = new Color(0, 200, 255); + public Window window; + //endregion + + //region Constructor + public Camera(Window window){ + requireComponent(Transform.class); + this.window = window; + } + public Camera(Window window, float fov, float nearPlane, float farPlane, Color backgroundColor) { + requireComponent(Transform.class); + this.window = window; + this.fov = fov; + this.nearPlane = nearPlane; + this.farPlane = farPlane; + this.backgroundColor = backgroundColor; + } + //endregion + + + public Window getWindow() { + return window; + } +} diff --git a/src/dev/euph/engine/ecs/components/MeshRenderer.java b/src/dev/euph/engine/ecs/components/MeshRenderer.java new file mode 100644 index 0000000..bc163eb --- /dev/null +++ b/src/dev/euph/engine/ecs/components/MeshRenderer.java @@ -0,0 +1,30 @@ +package dev.euph.engine.ecs.components; + +import dev.euph.engine.ecs.Component; +import dev.euph.engine.render.Material; +import dev.euph.engine.resources.Mesh; + +public class MeshRenderer extends Component { + + //region Fields + private final Mesh mesh; + private final Material material; + //endregion + + //region Constructor + public MeshRenderer(Mesh mesh, Material material) { + requireComponent(Transform.class); + this.mesh = mesh; + this.material = material; + } + //endregion + + //region Getter + public Mesh getMesh() { + return mesh; + } + public Material getMaterial() { + return material; + } + //endregion +} diff --git a/src/dev/euph/engine/ecs/components/Transform.java b/src/dev/euph/engine/ecs/components/Transform.java new file mode 100644 index 0000000..8102dce --- /dev/null +++ b/src/dev/euph/engine/ecs/components/Transform.java @@ -0,0 +1,108 @@ +package dev.euph.engine.ecs.components; + +import dev.euph.engine.ecs.Component; +import dev.euph.engine.math.TransformationMatrix; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import static java.lang.Math.toRadians; + +public class Transform extends Component { + //region Fields + private Vector3f position; + private Vector3f rotation; + private Vector3f scale; + private TransformationMatrix transformationMatrix; + //endregion + + //region Constructor + public Transform() { + this(new Vector3f(), new Vector3f(), new Vector3f(1)); + } + public Transform(Vector3f position, Vector3f rotation, Vector3f scale) { + this.position = position; + this.rotation = rotation; + this.scale = scale; + calculateTransformationMatrix(); + } + public Transform(Transform transform) { + this.position = transform.position; + this.rotation = transform.rotation; + this.scale = transform.scale; + calculateTransformationMatrix(); + } + //endregion + + //region Utility + private void calculateTransformationMatrix() { + transformationMatrix = new TransformationMatrix(this); + } + //endregion + + //region Getter/Setter + public Vector3f getPosition() { + return new Vector3f(position); + } + + public void setPosition(Vector3f position) { + this.position = position; + calculateTransformationMatrix(); + } + + public Vector3f getRotation() { + return new Vector3f(rotation); + } + + public void setRotation(Vector3f rotation) { + this.rotation = rotation; + calculateTransformationMatrix(); + } + public Vector3f getScale() { + return new Vector3f(scale); + } + + public void setScale(Vector3f scale) { + this.scale = scale; + calculateTransformationMatrix(); + } + public TransformationMatrix getTransformationMatrix() { + return transformationMatrix; + } + + public Vector3f getUp() { + Quaternionf q = new Quaternionf().rotateXYZ( + (float) toRadians(rotation.x), + (float) toRadians(rotation.y), + (float) toRadians(rotation.z) + ); + return q.positiveY(new Vector3f()); + } + + public Vector3f getDown() { + return new Vector3f(getUp()).negate(); + } + + public Vector3f getRight() { + Quaternionf q = new Quaternionf().rotateY((float) toRadians(rotation.y)); + return q.positiveX(new Vector3f()); + } + + public Vector3f getLeft() { + return new Vector3f(getRight()).negate(); + } + + public Vector3f getForward() { + return new Vector3f(getBack()).negate(); + } + + public Vector3f getBack() { + Quaternionf q = new Quaternionf().rotateXYZ( + (float) toRadians(rotation.x), + (float) toRadians(rotation.y), + (float) toRadians(rotation.z) + ); + return q.positiveZ(new Vector3f()); + } + //endregion +} \ No newline at end of file diff --git a/src/dev/euph/engine/ecs/components/lights/DirectionalLight.java b/src/dev/euph/engine/ecs/components/lights/DirectionalLight.java new file mode 100644 index 0000000..db70a71 --- /dev/null +++ b/src/dev/euph/engine/ecs/components/lights/DirectionalLight.java @@ -0,0 +1,11 @@ +package dev.euph.engine.ecs.components.lights; + +import org.joml.Vector3f; + +import java.awt.*; + +public class DirectionalLight extends LightSource{ + public DirectionalLight(Color color) { + super(color, LightType.Directional); + } +} diff --git a/src/dev/euph/engine/ecs/components/lights/LightSource.java b/src/dev/euph/engine/ecs/components/lights/LightSource.java new file mode 100644 index 0000000..a80d4d2 --- /dev/null +++ b/src/dev/euph/engine/ecs/components/lights/LightSource.java @@ -0,0 +1,45 @@ +package dev.euph.engine.ecs.components.lights; + +import dev.euph.engine.ecs.Component; +import dev.euph.engine.ecs.components.Transform; +import org.joml.Vector3f; + +import java.awt.*; + +public abstract class LightSource extends Component { + + public static class LightType { + public static final int Directional = 0; + public static final int Point = 1; + public static final int Spot = 2; + public static final int Area = 3; + } + + //region Fields + private Color color; + private final int type; + //endregion + + //region Constructor + public LightSource(Color color, int type) { + requireComponent(Transform.class); + this.color = color; + this.type = type; + } + //endregion + + //region Getter + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + public int getType() { + return type; + } + //endregion +} + diff --git a/src/dev/euph/engine/ecs/components/lights/PointLight.java b/src/dev/euph/engine/ecs/components/lights/PointLight.java new file mode 100644 index 0000000..68a7e65 --- /dev/null +++ b/src/dev/euph/engine/ecs/components/lights/PointLight.java @@ -0,0 +1,13 @@ +package dev.euph.engine.ecs.components.lights; + +import org.joml.Vector3f; + +import java.awt.*; + +public class PointLight extends LightSource{ + private Vector3f attenuation; + public PointLight(Color color, Vector3f attenuation) { + super(color, LightType.Point); + this.attenuation = attenuation; + } +} diff --git a/src/dev/euph/engine/ecs/components/lights/SpotLight.java b/src/dev/euph/engine/ecs/components/lights/SpotLight.java new file mode 100644 index 0000000..5284240 --- /dev/null +++ b/src/dev/euph/engine/ecs/components/lights/SpotLight.java @@ -0,0 +1,24 @@ +package dev.euph.engine.ecs.components.lights; + +import org.joml.Vector3f; + +import java.awt.*; + +public class SpotLight extends LightSource{ + //region Fields + public float coneAngle; + public float hardCutoff; + public float softCutoff; + private Vector3f attenuation; + //endregion + + //region Constructor + public SpotLight(Color color, Vector3f attenuation, float coneAngle, float hardCutoff, float softCutoff) { + super(color, LightType.Spot); + this.attenuation = attenuation; + this.coneAngle = coneAngle; + this.hardCutoff = hardCutoff; + this.softCutoff = softCutoff; + } + //endregion +} diff --git a/src/dev/euph/engine/managers/Input.java b/src/dev/euph/engine/managers/Input.java new file mode 100644 index 0000000..bae361c --- /dev/null +++ b/src/dev/euph/engine/managers/Input.java @@ -0,0 +1,12 @@ +package dev.euph.engine.managers; + +public class Input { + /* TODO: + - Add Some Kind of Input Action map + - Input Action Maps should be replace and editable. + - Input Action should have different Modes: Button, Axis, 2Axis + - Any Combination of Inputs that fits should be able to be bound to actions. + - Inputs that dont Fit should either be splittable in smaller parts, or combinable with some struct. + - The Output of an Input Action should either be directly readable, or/-and bindable to some callback. + */ +} diff --git a/src/dev/euph/engine/managers/ShaderManager.java b/src/dev/euph/engine/managers/ShaderManager.java new file mode 100644 index 0000000..7838c88 --- /dev/null +++ b/src/dev/euph/engine/managers/ShaderManager.java @@ -0,0 +1,41 @@ +package dev.euph.engine.managers; + +import dev.euph.engine.render.Shader; +import dev.euph.engine.util.Path; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ShaderManager { + + private final Map shaders; + + public ShaderManager() { + shaders = new HashMap<>(); + try { + Shader defaultShader = new Shader( + "default", + Path.VERTEX_SHADERS + "default.glsl", + Path.FRAGMENT_SHADERS + "default.glsl" + ); + addShader(defaultShader); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + public void addShader(Shader shader) { + shaders.put(shader.getName(), shader); + } + + public Shader getShader(String shaderName) { + return shaders.get(shaderName); + } + + public void cleanup() { + shaders.values().forEach(Shader::close); + shaders.clear(); + } +} diff --git a/src/dev/euph/engine/managers/Window.java b/src/dev/euph/engine/managers/Window.java new file mode 100644 index 0000000..814bf8f --- /dev/null +++ b/src/dev/euph/engine/managers/Window.java @@ -0,0 +1,157 @@ +package dev.euph.engine.managers; + +import dev.euph.engine.util.Time; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.glfw.GLFWWindowSizeCallback; +import org.lwjgl.opengl.GL; + +import java.util.Objects; + +import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks; +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.glViewport; + +public class Window { + private final long id; + private final int initalWidth; + private int width; + private final int initalHeight; + private int height; + private Time time; + private GLFWWindowSizeCallback windowSizeCallback; + public Window(int width, int height, String title){ + //setup error callback to use for errors + GLFWErrorCallback.createPrint(System.err).set(); + + //initialize GLFW + if (!glfwInit()) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + time = new Time(); + + //configure the window + glfwDefaultWindowHints(); + + //set the window to be resizable + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + initalWidth = width; + this.width = width; + initalHeight = height; + this.height = height; + + //create the window + id = glfwCreateWindow(width, height, title, 0, 0); + if (id == 0) { + throw new RuntimeException("Failed to create the GLFW window"); + } + + //set up the callback to close the window when the user presses the 'X' + glfwSetKeyCallback(id, (window, key, scancode, action, mods) -> { + if(key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { + glfwSetWindowShouldClose(window, true); + } + }); + + windowSizeCallback = new GLFWWindowSizeCallback() { + @Override + public void invoke(long window, int newWidth, int newHeight) { + adjustViewport(newWidth, newHeight); + } + }; + + glfwSetWindowSizeCallback(id, windowSizeCallback); + + //Get the resolution of the primary monitor + GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + //Center the window + assert vidmode != null; + glfwSetWindowPos( + id, + (vidmode.width() - width) / 2, + (vidmode.height() - height) / 2 + ); + + //set the opengl context to the current window + glfwMakeContextCurrent(id); + //enable v-sync + glfwSwapInterval(1); + //make the window visible + glfwShowWindow(id); + //bind glfw window context for lwjgl + GL.createCapabilities(); + //start the delta time + time.startDeltaTime(); + } + + private void adjustViewport(int newWidth, int newHeight) { + this.width = newWidth; + this.height = newHeight; + + float aspectRatio = (float) initalWidth / initalHeight; + + int newViewportWidth = newWidth; + int newViewportHeight = (int) (newWidth / aspectRatio); + + if (newViewportHeight > newHeight) { + newViewportWidth = (int) (newHeight * aspectRatio); + newViewportHeight = newHeight; + } + + // Calculate the position to center the viewport + int x = (newWidth - newViewportWidth) / 2; + int y = (newHeight - newViewportHeight) / 2; + + // Set the OpenGL viewport + glViewport(x, y, newViewportWidth, newViewportHeight); + } + + public boolean isCloseRequested() { + return glfwWindowShouldClose(id); + } + public void updateWindow() { + //update the window + glfwSwapBuffers(id); + + //process inputs + glfwPollEvents(); + //update the delta time + time.updateDeltaTime(); + } + public void destroyWindowy() { + //run the cleanup + cleanUp(); + //terminate GLFW and free the error callback + glfwTerminate(); + Objects.requireNonNull(glfwSetErrorCallback(null)).free(); + } + private void cleanUp() { + //free the window callbacks and destroy the window + glfwFreeCallbacks(id); + glfwDestroyWindow(id); + } + public long getId() { + return id; + } + + public int getInitalWidth() { + return initalWidth; + } + public int getInitalHeight() { + return initalHeight; + } + + public Time getTime() { + return time; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } +} diff --git a/src/dev/euph/engine/math/ProjectionMatrix.java b/src/dev/euph/engine/math/ProjectionMatrix.java new file mode 100644 index 0000000..3f10cc2 --- /dev/null +++ b/src/dev/euph/engine/math/ProjectionMatrix.java @@ -0,0 +1,34 @@ +package dev.euph.engine.math; + +import dev.euph.engine.ecs.components.Camera; +import dev.euph.engine.managers.Window; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; + +import java.nio.IntBuffer; + +import static org.lwjgl.glfw.GLFW.glfwGetWindowSize; + +public class ProjectionMatrix extends Matrix4f { + + public ProjectionMatrix(Camera camera) { + super(); + calcualteProjection(camera.getWindow(), camera.fov, camera.nearPlane, camera.farPlane); + } + + private void calcualteProjection(Window window, float fov, float nearPlane, float farPlane) { + float aspectRatio = (float) window.getInitalWidth() / (float) window.getInitalHeight(); + + float y_scale = (float) (1f / Math.tan(Math.toRadians(fov / 2f))) * aspectRatio; + float x_scale = y_scale / aspectRatio; + float frustum_length = farPlane - nearPlane; + + m00(x_scale); + m11(y_scale); + m22(-((farPlane + nearPlane) / frustum_length)); + m23(-1); + m32(-((2 * nearPlane * farPlane) / frustum_length)); + m33(0); + } + +} diff --git a/src/dev/euph/engine/math/TransformationMatrix.java b/src/dev/euph/engine/math/TransformationMatrix.java new file mode 100644 index 0000000..3c9d48f --- /dev/null +++ b/src/dev/euph/engine/math/TransformationMatrix.java @@ -0,0 +1,32 @@ +package dev.euph.engine.math; + +import dev.euph.engine.ecs.components.Transform; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public class TransformationMatrix extends Matrix4f { + + public TransformationMatrix(Vector3f translation, Vector3f rotation, Vector3f scale) { + super(); + super.identity(); + super.translate(translation); + super.rotate((float) Math.toRadians(rotation.x % 360), new Vector3f(1, 0, 0)); + super.rotate((float) Math.toRadians(rotation.y % 360), new Vector3f(0, 1, 0)); + super.rotate((float) Math.toRadians(rotation.z % 360), new Vector3f(0, 0, 1)); + super.scale(scale); + } + public TransformationMatrix(Transform transform) { + super(); + super.identity(); + super.translate(transform.getPosition()); + Vector3f rotation = transform.getRotation(); + super.rotate((float) Math.toRadians(rotation.x % 360), new Vector3f(1, 0, 0)); + super.rotate((float) Math.toRadians(rotation.y % 360), new Vector3f(0, 1, 0)); + super.rotate((float) Math.toRadians(rotation.z % 360), new Vector3f(0, 0, 1)); + super.scale(transform.getScale()); + } + public TransformationMatrix() { + super(); + super.identity(); + } +} diff --git a/src/dev/euph/engine/math/ViewMatrix.java b/src/dev/euph/engine/math/ViewMatrix.java new file mode 100644 index 0000000..541ec55 --- /dev/null +++ b/src/dev/euph/engine/math/ViewMatrix.java @@ -0,0 +1,21 @@ +package dev.euph.engine.math; + +import dev.euph.engine.ecs.components.Camera; +import dev.euph.engine.ecs.components.Transform; +import org.joml.Math; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public class ViewMatrix extends Matrix4f { + + public ViewMatrix(Camera camera) { + super(); + Transform cameraTransform = camera.getEntity().getComponent(Transform.class); + super.identity(); + super.rotate(Math.toRadians(cameraTransform.getRotation().x), new Vector3f(1, 0, 0)); + super.rotate(Math.toRadians(cameraTransform.getRotation().y), new Vector3f(0, 1, 0)); + super.rotate(Math.toRadians(cameraTransform.getRotation().z), new Vector3f(0, 0, 1)); + Vector3f pos = new Vector3f(cameraTransform.getPosition()); + super.translate(pos.negate()); + } +} diff --git a/src/dev/euph/engine/render/ForwardRenderer.java b/src/dev/euph/engine/render/ForwardRenderer.java new file mode 100644 index 0000000..8a8e883 --- /dev/null +++ b/src/dev/euph/engine/render/ForwardRenderer.java @@ -0,0 +1,109 @@ +package dev.euph.engine.render; + +import dev.euph.engine.ecs.Entity; +import dev.euph.engine.ecs.components.Camera; +import dev.euph.engine.ecs.components.MeshRenderer; +import dev.euph.engine.ecs.components.Transform; +import dev.euph.engine.managers.ShaderManager; +import dev.euph.engine.math.ProjectionMatrix; +import dev.euph.engine.math.TransformationMatrix; +import dev.euph.engine.math.ViewMatrix; +import dev.euph.engine.resources.Mesh; +import dev.euph.engine.resources.TexturedMesh; +import dev.euph.engine.ecs.Scene; + +import static org.lwjgl.opengl.GL30.*; +public class ForwardRenderer implements IRenderPipeline { + //region Fields + private Scene scene; + private final ShaderManager shaderManager; + public Camera activeCamera; + //endregion + + //region Constructor + public ForwardRenderer(Scene scene, ShaderManager shaderManager) { + this.scene = scene; + this.shaderManager = shaderManager; + } + //endregion + + //region Rendering + public void init(){ + glEnable(GL_DEPTH_TEST); + } + public void render() { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + if (activeCamera == null) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + return; + } + + glClearColor( + activeCamera.backgroundColor.getRed(), + activeCamera.backgroundColor.getGreen(), + activeCamera.backgroundColor.getBlue(), + activeCamera.backgroundColor.getAlpha() + ); + + Shader shader = shaderManager.getShader("default"); + if (shader == null) return; + + shader.start(); + ViewMatrix viewMatrix = new ViewMatrix(activeCamera); + shader.loadMatrix4(shader.getUniformLocation("viewMatrix"), viewMatrix); + ProjectionMatrix projectionMatrix = new ProjectionMatrix(activeCamera); + shader.loadMatrix4(shader.getUniformLocation("projectionMatrix"), projectionMatrix); + + shader.stop(); + + for (Entity entity : scene.getEntitiesWithComponents(false, MeshRenderer.class)) { + renderEntity(entity, entity.getComponent(MeshRenderer.class)); + } + } + private void renderEntity(Entity entity, MeshRenderer meshRenderer) { + Mesh mesh = meshRenderer.getMesh(); + Material material = meshRenderer.getMaterial(); + Shader shader = material.getShader(); + + if (shader == null || mesh == null) return; + + shader.start(); + shader.bindAttributes(); + + TransformationMatrix transformationMatrix = entity.getComponent(Transform.class).getTransformationMatrix(); + shader.loadMatrix4(shader.getUniformLocation("transformationMatrix"), transformationMatrix); + + material.useMaterial(); + + if (mesh instanceof TexturedMesh texturedMesh) { + glBindVertexArray(texturedMesh.getVaoId()); + } else { + glBindVertexArray(mesh.getVaoId()); + } + + shader.enableAttributes(); + + glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0); + + shader.disableAttributes(); + glBindVertexArray(0); + + shader.stop(); + } + //endregion + + //region Getter/Setter + public Scene getScene() { + return scene; + } + public void setScene(Scene scene) { + this.scene = scene; + } + public Camera getActiveCamera() { + return activeCamera; + } + public void setActiveCamera(Camera activeCamera) { + this.activeCamera = activeCamera; + } + //endregion +} diff --git a/src/dev/euph/engine/render/IRenderPipeline.java b/src/dev/euph/engine/render/IRenderPipeline.java new file mode 100644 index 0000000..19a38d8 --- /dev/null +++ b/src/dev/euph/engine/render/IRenderPipeline.java @@ -0,0 +1,6 @@ +package dev.euph.engine.render; + +public interface IRenderPipeline { + void render(); + void init(); +} diff --git a/src/dev/euph/engine/render/Material.java b/src/dev/euph/engine/render/Material.java new file mode 100644 index 0000000..7561858 --- /dev/null +++ b/src/dev/euph/engine/render/Material.java @@ -0,0 +1,63 @@ +package dev.euph.engine.render; + +import dev.euph.engine.resources.Texture; +import org.lwjgl.opengl.GL30; + +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; + +public class Material { + + private final Shader shader; + private Texture albedoTexture; + private Color color; + + public Material(Shader shader) { + this.shader = shader; + this.albedoTexture = null; + this.color = Color.WHITE; + } + + public void setAlbedoTexture(Texture albedoTexture) { + this.albedoTexture = albedoTexture; + } + + public void setColor(Color color) { + this.color = color; + } + + public void useMaterial() { + shader.start(); + + // Bind albedoTexture if it is set + int useAlbedoTextureLocation = shader.getUniformLocation("useAlbedoTexture"); + if (albedoTexture != null) { + int albedoTextureLocation = shader.getUniformLocation("albedoTexture"); + shader.loadInt(albedoTextureLocation, 0); // Use texture unit 0 + GL30.glActiveTexture(GL30.GL_TEXTURE0); + GL30.glBindTexture(GL_TEXTURE_2D, albedoTexture.getId()); + + shader.loadBoolean(useAlbedoTextureLocation, true); + } else { + shader.loadBoolean(useAlbedoTextureLocation, false); + } + + int colorLocation = shader.getUniformLocation("color"); + shader.loadVector4(colorLocation, new org.joml.Vector4f(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1f)); + } + + + public void cleanup() { + shader.close(); + if (albedoTexture != null) { + GL30.glDeleteTextures(albedoTexture.getId()); + } + } + + public Shader getShader() { + return shader; + } +} diff --git a/src/dev/euph/engine/render/PBRMaterial.java b/src/dev/euph/engine/render/PBRMaterial.java new file mode 100644 index 0000000..8118c51 --- /dev/null +++ b/src/dev/euph/engine/render/PBRMaterial.java @@ -0,0 +1,83 @@ +package dev.euph.engine.render; + +import dev.euph.engine.resources.Texture; +import org.lwjgl.opengl.GL30; + +import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; + +public class PBRMaterial extends Material { + private Texture normalTexture; + private Texture metallicTexture; + private Texture roughnessTexture; + private Texture aoTexture; + + public PBRMaterial(Shader shader) { + super(shader); + } + + public void setNormalTexture(Texture normalTexture) { + this.normalTexture = normalTexture; + } + + public void setMetallicTexture(Texture metallicTexture) { + this.metallicTexture = metallicTexture; + } + + public void setRoughnessTexture(Texture roughnessTexture) { + this.roughnessTexture = roughnessTexture; + } + + public void setAoTexture(Texture aoTexture) { + this.aoTexture = aoTexture; + } + + @Override + public void useMaterial() { + super.useMaterial(); // Call the base class useMaterial method to bind the albedoTexture + + if (normalTexture != null) { + int normalTextureLocation = getShader().getUniformLocation("normalTexture"); + getShader().loadInt(normalTextureLocation, 1); // Use texture unit 1 for normal texture + GL30.glActiveTexture(GL30.GL_TEXTURE1); + GL30.glBindTexture(GL_TEXTURE_2D, normalTexture.getId()); + } + + if (metallicTexture != null) { + int metallicTextureLocation = getShader().getUniformLocation("metallicTexture"); + getShader().loadInt(metallicTextureLocation, 2); // Use texture unit 2 for metallic texture + GL30.glActiveTexture(GL30.GL_TEXTURE2); + GL30.glBindTexture(GL_TEXTURE_2D, metallicTexture.getId()); + } + + if (roughnessTexture != null) { + int roughnessTextureLocation = getShader().getUniformLocation("roughnessTexture"); + getShader().loadInt(roughnessTextureLocation, 3); // Use texture unit 3 for roughness texture + GL30.glActiveTexture(GL30.GL_TEXTURE3); + GL30.glBindTexture(GL_TEXTURE_2D, roughnessTexture.getId()); + } + + if (aoTexture != null) { + int aoTextureLocation = getShader().getUniformLocation("aoTexture"); + getShader().loadInt(aoTextureLocation, 4); // Use texture unit 4 for ambient occlusion texture + GL30.glActiveTexture(GL30.GL_TEXTURE4); + GL30.glBindTexture(GL_TEXTURE_2D, aoTexture.getId()); + } + } + + @Override + public void cleanup() { + super.cleanup(); + if (normalTexture != null) { + GL30.glDeleteTextures(normalTexture.getId()); + } + if (metallicTexture != null) { + GL30.glDeleteTextures(metallicTexture.getId()); + } + if (roughnessTexture != null) { + GL30.glDeleteTextures(roughnessTexture.getId()); + } + if (aoTexture != null) { + GL30.glDeleteTextures(aoTexture.getId()); + } + } +} diff --git a/src/dev/euph/engine/render/Shader.java b/src/dev/euph/engine/render/Shader.java new file mode 100644 index 0000000..5c6de3e --- /dev/null +++ b/src/dev/euph/engine/render/Shader.java @@ -0,0 +1,167 @@ +package dev.euph.engine.render; + +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.BufferUtils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.Map; + +import static org.lwjgl.opengl.GL40.*; + +public class Shader implements AutoCloseable { + //region Fields + private final String name; + private final int programId; + private final int vertexShaderId; + private final int fragmentShaderId; + + private static final FloatBuffer matrixBuffer = BufferUtils.createFloatBuffer(16); + private final Map uniformLocations = new HashMap<>(); + //endregion + + //region Constructors + public Shader(String name, String vertexFile, String fragmentFile) throws IOException { + this.name = name; + vertexShaderId = loadShader(vertexFile, GL_VERTEX_SHADER); + fragmentShaderId = loadShader(fragmentFile, GL_FRAGMENT_SHADER); + programId = glCreateProgram(); + glAttachShader(programId, vertexShaderId); + glAttachShader(programId, fragmentShaderId); + bindAttributes(); + glLinkProgram(programId); + glValidateProgram(programId); + getAllUniformLocations(); + } + //endregion + + //region Public Methods + public void start() { + glUseProgram(programId); + } + + public void stop() { + glUseProgram(0); + } + + @Override + public void close() { + stop(); + glDetachShader(programId, vertexShaderId); + glDetachShader(programId, fragmentShaderId); + glDeleteShader(vertexShaderId); + glDeleteShader(fragmentShaderId); + glDeleteProgram(programId); + } + //endregion + + //region Uniform Locations + protected void getAllUniformLocations() { + int numUniforms = glGetProgrami(programId, GL_ACTIVE_UNIFORMS); + + for (int i = 0; i < numUniforms; i++) { + String name = glGetActiveUniformName(programId, i); + int location = glGetUniformLocation(programId, name); + + uniformLocations.put(name, location); + } + } + + public int getUniformLocation(String uniformName) { + if (uniformLocations.containsKey(uniformName)) { + return uniformLocations.get(uniformName); + } + + int location = glGetUniformLocation(programId, uniformName); + uniformLocations.put(uniformName, location); + return location; + } + //endregion + + //region Attributes + public void enableAttributes(){ + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + } + public void disableAttributes(){ + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + } + public void bindAttributes() { + bindAttribute(0, "vertexPositions"); + bindAttribute(1, "textureCoords"); + bindAttribute(2, "normals"); + } + protected void bindAttribute(int attribute, String variableName) { + glBindAttribLocation(programId, attribute, variableName); + } + //endregion + + //region Uniform Loaders + public void loadBoolean(int location, boolean value) { + glUniform1f(location, value ? 1f : 0f); + } + + public void loadInt(int location, int value) { + glUniform1i(location, value); + } + + public void loadFloat(int location, float value) { + glUniform1f(location, value); + } + + public void loadVector2(int location, Vector2f value) { + glUniform2f(location, value.x, value.y); + } + + public void loadVector3(int location, Vector3f value) { + glUniform3f(location, value.x, value.y, value.z); + } + public void loadVector4(int location, Vector4f value) { + glUniform4f(location, value.x, value.y, value.z, value.w); + } + + public void loadMatrix4(int location, Matrix4f value) { + value.get(matrixBuffer); + glUniformMatrix4fv(location, false, matrixBuffer); + } + //endregion + + //region Helper Methods + private static int loadShader(String file, int type) throws IOException{ + StringBuilder shaderSource = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + shaderSource.append(line).append("\n"); + } + } catch (IOException exception) { + System.err.println("Could not read file!"); + exception.printStackTrace(); + throw exception; + } + int shaderId = glCreateShader(type); + glShaderSource(shaderId, shaderSource); + glCompileShader(shaderId); + if(glGetShaderi(shaderId, GL_COMPILE_STATUS) == GL_FALSE){ + System.err.println(glGetShaderInfoLog(shaderId, 512)); + System.err.println("Couldn't compile shader!"); + System.exit(-1); + } + return shaderId; + } + //endregion + + + public String getName() { + return name; + } +} diff --git a/src/dev/euph/engine/resources/Mesh.java b/src/dev/euph/engine/resources/Mesh.java new file mode 100644 index 0000000..a92ec8c --- /dev/null +++ b/src/dev/euph/engine/resources/Mesh.java @@ -0,0 +1,51 @@ +package dev.euph.engine.resources; + +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import static org.lwjgl.opengl.GL30.*; + +public class Mesh { + protected int vaoId; + protected int vertexCount; + + public Mesh(float[] vertices, int[] indices) { + vertexCount = indices.length; + vaoId = glGenVertexArrays(); + glBindVertexArray(vaoId); + + int vboId = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + glBufferData(GL_ARRAY_BUFFER, createFloatBuffer(vertices), GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + int eboId = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, createIntBuffer(indices), GL_STATIC_DRAW); + + glBindVertexArray(0); + } + + public int getVaoId() { + return vaoId; + } + + public int getVertexCount() { + return vertexCount; + } + + protected FloatBuffer createFloatBuffer(float[] array) { + FloatBuffer buffer = BufferUtils.createFloatBuffer(array.length); + buffer.put(array).flip(); + return buffer; + } + + protected IntBuffer createIntBuffer(int[] array) { + IntBuffer buffer = BufferUtils.createIntBuffer(array.length); + buffer.put(array).flip(); + return buffer; + } +} diff --git a/src/dev/euph/engine/resources/Texture.java b/src/dev/euph/engine/resources/Texture.java new file mode 100644 index 0000000..d2d21b6 --- /dev/null +++ b/src/dev/euph/engine/resources/Texture.java @@ -0,0 +1,22 @@ +package dev.euph.engine.resources; + +public class Texture { + private final int textureId; + private final int height; + private final int width; + public Texture(int id, int width, int height) { + textureId = id; + this.width = width; + this.height = height; + } + + public int getId() { + return textureId; + } + public int getHeight() { + return height; + } + public int getWidth() { + return width; + } +} diff --git a/src/dev/euph/engine/resources/TexturedMesh.java b/src/dev/euph/engine/resources/TexturedMesh.java new file mode 100644 index 0000000..f5953cc --- /dev/null +++ b/src/dev/euph/engine/resources/TexturedMesh.java @@ -0,0 +1,36 @@ +package dev.euph.engine.resources; + +import java.nio.FloatBuffer; + +import static org.lwjgl.opengl.GL11.GL_FLOAT; +import static org.lwjgl.opengl.GL15.*; +import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW; +import static org.lwjgl.opengl.GL20.glVertexAttribPointer; +import static org.lwjgl.opengl.GL30.glBindVertexArray; +import static org.lwjgl.opengl.GL30.glGenVertexArrays; + +public class TexturedMesh extends Mesh { + private float[] textureCoords; + + public TexturedMesh(float[] vertices, float[] textureCoords, int[] indices) { + super(vertices, indices); + + glBindVertexArray(this.vaoId); + + // Create and bind a new vertex buffer for the texture coordinates + int vboTextureCoords = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, vboTextureCoords); + glBufferData(GL_ARRAY_BUFFER, createFloatBuffer(textureCoords), GL_STATIC_DRAW); + glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glBindVertexArray(0); + } + + + public float[] getTextureCoords() { + return textureCoords; + } + + // Additional methods specific to textured mesh, if needed... +} diff --git a/src/dev/euph/engine/resources/loader/MeshLoader.java b/src/dev/euph/engine/resources/loader/MeshLoader.java new file mode 100644 index 0000000..8cab836 --- /dev/null +++ b/src/dev/euph/engine/resources/loader/MeshLoader.java @@ -0,0 +1,112 @@ +package dev.euph.engine.resources.loader; + +import dev.euph.engine.resources.Mesh; +import dev.euph.engine.resources.TexturedMesh; +import org.lwjgl.assimp.*; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +public class MeshLoader { + + public static Mesh loadMesh(String filepath) { + try { + AIScene scene = Assimp.aiImportFile(filepath, Assimp.aiProcess_Triangulate | Assimp.aiProcess_FlipUVs); + + if (scene == null || scene.mNumMeshes() == 0) { + throw new RuntimeException("Failed to load mesh: " + filepath); + } + + AIMesh mesh = AIMesh.create(scene.mMeshes().get(0)); + + List verticesList = new ArrayList<>(); + List indicesList = new ArrayList<>(); + + for (int i = 0; i < mesh.mNumVertices(); i++) { + AIVector3D vertex = mesh.mVertices().get(i); + verticesList.add(vertex.x()); + verticesList.add(vertex.y()); + verticesList.add(vertex.z()); + } + + AIFace.Buffer faceBuffer = mesh.mFaces(); + while (faceBuffer.remaining() > 0) { + AIFace face = faceBuffer.get(); + IntBuffer buffer = face.mIndices(); + while (buffer.remaining() > 0) { + indicesList.add(buffer.get()); + } + } + + float[] vertices = listToArray(verticesList); + int[] indices = listToIntArray(indicesList); + + return new Mesh(vertices, indices); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Error loading mesh: " + filepath); + } + } + + public static TexturedMesh loadTexturedMesh(String filepath) { + try { + AIScene scene = Assimp.aiImportFile(filepath, Assimp.aiProcess_Triangulate | Assimp.aiProcess_FlipUVs); + + if (scene == null || scene.mNumMeshes() == 0) { + throw new RuntimeException("Failed to load mesh: " + filepath); + } + + AIMesh mesh = AIMesh.create(scene.mMeshes().get(0)); + + List verticesList = new ArrayList<>(); + List indicesList = new ArrayList<>(); + List texCoordsList = new ArrayList<>(); + + for (int i = 0; i < mesh.mNumVertices(); i++) { + AIVector3D vertex = mesh.mVertices().get(i); + verticesList.add(vertex.x()); + verticesList.add(vertex.y()); + verticesList.add(vertex.z()); + + AIVector3D texCoord = mesh.mTextureCoords(0).get(i); + texCoordsList.add(texCoord.x()); + texCoordsList.add(texCoord.y()); + } + + AIFace.Buffer faceBuffer = mesh.mFaces(); + while (faceBuffer.remaining() > 0) { + AIFace face = faceBuffer.get(); + IntBuffer buffer = face.mIndices(); + while (buffer.remaining() > 0) { + indicesList.add(buffer.get()); + } + } + + float[] vertices = listToArray(verticesList); + int[] indices = listToIntArray(indicesList); + float[] texCoords = listToArray(texCoordsList); + + return new TexturedMesh(vertices, texCoords, indices); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Error loading mesh: " + filepath); + } + } + + private static float[] listToArray(List list) { + float[] array = new float[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + private static int[] listToIntArray(List list) { + int[] array = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } +} diff --git a/src/dev/euph/engine/resources/loader/TextureLoader.java b/src/dev/euph/engine/resources/loader/TextureLoader.java new file mode 100644 index 0000000..1426b50 --- /dev/null +++ b/src/dev/euph/engine/resources/loader/TextureLoader.java @@ -0,0 +1,39 @@ +package dev.euph.engine.resources.loader; + +import dev.euph.engine.resources.Texture; +import dev.euph.engine.util.Path; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import static org.lwjgl.stb.STBImage.stbi_image_free; +import static org.lwjgl.stb.STBImage.stbi_load; + +public class TextureLoader { + + public static Texture loadTexture(String path){ + int textureID = GL11.glGenTextures(); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID); + + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + + IntBuffer width = BufferUtils.createIntBuffer(1); + IntBuffer height = BufferUtils.createIntBuffer(1); + IntBuffer channels = BufferUtils.createIntBuffer(1); + ByteBuffer image = stbi_load(Path.RES + path, width, height, channels, 0); + if(image == null){ + throw new RuntimeException("Failed to load texture: " + path); + }else{ + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, width.get(0), height.get(0), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, image); + GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); + } + stbi_image_free(image); + return new Texture(textureID, width.get(0), height.get(0)); + } +} diff --git a/src/dev/euph/engine/util/Config.java b/src/dev/euph/engine/util/Config.java new file mode 100644 index 0000000..b15a799 --- /dev/null +++ b/src/dev/euph/engine/util/Config.java @@ -0,0 +1,7 @@ +package dev.euph.engine.util; + +public class Config { + public static final int MAX_LIGHTS = 6; + public static final int MAX_LIGHT_DISTANCE = 100; + +} diff --git a/src/dev/euph/engine/util/Path.java b/src/dev/euph/engine/util/Path.java new file mode 100644 index 0000000..241f533 --- /dev/null +++ b/src/dev/euph/engine/util/Path.java @@ -0,0 +1,8 @@ +package dev.euph.engine.util; + +public class Path { + public static final String RES = "res/"; + public static final String SHADERS = RES + "shader/"; + public static final String VERTEX_SHADERS = SHADERS + "vs/"; + public static final String FRAGMENT_SHADERS = SHADERS + "fs/"; +} diff --git a/src/dev/euph/engine/util/Time.java b/src/dev/euph/engine/util/Time.java new file mode 100644 index 0000000..720f73a --- /dev/null +++ b/src/dev/euph/engine/util/Time.java @@ -0,0 +1,29 @@ +package dev.euph.engine.util; + +import org.lwjgl.glfw.GLFW; + +public class Time { + private long lastFrameTime; + private float delta; + + public void startDeltaTime(){ + lastFrameTime = getCurrentTime(); + } + public void updateDeltaTime(){ + long currentFrameTime = getCurrentTime(); + delta = (currentFrameTime - lastFrameTime) / 1000f; + lastFrameTime = currentFrameTime; + } + + public float getDeltaTime(){ + return delta; + } + + public float getScaledDeltaTime(){ + return delta*50f; + } + + private long getCurrentTime(){ + return (long) (GLFW.glfwGetTime() * 1000L); + } +} diff --git a/src/dev/euph/game/CameraController.java b/src/dev/euph/game/CameraController.java new file mode 100644 index 0000000..ceee1e9 --- /dev/null +++ b/src/dev/euph/game/CameraController.java @@ -0,0 +1,107 @@ +package dev.euph.game; + +import dev.euph.engine.ecs.Component; +import dev.euph.engine.ecs.components.Camera; +import dev.euph.engine.ecs.components.Transform; +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWKeyCallback; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class CameraController extends Component { + + Camera camera; + Transform transform; + private static final float THETA = 0.0001f; + + private float movementSpeed = 1.0f; + private float mouseSensitivity = 0.2f; + + private Vector2f mouseDelta = new Vector2f(); + private Vector2f prevMousePos; + private Vector2f inputVectorWASD = new Vector2f(); + private boolean justCapturedMouse = true; + public CameraController() { + super(); + requireComponent(Transform.class); + requireComponent(Camera.class); + } + + @Override + public void OnReady() { + camera = this.entity.getComponent(Camera.class); + transform = this.entity.getComponent(Transform.class); + + //Lock cursor + GLFW.glfwSetInputMode(camera.getWindow().getId(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); + + // Handling Button Input + GLFW.glfwSetMouseButtonCallback(camera.getWindow().getId(), (window, button, action, mods) -> { + if(button == GLFW.GLFW_MOUSE_BUTTON_LEFT && action == GLFW.GLFW_PRESS){ + GLFW.glfwSetInputMode(camera.getWindow().getId(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); + justCapturedMouse = true; + } + }); + + // Set up a GLFW key callback to handle keyboard input + GLFW.glfwSetKeyCallback(camera.getWindow().getId(), (window, key, scancode, action, mods) -> { + if(key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_PRESS){ + GLFW.glfwSetInputMode(camera.getWindow().getId(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + } + + if(!(key == GLFW.GLFW_KEY_W || key == GLFW.GLFW_KEY_S || key == GLFW.GLFW_KEY_A || key == GLFW.GLFW_KEY_D) || !(action == GLFW.GLFW_PRESS || action == GLFW.GLFW_RELEASE)){ + return; + } + + int x = key == GLFW.GLFW_KEY_A ? -1 : key == GLFW.GLFW_KEY_D ? 1 : 0; + int y = key == GLFW.GLFW_KEY_S ? -1 : key == GLFW.GLFW_KEY_W ? 1 : 0; + Vector2f changeVector = new Vector2f( x * (action == GLFW.GLFW_PRESS ? 1 : -1), y * (action == GLFW.GLFW_PRESS ? 1 : -1)); + inputVectorWASD = inputVectorWASD.add(changeVector); + }); + + // Set up a GLFW cursor position callback to handle mouse input + GLFW.glfwSetCursorPosCallback(camera.getWindow().getId(), (window, xpos, ypos) -> { + if(GLFW.glfwGetInputMode(camera.getWindow().getId(), GLFW.GLFW_CURSOR) != GLFW.GLFW_CURSOR_DISABLED){ + return; + } + Vector2f newMousePos = new Vector2f((float) xpos, (float) ypos); + if(justCapturedMouse){ + mouseDelta = new Vector2f(); + justCapturedMouse = false; + }else{ + mouseDelta = new Vector2f(prevMousePos.x - newMousePos.x, prevMousePos.y - newMousePos.y); + } + prevMousePos = newMousePos; + }); + } + + @Override + public void OnUpdate(float deltaTime) { + + //Handle Camera Movement + Vector3f oldRot = transform.getRotation(); + Vector3f newRot = new Vector3f(max(-90f + THETA ,min(90f - THETA, oldRot.x - mouseDelta.y)), oldRot.y - mouseDelta.x, 0); + transform.setRotation(newRot); + mouseDelta = new Vector2f(); + + // Handle Movement + Vector3f inputVector = new Vector3f(inputVectorWASD.x, 0, inputVectorWASD.y); + inputVector.normalize(); // Normalize input vector + + // Calculate camera movement directions + Vector3f forward = transform.getForward().mul(inputVector.z); + Vector3f right = transform.getRight().mul(inputVector.x); + + Vector3f movementDir = forward.add(right); + movementDir.normalize(); + + // Calculate new position based on movement speed and delta time, only if there's input + if (movementDir.length() > 0) { + Vector3f newPos = transform.getPosition().add(movementDir.mul(movementSpeed * deltaTime * 60)); + transform.setPosition(newPos); + } + } +} diff --git a/src/dev/euph/game/Main.java b/src/dev/euph/game/Main.java new file mode 100644 index 0000000..107f452 --- /dev/null +++ b/src/dev/euph/game/Main.java @@ -0,0 +1,118 @@ +package dev.euph.game; + +import dev.euph.engine.ecs.Entity; +import dev.euph.engine.ecs.components.Camera; +import dev.euph.engine.ecs.components.MeshRenderer; +import dev.euph.engine.ecs.components.Transform; +import dev.euph.engine.ecs.components.lights.DirectionalLight; +import dev.euph.engine.ecs.components.lights.PointLight; +import dev.euph.engine.render.ForwardRenderer; +import dev.euph.engine.managers.ShaderManager; +import dev.euph.engine.managers.Window; +import dev.euph.engine.render.Material; +import dev.euph.engine.resources.Texture; +import dev.euph.engine.resources.TexturedMesh; +import dev.euph.engine.resources.loader.MeshLoader; +import dev.euph.engine.resources.loader.TextureLoader; +import dev.euph.engine.ecs.Scene; +import dev.euph.engine.util.Path; +import org.joml.Vector3f; + +import java.awt.*; + +public class Main { + public static void main(String[] args) { + Window window = new Window(1200, 720, "Test Window"); + Scene scene = new Scene(); + + ShaderManager shaderManager = new ShaderManager(); + + //Creating a Camera + Entity cameraEntity = new Entity("Camera"); + Transform cameraTransform = new Transform( + new Vector3f(0, 0, 3), + new Vector3f(0f, 0f , 0), + new Vector3f(1) + ); + Camera cameraComponent = new Camera(window); + CameraController cameraControllerComponent = new CameraController(); + + cameraEntity.addComponent(cameraTransform); + cameraEntity.addComponent(cameraComponent); + cameraEntity.addComponent(cameraControllerComponent); + scene.addEntityToScene(cameraEntity); + + //creating a light + Entity lightEntity = new Entity("DirectionalLight"); + Transform directionalLightTransform = new Transform( + new Vector3f(0), + new Vector3f(-0.5f, -1.0f, -0.5f), + new Vector3f(1) + ); + DirectionalLight directionalLightComponent = new DirectionalLight(Color.WHITE); + lightEntity.addComponent(directionalLightTransform); + lightEntity.addComponent(directionalLightComponent); + scene.addEntityToScene(lightEntity); + + //creating a light + Entity lightEntity2 = new Entity("PointLight"); + Transform pointLightTransform = new Transform( + new Vector3f(0), + new Vector3f(-0.5f, -1.0f, -0.5f), + new Vector3f(1) + ); + PointLight pointLightComponent = new PointLight(Color.red, new Vector3f(10)); + lightEntity2.addComponent(pointLightTransform); + lightEntity2.addComponent(pointLightComponent); + scene.addEntityToScene(lightEntity2); + + + //creating some Mesh Entity + Entity modelEntity = new Entity("Model"); + TexturedMesh modelMesh = MeshLoader.loadTexturedMesh(Path.RES + "human_rigged.obj"); + Material modelMaterial = new Material(shaderManager.getShader("default")); + modelMaterial.setColor(new Color(7, 77, 255, 255)); + + Texture uvTexture = TextureLoader.loadTexture("uv.png"); + modelMaterial.setAlbedoTexture(uvTexture); + + Transform modelTransform = new Transform( + new Vector3f(0, -4, -6), + new Vector3f(0, 0, 0), + new Vector3f(3, 3, 3) + ); + + MeshRenderer modelMeshRender = new MeshRenderer(modelMesh, modelMaterial); + modelEntity.addComponent(modelTransform); + modelEntity.addComponent(modelMeshRender); + scene.addEntityToScene(modelEntity); + + //initialize renderer + ForwardRenderer forwardRenderer = new ForwardRenderer(scene, shaderManager); + forwardRenderer.activeCamera = cameraComponent; + forwardRenderer.init(); + + //logic + //float rotationAngle = 0; + //float rotationSpeed = 0.5f; + + + scene.start(); + + //Base gameloop + while (!window.isCloseRequested()) { + scene.update(window.getTime().getDeltaTime()); + + //rotationAngle += rotationSpeed; + // Keep the rotation angle within a valid range (e.g., 0 to 360 degrees) + //rotationAngle %= 360; + + // Update the model's rotation based on the new angle + //modelEntity.getComponent(Transform.class).setRotation(new Vector3f(0, rotationAngle, 0)); + + forwardRenderer.render(); + window.updateWindow(); + } + window.destroyWindowy(); + } +} diff --git a/src/dev/euph/game/OctreeTest.java b/src/dev/euph/game/OctreeTest.java new file mode 100644 index 0000000..09e9e73 --- /dev/null +++ b/src/dev/euph/game/OctreeTest.java @@ -0,0 +1,115 @@ +package dev.euph.game; + +import dev.euph.engine.datastructs.octree.Octree; +import org.joml.Vector3f; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class OctreeTest { + + public static void main(String[] args) { + // Create an octree with a center at (0, 0, 0) and a half-size of 20 units + Octree octree = new Octree<>(8, new Vector3f(0, 0, 0), 20); + + // Generate clustered noise for more interesting patterns + Random random = new Random(); + + // Generate clustered noise for more interesting patterns + for (int i = 0; i < 50000; i++) { + float x = layeredNoise(random, i); + float y = layeredNoise(random, i + 100000); + float z = layeredNoise(random, i + 200000); + + x *= 20; // Scale to range [-20, 20] + y *= 20; // Scale to range [-20, 20] + z *= 20; // Scale to range [-20, 20] + + String itemName = "Item " + i; + octree.insert(new Vector3f(x, y, z), itemName); + } + + // Print the ASCII representation of the octree + String asciiRepresentation = octree.toString(); + // Define the file path + String filePath = "octree_ascii_representation.txt"; + + String[] lines = asciiRepresentation.split("\n"); + int imageSizeX = lines[0].length(); + int imageSizeY = lines.length; + int charSize = 1; + + BufferedImage image = new BufferedImage(imageSizeX, imageSizeY, BufferedImage.TYPE_INT_RGB); + Graphics graphics = image.getGraphics(); + + // Define a mapping of characters to colors + Map charColorMap = getCharacterColorMap(); + // Add more character-color mappings as needed + + // Create the BufferedImage and graphics context + for (int y = 0; y < imageSizeY; y++) { + String line = lines[y]; + for (int x = 0; x < imageSizeX; x++) { + char c = x < line.length() ? line.charAt(x) : ' '; + Color color = charColorMap.get(c); + graphics.setColor(color); + graphics.fillRect(x, y, charSize, charSize); + } + } + + graphics.dispose(); + + int divisor = 2; + + BufferedImage resizedImage = new BufferedImage(imageSizeY / divisor, imageSizeY / divisor, BufferedImage.TYPE_INT_RGB); + Graphics2D resizedGraphics = resizedImage.createGraphics(); + resizedGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + resizedGraphics.drawImage(image, 0, 0, imageSizeY / divisor, imageSizeY / divisor, null); + resizedGraphics.dispose(); + +// Save the image to a file + File imageFile = new File("ascii_image.png"); + + try { + ImageIO.write(resizedImage, "png", imageFile); + System.out.println("ASCII image has been saved to: " + imageFile.getAbsolutePath()); + } catch (IOException e) { + System.err.println("An error occurred while saving the image: " + e.getMessage()); + } + } + + private static Map getCharacterColorMap() { + Map charColorMap = new HashMap<>(); + charColorMap.put(' ', Color.BLACK); // Space character + charColorMap.put('#', Color.red); // Hash character + charColorMap.put('\u2500', new Color(195, 214, 234)); // HORIZONTAL_LINE + charColorMap.put('\u2502', new Color(195, 214, 234)); // VERTICAL_LINE + charColorMap.put('\u250C', new Color(195, 214, 234)); // TOP_LEFT_CORNER + charColorMap.put('\u2510', new Color(195, 214, 234)); // TOP_RIGHT_CORNER + charColorMap.put('\u2514', new Color(195, 214, 234)); // BOTTOM_LEFT_CORNER + charColorMap.put('\u2518', new Color(195, 214, 234)); // BOTTOM_RIGHT_CORNER + charColorMap.put('\u251C', new Color(195, 214, 234)); // T_INTERSECTION + charColorMap.put('\u253C', new Color(195, 214, 234)); // CROSS_INTERSECTION + charColorMap.put('\u2524', new Color(195, 214, 234)); // LEFT_T_INTERSECTION + charColorMap.put('\u252C', new Color(195, 214, 234)); // UP_T_INTERSECTION + charColorMap.put('\u2534', new Color(195, 214, 234)); // DOWN_T_INTERSECTION + return charColorMap; + } + + private static float layeredNoise(Random random, int seed) { + int numLayers = 4; + float result = 0.0f; + + for (int i = 0; i < numLayers; i++) { + float layerStrength = 1.0f / (i + 1); + result += layerStrength * (random.nextFloat() * 2 - 1); + } + + return result; + } +} diff --git a/src/dev/euph/game/PipelineTest.java b/src/dev/euph/game/PipelineTest.java new file mode 100644 index 0000000..39652b5 --- /dev/null +++ b/src/dev/euph/game/PipelineTest.java @@ -0,0 +1,37 @@ +package dev.euph.game; + +import dev.euph.engine.datastructs.pipeline.Pipeline; +import dev.euph.engine.datastructs.pipeline.PipelineStage; + +public class PipelineTest { + public static void main(String[] args) { + Pipeline pipeline = new Pipeline() + .addStage(new AddStage()) + .addStage(new MultiplyStage()) + .addStage(new LogStage()); + + System.out.println(pipeline.execute(10)); + } + + static class AddStage implements PipelineStage { + + @Override + public Integer execute(Integer integer) { + return integer + 16; + } + } + static class MultiplyStage implements PipelineStage { + + @Override + public Integer execute(Integer integer) { + return integer * 2; + } + } + static class LogStage implements PipelineStage { + + @Override + public String execute(Integer integer) { + return "Log: " + integer.toString(); + } + } +}