From c07be8ad62f20d8f9daf2b53a6f81f1c6fa0311f Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Mon, 13 Nov 2023 22:31:36 +0100 Subject: [PATCH] Migrate to Gitea --- .gitattributes | 3 + .gitignore | 56 + .idea/compiler.xml | 6 + .idea/gradle.xml | 15 + .idea/jarRepositories.xml | 20 + .idea/misc.xml | 8 + .idea/vcs.xml | 6 + Readme.md | 58 + build.gradle.kts | 66 + gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 + profiler/LICENSE | 175 + profiler/lib/Remotery.c | 10142 ++++++++++++++++ profiler/lib/Remotery.h | 1095 ++ profiler/lib/RemoteryMetal.mm | 59 + profiler/readme.md | 232 + profiler/sample/dump.c | 184 + profiler/sample/sample.c | 64 + profiler/screenshot.png | Bin 0 -> 168305 bytes profiler/vis/Code/Console.js | 218 + profiler/vis/Code/DataViewReader.js | 94 + profiler/vis/Code/GLCanvas.js | 123 + profiler/vis/Code/GridWindow.js | 291 + profiler/vis/Code/MouseInteraction.js | 106 + profiler/vis/Code/NameMap.js | 53 + profiler/vis/Code/PixelTimeRange.js | 61 + profiler/vis/Code/Remotery.js | 756 ++ profiler/vis/Code/SampleGlobals.js | 28 + profiler/vis/Code/Shaders/Grid.glsl | 162 + profiler/vis/Code/Shaders/Shared.glsl | 154 + profiler/vis/Code/Shaders/Timeline.glsl | 337 + profiler/vis/Code/Shaders/Window.glsl | 33 + profiler/vis/Code/ThreadFrame.js | 34 + profiler/vis/Code/TimelineMarkers.js | 186 + profiler/vis/Code/TimelineRow.js | 400 + profiler/vis/Code/TimelineWindow.js | 496 + profiler/vis/Code/TitleWindow.js | 105 + profiler/vis/Code/TraceDrop.js | 147 + profiler/vis/Code/WebGL.js | 252 + profiler/vis/Code/WebGLFont.js | 125 + profiler/vis/Code/WebSocketConnection.js | 149 + profiler/vis/Code/tsconfig.json | 8 + profiler/vis/Styles/Fonts/FiraCode/LICENSE | 93 + profiler/vis/Styles/Remotery.css | 237 + .../extern/BrowserLib/Core/Code/Animation.js | 65 + .../vis/extern/BrowserLib/Core/Code/Bind.js | 92 + .../extern/BrowserLib/Core/Code/Convert.js | 218 + .../vis/extern/BrowserLib/Core/Code/Core.js | 26 + .../vis/extern/BrowserLib/Core/Code/DOM.js | 526 + .../extern/BrowserLib/Core/Code/Keyboard.js | 149 + .../extern/BrowserLib/Core/Code/LocalStore.js | 40 + .../vis/extern/BrowserLib/Core/Code/Mouse.js | 83 + .../BrowserLib/Core/Code/MurmurHash3.js | 68 + .../BrowserLib/WindowManager/Code/Button.js | 131 + .../BrowserLib/WindowManager/Code/ComboBox.js | 237 + .../WindowManager/Code/Container.js | 48 + .../BrowserLib/WindowManager/Code/EditBox.js | 119 + .../BrowserLib/WindowManager/Code/Grid.js | 248 + .../BrowserLib/WindowManager/Code/Label.js | 31 + .../BrowserLib/WindowManager/Code/Treeview.js | 352 + .../WindowManager/Code/TreeviewItem.js | 109 + .../BrowserLib/WindowManager/Code/Window.js | 318 + .../WindowManager/Code/WindowManager.js | 65 + .../WindowManager/Styles/WindowManager.css | 652 + profiler/vis/index.html | 69 + res/human.blend | Bin 0 -> 755512 bytes res/human_rigged.mtl | 13 + res/human_rigged.obj | 3720 ++++++ res/shader/fs/default.glsl | 29 + res/shader/fs/testFS.glsl | 9 + res/shader/vs/default.glsl | 20 + res/shader/vs/testVS.glsl | 9 + res/uv.png | Bin 0 -> 40135 bytes .../euph/engine/datastructs/Archetype.java | 56 + .../engine/datastructs/octree/Octree.java | 22 + .../engine/datastructs/octree/OctreeNode.java | 153 + .../engine/datastructs/pipeline/Pipeline.java | 42 + .../pipeline/PipelineRuntimeException.java | 16 + .../datastructs/pipeline/PipelineStage.java | 5 + src/dev/euph/engine/ecs/Component.java | 37 + src/dev/euph/engine/ecs/Entity.java | 101 + src/dev/euph/engine/ecs/Scene.java | 186 + .../euph/engine/ecs/components/Camera.java | 38 + .../engine/ecs/components/MeshRenderer.java | 30 + .../euph/engine/ecs/components/Transform.java | 108 + .../components/lights/DirectionalLight.java | 11 + .../ecs/components/lights/LightSource.java | 45 + .../ecs/components/lights/PointLight.java | 13 + .../ecs/components/lights/SpotLight.java | 24 + src/dev/euph/engine/managers/Input.java | 12 + .../euph/engine/managers/ShaderManager.java | 41 + src/dev/euph/engine/managers/Window.java | 157 + .../euph/engine/math/ProjectionMatrix.java | 34 + .../engine/math/TransformationMatrix.java | 32 + src/dev/euph/engine/math/ViewMatrix.java | 21 + .../euph/engine/render/ForwardRenderer.java | 109 + .../euph/engine/render/IRenderPipeline.java | 6 + src/dev/euph/engine/render/Material.java | 63 + src/dev/euph/engine/render/PBRMaterial.java | 83 + src/dev/euph/engine/render/Shader.java | 167 + src/dev/euph/engine/resources/Mesh.java | 51 + src/dev/euph/engine/resources/Texture.java | 22 + .../euph/engine/resources/TexturedMesh.java | 36 + .../engine/resources/loader/MeshLoader.java | 112 + .../resources/loader/TextureLoader.java | 39 + src/dev/euph/engine/util/Config.java | 7 + src/dev/euph/engine/util/Path.java | 8 + src/dev/euph/engine/util/Time.java | 29 + src/dev/euph/game/CameraController.java | 107 + src/dev/euph/game/Main.java | 118 + src/dev/euph/game/OctreeTest.java | 115 + src/dev/euph/game/PipelineTest.java | 37 + 112 files changed, 26831 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 Readme.md create mode 100644 build.gradle.kts create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 profiler/LICENSE create mode 100644 profiler/lib/Remotery.c create mode 100644 profiler/lib/Remotery.h create mode 100644 profiler/lib/RemoteryMetal.mm create mode 100644 profiler/readme.md create mode 100644 profiler/sample/dump.c create mode 100644 profiler/sample/sample.c create mode 100644 profiler/screenshot.png create mode 100644 profiler/vis/Code/Console.js create mode 100644 profiler/vis/Code/DataViewReader.js create mode 100644 profiler/vis/Code/GLCanvas.js create mode 100644 profiler/vis/Code/GridWindow.js create mode 100644 profiler/vis/Code/MouseInteraction.js create mode 100644 profiler/vis/Code/NameMap.js create mode 100644 profiler/vis/Code/PixelTimeRange.js create mode 100644 profiler/vis/Code/Remotery.js create mode 100644 profiler/vis/Code/SampleGlobals.js create mode 100644 profiler/vis/Code/Shaders/Grid.glsl create mode 100644 profiler/vis/Code/Shaders/Shared.glsl create mode 100644 profiler/vis/Code/Shaders/Timeline.glsl create mode 100644 profiler/vis/Code/Shaders/Window.glsl create mode 100644 profiler/vis/Code/ThreadFrame.js create mode 100644 profiler/vis/Code/TimelineMarkers.js create mode 100644 profiler/vis/Code/TimelineRow.js create mode 100644 profiler/vis/Code/TimelineWindow.js create mode 100644 profiler/vis/Code/TitleWindow.js create mode 100644 profiler/vis/Code/TraceDrop.js create mode 100644 profiler/vis/Code/WebGL.js create mode 100644 profiler/vis/Code/WebGLFont.js create mode 100644 profiler/vis/Code/WebSocketConnection.js create mode 100644 profiler/vis/Code/tsconfig.json create mode 100644 profiler/vis/Styles/Fonts/FiraCode/LICENSE create mode 100644 profiler/vis/Styles/Remotery.css create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Animation.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Bind.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Convert.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Core.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/DOM.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Keyboard.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/LocalStore.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/Mouse.js create mode 100644 profiler/vis/extern/BrowserLib/Core/Code/MurmurHash3.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Button.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Container.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/EditBox.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Grid.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Label.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Treeview.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/Window.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js create mode 100644 profiler/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css create mode 100644 profiler/vis/index.html create mode 100644 res/human.blend create mode 100644 res/human_rigged.mtl create mode 100644 res/human_rigged.obj create mode 100644 res/shader/fs/default.glsl create mode 100644 res/shader/fs/testFS.glsl create mode 100644 res/shader/vs/default.glsl create mode 100644 res/shader/vs/testVS.glsl create mode 100644 res/uv.png create mode 100644 src/dev/euph/engine/datastructs/Archetype.java create mode 100644 src/dev/euph/engine/datastructs/octree/Octree.java create mode 100644 src/dev/euph/engine/datastructs/octree/OctreeNode.java create mode 100644 src/dev/euph/engine/datastructs/pipeline/Pipeline.java create mode 100644 src/dev/euph/engine/datastructs/pipeline/PipelineRuntimeException.java create mode 100644 src/dev/euph/engine/datastructs/pipeline/PipelineStage.java create mode 100644 src/dev/euph/engine/ecs/Component.java create mode 100644 src/dev/euph/engine/ecs/Entity.java create mode 100644 src/dev/euph/engine/ecs/Scene.java create mode 100644 src/dev/euph/engine/ecs/components/Camera.java create mode 100644 src/dev/euph/engine/ecs/components/MeshRenderer.java create mode 100644 src/dev/euph/engine/ecs/components/Transform.java create mode 100644 src/dev/euph/engine/ecs/components/lights/DirectionalLight.java create mode 100644 src/dev/euph/engine/ecs/components/lights/LightSource.java create mode 100644 src/dev/euph/engine/ecs/components/lights/PointLight.java create mode 100644 src/dev/euph/engine/ecs/components/lights/SpotLight.java create mode 100644 src/dev/euph/engine/managers/Input.java create mode 100644 src/dev/euph/engine/managers/ShaderManager.java create mode 100644 src/dev/euph/engine/managers/Window.java create mode 100644 src/dev/euph/engine/math/ProjectionMatrix.java create mode 100644 src/dev/euph/engine/math/TransformationMatrix.java create mode 100644 src/dev/euph/engine/math/ViewMatrix.java create mode 100644 src/dev/euph/engine/render/ForwardRenderer.java create mode 100644 src/dev/euph/engine/render/IRenderPipeline.java create mode 100644 src/dev/euph/engine/render/Material.java create mode 100644 src/dev/euph/engine/render/PBRMaterial.java create mode 100644 src/dev/euph/engine/render/Shader.java create mode 100644 src/dev/euph/engine/resources/Mesh.java create mode 100644 src/dev/euph/engine/resources/Texture.java create mode 100644 src/dev/euph/engine/resources/TexturedMesh.java create mode 100644 src/dev/euph/engine/resources/loader/MeshLoader.java create mode 100644 src/dev/euph/engine/resources/loader/TextureLoader.java create mode 100644 src/dev/euph/engine/util/Config.java create mode 100644 src/dev/euph/engine/util/Path.java create mode 100644 src/dev/euph/engine/util/Time.java create mode 100644 src/dev/euph/game/CameraController.java create mode 100644 src/dev/euph/game/Main.java create mode 100644 src/dev/euph/game/OctreeTest.java create mode 100644 src/dev/euph/game/PipelineTest.java 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 0000000000000000000000000000000000000000..11f4ce4775fe3d227f7d54b73ec6cad91f2dbd11 GIT binary patch literal 168305 zcmd43bzD?y+cr#>baw~>B1pp^jR?{$3P_5yfFKQnG?Gepmx?qf-3Zcyq_mWP)X>B5 ztpRoGe)j!)p8NUT@AtlcxOJ~tYu0t0d7Q_2UK6UWDvyUlje~@Qgr{&XE4*Wm1!`%l?NJ#jth(E|=?25n>NR-ZZ?mKJQ zKX!ICd1{X2U}E*e`Q{UIXGVdWJU2yb`F&K8kj^6&WTmy;3^&faN9gV(ZADZ(KJKDY zSI%DL=`thDCMLxtbmo(qD6hUZ7L)Zzr7O9@8ru+5|V(4fFd zhUn?~QiQii|5#6MTfO_9s`&oMVx;JkzGT5<^#k_1Qr!C08)sYKbJ+1-mD1nt2d=I3 zCOORqQdrAO6db=iDVOoA>dF;Jh#DC^v83t=Bqw_W^-OkH;m@T zGJgLkSN`NEiZFXth&7WN5W3G<((fWa=>g2aLs2Rx>|9yuykF@SP206U^iLA%+H^WC zel7Fb{#!tqS4Awf7l?mfaWr;YSr@%0bV&a2gl15l-u~a5Ml%%2OwcmBa_gn;i zv^cvH=}PO^PSgO$sDv>G$TN6D-#(~#{QHuylD2!31=pvyd=9TSoHGZ6{VvpiXe>c5 zR6Q_i@*j^-6{Kg(8KeAhF$9%l@b?ACoS^)h7oz7EJ~Bsrv2Qw?f#wTuLQGBynIUM2 z`@jZO9|jUSt@Mj;Rzl)AA3TL0uW%bRdOgSx*{a{`7g{w!*B!)e#qv%^_%XE+rmz1tiz2~yJad@=76s4V_TB}7#p2q~D7M=cct z5}rE?9bx2L9$-o7bIPT+75%pWX-0`3jK`=+JXrh~guVK50SZx*-0S5ZU=-Kz4s3DKXtnJ5ks5@SscgsN}>mAuF3Y`4kYTQCW_L88`e}#x0=p- zllZT3o$B5m7#|;(#-!6$Dzx{Un!*Ro`FbBr$d9!y!uAFvPd6&mL^ms0V~8nj=34_R z9{1f^PBg3Jps6%R@gOxmNQ!>$E3d{N^UEAZ+9ggm_{}6@P; zIkzLDh%^cZMf6nC#i;P!2-ZmJ_lm#tH3UK|ATG|^9=6;k?m&G*@NmD1yyqU zS^a8T_vv-R8bF#&m2J5SG5EJu89(UZ3COdpejC-dKjS`MYTBrtdcqAp{sd^)D7+4E zkl0NxTU*?RSzOfjET8Nh)HI2o;jIEuo=dOtV}I9-+hPRp`jc4vjC0t^0BA8BM>19e zOHS5Up6*#}x($UR?vnj@LSG@I^Ox_NpxR(kx@kUIC6H z7>J0ZbO~isPwF-SLnavgNQlI2>Q}5LHIT?_MiDkiQiaWv8sEb_^i4syk0tZkm3!PJ zKKpMF%4D0lzPz4FolEaDvgf$n!qkuQ1=@PE&QeD>5Fh2;92QT{;l9B`Lh?{D|QdCA!V%TaovuJtg3?b&c_I^G|@S z?d1zMU<;mZ*38CU*HSf~8ZqA<|UC1sO zhSpVFGjj_JhXJB{^5wnb+Hf9)k=IVLpv}P-C$;YL?|`FNMz(%3JSab`Ul9kKD@nj= z*ynUJ_zh;DPuhft=}8@So5(!Icaz1uyKqO0439oRQh!;o4j4LdszhD zcU9441w9Wo5HVEF${Xcmwf|6aKbPjME*s|iD>$0|yEhDXaA;M6CQFid;pWycBk@dV zv2>WE?OYZh8c5T1B#21!{T6D{TzyH(DM<~pLE8J6F&gOaqg1HSQ?rdr{6{!fy^bmN z>AKRICj`*?uls^|OgPfkyFosA;q2mfAAG|L#K%}<4XF^Pm^-4}Xbz8(OofQW`&R2> ztK>Q{7QAOEU{6$9h{9hYb=HeH$cno9EBAgg_Had^E}2O%^jHG|F2Tlj2jHlHfBQv! z)ofu3rY#>c^z3%cHaZzj$S!{9)^WqoWcQOqD4KJrLoaWa*KSM(Y)|rh53n`70;Jnr zsVXcj_Y#L_W{)}(nqW&cGahfe;%Jbym81~aAcj>XSRfc(aFSb+_Ilvf}L_6iw}om3fcXo2gjw<{DC^DWKQfT(aj9BfO;5{?bN zbn5CW+iD}3wiemf;%rm#Tq369j2wdaGcEWDarg)$AKxsL)o;Q;huHDxfdx(1QCZMg z=ki}x@--9YhofM;+IX!{(4$k-u2Y0oq6&v&l!}(MtU8RwII1%z-SZE(Oh}u-p~LOJ z=2*RgV%oRaTuRE9-Qep7p)FQtZYRe}*m%}l)G*@m!AYE&$?D1pJMqsXlZ$^aVUdAa z3-&YXH66;ng9tf?Q}#_Y4V!iuCnE(nn_#;SU8ncC?A^oOH&lue*@E|v8bBv8JWYJR69o$}Cc3dE6{2&d95AvwA9buUNlE zHSN|u5TonngJ>L1^0>XRyA)zr6f0tk?_{@=BtL<6y5otcTu| zYDk;!xQ^4we-C5})9-BsGWy`4+T6mc_|8vBZB&D79C&r2_tMc5YbuR_+D1(Y$WyOyhwY>qw#L7747I5=%ZBQ70{i$^zcRPlLoR| zl*9?R(hL?@sfzg2nQyD;)JbSjY;11MuRcoMWIeDd-8hOF{nH|WRGGwU7L>**0VK?& zL*eF6M(|zX#+`^=?|Cl$>P$w-!*GZ{{;j({eiNiKKtgQ*qE~cv%dD?loA}`utz=XP zk4AII?o{6@NtYEQpU$Bf*<63K;c&*@{gXc7tJEIhOd=YT&M!*WGSxuyXua#(Z3M!S z3CNL>@S`HFg2rrA98lm?3V_!DXtXF;;@)?|=(y*w@vs5Nqa#MJ4kP6xzB`S=-V32@ zt(Zy?TH1}@bV_8za`x~nOLe~rXS(j}HU*+XMmqs-Ounz%;x`v{wR@?pwuzkXt#k{N zJbcu+By?`f65o~<8omha*G+%^HB-fdRM3G?JzkOK2vvMX6}@twB)<^X<0L1O>)U2I zL_f=)$+4RdDxHVjOD{9pb)bf8a?A1F^`YDUEJ6g@xYog0f3|5axti&(j;-!CH8m5Y zSpUi%s3H-`#_Me2{k$bkVL(Ntpddc$DXwyxEb@~OJo7az;x1kbt!@2@=L&2{EgMbe zaC;9(iZhp3zvxyy;HDGD;~$(>(%lw9S+zJbcY)Lkw#OuOz)=Ow2bckP5-`S?5}^#Q z-EQ_~WclM60jZV}G|C-cRD4Sy+VqzCuv5`aD~u%D!?PKmuv3t4y!(2iEuVo=e5-2A zkX=cFA~3CQH;82L%MlhRZYI~FgZDv86jf74=slrIKgM`awy zSk*RYCDnUPRD8XuIdkUPl_vuwUi<_fSVjJ>jeBX30<~H2`56HKjR9OfN4Z!`2hHuH zb(?#hcs;)!vHqP1d#|y&nLL|$Ui+DP0Qav0g@FyP5Hl|cnj?CQuk)$1%b`!V63CDrM?)lfiJTQ%dTpNTX5}nQa?aV#fJX+XY zU;e{P0uGf4;{lm?bhMOpcg&(4UF-y9wq^;u_-hGxx#^Wx#C(4bk*qVv{jT2;paohy2 z@Q|=(%A>6)4aPY(27KE3y4yEaqkw<_#9QEb5rpTzJVQG(BMlEE z;=KvfV=s>8FeTmE@PrKjz5qb}SmS=RPX_~{mH((`nA(G}SBJg=Rh0#Gax_M>f2YyR zud)E^dH}ZN&#ZCrYQb&zVbeLl@thxZNxob88$nfs&YJlPA|aj=kS)PR;D26ZmEQ|o zl6Z)!=w|KuyQxwW|8)Ji;ZzZ)oAui=D(s%5ESb9R3_bwl5P|@jsdt~e_6rOEUlEOc zn-f_auo0l*VQ~H82msolOSWnE6L$i)bcV;bqP;(s5PiIdsK5~S10^}g(C2vRk`6+I z8AM?qZ~(kL^6D?i0jxEP&F0B8wRN79F{QEpS@c!X}iE?v*%R5~e$S6S_?HeCYjHk|; zpTGFdkK3rlq89zBSGeod$s80;xDtcO6>Sjq7>z9x<@e!|-lGyf3sv(%;ear<9o{vO zJczn}?MwNR*cKi^f4LBa3QxQ-<)^3B5jEZKPAu>t0#pa~Zs zY%K;?k?qW&0=X>EYdjqHzCNx+x@K*oIP$@uWW zx7jukZi46`pUTnodAlyY|DMeyT`A{}K|C1UH(cC;r5YWbBcm62>lNkK5C?`I%O!pk zjcpl@MrXy-O1+?C{j_pRwFy68q(}3p{Ybb-dixWRxenjLxZpge zR2zj__l^a%uokMs;Binev%E$T+v2y85@cYVN-e>c@(wtHD)8vSup_OdT&+Vdwi}J{ zJn=iEsYtWLnDx?i7@=wi*Wu;(!Qed#ND{U;)NuM|*voIh0o$uNQR#vH$%vTeM?Sgx?Em_0kFM^LBV-ZHR~UrVf+j$v03BsR zY_|Khpez<^X1ApM=bNf zQoyI?)HGOs-k?o|n|pc7)ocQJdKk0_7xT5U87lHS!{6_x)xI47ki+3DFv7xryn)02 z`Q8 z0UvE4Uj!anGOf~_k4_NkK8kJ$AMIUrJR(1AOTJgD#j^}YEt*vL+DV-lC(5Q|HFh3JA0qQ0p z^5(;0A}8$$h-QR25dPe@$2y5jPn%>Ka8+4~A6bQ!{Nxd#3@3XmHk0(xJmc@mi46wd z0Pi45*#^=m6lVUUb|_(a0#cp!E@82iqCYi(YCmdf$9lOX`iX zD1}A!)cV-rew|CP*y#~rG_@5zE3oz7zMU0!9KD7Jt zo~ZwbKip4X4YpSQfSOnaLv@P*e^r{^T`O^p0iOTI-x0{Axakndy;RA#C3Gin)6lc@6gI{GbM*37}Ty(3z6}WwF9H zfMyEcTv=le;Q`DLuraEDaGawY^xW_!IY6>>@cQ|vtMGI(I2YQkE2;QL2BG)!B}Gt@*7&rQ2k~tBNvCb z#pUX%^-Ds)9xiQ*3VTB`00TDN${+*_<$&q4r)$a~6NJF5WW`~57O6e6GCR<7@&)3^ zX6HLtRG_qhpl=NL7|woUR@ark5@23_)8%$pT;TJ$?^pxeoF!-q--Hoi69Isl4`J@n z2mVcVM&~DEJDAvsO(8%H2CuX1pOrZtC`AJ71phhgZ7is%L=Khxwx-heUY3e|OP%no$*Af2$k6icvHb&?Tq-qzYT*qH=Z{H>a=mS~+ zESc+f1MvY=_|;IU_2d|76MI@WZp3qUskAi%R$Wnm8(g`H$$|#B3xe)VOqM4Whs3x(Zs^?4#rrT%j{e%{ zHdGW@rA0Zpg{b%G3dQcg`K}IE;5htPMeo~o6T=4uB_iSm^eK!plAPwIWUJFBJaw6C zODwAA4JijSOmsT^jc6*|!BQEKYk^l&nY^@`K>hFA?5SOaTas?!1rG`g5|&X}T@RRm z1{B!{yWDLo(xNSxYGp0s@f&|h39+H~8BlGbR>53l1ql{p@ix$21rc!t`hxSV{2e#k zf_elR3E`YlwV{NzSR0OXT2Y|KCVMLSkvS)V?Y<;~pch{z_Iyc<%IDw@$5 zY4y_1e2ifoZ@&j#4ZH7l*w7Z5K;+fO?1>6ZW#|=OjsuUlGlW!UDz-O;hxAMB1|@lM zU|?vN=Z(bbEsP4!<;a24{Dc_oNS4Qw_5m{_gZ&Ps9wKNbUf*587Ac18*$(C-+>w&d z=#253f!@`IvizGFtPo#!id6EO5Bgc54Y$Ck!wRY zlOgw%xr4D(WtEubwrrhyD_cNk`Wf^pi&u#BA8~bH3!#PHQI7&0->_HKk3^lPFuPAY zh|SA|0SH1`8U_?wVuwIW4SrBFAhx$&knV93$_75(GZKSCx_k~+)!>`+VDGQNl0HY@ zB4|2394upBBwYsU4RBi!UWLWLXpBbtC`8ofnOTPNOkP?KQ)P8~=m7UktB0fU9kzE; z?H=@2i%ED5+X#0)C;<$L(1({A7y)=~%-ZJ9mAKT;&2oUkiEDv!Vlg0bf3(tD_?0ib zO}s|rFL~(^5B7)fG|4w@$n{7P>sxmTQ}x1)Z9V$hwmyIleenlXY{&d~SC&&m)ih!{8>yAMvzcU&i@0}}3!S=gxd`^nxrJ=Y*DqyLO>7|MDELOLm~ zR)5rcL1K$V9F|G%xT`ypSWSxwtW%vWQ`mdal@Jm=@{|K%4|Ia{PnS6K@xX`AF(Dqe zxbh)=OLfGWEnOLY7p)DHmw`NUAyH$JK|7+}W>u-%elKVP(F7;SbVZZ3&vhjk z`Sc0%tSjFP)rbJ4-%~PqeWXQgCA!bzlarv3Uv3#Lh2n7_`gp!@v(5^H%=Hq)8do#D zBeE$Bj6(=ceo&g(>)@iWJ3R=>fI;JPmT3bZ?t2!Z5_!st-&tMk364j8M0eSKv+Q@% zxfF_N3!togf3%jO4y?}nF)>*BsVl)M(sa8E7p1=;K2d5&yiBv?9*HJRIHYNx=nX8~ z(u>J7>}?*XC5W*M>l+apcqzi_lOrR{6jvAJB;=VIckQt-fFn#jPD#MYl~BhbB806* zQcHk4Z~%R*z%hp}^^v6#QTq(2mvOcQl1$mnG@9j(ZuLv2KKHZBV+F-apGfDe5%v!| zXqW8^7+S6{2$KY%7;^6Het^G<+1?G+av$qZx*oZ!iF2EZyCL5;)zJemYlqvq1Mnm3 zRPsipE%9drI|4!PHp;zwE)2cHip#OuoL8U7LZ7^XD~ce)xs$45{=*&Q0Hbh#+YqGFWz3J?hP@$2J4`&Gay>#3P#~qD0^7&4sWPqpI)WfL5UX!4gi> z`z!MyJ$AD*+kndEUu_04zsu&ahua=0HC%5_KkiG|poY+*LxVI1B`33;ifZuU-lIX@ z*M&|`-Ifr<6Ogr))5CeWmsn^cycVcg08%YnW0ZEV=JhLE6C`L_fls5}k`gX^c%?Hl z;Wipub{^sO04!hxV-afaojXHnoR>}%!7Xe1vWk>#oREcFT72PaN&WFuE?3v{?J!;g z?A4O9If8++`3|atGn}B-J+Rl#GYYZm2{`+eh4a8HM74kIhqYz>%%CRk*)uC~yLM=N zvC*p_-C}+FcIR4=$|B}SyvXrDMY$HFY1^CVS0p0SRV(o#19MUpB|&i>JXyqpyNcK` z+?w-&d;Q#;ZP#&@?`Y#JkNC(}rR_WkPtX~e>mVx#+!|IL@QL`L!8oOcu8BWV7?nkD zYWf-VX=TYJ2yj5D&GVy@vU@>@F6WociH@ZaV?h28hjxu-q$YnDx;Anop5yyQ)67hl z51H#ShGjj&wyam0E#FVZ1V4>Z*ie+7a)pecIdc-19i}^w-kgY=vwRpnEuUHouXiZo^6WYH=}7~hcliFSTQrX4A-a7 zL798qF~IO!=R~5Hw@jKe1RR%BD&iYowO{t~gItGVOMDYXSJ%y> zuWXtAW=YGMzPE%W*u$$oRs$}`xub~P_UU!KOY)&wfwAhQt9IR-+vw@dz6)U0ad7725eyW+vYmj%8$CNE z+f0stw8Wmswh;Y>+*g5`>8(SW|1X4>UMU8=g`>v8>Y~>IT z_z?c|&}+wwcJrR;uzI7cm642ea!%>y6iu#e%EKZ@SCYD#U@vQG%aHN#O46$n5S>7O7j;~2XLuXAnoWW2atQJk6DC}jMq z&2`mMrn{Ve9_`&m$K&KuM;(+vw+WZ}7%6l8+Ih`3%a%vEP2uSdktzhSX#cn`dlRLuY3~+xANZa*W6g zv*F&3BL~w0slM@dH+BVuy~W~D9E?#Wg+$c?7N4b^tH1%nm!Xo6ZSv{r`pPab)!5MT zH8EEXNA*PEW)`s^`wTUTdOW}IVX`r$5de-a4WKWV z*o~YZT^gN4y8cbAYt3ktb`<)OpIuQnS}IGms*wuc`F@5pDqw#DGu5 zl=XSwvTm^`s)VLL%9q@YL{Qs*1b8x}wkkV;5|u>$DWFs%>Vu$b$Y&N?r!RBT={&pr zPCH1@&>i*gX5Gi`>!}?Mr&glqt4-O*?d#106G`{=7ur`S5-M-n09D%6exP8}r)Q8@ ztD;$aJLV9Eg4IMBaSfnQf7WR~@~K(=X5E?%O6^-KM6!Hmy9-`zQ+K%e+5A6aI8H@x zt^Or&Q9|(;KvfMebrGoKHSqIy^UGvZeKS9`^$*R0b%x*+!hB89`N)E#uJS;8~~h@5pqSDdX?Z z%?3X)`H>81VjWrd$XNS3p43S6=V{(M&0#{L@oT?YD4z<86VAU(;+)v4niqNk?;nUl zetN;Ad)eEeqkf~3`MoeDy@aaY5fOAU!g18*{RJfIBp%GmG-f6HzgF_d5)V#QZ^DGt za8y!l?Eft-C?OaZQqx~{#ri z>i}A7-Zm8=DG?0~_d*{$1mR+NzZ}0>go?DkbFEnIl1c+s?yn|AGAtH+(fNp#21qiX zB^4kL1<;;9p9R_nzp-z?BM2h?FQU0A2w=xNM&CXFr1*JHY>xXCR^>b3H3yx@XJAys zdt`Ri6^OEDu`O{P590!t8}=_cGKj0D&DfTOscVpebxF^cT|jR-|Ni-F1;qLRT5i1b zKx5J}(dXkchnt~mg1}aCg|(Z4eo}PIiIVl-pHN50t^0tBk?Yg{WCG99x5`qHgJ$ zctij@Uc&=I9-z%dcGDogBq)tO4|q3-VbD?l@t&poP3TT`*sB4FMMDG8q!O|n z=kYwS6%W-a-eXK+778jZ5T1SSf3Y4vmP`_S1R-gTNK^5?`Nphgo#ee4jr$tE?e-lP z@@AohSxmeH{ACkZwFd!UK*d1}lkm36^zV5hloj`@tm8(%SB(K=LmJClBeTH+GqdfX z*Mts(npCOIrMQnOfFYbBir^VwMt;<{+ak3Y4d`=4!;$AHa>~#uH9kgG-Ql@S`(gvF z+L&G_NTppUlq)ffLg~jM68H6a4#knrisj*EU6FM1Z#}8a5x_O|UfV3#!4r{dXr<(A zigF%2kNw1&ndiIw3LoDzDvsa;H*%e&?#(lL7NLdA(*4SqoZPChul%YPrwHzz1NJHP7GpkB(UP{9 zv^t@vwcApU4gi|JqY~urJL@SIq@c3d+m!bLoj~1Uz0WUO&X21s7cm$x9&|) zRCXey9do|Xj3)@nzEObDcV^5H4q~?z-~7sMwJCTUPIA%A8A%|gi2S6epj8fZkR zhs#-?ERe@(qRQTn}t@Xfd}1LXEW*GaRC+n~Vvo@W9uRpd_?oAgoozFB|=Ntd!wQ z;C5+~bt^V`1)GvTwK5wj7ZlY&#JqH`1|G-}Fl8&ZZj7stct4Sx&jF9e_k- zdk@2bA)qPiqm-%dO6yhyAI@|lTbG;TO9!ju7EewDvn*BEEb*tq{kRhFI{UaOBW|KQ znpy-oU*=hz_rsR&-uFM= zMfdgjJc-{z?eG1f9LpIx>i8uT8ZOhdB-!-EB)5L;OQD^ma}lyQYvywXK$kry=q((rGQHQ6^^)b%@%61BI!@;>`E za$PGwgv0uw6Ce9V1gdJ}t5L(40nqb{;e)f=K;HI2j5;XrgZ^$=CD@ioar_@}w}XXV zRcAiz>&|o#Ql-C`l!#WoOsCWWoDVRkQgLxC7JqM-wXkJkTczK=NZ~!1CxWu5z}u4L zfAz(hf`Et(^zDIGHIO;GfkhxD3+`suT%3@+OAqwEa6!o8z%(2%_xjD9ew#u8O>Yc% zjpw2-yb1XSz{tfyfD?3nD-7WBFW8Zb#XE;4pxqB$gY0KCd^AIJ-QI#f=OWf$DvU37 z@=rf?|=!K^78%nbl){8_r$F5CXUFp z-C{Wes2=axt6KOyuMA@6vb$M5@?PNkA=hN$uc%oDyJD^wAFVzWs8sIz-*!LXbFYerEwdhE!fu5Ga zOE=J(E|Z}w-@{DF;&7szD74Bt%H@!JXgO&`Z+=GCTivOAcC}rOK!|e*mssmf9vGE+ zIi>pgRiV{9nlW7-JKL9uu_UE---wXCVB;@fv#Ob)3LO6{!Vz7oF9>6T^n2`d!znOT*SvVoSVY%8M*Q3U3m%57ZY8C8*A-B2n)0XFFvGdsL?MAp|Y@u^{aZfQqi#`tn@iUF$i1R1X)BnV;WLATa6m z{dgq4He5)fRz$EzLrnD&*LE?V>&Ax zxLH`r%3~*{dX+;mS5HW|%ULYUI)=;qmL`4Kvrkc4G|n+Ik<_|UqJ3>mB#IhtQ-wwW zNkY|kl{t9n_CtInl)Qs1E3o_(r3(CUZ{h>l#Y90$ z^xMREZeA4l>sCfg`H$EhTz$@iDARA+j+dI)(#BR*KXLZO2YP_RdISc~a4>+Bm4RX1 zF~!&V-6-|wh<&4}1bI-A>cDg*9`DTMqqjMI&C7c5qsYLidCovy2`zuV3OX|SJRFsv z30MjwDRV0t{Ol1fBmihJ|7r}{RizG=p>}C}Vz(JIGh;5vwrkm_*PIMuyBY)4Z-^J` zX{?#bf9M?J(?FSYUhL9Z<6H$RjdnkHN0yE12o%VPnjtvT=bCD%` z_x5XZCnx2uibA0Y&3xu1BK;Z|s7hLRZL`o3y%Hh@6U`zGtcf_e6B0ph@J6$rk+(|k zL^#s~eqfby)zqI4c~kwps8Far3fkh>FJolOY})?_-WV@i1UfT0KRczT1W(iu=@4~Q z8#sPfgbDz|9-CX2R+!ZGD1p{K_BYEcQs4J7HKOZ&?my2Vz+Uy>cEZ*i#es| zjTnP|@p^h)E}~@U z1dnYh*y$Dtly0}=!1VSrU9)6>Si(s6ee%-Su+;c@Y?hgdyBp7k$NF3;>W;lHXN!RX zWu@3?tzL)vmw}qkPT4_j)+#hTzT#6DSnLrBNjxwo^2$?7sSb}0(Gp*1_Z#{|M0|?R zVJjaf`1~Sz9y5I||A2kk!!1$g1xd1aC$4iL<3x0ZN54k``G^~t|2h!t)u>I864AtB zTL!mImnnQ9W~6K~q+xpxLXcy(kQ>%$n*0-ka^tmrpsctXx)at%bp&FGqYx|Jv?n(; zb$T#z3?ib@*@5{|#hz@VJl`BJHgKj#2^r4XWye8+4k3$f_Df4K&V3vUR4^GB9A5hA zd&_5OGJ~eNB*hL2oCo<0U${e zH;sDu!4cluB%=#jAT{gG^xWF97PT|M-CrOS6YMkN)fM^|F@t0?$xkFI)NEbtmQL!H zK{a)O+xM@e`#!o2i4&1HQJR6QykQSjDqF9W)e80h4BY;Y2c5-h6 zOt>vdEtenFIJPC0hA&_zj%4V@U(~M)GUgjQS;NwoI3gRr-)VS=&A{cJAV;9jvvoek zpIW162INVFrq4FrIUAmJc9VC6*T!sH2&zhTxE-k>jhucZ(9VASCSv`Hsqb~0`^ybc zlItu*BmYfIGT9Pee@HP~zM*w~L_{}`J^9Ft+$;K7u9)e1JVODHMH}b@-}$;E2ZPyS zv$v}?ny|`-{B3l4s95svDFGv#y|WT_TC%o-N@2u=ZDwIVod)jpR;&f)Yal^-nCW`?mD+w(EB=V{rk* z^wu;j-$CCrS7`#LCkszeXt}j`fcQb;davY zE>d+XT0)oeh4#x#o!J>kT%nfc=+wI-OF3ZZodeM4EnTYDs-6cSOs9^%djIX-oL5^b z|6m1YWG}^{;tZGFzwmh0IsR+&9lc_L3sKBH%i5eE%L$7fz!&9Z&;$PwkzELG{wQW2X3>WkW)IGrSSQEE%~hT29UK-Axu;)~QDxV1I$ z#y-{n+t6D>NH;J1@V+~P(7x${?c~C%dN-vP?QSRNzrhfxmXRLHpn@y339<-KX+D z&|!-2K1Tj=14<~|pGzVgTL?S-(ilf_Aj_|%UP?Tu{qW|PMtd&a_`7Kdgb$G?3Mhyr|8T$_xaS7rrofC?9x5aT#JS)aiE!# zHkCgj6TQzaXRJTf9>S(ry;0jR{t<=JR_%a73t`dL69xux9!HtDU`LSCW|3zj{4=Qa z0yTM$)r+_p=NaE@@gpdYjFq>Y-d!BQn9i-q=_DIe-P_&@&?KD^`bNPd_8!!usqE^Y$wtU*H__CQe9PHqKn7 zYTnOm#uCem!_k{C#_b$GtJ1d30_aM8KK};k+=KDsRe&Xu#M7R)6LEPlrd!Vj^wYY% zDKW=qJnN}-P29!=6Ii-URsDZAn$qcZ4(?bm^dR@Ei5(_p{PrCN<`tCnUt zBHSqs+}L{`@%4<^iSrA-#r1HNUJD%Zy3ZP+SoZhbg1(aQ zGRH0jgW6lR6mg3kWW|zxnM^3;x&1E)g3*V~?08FC?bao4QB2-TnCHP4SuAO$IZCKq zig9kkke^>_LBsYbTnn2f4o=7%#Fcn>zE0)xqDx4OC@FXLTh-T>doxccX55+$ck!dj zU4~Rn1ogLXbJ>1YN(ErX{(^pdgY)o{$%H%GaWlLXg2GQuGqx6lGewu*b{E~4DTwG& zkX;Ja{3>Ia(KQf6J)_y=&33X41VoXY!Fsgx5I*>%{$$6s1n*e>-EfqCqv- zXm2c`S~N_5&8aH8W2zatRw$1KeejK6Hsy{n3iZf2&EVkR!1>Od5Uaf=a_S561(?*? zsyDBurmwTm*HbI#aIXD;B#6s&W4veF3mr8io=k11gx$H`rM%}P>2Wvbsi4PjOStK+ zb`5KoA6!=F3;DWXHcg zbd5pjT~kzs5veWnn**^wM>{LD-50|2<+p4VF!Z8>H^@rnf-=OjDKZYC1%4VM!dB=> zW3&q?=imD1B(^rXinz?-fWd5enLjNN-fGzCZ*u%Cx=b7%09i^5_Vob(O>oWs%u%xL zE4rrQ0Nja8g7#4{NOTQU-eOtMjsle#1KdD+qd(4+hKY*$;IFD-iCqGb?LH1Tha@du z6uI!vskd|GaLtc03I8`>#~FqCV6wpw{8a7>6xUqwOLquE-^$S3Tw4u z#3Do}3}E+HKS|19DR{S54U7w#{%9C%(buv4>YSblsLxOBrhU4{EdqPUy8gTdA}G&b zue#Itf-kZ#)_!H4)I8>D%MAhRuUyn|4pLP3pbnyjtj_O>@3Jux+STR8&%#I;7y{1@ z1(B=_Kgoy`bUx_puxgznU~r75#(l0Hf!ecrZ$D)dabg@wa0s9S1b%XukLyA}CKe)w9mk*Q1XC1N2_R`g5IN}`A?q&@2V zSwmY7PP$#zF3DDcP&cD z4b5nct}Xv#ji@lt3kD0W5dKp|xB%uAyCf|K|%J^+6C`I z-U8rYGCJ?SUBXoIAbWqm^9Db4|KN6%SJ$0&?yj}$B_;LIPV!KNYxz0!M1vMZ1B7FJk8(0FbI2?&E_(0^{VGMn0u<{b=sBQhG#M%T(MXLixlv$y@TqQ;q>BKcoc z)DhhT&dtCe!I~TSXIA~5|4kTm9<2PQ;2G~b0}QffCDP&9;U>&6{unqp5dE(Di)sck z;(?vJ&FZP^*}u=)?EPhOkyT3y!50Js(AP$E5JgL3tLJ|r;PS1_YAcHAT%!tf$&uE3 z4eeISr{!3U!v!k;glwBDe?bwmg!4@xu;NcPwKC-|KG06Fy$*S~8&a(AlRBB10(BXA zj{>lZ@HF)oty6Lr?BY9}>lV?8dR%8TgFN8VN)@6q=_Fql_AhiP?Oe!yJL^@Beqlc* z+Vd`K1^#W*6=<4q_bJ@CENV}y#RzZz=F@uD+YC3plxNN2e<ub$xNm!u7hi}l~gv-@ug$NzKG6+6iweqfzh%=>Fn zJU$IVst$-z*zmh39#hq6=gSINp#Z7(WAFbN5-J|SY+rBxe{u1tQuY=SsmoFhVXG!ShS>9$wTN-z5+zx6&S zy-<>)mqjkR7`eDWuR|c(mo?Y@w+70ObC#E_7BQgJa&Zz8aU>9EseH}4JYoa<_*$~4 z>$~(7WbjMb%x9PFjn#9Si{C_i6|DH;=WGek^;rEmw*g#51%}d>lrtnAo==2{}F4hmUQ6#9$-5zb`n0gonb?WI{DNnfP@k;&yI7{ZuJi|w!;=X(t5_LO9Gd-Q4 z9z1Dof9oP#GCdvo%TL}|o5k%@a8$k)8yxJv8;#Xb$&vCn8HofqJQOWo#j3~QkE?A0 z%9^?N$I+&QX?T--CzSUILBbOQwLKNxU_w4U+jAJ}q$Q<`*1z4Xh>rT8Irj|eItHW9 zUop4bsQRE6-B39kBbID{$rYE?{rpkO4Ng(Zr!H~+Xu}3-ikctnV#0Fr_&(N$TiFSb z#dm}kuDENbr^S~S{n*Bhw_=4nmP->!j5h)}`u16(W)??Y^J&v-D@_BvWniX+TAqmi zvJ-f$b-v}qY<5Cf6RafsZq{KvLZHj}85UDc$H9QT2XGh(BG+7+g5VObA*T^SOo0&- zU}&DZe!JCaK=ORU9(>x%-{kYHs35~_H$n2eV^Pw3!y*d)b(S7JChv2!7+f;*UgPJK z)RNgd(p`KQ*#1k_ih1E7I!L;T(bFoA&0XLFz8=Tz7}RdRFknk6{r&AlI_A;K9-6Ou z>{1M9x#FakKYtoccu3BloxY;s?xNR|zbi#HDy?K*IEr&OAkOuH(gYv@Z{c`6o#b@| z`i3sx`9iV6_*!6Q9AL+PBq#9W({m~+NY{JuyC^4<+}`PrQ{Le-J*tdGgED;o5Hae~ z_5=XZl<{blIq4B9Kp`p*hl^bQ0WN$jPYEt&)r@UZfge1zA*15673X4b%H!E;25is6 z$d^R>hxNs*XXfzzDKJWV{JgkScW{f$; zn4e&O#|c(dXI@l3Lp815L!O?r?e(&7$pv@nx1N#hGWj<`5NS3`AXCYGVn$1t%Q_tP zz#$ksFJmu5_X36W^0-4A%FZO!OGGjBFzWfuM(N%Ns`X(e}J{= zy_`-s?A|*%Ban-%rbM^%p*t7F0LOA?h$ryJZN(;E6%QmeMkhl?AZlC!T5uhAY{(df3_U%a z7(at*-ScC$1J93gwN>+j&tq-kmXCVxOl!5%@H(_X8i`zNiuN|ep#3Ad>*fw}eA}FN ztC{r;IhPv{grwv3xuExZ{X4A%DkpOcF30K!^*6GtFHSVLc|UjVt`Ab8Le0FUK&Ue` zmtTo*v>nyGDetmvoYcf`aIejnNR~Z|@g4PoAWOG0+FK!Qw*BJ{r4D{8hmTw+ACK~s z)J?SAddg{f*d?!G84Z*(AgkS+JSC(`yT(*|=F10md)44;P)2Brpe5C7d*yrATVKE! z@+9C6tSlzqF|fNWM!=sEJI_r0XuVOuNbH8TIzFb!gTPM|y#E(7d zb&uSUn;%>xi}YCvgcyg3-V*VcG@Qn4Tr7PVcOj$Fo6fx0lcl*>VwdOD`uK#iku(d1 zz7{9z>Ddo%V~0N_A3J{O%Qz z#$FIQ)3JM%CU-d~HfX$Vkk-zBP%-y$o*$EbR(8G(8hhr$leSIYvay3cem>Ht6NFOg zyPjonT_5LEK8!okV^eZF3t)i7YA`PrCm0xZ37L1pdFg- zEt8sha_Iuy;PqCG)3U)4o|CyTZ;H?(HYH+?C7Z%}r*f*a6bTgxxzEwB51saM{dFw^ zgc?1%iz~Luq`Rsr_!eozsl6w`ed>SCGP+KOLTtjy1j|H?78)%qP;ehk4M;h!4jraf zLmPOA3FN)@`M-#(fGlBFsOkOu1{+F%5zrW`BY~Hq=F0Yy%vc%7`Q|7LrIgVEG(Kj* zs$o1U3=Vx;U|#i{GC9g=acwJnev{^6e9FL|pp97^8BedIjgy3Z^J$w%+=KEkPTVlr znXoW7?-;!1UPbNi>jdn5-=kT7P|f=|X(%&SP*fT(d1)N2Dyf6TV$Gm>H=i61HEh1z z+Rxjf3G>{_Pp4=mDR#a@;XV(y77hVHYU~91u6;t`n8!#7cgme_aJMbG?*JX zg0tQ3rEDFD8J160XFZuuKhFAD<9^+XDs1UXm*2q2Rd<8ApR`*tNiI&a*U}rY8^9do z(z9Qc8Nn2mz2`&!;N!hzZ13)c(DX)G^TjW$7_plHE{b-cCswO?$39s{iF@wmV}zyW zUnL1UD@*$(4E=ws4RBL7ecqg88vygfw3$`n5{o(gQMtn89S9rmHtNa}ZDYqZ7Vep# z2fXsi_L_gxp!gnsPBAy+EAj`ZEX~`WzO;`xsPZm~X-&EB)u8UxU8Bs#ea?`-OCC%y z43ZiHE3z=@919UPE^h2B#RvhA(uh)5np#x3n=Y(Nx<_d+sUe{@SuaygtWYmtd|yMY63{JE&_o=Sm{qi(Jjd@L`-rb8^lu~Q8opfeC zO(?VKY?j;})8;%|Cgu|m0C|QCpzml%@LCLwZ#b1PaWkx^2SDbzfz-R=Jcl#Mv5$H% zZa}kv?}d{GkACd)q=mczMp3P`)$4-7{_(J7>YQ7Wj&V;{Z!FQ)fcd1XHQ_iqXEy5Q z{hDp8lvwL9>${kBOTs#ul~vr=1f$yee9FNfBvl2#CcSg=?*}PMd)W`m{4OVXhCu+qkOlA%Jxy^AS)alwbL1u z$#|DI-{%OP_ihK}3n9qiz`djvpoLz~TY)Vr^pQjhIW0`{1F}a$2MK#lHSmB-NDCLI zIj*^ot&Os{m#|4I4n17CGKyqi<57@aGtmD&WTRdr9{QaB&>q*kUNTKr?hAJ zqR4(pOa(HbhlIU_lWTj69UpJLaIqFzmDM3izh=!`-_LGK-Q8arpK5|JyQ`NZhJEC0 z#I@(x%~a-5+Q!?IPIDE<*`b_Gmz0?2G_+E=;cUx9<< z%NuAw$KNON@xC+}gNCtT1dq~5)I>WDhzqbS(*ApQv!6QaSfx7TM zl>d{%+6kIK7?5w=>7x0)I7=Aw_=#baA*r<<+oX5q!`%ka78t*~s6NGMu>QiQBshe~f9wnzlKAfF= z#+XvUYfda^bs1pFN8OyPo%>$7p-I4vn*-UTY8y& zeL6xjsMfq7Xt6UNBQul;GiJepZa) ztG28XSA(7yotYrumQGZ|KJB%KebZ83??mVFGkpfjN zNp|CXgB;Y)s!+Z~+w{gD{+E_+wv?oZS%aeD0P=hU!e=>-t5zi7>A2{i=u!HN8$qgG z!1-dm+Medn$9V~^g4gw21*$wmDO=+6>OQlX<)Kvr5FSMJ;~(`%ojM~LYhc#w@mHOp zq-5wmJyTr~C6O!uWoz#|KSh$<(aErnD*wI?WALlw-%=f8j#WiEVHQMX}$6nzM}MUZO0T#^6w+nNqYDZjp6U2KvKzI%I_2RtvdAW%Mt2FkHtm|kH`rsa@Y z18{F+FBfe#2b3gXvKGg!f85mk-+$Q(atJ2*)@5;;&+Z&B4vZlL20jC~5?rAD8F2Zg zf8K?h|KF{%e@8F+qlUpBwGHQWE(PSsvB4i4C%D0*i4QffQ+hUza{gpIoe^knK|s_? zmwYRr&IYJi{E$8(L{tp41<5hMvp*!CuBcOI1m6|Qe&cL`-d_(;asN8;&LI)u^bwZ( z5F7Y6Q=G{c=GPz(Jtm>168s@M1b+PkkPUtdMjI`H#rfFgOrK#pn%#5^XHA36Q0WR) z9$au%y-z@5n3ZWy=#3Z4+8s5dH0NXz+>YFOWXUE_wsJtK@4ZUc5LkH}$QL5c?e-kg5X zDOW5iz7vh~VBQO3y2oN&>NSG(OW=QPaBk$ncsXLp+KC0RV zZ`dA8yBBUe9G`YqQ%9L9P&Z~Cn^LM$$9S}nf^Ko#UG@68+wb_Ru4i@^f*q`9(b=9h z&|~;fk?i23u)s%g1rFgtJ_>00b*~Gzfe9doI$I9j#AEUVB^@o2fd~&_9N@+TpPpIE z{m`lD-6y@g1!@@KjN>pL$_)({J6zSo1?~nR4wlQrKtg9|OZKzlJkP$2I4WI<_Y(J3 zd4jlS^dIo63fr7Sg0n%_=?iI}?H4PnG zW}9GoWaKr~tOVo+k$nIQvXnr7E)c9CAgf%~$5I=t22^pR_HAKNA$aY70+D^KK*g^t zPG3d)8LLjJZv0grw*zv4#i>iDi6@n&%W!Oyyv*59r-o=#{%+WV-GQZ zLH82q-nAJxYRua5;FSB^|FFV6cRLgc8pQt@q~!`kJvB<9X2pTdGnoFHm}2k;-ezW|_bjJx;>v z6*?hbeZURr!Z93}o{i{|KCLw>23@!eM{h7QgGkGg(}%>(tmNhKepels!l&=r0kh6| zrlBlR%74Yi8t^ayI@M@tnDTAmBXN9@G_ydAkXPoZ5lNV~;l4e>hV)H$>0{|y#58|*Fqme$T6`9h( zC~v-g`3Gy3d)kY3+8BddP6%}s*ru+=q}e7)e>Pf0NXxiLdLoqm=dTu7wSj2PZH0?z zUN^BEsN0f(%4{C&SM4@Dg_>+uQ%W2Nl_$;+X&~75el`=-*|h#?ggi`Gd!fd+QG7>f zj)-o+gj?ii3kmA+x*A4SAC~u_&*QCEqe~CF`7>HFo

w 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 qD|*iV@$di5E&Au8b5y{h?;SopIA|`Ta$^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..261a5b331cbebc4fe596424b1604d3b17c293a9c GIT binary patch literal 755512 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

>68ngq#R1|$aiG{! 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%0DnU64*m#A-V6H&gKapk{`GJ4Dd6v*`pIfg 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%0DSY5-}yv6ss!@u}{S1WROcI^9fMd8@r>s_9N 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 { + //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(); + } + } +}