Migrate to Gitea

This commit is contained in:
Snoweuph 2023-11-13 22:31:36 +01:00
commit c07be8ad62
Signed by: Snoweuph
GPG key ID: A494330694B208EF
112 changed files with 26831 additions and 0 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
.idea/** linguist-vendored
gradle/** linguist-vendored
profiler/** linguist-vendored

56
.gitignore vendored Normal file
View file

@ -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

6
.idea/compiler.xml vendored Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="16" />
</component>
</project>

15
.idea/gradle.xml vendored Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

20
.idea/jarRepositories.xml vendored Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

8
.idea/misc.xml vendored Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="corretto-16" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml vendored Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

58
Readme.md Normal file
View file

@ -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)
}
```

66
build.gradle.kts Normal file
View file

@ -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)
}

View file

@ -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

248
gradlew vendored Executable file
View file

@ -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" "$@"

175
profiler/LICENSE vendored Normal file
View file

@ -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.

10142
profiler/lib/Remotery.c vendored Normal file

File diff suppressed because it is too large Load diff

1095
profiler/lib/Remotery.h vendored Normal file

File diff suppressed because it is too large Load diff

59
profiler/lib/RemoteryMetal.mm vendored Normal file
View file

@ -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 <Foundation/NSThread.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#import <Metal/Metal.h>
// 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 <MTLCommandBuffer>){ SetTimestamp(out_start); }];
[command_buffer addCompletedHandler:^(id <MTLCommandBuffer>){ SetTimestamp(out_end); *out_ready = 1; }];
}

232
profiler/readme.md vendored Normal file
View file

@ -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;

184
profiler/sample/dump.c vendored Normal file
View file

@ -0,0 +1,184 @@
#include <stdlib.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include "../lib/Remotery.h"
#include <assert.h>
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;
}

64
profiler/sample/sample.c vendored Normal file
View file

@ -0,0 +1,64 @@
#include <stdlib.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#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;
}

BIN
profiler/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

218
profiler/vis/Code/Console.js vendored Normal file
View file

@ -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 + "<br>";
// 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;
})();

94
profiler/vis/Code/DataViewReader.js vendored Normal file
View file

@ -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;
})();

123
profiler/vis/Code/GLCanvas.js vendored Normal file
View file

@ -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));
}
};

291
profiler/vis/Code/GridWindow.js vendored Normal file
View file

@ -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 = `<div class="GridNameHeader"></div>`;
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 = `
<div style="height:100%; overflow: hidden; position: relative; display:flex; flex-flow: column;">
<div style="height: 16px; flex: 0 1 auto;"></div>
<div style="flex: 1 1 auto;"></div>
</div>
`;
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);
}
}

106
profiler/vis/Code/MouseInteraction.js vendored Normal file
View file

@ -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);
}
}
};

53
profiler/vis/Code/NameMap.js vendored Normal file
View file

@ -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;
}
}

61
profiler/vis/Code/PixelTimeRange.js vendored Normal file
View file

@ -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);
}
}

756
profiler/vis/Code/Remotery.js vendored Normal file
View file

@ -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;
})();

28
profiler/vis/Code/SampleGlobals.js vendored Normal file
View file

@ -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;

162
profiler/vis/Code/Shaders/Grid.glsl vendored Normal file
View file

@ -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);
}
`;

154
profiler/vis/Code/Shaders/Shared.glsl vendored Normal file
View file

@ -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;
}
`;

337
profiler/vis/Code/Shaders/Timeline.glsl vendored Normal file
View file

@ -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);
}
`;

33
profiler/vis/Code/Shaders/Window.glsl vendored Normal file
View file

@ -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);
}
`;

34
profiler/vis/Code/ThreadFrame.js vendored Normal file
View file

@ -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;
}
}

186
profiler/vis/Code/TimelineMarkers.js vendored Normal file
View file

@ -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;
}
}

400
profiler/vis/Code/TimelineRow.js vendored Normal file
View file

@ -0,0 +1,400 @@
TimelineRow = (function()
{
const RowLabelTemplate = `
<div class='TimelineRow'>
<div class='TimelineRowCheck TimelineBox'>
<input class='TimelineRowCheckbox' type='checkbox' />
</div>
<div class='TimelineRowExpand TimelineBox NoSelect'>
<div class='TimelineRowExpandButton'>+</div>
</div>
<div class='TimelineRowExpand TimelineBox NoSelect'>
<div class='TimelineRowExpandButton'>-</div>
</div>
<div class='TimelineRowLabel TimelineBox'></div>
<div style="clear:left"></div>
</div>`
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;
})();

496
profiler/vis/Code/TimelineWindow.js vendored Normal file
View file

@ -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;
})();

105
profiler/vis/Code/TitleWindow.js vendored Normal file
View file

@ -0,0 +1,105 @@
TitleWindow = (function()
{
function TitleWindow(wm, settings, server, connection_address)
{
this.Settings = settings;
this.Window = wm.AddWindow("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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;
})();

147
profiler/vis/Code/TraceDrop.js vendored Normal file
View file

@ -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("<div id='DropZone' class='DropZone'>Load Remotery Trace</div>");
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);
}
}

252
profiler/vis/Code/WebGL.js vendored Normal file
View file

@ -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();
}
};

125
profiler/vis/Code/WebGLFont.js vendored Normal file
View file

@ -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);
}
}

149
profiler/vis/Code/WebSocketConnection.js vendored Normal file
View file

@ -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;
})();

8
profiler/vis/Code/tsconfig.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"outFile": "out.js",
"allowJs": true,
"sourceMap": true
},
"include": ["ThreadFrame.js"]
}

View file

@ -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.

237
profiler/vis/Styles/Remotery.css vendored Normal file
View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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) + ';';
});
};
})();

View file

@ -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");
}

View file

@ -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 id='" + id + "' code='" + code + "' archive='" + archive + "'";
applet += " width='" + dest.offsetWidth + "' height='" + dest.offsetHeight + "'>";
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 <div> 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";
}

View file

@ -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
};
// =====================================================================================================================

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,68 @@
namespace("Hash");
/**
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
*
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
* @see http://github.com/garycourt/murmurhash-js
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
* @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;
}

View file

@ -0,0 +1,131 @@
namespace("WM");
WM.Button = (function()
{
var template_html = "<div class='Button notextsel'></div>";
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;
})();

View file

@ -0,0 +1,237 @@
namespace("WM");
WM.ComboBoxPopup = (function()
{
var body_template_html = "<div class='ComboBoxPopup'></div>";
var item_template_html = " \
<div class='ComboBoxPopupItem notextsel'> \
<div class='ComboBoxPopupItemText'></div> \
<div class='ComboBoxPopupItemIcon'><img src='BrowserLibImages/tick.gif'></div> \
<div style='clear:both'></div> \
</div>";
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 = " \
<div class='ComboBox'> \
<div class='ComboBoxText notextsel'></div> \
<div class='ComboBoxIcon'><img src='BrowserLibImages/up_down.gif'></div> \
<div style='clear:both'></div> \
</div>";
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("&lt;empty&gt;");
// 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;
})();

View file

@ -0,0 +1,48 @@
namespace("WM");
WM.Container = (function()
{
var template_html = "<div class='Container'></div>";
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;
})();

View file

@ -0,0 +1,119 @@
namespace("WM");
WM.EditBox = (function()
{
var template_html = " \
<div class='EditBoxContainer'> \
<div class='EditBoxLabel'>Label</div> \
<input class='EditBox'> \
</div>";
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;
})();

View file

@ -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 = "<div class='GridRow'></div>";
//
// '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, "<div class='GridRowCell'></div>");
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, "<div class='GridRowBody'></div>");
// 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 = " \
<div class='Grid'> \
<div class='GridBody'></div> \
</div>";
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;
})();

View file

@ -0,0 +1,31 @@
namespace("WM");
WM.Label = (function()
{
var template_html = "<div class='Label'></div>";
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;
})();

View file

@ -0,0 +1,352 @@
namespace("WM");
WM.Treeview = (function()
{
var Margin = 10;
var tree_template_html = " \
<div class='Treeview'> \
<div class='TreeviewItemChildren' style='width:90%;float:left'></div> \
<div class='TreeviewScrollbarInset'> \
<div class='TreeviewScrollbar'></div> \
</div> \
<div style='clear:both'></div> \
</div>";
var item_template_html = " \
<div class='TreeViewItem basicfont notextsel'> \
<img src='' class='TreeviewItemImage'> \
<div class='TreeviewItemText'></div> \
<div style='clear:both'></div> \
<div class='TreeviewItemChildren'></div> \
<div style='clear:both'></div> \
</div>";
// 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;
})();

View file

@ -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;
})();

View file

@ -0,0 +1,318 @@
namespace("WM");
WM.Window = (function()
{
var template_html = multiline(function(){/* \
<div class='Window'>
<div class='WindowTitleBar'>
<div class='WindowTitleBarText notextsel' style='float:left'>Window Title Bar</div>
<div class='WindowTitleBarClose notextsel' style='float:right'>&#10005;</div>
</div>
<div class='WindowBody'>
</div>
<div class='WindowResizeHandle notextsel'>&#8944;</div>
</div>
*/});
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;
})();

View file

@ -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;
})();

View file

@ -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;
}

69
profiler/vis/index.html vendored Normal file
View file

@ -0,0 +1,69 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Remotery Viewer</title>
<!-- Style Sheets -->
<link rel="stylesheet" type="text/css" href="extern/BrowserLib/WindowManager/Styles/WindowManager.css" />
<link rel="stylesheet" type="text/css" href="Styles/Remotery.css" />
</head>
<body>
<!-- Utilities -->
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Core.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/DOM.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Bind.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Animation.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Convert.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/LocalStore.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Mouse.js"></script>
<script type="text/javascript" src="extern/BrowserLib/Core/Code/Keyboard.js"></script>
<!-- User Interface Window Manager -->
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/WindowManager.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/Window.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/Container.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/EditBox.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/Grid.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/Label.js"></script>
<script type="text/javascript" src="extern/BrowserLib/WindowManager/Code/Button.js"></script>
<!-- Main Application -->
<script type="text/javascript" src="Code/WebGL.js"></script>
<script type="text/javascript" src="Code/WebGLFont.js"></script>
<script type="text/javascript" src="Code/NameMap.js"></script>
<script type="text/javascript" src="Code/GLCanvas.js"></script>
<script type="text/javascript" src="Code/DataViewReader.js"></script>
<script type="text/javascript" src="Code/Console.js"></script>
<script type="text/javascript" src="Code/WebSocketConnection.js"></script>
<script type="text/javascript" src="Code/MouseInteraction.js"></script>
<script type="text/javascript" src="Code/TitleWindow.js"></script>
<script type="text/javascript" src="Code/SampleGlobals.js"></script>
<script type="text/javascript" src="Code/GridWindow.js"></script>
<script type="text/javascript" src="Code/PixelTimeRange.js"></script>
<script type="text/javascript" src="Code/TimelineRow.js"></script>
<script type="text/javascript" src="Code/TimelineMarkers.js"></script>
<script type="text/javascript" src="Code/TimelineWindow.js"></script>
<script type="text/javascript" src="Code/ThreadFrame.js"></script>
<script type="text/javascript" src="Code/TraceDrop.js"></script>
<script type="text/javascript" src="Code/Remotery.js"></script>
<!-- Shaders -->
<script type="text/javascript" src="Code/Shaders/Shared.glsl"></script>
<script type="text/javascript" src="Code/Shaders/Grid.glsl"></script>
<script type="text/javascript" src="Code/Shaders/Timeline.glsl"></script>
<script type="text/javascript" src="Code/Shaders/Window.glsl"></script>
<script type="text/javascript">
const remotery = new Remotery();
</script>
</body>
</html>

BIN
res/human.blend Normal file

Binary file not shown.

13
res/human_rigged.mtl Normal file
View file

@ -0,0 +1,13 @@
# Blender MTL File: 'human.blend'
# Material Count: 1
newmtl Material.001
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd uv.png

3720
res/human_rigged.obj Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
#version 330 core
in vec2 passTextureCoords;
in vec3 passNormals;
uniform vec4 color;
uniform sampler2D albedoTexture;
uniform bool useAlbedoTexture;
out vec4 fragmentColor;
void main() {
if (useAlbedoTexture) {
vec4 textureColor = texture(albedoTexture, passTextureCoords);
vec3 lightDirection = normalize(vec3(0.5, 0.5, 1.0)); // Direction of the light source
// Calculate diffuse lighting
float diffuseFactor = max(dot(normalize(passNormals), lightDirection), 0.0);
vec3 diffuseLight = vec3(1.0) * diffuseFactor;
// Combine diffuse lighting and texture color
vec3 finalColor = (diffuseLight + color.xyz) * textureColor.rgb;
fragmentColor = vec4(finalColor, textureColor.a);
} else {
// If no texture, use the solid color
fragmentColor = color;
}
}

View file

@ -0,0 +1,9 @@
#version 400 core
in vec4 vertexColor;
out vec4 fragColor;
void main()
{
fragColor = vertexColor;
}

View file

@ -0,0 +1,20 @@
#version 330 core
layout (location = 0) in vec3 vertexPositions;
layout (location = 1) in vec2 textureCoords;
layout (location = 2) in vec3 normals;
uniform mat4 transformationMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
out vec2 passTextureCoords;
out vec3 passNormals;
void main() {
vec4 worldPosition = transformationMatrix * vec4(vertexPositions, 1.0);
gl_Position = projectionMatrix * (viewMatrix * worldPosition);
passTextureCoords = textureCoords;
passNormals = mat3(transpose(inverse(transformationMatrix))) * normals;
}

View file

@ -0,0 +1,9 @@
#version 400 core
layout (location = 0) in vec3 position;
out vec4 vertexColor;
void main(){
gl_Position = vec4(position, 1.0);
vertexColor = vec4(1.0, 0.0, 0.0, 1.0);
}

BIN
res/uv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,56 @@
package dev.euph.engine.datastructs;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Archetype<T> {
//region Fields
private final Set<Class<? extends T>> componentClasses;
//endregion
//region Constructor
@SafeVarargs
public Archetype(Class<? extends T>... componentClasses){
this.componentClasses = new HashSet<>();
this.componentClasses.addAll(Arrays.asList(componentClasses));
}
public Archetype(List<Class<? extends T>> componentClasses){
this.componentClasses = new HashSet<>();
this.componentClasses.addAll(componentClasses);
}
//endregion
public boolean contains(Class<? extends T> componentClass) {
for(Class<?extends T> _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<Class<? extends T>> getComponentClasses() {
return componentClasses;
}
//endregion
}

View file

@ -0,0 +1,22 @@
package dev.euph.engine.datastructs.octree;
import org.joml.Vector3f;
import java.util.List;
public class Octree<T> {
private final OctreeNode<T> root;
public Octree(int maxCapacity, Vector3f center, float halfSize) {
root = new OctreeNode<T>(maxCapacity, center, halfSize);
}
public void insert(Vector3f position, T data) {
root.insert(position, data);
}
public List<T> query(Vector3f position, float radius) {
return root.query(position, radius);
}
}

View file

@ -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<T> {
private final int maxCapacity;
private final Vector3f center;
private final float halfSize;
private final List<Vector3f> positions;
private final Map<Vector3f, T> dataMap;
private OctreeNode<T>[] 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<T> child : children) {
child.insert(position, data);
}
}
}
public List<T> query(Vector3f position, float radius) {
List<T> 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<T> 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<T>[] 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<T> child : children) {
if (!child.isOutOfBounds(position)) {
child.insert(position, data);
break;
}
}
// Remove the position from the current node
positions.remove(i);
}
}
public List<Vector3f> getPositions() {
List<Vector3f> allPositions = new ArrayList<>(positions);
if (children != null) {
for (OctreeNode<T> child : children) {
allPositions.addAll(child.getPositions());
}
}
return allPositions;
}
public OctreeNode<T>[] getChildren() {
return children;
}
}

View file

@ -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<Input, Output> {
private final Collection<PipelineStage<?, ?>> pipelineStages;
public Pipeline() {
this.pipelineStages = new ArrayList<>();
}
public Pipeline(PipelineStage<Input, Output> pipelineStage) {
pipelineStages = Collections.singletonList(pipelineStage);
}
private Pipeline(Collection<PipelineStage<?, ?>> pipelineStages) {
this.pipelineStages = new ArrayList<>(pipelineStages);
}
public <NewOutput> Pipeline<Input, NewOutput> addStage(PipelineStage<Output, NewOutput> pipelineStage){
final ArrayList<PipelineStage<?, ?>> 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);
}
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
package dev.euph.engine.datastructs.pipeline;
public interface PipelineStage<Input, Output> {
Output execute(Input input);
}

View file

@ -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<Class<? extends Component>> requiredComponents = new ArrayList<>();
//endregion
public void OnReady(){}
public void OnUpdate(float deltaTime){}
public void OnDestroy(){}
//region Component Management
protected void requireComponent(Class<? extends Component> componentClass) {
requiredComponents.add(componentClass);
}
protected boolean hasRequiredComponents(Entity entity) {
for (Class<? extends Component> requiredClass : requiredComponents) {
if (entity.getComponent(requiredClass) == null) {
return false;
}
}
return true;
}
//endregion
//region Getter
public Entity getEntity() {
return entity;
}
//endregion
}

View file

@ -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<Component> components;
//endregion
//region Constructor
public Entity(String name){
this.name = name;
components = new ArrayList<>();
}
//endregion
//region Component Management
public <T extends Component> 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 extends Component> T getComponent(Class<T> 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 <T extends Component> void removeComponent(Class<T> 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<Component> 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
}

View file

@ -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<Entity> entities;
private final Map<Archetype<Component>, List<Entity>> entitiesByArchetype;
private final Map<Class<? extends Component>, Set<Class<? extends Component>>> subclassesByComponent;
private final Map<Class<? extends Component>, List<Component>> 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<Entity> 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<? extends Component> componentClass = component.getClass();
List<Component> componentInstances = componentInstancesByType.get(componentClass);
if (componentInstances != null) {
componentInstances.remove(component);
}
}
} else if (entity.areComponentsChanged()) {
removeEntityFromArchetype(entity);
List<Class<? extends Component>> components = new ArrayList<>();
for (Component component : entity.getComponents()) {
components.add(component.getClass());
}
Archetype<Component> newArchetype = new Archetype<>(components);
List<Entity> archetypeEntities = entitiesByArchetype.computeIfAbsent(newArchetype, key -> new ArrayList<>());
archetypeEntities.add(entity);
for (Component component : entity.getComponents()) {
Class<? extends Component> componentClass = component.getClass();
List<Component> 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<Class<? extends Component>> components = new ArrayList<>();
for (Component component : entity.getComponents()) {
components.add(component.getClass());
}
Archetype<Component> newArchetype = new Archetype<>(components);
for (Class<? extends Component> componentClass : components) {
// Update subclasses mapping
updateSubclassesMapping(componentClass);
// Add component instance to the componentInstancesByType map
List<Component> 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<Component> superClassInstances = componentInstancesByType.computeIfAbsent(superClass.asSubclass(Component.class), key -> new ArrayList<>());
superClassInstances.add(entity.getComponent(componentClass));
}
superClass = superClass.getSuperclass();
}
}
List<Entity> archetypeEntities = entitiesByArchetype.computeIfAbsent(newArchetype, key -> new ArrayList<>());
archetypeEntities.add(entity);
if (!isRunning) {
return;
}
entity.ready();
}
@SafeVarargs
public final List<Entity> getEntitiesWithComponents(boolean exactMatch, Class<? extends Component>... componentClasses) {
List<Entity> filteredEntities = new ArrayList<>();
if (exactMatch) {
Archetype<Component> requestedArchetype = new Archetype<>(componentClasses);
List<Entity> matchingEntities = entitiesByArchetype.get(requestedArchetype);
if (matchingEntities != null) {
filteredEntities.addAll(matchingEntities);
}
} else {
Set<Class<? extends Component>> componentSet = new HashSet<>();
for (Class<? extends Component> componentClass : componentClasses) {
componentSet.add(componentClass);
componentSet.addAll(subclassesByComponent.getOrDefault(componentClass, Collections.emptySet()));
}
for (Class<? extends Component> componentClass : componentSet) {
List<Component> componentInstances = componentInstancesByType.get(componentClass);
if (componentInstances != null) {
filteredEntities.addAll(componentInstances.stream()
.map(Component::getEntity)
.toList());
}
}
}
return filteredEntities;
}
public List<Component> getComponentInstances(Class<? extends Component> componentClass){
return componentInstancesByType.get(componentClass);
}
//region ECS Utility Functions
private void removeEntityFromArchetype(Entity entity) {
for (Map.Entry<Archetype<Component>, List<Entity>> entry : entitiesByArchetype.entrySet()) {
Archetype<Component> archetype = entry.getKey();
List<Entity> archetypeEntities = entry.getValue();
if (archetypeEntities.remove(entity)) {
if (archetypeEntities.isEmpty()) {
entitiesByArchetype.remove(archetype);
}
break;
}
}
}
private void updateSubclassesMapping(Class<? extends Component> componentClass) {
Set<Class<? extends Component>> subclasses = getSubclasses(componentClass);
subclassesByComponent.put(componentClass, subclasses);
}
private Set<Class<? extends Component>> getSubclasses(Class<? extends Component> componentClass) {
Set<Class<? extends Component>> subclasses = new HashSet<>();
for (Class<? extends Component> clazz : subclassesByComponent.keySet()) {
if (componentClass.isAssignableFrom(clazz)) {
subclasses.add(clazz);
}
}
return subclasses;
}
//endregion
//region Getter
public boolean isRunning() {
return isRunning;
}
public List<Entity> getEntities() {
return entities;
}
//endregion
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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.
*/
}

View file

@ -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<String, Shader> 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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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
}

View file

@ -0,0 +1,6 @@
package dev.euph.engine.render;
public interface IRenderPipeline {
void render();
void init();
}

View file

@ -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;
}
}

View file

@ -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());
}
}
}

View file

@ -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<String, Integer> 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;
}
}

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