Compare commits

...

5 commits

Author SHA1 Message Date
David
52806e4457 Per-thread RNGs and RNG bugfix 2021-08-30 18:34:14 +02:00
David
a45ae025d6 Add progress bars and argument parsing 2021-08-29 21:24:17 +02:00
David
742ef283e4 Faster random implementation 2021-08-29 16:17:13 +02:00
David
321c677da2 More timing functions 2021-08-28 01:04:31 +02:00
David
6331a2bf79 Add profiling with Remotery 2021-08-24 20:55:39 +02:00
58 changed files with 21993 additions and 147 deletions

6
.gitignore vendored
View file

@ -8,3 +8,9 @@ raytracer
# Actual output image # Actual output image
image.ppm image.ppm
# Profiler data
perf.*
# Core dumps
core

View file

@ -1,8 +1,14 @@
raytracer: camera.hpp color.hpp hittable.hpp hittable_list.hpp main.cpp material.hpp ray.hpp rtweekend.hpp sphere.hpp vec3.hpp INCLUDE=./include
@g++ -g -O2 -Wall -Wextra -Wpedantic main.cpp -o raytracer LIBS=-pthread -lm
FLAGS=-Ofast -march=native -g -Wall -Wextra -Wpedantic
raytracer: camera.hpp color.hpp hittable.hpp hittable_list.hpp main.cpp material.hpp random.h ray.hpp rtweekend.hpp sphere.hpp vec3.hpp $(INCLUDE)/Remotery.c $(INCLUDE)/Remotery.h
g++ $(FLAGS) -I$(INCLUDE) $(LIBS) main.cpp -o raytracer
make debug:
image: raytracer image: raytracer
@./raytracer > image.ppm @./raytracer -o image.ppm
@if [ $$TERM = "xterm-kitty" ]; then\ @if [ $$TERM = "xterm-kitty" ]; then\
kitty icat image.ppm;\ kitty icat image.ppm;\
fi fi

View file

@ -10,21 +10,21 @@ struct camera {
vec3 horizontal; vec3 horizontal;
vec3 vertical; vec3 vertical;
vec3 u,v,w; vec3 u,v,w;
double lens_radius; float lens_radius;
/* Constructors */ /* Constructors */
camera(point3 lookfrom, camera(point3 lookfrom,
point3 lookat, point3 lookat,
vec3 vup, vec3 vup,
double vfov, float vfov,
double aspect_ratio, float aspect_ratio,
double aperture, float aperture,
double focus_dist) float focus_dist)
{ {
double theta = degrees_to_radians(vfov); float theta = degrees_to_radians(vfov);
double h = tan(theta/2); float h = tan(theta/2);
double viewport_height = 2.0 * h; float viewport_height = 2.0 * h;
double viewport_width = aspect_ratio * viewport_height; float viewport_width = aspect_ratio * viewport_height;
w = normalize(lookfrom - lookat); w = normalize(lookfrom - lookat);
u = normalize(cross(vup,w)); u = normalize(cross(vup,w));
@ -40,9 +40,9 @@ struct camera {
/* Methods */ /* Methods */
ray get_ray(double s, double t) const ray get_ray(float s, float t, int32_t thread_id = 0) const
{ {
vec3 rd = lens_radius * random_in_unit_disk(); vec3 rd = lens_radius * random_in_unit_disk(thread_id);
vec3 offset = u * rd.x + v * rd.y; vec3 offset = u * rd.x + v * rd.y;
return ray(origin + offset, lower_left_corner + s*horizontal + t*vertical - origin - offset); return ray(origin + offset, lower_left_corner + s*horizontal + t*vertical - origin - offset);

View file

@ -9,12 +9,12 @@
/* Writes color components as a space-delimited string of numbers in the range [0,255] */ /* Writes color components as a space-delimited string of numbers in the range [0,255] */
void write_color(FILE *fp, color c, uint32_t samples_per_pixel) void write_color(FILE *fp, color c, uint32_t samples_per_pixel)
{ {
double scale = 1.0 / samples_per_pixel; float scale = 1.0 / samples_per_pixel;
// Divide the color by the number of samples // Divide the color by the number of samples
double r = sqrt(c.x * scale); float r = sqrt(c.x * scale);
double g = sqrt(c.y * scale); float g = sqrt(c.y * scale);
double b = sqrt(c.z * scale); float b = sqrt(c.z * scale);
/* Write output */ /* Write output */
fprintf(fp, fprintf(fp,
@ -24,4 +24,12 @@ void write_color(FILE *fp, color c, uint32_t samples_per_pixel)
(uint8_t) (255 * clamp(b, 0, 1))); (uint8_t) (255 * clamp(b, 0, 1)));
} }
void write_image(color *image, uint64_t n, FILE *fp, uint32_t samples_per_pixel)
{
for (int64_t i = n-1; i >= 0; --i)
{
write_color(fp, image[i], samples_per_pixel);
}
}
#endif #endif

View file

@ -6,7 +6,7 @@
/* Virtual class that represents objects who could collide against a ray */ /* Virtual class that represents objects who could collide against a ray */
struct hittable { struct hittable {
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const = 0;
}; };
#endif #endif

View file

@ -2,6 +2,7 @@
#define HITTABLE_LIST_H #define HITTABLE_LIST_H
#include "hittable.hpp" #include "hittable.hpp"
#include "sphere.hpp"
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -9,29 +10,34 @@
using std::shared_ptr; using std::shared_ptr;
using std::make_shared; using std::make_shared;
struct hittable_list : hittable { template <typename T = sphere>
struct hittable_list {
/* Attributes */ /* Attributes */
std::vector<shared_ptr<hittable>> objects; std::vector<T> objects;
/* Constructors */ /* Constructors */
hittable_list () {} hittable_list () {}
hittable_list(shared_ptr<hittable> h) { add(h); } hittable_list(T object) { add(object); }
/* Methods */ /* Methods */
void clear() { objects.clear(); } void clear() { objects.clear(); }
void add (shared_ptr<hittable> h) { objects.push_back(h); } void add (T h) { objects.push_back(h); }
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override; bool hit(const ray& r, float t_min, float t_max, hit_record& rec);
}; };
bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const template <typename T>
bool hittable_list<T>::hit(const ray& r, float t_min, float t_max, hit_record& rec)
{ {
rmt_ScopedCPUSample(HittableList_Hit, RMTSF_Aggregate);
hit_record temp_rec; hit_record temp_rec;
bool hit_anything = false; bool hit_anything = false;
double closest_so_far = t_max; float closest_so_far = t_max;
for (const auto& object : objects) uint32_t s = objects.size();
for (uint32_t i = 0; i < s; ++i)
{ {
T *object = &objects[i];
if (object->hit(r, t_min, closest_so_far, temp_rec)) if (object->hit(r, t_min, closest_so_far, temp_rec))
{ {
hit_anything = true; hit_anything = true;

8809
include/Remotery.c Normal file

File diff suppressed because it is too large Load diff

679
include/Remotery.h Normal file
View file

@ -0,0 +1,679 @@
/*
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.
*/
/*
Compiling
---------
* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include
directories to add Remotery/lib path. The required library ws2_32.lib should be picked
up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c.
* Mac OS X (XCode) - simply add lib/Remotery.c and lib/Remotery.h to your program.
* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for
library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c
-I lib -pthread -lm
You can define some extra macros to modify what features are compiled into Remotery. These are
documented just below this comment.
*/
#ifndef RMT_INCLUDED_H
#define RMT_INCLUDED_H
// Set to 0 to not include any bits of Remotery in your build
#ifndef RMT_ENABLED
#define RMT_ENABLED 1
#endif
// Help performance of the server sending data to the client by marking this machine as little-endian
#ifndef RMT_ASSUME_LITTLE_ENDIAN
#define RMT_ASSUME_LITTLE_ENDIAN 0
#endif
// Used by the Celtoys TinyCRT library (not released yet)
#ifndef RMT_USE_TINYCRT
#define RMT_USE_TINYCRT 0
#endif
// Assuming CUDA headers/libs are setup, allow CUDA profiling
#ifndef RMT_USE_CUDA
#define RMT_USE_CUDA 0
#endif
// Assuming Direct3D 11 headers/libs are setup, allow D3D11 profiling
#ifndef RMT_USE_D3D11
#define RMT_USE_D3D11 0
#endif
// Allow OpenGL profiling
#ifndef RMT_USE_OPENGL
#define RMT_USE_OPENGL 0
#endif
// Allow Metal profiling
#ifndef RMT_USE_METAL
#define RMT_USE_METAL 0
#endif
// Initially use POSIX thread names to name threads instead of Thread0, 1, ...
#ifndef RMT_USE_POSIX_THREADNAMES
#define RMT_USE_POSIX_THREADNAMES 0
#endif
// How many times we spin data back and forth between CPU & GPU
// to calculate average RTT (Roundtrip Time). Cannot be 0.
// Affects OpenGL & D3D11
#ifndef RMT_GPU_CPU_SYNC_NUM_ITERATIONS
#define RMT_GPU_CPU_SYNC_NUM_ITERATIONS 16
#endif
// Time in seconds between each resync to compensate for drifting between GPU & CPU timers,
// effects of power saving, etc. Resyncs can cause stutter, lag spikes, stalls.
// Set to 0 for never.
// Affects OpenGL & D3D11
#ifndef RMT_GPU_CPU_SYNC_SECONDS
#define RMT_GPU_CPU_SYNC_SECONDS 30
#endif
// Whether we should automatically resync if we detect a timer disjoint (e.g.
// changed from AC power to battery, GPU is overheating, or throttling up/down
// due to laptop savings events). Set it to 0 to avoid resync in such events.
// Useful if for some odd reason a driver reports a lot of disjoints.
// Affects D3D11
#ifndef RMT_D3D11_RESYNC_ON_DISJOINT
#define RMT_D3D11_RESYNC_ON_DISJOINT 1
#endif
/*
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
Compiler/Platform Detection and Preprocessor Utilities
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
*/
// Platform identification
#if defined(_WINDOWS) || defined(_WIN32)
#define RMT_PLATFORM_WINDOWS
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__)
#define RMT_PLATFORM_LINUX
#define RMT_PLATFORM_POSIX
#elif defined(__APPLE__)
#define RMT_PLATFORM_MACOS
#define RMT_PLATFORM_POSIX
#endif
// Architecture identification
#ifdef RMT_PLATFORM_WINDOWS
#ifdef _M_AMD64
#define RMT_ARCH_64BIT
#else
#define RMT_ARCH_32BIT
#endif
#endif
#ifdef RMT_DLL
#if defined (RMT_PLATFORM_WINDOWS)
#if defined (RMT_IMPL)
#define RMT_API __declspec(dllexport)
#else
#define RMT_API __declspec(dllimport)
#endif
#elif defined (RMT_PLATFORM_POSIX)
#if defined (RMT_IMPL)
#define RMT_API __attribute__((visibility("default")))
#else
#define RMT_API
#endif
#endif
#else
#define RMT_API
#endif
// Allows macros to be written that can work around the inability to do: #define(x) #ifdef x
// with the C preprocessor.
#if RMT_ENABLED
#define IFDEF_RMT_ENABLED(t, f) t
#else
#define IFDEF_RMT_ENABLED(t, f) f
#endif
#if RMT_ENABLED && RMT_USE_CUDA
#define IFDEF_RMT_USE_CUDA(t, f) t
#else
#define IFDEF_RMT_USE_CUDA(t, f) f
#endif
#if RMT_ENABLED && RMT_USE_D3D11
#define IFDEF_RMT_USE_D3D11(t, f) t
#else
#define IFDEF_RMT_USE_D3D11(t, f) f
#endif
#if RMT_ENABLED && RMT_USE_OPENGL
#define IFDEF_RMT_USE_OPENGL(t, f) t
#else
#define IFDEF_RMT_USE_OPENGL(t, f) f
#endif
#if RMT_ENABLED && RMT_USE_METAL
#define IFDEF_RMT_USE_METAL(t, f) t
#else
#define IFDEF_RMT_USE_METAL(t, f) f
#endif
// Public interface is written in terms of these macros to easily enable/disable itself
#define RMT_OPTIONAL(macro, x) IFDEF_ ## macro(x, )
#define RMT_OPTIONAL_RET(macro, x, y) IFDEF_ ## macro(x, (y))
/*
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
Types
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
*/
// Boolean
typedef unsigned int rmtBool;
#define RMT_TRUE ((rmtBool)1)
#define RMT_FALSE ((rmtBool)0)
// Unsigned integer types
typedef unsigned char rmtU8;
typedef unsigned short rmtU16;
typedef unsigned int rmtU32;
typedef unsigned long long rmtU64;
// Signed integer types
typedef char rmtS8;
typedef short rmtS16;
typedef int rmtS32;
typedef long long rmtS64;
// Const, null-terminated string pointer
typedef const char* rmtPStr;
// Handle to the main remotery instance
typedef struct Remotery Remotery;
// All possible error codes
// clang-format off
typedef enum rmtError
{
RMT_ERROR_NONE,
RMT_ERROR_RECURSIVE_SAMPLE, // Not an error but an internal message to calling code
RMT_ERROR_UNKNOWN, // An error with a message yet to be defined, only for internal error handling
// System errors
RMT_ERROR_MALLOC_FAIL, // Malloc call within remotery failed
RMT_ERROR_TLS_ALLOC_FAIL, // Attempt to allocate thread local storage failed
RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL, // Failed to create a virtual memory mirror buffer
RMT_ERROR_CREATE_THREAD_FAIL, // Failed to create a thread for the server
RMT_ERROR_OPEN_THREAD_HANDLE_FAIL, // Failed to open a thread handle, given a thread id
// Network TCP/IP socket errors
RMT_ERROR_SOCKET_INIT_NETWORK_FAIL, // Network initialisation failure (e.g. on Win32, WSAStartup fails)
RMT_ERROR_SOCKET_CREATE_FAIL, // Can't create a socket for connection to the remote viewer
RMT_ERROR_SOCKET_BIND_FAIL, // Can't bind a socket for the server
RMT_ERROR_SOCKET_LISTEN_FAIL, // Created server socket failed to enter a listen state
RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL, // Created server socket failed to switch to a non-blocking state
RMT_ERROR_SOCKET_INVALID_POLL, // Poll attempt on an invalid socket
RMT_ERROR_SOCKET_SELECT_FAIL, // Server failed to call select on socket
RMT_ERROR_SOCKET_POLL_ERRORS, // Poll notified that the socket has errors
RMT_ERROR_SOCKET_ACCEPT_FAIL, // Server failed to accept connection from client
RMT_ERROR_SOCKET_SEND_TIMEOUT, // Timed out trying to send data
RMT_ERROR_SOCKET_SEND_FAIL, // Unrecoverable error occured while client/server tried to send data
RMT_ERROR_SOCKET_RECV_NO_DATA, // No data available when attempting a receive
RMT_ERROR_SOCKET_RECV_TIMEOUT, // Timed out trying to receive data
RMT_ERROR_SOCKET_RECV_FAILED, // Unrecoverable error occured while client/server tried to receive data
// WebSocket errors
RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET, // WebSocket server handshake failed, not HTTP GET
RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION, // WebSocket server handshake failed, can't locate WebSocket version
RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION, // WebSocket server handshake failed, unsupported WebSocket version
RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST, // WebSocket server handshake failed, can't locate host
RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST, // WebSocket server handshake failed, host is not allowed to connect
RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY, // WebSocket server handshake failed, can't locate WebSocket key
RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY, // WebSocket server handshake failed, WebSocket key is ill-formed
RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL, // WebSocket server handshake failed, internal error, bad string code
RMT_ERROR_WEBSOCKET_DISCONNECTED, // WebSocket server received a disconnect request and closed the socket
RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER, // Couldn't parse WebSocket frame header
RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE, // Partially received wide frame header size
RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_MASK, // Partially received frame header data mask
RMT_ERROR_WEBSOCKET_RECEIVE_TIMEOUT, // Timeout receiving frame header
RMT_ERROR_REMOTERY_NOT_CREATED, // Remotery object has not been created
RMT_ERROR_SEND_ON_INCOMPLETE_PROFILE, // An attempt was made to send an incomplete profile tree to the client
// CUDA error messages
RMT_ERROR_CUDA_DEINITIALIZED, // This indicates that the CUDA driver is in the process of shutting down
RMT_ERROR_CUDA_NOT_INITIALIZED, // This indicates that the CUDA driver has not been initialized with cuInit() or that initialization has failed
RMT_ERROR_CUDA_INVALID_CONTEXT, // This most frequently indicates that there is no context bound to the current thread
RMT_ERROR_CUDA_INVALID_VALUE, // This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values
RMT_ERROR_CUDA_INVALID_HANDLE, // This indicates that a resource handle passed to the API call was not valid
RMT_ERROR_CUDA_OUT_OF_MEMORY, // The API call failed because it was unable to allocate enough memory to perform the requested operation
RMT_ERROR_ERROR_NOT_READY, // This indicates that a resource handle passed to the API call was not valid
// Direct3D 11 error messages
RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY, // Failed to create query for sample
// OpenGL error messages
RMT_ERROR_OPENGL_ERROR, // Generic OpenGL error, no need to expose detail since app will need an OpenGL error callback registered
RMT_ERROR_CUDA_UNKNOWN,
} rmtError;
// clang-format on
typedef enum rmtSampleFlags
{
// Default behaviour
RMTSF_None = 0,
// Search parent for same-named samples and merge timing instead of adding a new sample
RMTSF_Aggregate = 1,
// Merge sample with parent if it's the same sample
RMTSF_Recursive = 2,
// Set this flag on any of your root samples so that Remotery will assert if it ends up *not* being the root sample.
// This will quickly allow you to detect Begin/End mismatches causing a sample tree imbalance.
RMTSF_Root = 4,
} rmtSampleFlags;
/*
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
Public Interface
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
*/
// Can call remotery functions on a null pointer
// TODO: Can embed extern "C" in these macros?
#define rmt_Settings() \
RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_Settings(), NULL )
#define rmt_CreateGlobalInstance(rmt) \
RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_CreateGlobalInstance(rmt), RMT_ERROR_NONE)
#define rmt_DestroyGlobalInstance(rmt) \
RMT_OPTIONAL(RMT_ENABLED, _rmt_DestroyGlobalInstance(rmt))
#define rmt_SetGlobalInstance(rmt) \
RMT_OPTIONAL(RMT_ENABLED, _rmt_SetGlobalInstance(rmt))
#define rmt_GetGlobalInstance() \
RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_GetGlobalInstance(), NULL)
#define rmt_SetCurrentThreadName(rmt) \
RMT_OPTIONAL(RMT_ENABLED, _rmt_SetCurrentThreadName(rmt))
#define rmt_LogText(text) \
RMT_OPTIONAL(RMT_ENABLED, _rmt_LogText(text))
#define rmt_BeginCPUSample(name, flags) \
RMT_OPTIONAL(RMT_ENABLED, { \
static rmtU32 rmt_sample_hash_##name = 0; \
_rmt_BeginCPUSample(#name, flags, &rmt_sample_hash_##name); \
})
#define rmt_BeginCPUSampleDynamic(namestr, flags) \
RMT_OPTIONAL(RMT_ENABLED, _rmt_BeginCPUSample(namestr, flags, NULL))
#define rmt_EndCPUSample() \
RMT_OPTIONAL(RMT_ENABLED, _rmt_EndCPUSample())
// Callback function pointer types
typedef void* (*rmtMallocPtr)(void* mm_context, rmtU32 size);
typedef void* (*rmtReallocPtr)(void* mm_context, void* ptr, rmtU32 size);
typedef void (*rmtFreePtr)(void* mm_context, void* ptr);
typedef void (*rmtInputHandlerPtr)(const char* text, void* context);
// Struture to fill in to modify Remotery default settings
typedef struct rmtSettings
{
// Which port to listen for incoming connections on
rmtU16 port;
// When this server exits it can leave the port open in TIME_WAIT state for a while. This forces
// subsequent server bind attempts to fail when restarting. If you find restarts fail repeatedly
// with bind attempts, set this to true to forcibly reuse the open port.
rmtBool reuse_open_port;
// Only allow connections on localhost?
// For dev builds you may want to access your game from other devices but if
// you distribute a game to your players with Remotery active, probably best
// to limit connections to localhost.
rmtBool limit_connections_to_localhost;
// Whether to enable runtime thread sampling that discovers which processors a thread is running
// on. This will suspend and resume threads from outside repeatdly and inject code into each
// thread that automatically instruments the processor.
// Default: Enabled
rmtBool enableThreadSampler;
// How long to sleep between server updates, hopefully trying to give
// a little CPU back to other threads.
rmtU32 msSleepBetweenServerUpdates;
// Size of the internal message queues Remotery uses
// Will be rounded to page granularity of 64k
rmtU32 messageQueueSizeInBytes;
// If the user continuously pushes to the message queue, the server network
// code won't get a chance to update unless there's an upper-limit on how
// many messages can be consumed per loop.
rmtU32 maxNbMessagesPerUpdate;
// Callback pointers for memory allocation
rmtMallocPtr malloc;
rmtReallocPtr realloc;
rmtFreePtr free;
void* mm_context;
// Callback pointer for receiving input from the Remotery console
rmtInputHandlerPtr input_handler;
// Context pointer that gets sent to Remotery console callback function
void* input_handler_context;
rmtPStr logPath;
} rmtSettings;
// Structure to fill in when binding CUDA to Remotery
typedef struct rmtCUDABind
{
// The main context that all driver functions apply before each call
void* context;
// Driver API function pointers that need to be pointed to
// Untyped so that the CUDA headers are not required in this file
// NOTE: These are named differently to the CUDA functions because the CUDA API has a habit of using
// macros to point function calls to different versions, e.g. cuEventDestroy is a macro for
// cuEventDestroy_v2.
void* CtxSetCurrent;
void* CtxGetCurrent;
void* EventCreate;
void* EventDestroy;
void* EventRecord;
void* EventQuery;
void* EventElapsedTime;
} rmtCUDABind;
// Call once after you've initialised CUDA to bind it to Remotery
#define rmt_BindCUDA(bind) \
RMT_OPTIONAL(RMT_USE_CUDA, _rmt_BindCUDA(bind))
// Mark the beginning of a CUDA sample on the specified asynchronous stream
#define rmt_BeginCUDASample(name, stream) \
RMT_OPTIONAL(RMT_USE_CUDA, { \
static rmtU32 rmt_sample_hash_##name = 0; \
_rmt_BeginCUDASample(#name, &rmt_sample_hash_##name, stream); \
})
// Mark the end of a CUDA sample on the specified asynchronous stream
#define rmt_EndCUDASample(stream) \
RMT_OPTIONAL(RMT_USE_CUDA, _rmt_EndCUDASample(stream))
#define rmt_BindD3D11(device, context) \
RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BindD3D11(device, context))
#define rmt_UnbindD3D11() \
RMT_OPTIONAL(RMT_USE_D3D11, _rmt_UnbindD3D11())
#define rmt_BeginD3D11Sample(name) \
RMT_OPTIONAL(RMT_USE_D3D11, { \
static rmtU32 rmt_sample_hash_##name = 0; \
_rmt_BeginD3D11Sample(#name, &rmt_sample_hash_##name); \
})
#define rmt_BeginD3D11SampleDynamic(namestr) \
RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BeginD3D11Sample(namestr, NULL))
#define rmt_EndD3D11Sample() \
RMT_OPTIONAL(RMT_USE_D3D11, _rmt_EndD3D11Sample())
#define rmt_BindOpenGL() \
RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BindOpenGL())
#define rmt_UnbindOpenGL() \
RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_UnbindOpenGL())
#define rmt_BeginOpenGLSample(name) \
RMT_OPTIONAL(RMT_USE_OPENGL, { \
static rmtU32 rmt_sample_hash_##name = 0; \
_rmt_BeginOpenGLSample(#name, &rmt_sample_hash_##name); \
})
#define rmt_BeginOpenGLSampleDynamic(namestr) \
RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BeginOpenGLSample(namestr, NULL))
#define rmt_EndOpenGLSample() \
RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_EndOpenGLSample())
#define rmt_BindMetal(command_buffer) \
RMT_OPTIONAL(RMT_USE_METAL, _rmt_BindMetal(command_buffer));
#define rmt_UnbindMetal() \
RMT_OPTIONAL(RMT_USE_METAL, _rmt_UnbindMetal());
#define rmt_BeginMetalSample(name) \
RMT_OPTIONAL(RMT_USE_METAL, { \
static rmtU32 rmt_sample_hash_##name = 0; \
_rmt_BeginMetalSample(#name, &rmt_sample_hash_##name); \
})
#define rmt_BeginMetalSampleDynamic(namestr) \
RMT_OPTIONAL(RMT_USE_METAL, _rmt_BeginMetalSample(namestr, NULL))
#define rmt_EndMetalSample() \
RMT_OPTIONAL(RMT_USE_METAL, _rmt_EndMetalSample())
/*
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
C++ Public Interface Extensions
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
*/
#ifdef __cplusplus
#if RMT_ENABLED
// Types that end samples in their destructors
extern "C" RMT_API void _rmt_EndCPUSample(void);
struct rmt_EndCPUSampleOnScopeExit
{
~rmt_EndCPUSampleOnScopeExit()
{
_rmt_EndCPUSample();
}
};
#if RMT_USE_CUDA
extern "C" RMT_API void _rmt_EndCUDASample(void* stream);
struct rmt_EndCUDASampleOnScopeExit
{
rmt_EndCUDASampleOnScopeExit(void* stream) : stream(stream)
{
}
~rmt_EndCUDASampleOnScopeExit()
{
_rmt_EndCUDASample(stream);
}
void* stream;
};
#endif
#if RMT_USE_D3D11
extern "C" RMT_API void _rmt_EndD3D11Sample(void);
struct rmt_EndD3D11SampleOnScopeExit
{
~rmt_EndD3D11SampleOnScopeExit()
{
_rmt_EndD3D11Sample();
}
};
#endif
#if RMT_USE_OPENGL
extern "C" RMT_API void _rmt_EndOpenGLSample(void);
struct rmt_EndOpenGLSampleOnScopeExit
{
~rmt_EndOpenGLSampleOnScopeExit()
{
_rmt_EndOpenGLSample();
}
};
#endif
#if RMT_USE_METAL
extern "C" RMT_API void _rmt_EndMetalSample(void);
struct rmt_EndMetalSampleOnScopeExit
{
~rmt_EndMetalSampleOnScopeExit()
{
_rmt_EndMetalSample();
}
};
#endif
#endif
// Pairs a call to rmt_Begin<TYPE>Sample with its call to rmt_End<TYPE>Sample when leaving scope
#define rmt_ScopedCPUSample(name, flags) \
RMT_OPTIONAL(RMT_ENABLED, rmt_BeginCPUSample(name, flags)); \
RMT_OPTIONAL(RMT_ENABLED, rmt_EndCPUSampleOnScopeExit rmt_ScopedCPUSample##name);
#define rmt_ScopedCUDASample(name, stream) \
RMT_OPTIONAL(RMT_USE_CUDA, rmt_BeginCUDASample(name, stream)); \
RMT_OPTIONAL(RMT_USE_CUDA, rmt_EndCUDASampleOnScopeExit rmt_ScopedCUDASample##name(stream));
#define rmt_ScopedD3D11Sample(name) \
RMT_OPTIONAL(RMT_USE_D3D11, rmt_BeginD3D11Sample(name)); \
RMT_OPTIONAL(RMT_USE_D3D11, rmt_EndD3D11SampleOnScopeExit rmt_ScopedD3D11Sample##name);
#define rmt_ScopedOpenGLSample(name) \
RMT_OPTIONAL(RMT_USE_OPENGL, rmt_BeginOpenGLSample(name)); \
RMT_OPTIONAL(RMT_USE_OPENGL, rmt_EndOpenGLSampleOnScopeExit rmt_ScopedOpenGLSample##name);
#define rmt_ScopedMetalSample(name) \
RMT_OPTIONAL(RMT_USE_METAL, rmt_BeginMetalSample(name)); \
RMT_OPTIONAL(RMT_USE_METAL, rmt_EndMetalSampleOnScopeExit rmt_ScopedMetalSample##name);
#endif
/*
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
Private Interface - don't directly call these
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
*/
#if RMT_ENABLED
#ifdef __cplusplus
extern "C" {
#endif
RMT_API rmtSettings* _rmt_Settings( void );
RMT_API enum rmtError _rmt_CreateGlobalInstance(Remotery** remotery);
RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery);
RMT_API void _rmt_SetGlobalInstance(Remotery* remotery);
RMT_API Remotery* _rmt_GetGlobalInstance(void);
RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name);
RMT_API void _rmt_LogText(rmtPStr text);
RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32 flags, rmtU32* hash_cache);
RMT_API void _rmt_EndCPUSample(void);
#if RMT_USE_CUDA
RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind);
RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream);
RMT_API void _rmt_EndCUDASample(void* stream);
#endif
#if RMT_USE_D3D11
RMT_API void _rmt_BindD3D11(void* device, void* context);
RMT_API void _rmt_UnbindD3D11(void);
RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache);
RMT_API void _rmt_EndD3D11Sample(void);
#endif
#if RMT_USE_OPENGL
RMT_API void _rmt_BindOpenGL();
RMT_API void _rmt_UnbindOpenGL(void);
RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache);
RMT_API void _rmt_EndOpenGLSample(void);
#endif
#if RMT_USE_METAL
RMT_API void _rmt_BeginMetalSample(rmtPStr name, rmtU32* hash_cache);
RMT_API void _rmt_EndMetalSample(void);
#endif
#ifdef __cplusplus
}
#endif
#if RMT_USE_METAL
#ifdef __OBJC__
RMT_API void _rmt_BindMetal(id command_buffer);
RMT_API void _rmt_UnbindMetal();
#endif
#endif
#endif // RMT_ENABLED
#endif

59
include/RemoteryMetal.mm 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; }];
}

4649
include/indicators.hpp Normal file

File diff suppressed because it is too large Load diff

382
main.cpp
View file

@ -1,31 +1,81 @@
#include <stdio.h> #include <stdio.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <getopt.h>
#include <time.h>
#define RMT_ENABLED 0
// Lib includes
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wsign-compare"
#include <Remotery.c>
#pragma GCC diagnostic pop
#include <indicators.hpp>
// Internal includes
#include "rtweekend.hpp" #include "rtweekend.hpp"
#include "color.hpp" #include "color.hpp"
#include "hittable_list.hpp" #include "hittable_list.hpp"
#include "sphere.hpp" #include "sphere.hpp"
#include "camera.hpp" #include "camera.hpp"
#ifdef DEBUG
#define print_timers() print_timers_()
#else
#define print_timers()
#endif
// Threading structs
struct thread_args
{
int32_t thread_id;
int32_t start;
int32_t end;
};
// Function signatures
color ray_color(const ray& r, const hittable& world, int32_t depth); color ray_color(const ray& r, const hittable& world, int32_t depth);
double hit_sphere(const point3& center, double radius, const ray& r); float hit_sphere(const point3& center, float radius, const ray& r);
hittable_list random_scene(); void *raytrace_lines(void *arg);
hittable_list<sphere> random_scene();
hittable_list random_scene() { // Global vars
hittable_list world; indicators::DynamicProgress<indicators::BlockProgressBar> progress_bars;
const char *default_file = "image.ppm";
FILE *output_file_handle;
// Image
float aspect_ratio;
int32_t image_width;
int32_t image_height;
int32_t samples_per_pixel;
int32_t max_depth;
color *image;
uint64_t bytes_per_line;
uint64_t bytes_per_pixel;
// World
hittable_list<sphere> world;
camera *global_camera;
hittable_list<sphere> random_scene() {
hittable_list<sphere> world;
auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5)); auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));
world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material)); world.add(sphere(point3(0,-1000,0), 1000, ground_material));
for (int32_t a = -11; a < 11; a++) for (int32_t a = -11; a < 11; a++)
{ {
for (int32_t b = -11; b < 11; b++) for (int32_t b = -11; b < 11; b++)
{ {
double choose_mat = random_double(); float choose_mat = random_float();
point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); point3 center(a + 0.9*random_float(), 0.2, b + 0.9*random_float());
if ((center - point3(4, 0.2, 0)).length() > 0.9) if ((center - point3(4, 0.2, 0)).length() > 0.9)
{ {
@ -35,40 +85,42 @@ hittable_list random_scene() {
// diffuse // diffuse
color albedo = color::random() * color::random(); color albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo); sphere_material = make_shared<lambertian>(albedo);
world.add(make_shared<sphere>(center, 0.2, sphere_material)); world.add(sphere(center, 0.2, sphere_material));
} }
else if (choose_mat < 0.95) else if (choose_mat < 0.95)
{ {
// metal // metal
color albedo = color::random(0.5, 1); color albedo = color::random(0.5, 1);
double fuzz = random_double(0, 0.5); float fuzz = random_float(0, 0.5);
sphere_material = make_shared<metal>(albedo, fuzz); sphere_material = make_shared<metal>(albedo, fuzz);
world.add(make_shared<sphere>(center, 0.2, sphere_material)); world.add(sphere(center, 0.2, sphere_material));
} }
else else
{ {
// glass // glass
sphere_material = make_shared<dielectric>(1.5); sphere_material = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(center, 0.2, sphere_material)); world.add(sphere(center, 0.2, sphere_material));
} }
} }
} }
} }
auto material1 = make_shared<dielectric>(1.5); auto material1 = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1)); world.add(sphere(point3(0, 1, 0), 1.0, material1));
auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1)); auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2)); world.add(sphere(point3(-4, 1, 0), 1.0, material2));
auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0); auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0);
world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3)); world.add(sphere(point3(4, 1, 0), 1.0, material3));
return world; return world;
} }
color ray_color(const ray& r, const hittable& world, int32_t depth) template<typename T>
color ray_color(const ray& r, hittable_list<T>& world, int32_t depth, int32_t thread_id)
{ {
rmt_ScopedCPUSample(Scatter, RMTSF_Aggregate | RMTSF_Recursive);
if (depth <= 0) if (depth <= 0)
{ {
return color(0,0,0); return color(0,0,0);
@ -79,9 +131,12 @@ color ray_color(const ray& r, const hittable& world, int32_t depth)
{ {
ray scattered; ray scattered;
color attenuation; color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) rmt_BeginCPUSample(Scatter, RMTSF_Aggregate);
bool visible = rec.mat_ptr->scatter(r, rec, attenuation, scattered, thread_id);
rmt_EndCPUSample();
if (visible)
{ {
return attenuation * ray_color(scattered, world, depth-1); return attenuation * ray_color(scattered, world, depth-1, thread_id);
} }
else else
{ {
@ -89,74 +144,305 @@ color ray_color(const ray& r, const hittable& world, int32_t depth)
} }
} }
vec3 unit_direction = normalize(r.direction); vec3 unit_direction = normalize(r.direction);
double t = 0.5 * (unit_direction.y + 1.0); float t = 0.5 * (unit_direction.y + 1.0);
return (1-t) * color(1,1,1) + t*color(0.5,0.7,1.0); return (1-t) * color(1,1,1) + t*color(0.5,0.7,1.0);
} }
double hit_sphere(const point3& center, double radius, const ray& r) float hit_sphere(const point3& center, float radius, const ray& r)
{ {
vec3 oc = r.origin - center; vec3 oc = r.origin - center;
double a = r.direction.length_squared(); float a = r.direction.length_squared();
double half_b = dot(oc, r.direction); float half_b = dot(oc, r.direction);
double c = oc.length_squared() - radius*radius; float c = oc.length_squared() - radius*radius;
double discriminant = half_b*half_b - a*c; float discriminant = half_b*half_b - a*c;
if (discriminant < 0) if (discriminant < 0)
return -1; return -1;
else else
return (-half_b - sqrt(discriminant)) / a; return (-half_b - sqrt(discriminant)) / a;
} }
int32_t main() int32_t main(int argc, char *argv[])
{ {
/* Argument parsing */
int32_t c;
bool using_default_output = true;
while (1)
{
static struct option long_options[] =
{
{"output", required_argument, 0, 'o'},
{0, 0, 0, 0}
};
/* getopt_long stores the option index here. */
int option_index = 0;
c = getopt_long (argc, argv, "o:",
long_options, &option_index);
/* Detect the end of the options. */
if (c == -1)
break;
switch (c)
{
case 0:
/* If this option set a flag, do nothing else now. */
if (long_options[option_index].flag != 0)
break;
printf ("option %s", long_options[option_index].name);
if (optarg)
printf (" with arg %s", optarg);
printf ("\n");
break;
case 'o':
using_default_output = false;
output_file_handle = fopen(optarg, "w");
break;
case '?':
/* getopt_long already printed an error message. */
break;
default:
abort();
}
}
if (using_default_output)
{
output_file_handle = fopen(default_file, "w");
}
/* Profiling library initialization */
Remotery *rmt;
if (RMT_ERROR_NONE != rmt_CreateGlobalInstance(&rmt))
{
fprintf(stderr, "Error starting Remotery\n");
}
//indicators::show_console_cursor(false);
// Get the number of logical CPUs
int32_t ncores = sysconf(_SC_NPROCESSORS_ONLN);
// Initialize and seed the random number generators
pcg_table = (pcg32_random_t *) malloc(sizeof(pcg32_random_t) * ncores);
for (int32_t i = 0; i < ncores; ++i)
{
struct timespec ts;
if (timespec_get(&ts, TIME_UTC))
{
// Use higher quality seed
uint64_t seed = (uint64_t)(ts.tv_nsec ^ ts.tv_sec);
pcg_table[i] = { seed, seed };
}
else
{
// Error, use default seed
pcg_table[i] = default_pcg;
}
}
// Image // Image
const double aspect_ratio = 3.0 / 2.0; aspect_ratio = 3.0 / 2.0;
const int32_t image_width = 1200; image_width = 1200;
const int32_t image_height = (int32_t) (image_width / aspect_ratio); image_height = (int32_t) (image_width / aspect_ratio);
int32_t samples_per_pixel = 500; samples_per_pixel = 500;
const int32_t max_depth = 50; max_depth = 50;
image = (color*) malloc(image_width * image_height * sizeof(color));
bytes_per_line = sizeof(color) * image_width;
bytes_per_pixel = sizeof(color);
if (getenv("SPP")) if (getenv("SPP"))
{ {
samples_per_pixel = strtol(getenv("SPP"), NULL, 10); samples_per_pixel = strtol(getenv("SPP"), NULL, 10);
} }
// World // World
hittable_list world = random_scene(); world = random_scene();
// Camera // Camera
point3 lookfrom(13,2,3); point3 lookfrom(13,2,3);
point3 lookat(0,0,0); point3 lookat(0,0,0);
vec3 vup(0,1,0); vec3 vup(0,1,0);
double dist_to_focus = 10.0; float dist_to_focus = 10.0;
double aperture = 0.1; float aperture = 0.1;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); camera cam = camera(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
global_camera = &cam;
// Render // Render
printf("P3\n%d %d\n255\n", image_width, image_height); fprintf(output_file_handle, "P3\n%d %d\n255\n", image_width, image_height);
for (int32_t j = image_height - 1; j >= 0; --j)
std::vector<pthread_t> threads;
std::vector<thread_args> args;
threads.reserve(ncores);
args.reserve(ncores);
std::vector<indicators::BlockProgressBar*> bar_memory;
bar_memory.reserve(ncores);
for (int32_t i = 0; i < ncores; ++i)
{ {
fprintf(stderr, "\rScanlines remaining: %d ", j);
fflush(stderr); bar_memory[i] = new indicators::BlockProgressBar{indicators::option::BarWidth{50},
indicators::option::ForegroundColor{indicators::Color::white},
indicators::option::ShowElapsedTime{true},
indicators::option::ShowRemainingTime{true},
indicators::option::PrefixText{"Thread #" + std::to_string(i)}
};
progress_bars.push_back(*bar_memory[i]);
int32_t start;
int32_t end;
// Divide work among cores
start = image_height/ncores * i;
end = image_height/ncores * (i+1);
// Make sure we complete the whole picture even if the work is not perfectly divisible
if (i == ncores)
end = image_height;
args[i].start = start;
args[i].end = end;
args[i].thread_id = i;
// TODO: Check for errors
pthread_create(&threads[i], NULL, raytrace_lines, &args[i]);
}
for (int32_t i = 0; i < ncores; ++i)
{
switch (pthread_join(threads[i], NULL))
{
case EDEADLK:
fprintf(stderr, "A deadlock was detected (e.g., two threads tried to join with each other); or thread specifies the calling thread.\n");
break;
case EINVAL:
fprintf(stderr, "thread is not a joinable thread OR\n"
"Another thread is already waiting to join with this thread.\n");
break;
case ESRCH:
fprintf(stderr, "No thread with the ID thread could be found.\n");
break;
default:
break;
}
}
write_image(image, image_width*image_height, output_file_handle, samples_per_pixel);
/* Obsolete non-threaded implementation */
// for (int32_t j = image_height - 1; j >= 0; --j)
// {
// rmt_ScopedCPUSample(OuterLoop, RMTSF_Aggregate);
// fprintf(stderr, "\rScanlines remaining: %d ", j);
// print_timers();
// fflush(stderr);
// for (int32_t i = 0; i < image_width; ++i)
// {
// rmt_ScopedCPUSample(InnerLoop, RMTSF_Aggregate);
// color pixel_color = color(0,0,0);
// for (int32_t s = 0; s < samples_per_pixel; ++s)
// {
// float u = ((i + random_float()) / (image_width-1));
// float v = ((j + random_float()) / (image_height-1));
// ray r = cam.get_ray(u,v);
// pixel_color += ray_color(r, world, max_depth);
// }
// write_color(output_file_handle, pixel_color, samples_per_pixel);
// }
// }
fprintf(stderr, "\nDone\n");
rmt_DestroyGlobalInstance(rmt);
free(image);
fclose(output_file_handle);
//indicators::show_console_cursor(true);
}
void *raytrace_lines(void *arg)
{
thread_args arguments = *((thread_args*)arg);
int32_t start = arguments.start;
int32_t end = arguments.end;
int32_t thread_id = arguments.thread_id;
for (int32_t j = end - 1; j >= start; --j)
{
int32_t lines_expected = end-start;
int32_t lines_completed = end-j;
progress_bars[thread_id].set_option(indicators::option::PostfixText{std::to_string(lines_completed) + "/" + std::to_string(lines_expected)});
progress_bars[thread_id].set_progress(((float)lines_completed/lines_expected)*100);
rmt_ScopedCPUSample(OuterLoop, RMTSF_Aggregate);
for (int32_t i = 0; i < image_width; ++i) for (int32_t i = 0; i < image_width; ++i)
{ {
color pixel_color = color(0,0,0); color pixel_color = color(0,0,0);
for (int32_t s = 0; s < samples_per_pixel; ++s) for (int32_t s = 0; s < samples_per_pixel; ++s)
{ {
double u = ((i + random_double()) / (image_width-1)); float u = ((i + random_float(thread_id)) / (image_width-1));
double v = ((j + random_double()) / (image_height-1)); float v = ((j + random_float(thread_id)) / (image_height-1));
ray r = cam.get_ray(u,v); ray r = global_camera->get_ray(u,v, thread_id);
pixel_color += ray_color(r, world, max_depth); pixel_color += ray_color(r, world, max_depth, thread_id);
}
int32_t index = j * image_width + i;
image[index] = pixel_color;
}
}
return nullptr;
} }
write_color(stdout, pixel_color, samples_per_pixel); #ifdef DEBUG
debug_record debug_record_array[__COUNTER__];
void print_timers_()
{
for (uint32_t i = 0;
i < sizeof(debug_record_array) / sizeof(debug_record_array[0]);
++i)
{
debug_record *record = &debug_record_array[i];
fprintf(stderr,
"%d: %s:%s:%d; "
"Cycles = %ld; "
"Hit count %ld; "
"Cycles/hit %f; "
"Time %f",
i, record->filename, record->function_name, record->line_number,
record->cycles,
record->hit_count,
(double)record->cycles / record->hit_count,
(double)record->cycles / CLOCKS_PER_SEC);
} }
} }
fprintf(stderr, "\nDone\n");
} #endif

View file

@ -4,7 +4,7 @@
#include "rtweekend.hpp" #include "rtweekend.hpp"
struct material { struct material {
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0; virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, int32_t thread_id = 0) const = 0;
}; };
struct lambertian : material { struct lambertian : material {
@ -15,9 +15,9 @@ struct lambertian : material {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-parameter"
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, int32_t thread_id = 0) const override
{ {
vec3 scatter_direction = rec.normal + random_unit_vector(); vec3 scatter_direction = rec.normal + random_unit_vector(thread_id);
/* NOTE: it is possible that the random vector we generate is exactly opposite to the normal vector, /* NOTE: it is possible that the random vector we generate is exactly opposite to the normal vector,
in which case it will sum to a near-zero scatter vector and generate degenerate results. in which case it will sum to a near-zero scatter vector and generate degenerate results.
@ -37,18 +37,18 @@ struct lambertian : material {
struct metal : material { struct metal : material {
/* Attributes */ /* Attributes */
color albedo; color albedo;
double fuzz; float fuzz;
// Constructor // Constructor
metal(const color& c, double f) metal(const color& c, float f)
{ {
albedo = c; albedo = c;
fuzz = f; fuzz = f;
}; };
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, int32_t thread_id) const override
{ {
vec3 reflected = reflect(normalize(r_in.direction), rec.normal); vec3 reflected = reflect(normalize(r_in.direction), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere()); scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere(thread_id));
attenuation = albedo; attenuation = albedo;
return (dot(scattered.direction, rec.normal) > 0); return (dot(scattered.direction, rec.normal) > 0);
} }
@ -57,37 +57,37 @@ struct metal : material {
struct dielectric : material struct dielectric : material
{ {
/* Attributes */ /* Attributes */
double ri; // refraction index float ri; // refraction index
// Constructor // Constructor
dielectric(double refraction_index) { ri = refraction_index; } dielectric(float refraction_index) { ri = refraction_index; }
/* Methods */ /* Methods */
// Schlick's approximation of reflectance // Schlick's approximation of reflectance
static double reflectance(double cosine, double ref_idx) static float reflectance(float cosine, float ref_idx)
{ {
double r0 = (1-ref_idx) / (1+ref_idx); float r0 = (1-ref_idx) / (1+ref_idx);
r0 = r0*r0; r0 = r0*r0;
return r0 + (1-r0)*pow((1 - cosine), 5); return r0 + (1-r0)*pow((1 - cosine), 5);
} }
/* Virtual methods */ /* Virtual methods */
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, int32_t thread_id) const override
{ {
attenuation = color(1,1,1); attenuation = color(1,1,1);
double refraction_ratio = rec.front_face ? (1.0/ri) : ri; float refraction_ratio = rec.front_face ? (1.0/ri) : ri;
vec3 unit_direction = normalize(r_in.direction); vec3 unit_direction = normalize(r_in.direction);
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1); float cos_theta = fmin(dot(-unit_direction, rec.normal), 1);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta); float sin_theta = sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = refraction_ratio * sin_theta > 1.0; bool cannot_refract = refraction_ratio * sin_theta > 1.0;
vec3 direction; vec3 direction;
if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double()) if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_float(thread_id))
direction = reflect(unit_direction, rec.normal); direction = reflect(unit_direction, rec.normal);
else else
direction = refract(unit_direction, rec.normal, refraction_ratio); direction = refract(unit_direction, rec.normal, refraction_ratio);

15
random.h Normal file
View file

@ -0,0 +1,15 @@
// *Really* minimal PCG32 code / (c) 2014 M.E. O'Neill / pcg-random.org
// Licensed under Apache License 2.0 (NO WARRANTY, etc. see website)
typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t;
uint32_t pcg32_random_r(pcg32_random_t* rng)
{
uint64_t oldstate = rng->state;
// Advance internal state
rng->state = oldstate * 6364136223846793005ULL + (rng->inc|1);
// Calculate output function (XSH RR), uses old state for max ILP
uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
uint32_t rot = oldstate >> 59u;
return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
}

View file

@ -24,7 +24,7 @@ struct ray {
} }
// Returns position after time t // Returns position after time t
point3 at(double t) const point3 at(float t) const
{ {
return origin + t*direction; return origin + t*direction;
} }

View file

@ -3,27 +3,40 @@
#include <math.h> #include <math.h>
#include <memory> #include <memory>
#include <pthread.h>
#include <unistd.h>
#include "timer.hpp"
#include "random.h"
pcg32_random_t *pcg_table;
pcg32_random_t default_pcg = { 0x853c49e6748fea9bULL, 0xda3e39cb94b95bdbULL };
/* Utility functions */ /* Utility functions */
double degrees_to_radians(double d) inline float degrees_to_radians(float d)
{ {
return d * M_PI / 180; return d * M_PI / 180;
} }
/* Returns a double in the range [0,1) */ /* Returns a float in the range [0,1) */
inline double random_double() inline float random_float_()
{ {
return rand() * (1.0 / RAND_MAX); return rand() * (1.0 / RAND_MAX);
} }
/* Returns a double in the range [min,max) */ /* Returns a float in the range [0,1) */
inline double random_double(double min, double max) inline float random_float(int32_t thread_id = 0)
{ {
return min + (max-min) * random_double(); return pcg32_random_r(&pcg_table[thread_id]) * (1.0 / UINT32_MAX);
}
/* Returns a float in the range [min,max) */
inline float random_float(float min, float max, int32_t thread_id = 0)
{
return min + (max-min) * random_float(thread_id);
} }
/* Clamps a value between [min,max] */ /* Clamps a value between [min,max] */
inline double clamp(double v, double min, double max) inline float clamp(float v, float min, float max)
{ {
return v < min ? min : v > max ? max : v; return v < min ? min : v > max ? max : v;
} }
@ -41,7 +54,7 @@ struct hit_record {
point3 p; point3 p;
vec3 normal; vec3 normal;
std::shared_ptr<material> mat_ptr; std::shared_ptr<material> mat_ptr;
double t; float t;
bool front_face; bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) inline void set_face_normal(const ray& r, const vec3& outward_normal)

View file

@ -7,11 +7,11 @@
struct sphere : hittable { struct sphere : hittable {
/* Attributes */ /* Attributes */
point3 center; point3 center;
double radius; float radius;
std::shared_ptr<material> mat_ptr; std::shared_ptr<material> mat_ptr;
/* Contructor */ /* Contructor */
sphere(point3 c, double r, std::shared_ptr<material> m) sphere(point3 c, float r, std::shared_ptr<material> m)
{ {
center = c; center = c;
radius = r; radius = r;
@ -19,24 +19,29 @@ struct sphere : hittable {
} }
/* Virtual methods declaration */ /* Virtual methods declaration */
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override; bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const;
}; };
/* Virtual method implementations */ /* Virtual method implementations */
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const bool sphere::hit(const ray& r, float t_min, float t_max, hit_record& rec) const
{ {
vec3 oc = r.origin - center; /* NOTE: This function is called too many times (and too fast) for it to be
double a = r.direction.length_squared(); profiled in a usual way using Remotery. */
double half_b = dot(oc, r.direction);
double c = oc.length_squared() - radius*radius;
double discriminant = half_b*half_b - a*c; TIMED_BLOCK();
vec3 oc = r.origin - center;
float a = r.direction.length_squared();
float half_b = dot(oc, r.direction);
float c = oc.length_squared() - radius*radius;
float discriminant = half_b*half_b - a*c;
if (discriminant < 0) if (discriminant < 0)
return false; return false;
double sqrtd = sqrt(discriminant); float sqrtd = sqrt(discriminant);
// Find the nearest root that lies in the acceptable range // Find the nearest root that lies in the acceptable range
double root = (-half_b - sqrtd) / a; float root = (-half_b - sqrtd) / a;
if (root < t_min || t_max < root) if (root < t_min || t_max < root)
{ {
root = (-half_b + sqrtd) / a; root = (-half_b + sqrtd) / a;

45
timer.hpp Normal file
View file

@ -0,0 +1,45 @@
#include <x86intrin.h>
#include "rtweekend.hpp"
#ifdef DEBUG
#define TIMED_BLOCK__(number, ...) timed_block timed_block_##Number(__COUNTER__, __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__)
#define TIMED_BLOCK_(number, ...) TIMED_BLOCK__(number, ##__VA_ARGS__)
#define TIMED_BLOCK(...) TIMED_BLOCK_(__LINE__, ##__VA_ARGS__)
#else
#define TIMED_BLOCK(...)
#endif
#ifdef DEBUG
struct debug_record
{
uint64_t cycles;
uint64_t hit_count;
const char *filename;
const char *function_name;
uint32_t line_number;
};
extern debug_record debug_record_array[];
struct timed_block {
// TODO: Thread safety
debug_record *record;
timed_block(int counter, const char *filename, int line_number, const char *function_name, int hit_count = 1)
{
record = debug_record_array + counter;
record->filename = filename;
record->line_number = line_number;
record->function_name = function_name;
record->cycles -= __rdtsc();
record->hit_count += hit_count;
}
~timed_block()
{
record->cycles += __rdtsc();
}
};
#endif

View file

@ -5,12 +5,12 @@
struct vec3 { struct vec3 {
/* Members */ /* Members */
double x; float x;
double y; float y;
double z; float z;
// Constructor proper. Values default to 0 // Constructor proper. Values default to 0
vec3(double x = 0, double y = 0, double z = 0) vec3(float x = 0, float y = 0, float z = 0)
{ {
this->x = x; this->x = x;
this->y = y; this->y = y;
@ -35,7 +35,7 @@ struct vec3 {
} }
// Scalar multiplication // Scalar multiplication
vec3& operator*=(const double t) vec3& operator*=(const float t)
{ {
x *= t; x *= t;
y *= t; y *= t;
@ -44,7 +44,7 @@ struct vec3 {
} }
// Division by a scalar t // Division by a scalar t
vec3& operator/=(const double t) vec3& operator/=(const float t)
{ {
x /= t; x /= t;
y /= t; y /= t;
@ -54,33 +54,33 @@ struct vec3 {
/* Methods */ /* Methods */
double length() const float length() const
{ {
return sqrt(x*x + y*y + z*z); return sqrt(x*x + y*y + z*z);
} }
// Length squared, useful for some calculations // Length squared, useful for some calculations
double length_squared() const float length_squared() const
{ {
return x*x + y*y + z*z; return x*x + y*y + z*z;
} }
// Get a vec3 with random components in the range [0,1) // Get a vec3 with random components in the range [0,1)
inline static vec3 random() inline static vec3 random(int32_t thread_id = 0)
{ {
return vec3(random_double(), random_double(), random_double()); return vec3(random_float(thread_id), random_float(thread_id), random_float(thread_id));
} }
// Get a vec3 with random components in the range [min, max) // Get a vec3 with random components in the range [min, max)
inline static vec3 random(double min, double max) inline static vec3 random(float min, float max, int32_t thread_id = 0)
{ {
return vec3(random_double(min, max), random_double(min, max), random_double(min, max)); return vec3(random_float(min, max, thread_id), random_float(min, max, thread_id), random_float(min, max, thread_id));
} }
// Check if all vector components are near zero // Check if all vector components are near zero
bool near_zero() const bool near_zero() const
{ {
double s = 1e-8; float s = 1e-8;
return (fabs(x) < s) && (fabs(y) < s) && (fabs(z) < s); return (fabs(x) < s) && (fabs(y) < s) && (fabs(z) < s);
} }
}; };
@ -92,6 +92,11 @@ typedef vec3 color;
/* More overloads */ /* More overloads */
inline bool operator==(const vec3 &u, const vec3 &v)
{
return u.x == v.x && u.y == v.y && u.z == v.z;
}
// Straightforward vector sum // Straightforward vector sum
inline vec3 operator+(const vec3 &u, const vec3 &v) inline vec3 operator+(const vec3 &u, const vec3 &v)
{ {
@ -117,31 +122,33 @@ inline vec3 operator*(const vec3 &u, const vec3 &v)
} }
// Scalar product // Scalar product
inline vec3 operator*(double t,const vec3 &v) inline vec3 operator*(float t,const vec3 &v)
{ {
return vec3(t*v.x, t*v.y, t*v.z); return vec3(t*v.x, t*v.y, t*v.z);
} }
inline vec3 operator*(const vec3 &v, double t) inline vec3 operator*(const vec3 &v, float t)
{ {
return t * v; return t * v;
} }
// Vector division by scalar. Note that we redefine it as multiplying by 1/t to avoid division by 0 // Vector division by scalar. Note that we redefine it as multiplying by 1/t to avoid division by 0
inline vec3 operator/(vec3 v, double t) inline vec3 operator/(vec3 v, float t)
{ {
return 1/t * v; return 1/t * v;
} }
// Straightforward dot product // Straightforward dot product
inline double dot(const vec3 &u, const vec3 &v) inline float dot(const vec3 &u, const vec3 &v)
{ {
return u.x*v.x + u.y*v.y + u.z*v.z; return u.x*v.x + u.y*v.y + u.z*v.z;
} }
// Cross product between two vectors // Cross product between two vectors
inline vec3 cross(const vec3 &u, const vec3 &v) inline vec3 cross(const vec3 &u, const vec3 &v)
{ {
return vec3(u.y * v.z - u.z * v.y, return vec3(u.y * v.z - u.z * v.y,
u.z * v.x - u.x * v.z, u.z * v.x - u.x * v.z,
u.x * v.y - u.y * v.x); u.x * v.y - u.y * v.x);
@ -150,16 +157,17 @@ inline vec3 cross(const vec3 &u, const vec3 &v)
// Normalize vector so its length = 1 // Normalize vector so its length = 1
inline vec3 normalize(const vec3 v) inline vec3 normalize(const vec3 v)
{ {
return v / v.length(); return v / v.length();
} }
// Returns a vec3 of random components between [-1,1) that is inside a unit sphere // Returns a vec3 of random components between [-1,1) that is inside a unit sphere
vec3 random_in_unit_sphere() vec3 random_in_unit_sphere(int32_t thread_id)
{ {
// Iterate until we find a vector with length < 1 // Iterate until we find a vector with length < 1
while (true) while (true)
{ {
vec3 p = vec3::random(-1,1); vec3 p = vec3::random(-1,1, thread_id);
if (p.length_squared() >= 1) if (p.length_squared() >= 1)
continue; continue;
return p; return p;
@ -167,14 +175,14 @@ vec3 random_in_unit_sphere()
} }
// Returns a normalized version of the above vector // Returns a normalized version of the above vector
vec3 random_unit_vector() vec3 random_unit_vector(int32_t thread_id)
{ {
return normalize(random_in_unit_sphere()); return normalize(random_in_unit_sphere(thread_id));
} }
vec3 random_in_hemisphere(const vec3& normal) vec3 random_in_hemisphere(const vec3& normal, int32_t thread_id)
{ {
vec3 in_unit_sphere = random_in_unit_sphere(); vec3 in_unit_sphere = random_in_unit_sphere(thread_id);
if (dot(in_unit_sphere, normal) > 0.0) if (dot(in_unit_sphere, normal) > 0.0)
return in_unit_sphere; return in_unit_sphere;
@ -188,19 +196,19 @@ vec3 reflect(const vec3& v, const vec3 n)
return v - 2*dot(v,n)*n; return v - 2*dot(v,n)*n;
} }
vec3 refract (const vec3& uv, const vec3& n, double etai_over_etat) vec3 refract(const vec3& uv, const vec3& n, float etai_over_etat)
{ {
double cos_theta = fmin(dot(-uv, n), 1.0); float cos_theta = fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n); vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n);
vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n; vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel; return r_out_perp + r_out_parallel;
} }
vec3 random_in_unit_disk() vec3 random_in_unit_disk(int32_t thread_id)
{ {
while (true) while (true)
{ {
auto p = vec3(random_double(-1,1), random_double(-1,1), 0); auto p = vec3(random_float(-1,1,thread_id), random_float(-1,1,thread_id), 0);
if (p.length_squared() >= 1) continue; if (p.length_squared() >= 1) continue;
return p; return p;
} }

218
vis/Code/Console.js 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;
})();

View file

@ -0,0 +1,52 @@
//
// 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.GetUInt32 = function ()
{
var v = this.DataView.getUint32(this.Offset, true);
this.Offset += 4;
return v;
}
DataViewReader.prototype.GetUInt64 = 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;
})();

53
vis/Code/NameMap.js 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;
}
}

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.usStart", this.Start_us);
glSetUniform(gl, program, "inTimeRange.usPerPixel", this.usPerPixel);
}
}

540
vis/Code/Remotery.js Normal file
View file

@ -0,0 +1,540 @@
//
// 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));
// 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.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this));
this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null);
this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.TraceDrop = new TraceDrop(this);
this.NbSampleWindows = 0;
this.SampleWindows = { };
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.SelectedFrames = { };
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this));
this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this));
this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this));
this.Server.AddMessageHandler("THRN", Bind(OnThreadNames, this));
// Kick-off the auto-connect loop
AutoConnect(this);
// Hook up resize event handler
DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this));
OnResizeWindow(this);
// Hook up browser-native canvas refresh
this.DisplayFrame = 0;
this.LastKnownPause = this.Settings.IsPaused;
var self = this;
(function display_loop()
{
window.requestAnimationFrame(display_loop);
DrawTimeline(self);
})();
}
Remotery.prototype.Clear = function()
{
// Clear timelines
this.SampleTimelineWindow.Clear();
this.ProcessorTimelineWindow.Clear();
// Close and clear all sample windows
for (var i in this.SampleWindows)
{
var sample_window = this.SampleWindows[i];
sample_window.Close();
}
this.NbSampleWindows = 0;
this.SampleWindows = { };
// Clear runtime data
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.SelectedFrames = { };
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
// Resize everything to fit new layout
OnResizeWindow(this);
}
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.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;
}
function DrawTimeline(self)
{
// Has pause state changed?
if (self.Settings.IsPaused != self.LastKnownPaused)
{
// When switching TO paused, draw one last frame to ensure the sample text gets drawn
self.LastKnownPaused = self.Settings.IsPaused;
self.SampleTimelineWindow.DrawAllRows();
self.ProcessorTimelineWindow.DrawAllRows();
return;
}
// Don't waste time drawing the timeline when paused
if (self.Settings.IsPaused)
return;
// requestAnimationFrame can run up to 60hz which is way too much for drawing the timeline
// Assume it's running at 60hz and skip frames to achieve 10hz instead
// Doing this instead of using setTimeout because it's better for browser rendering (or; will be once WebGL is in use)
// TODO: Expose as config variable because high refresh rate is great when using a separate viewiing machine
if ((self.DisplayFrame % 10) == 0)
{
self.SampleTimelineWindow.DrawAllRows();
self.ProcessorTimelineWindow.DrawAllRows();
}
self.DisplayFrame++;
}
function DecodeSample(self, data_view_reader)
{
var sample = {};
// Get name hash and lookup name it map
sample.name_hash = data_view_reader.GetUInt32();
let [ name_exists, name ] = self.sampleNames.Get(sample.name_hash);
sample.name = name;
// 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" + sample.name_hash);
}
}
// Get the rest of the sample data
sample.id = data_view_reader.GetUInt32();
sample.colour = data_view_reader.GetStringOfLength(7);
sample.us_start = data_view_reader.GetUInt64();
sample.us_length = data_view_reader.GetUInt64();
sample.us_self = data_view_reader.GetUInt64();
sample.call_count = data_view_reader.GetUInt32();
sample.recurse_depth = data_view_reader.GetUInt32();
// TODO(don): Get the profiler to pass these directly instead of hex colour
const colour = parseInt(sample.colour.slice(1), 16);
const r = (colour >> 16) & 255;
const g = (colour >> 8) & 255;
const b = colour & 255;
sample.rgbColour = [ r, g, b ];
// Calculate dependent properties
sample.ms_length = (sample.us_length / 1000.0).toFixed(3);
sample.ms_self = (sample.us_self / 1000.0).toFixed(3);
// Recurse into children
sample.children = [];
DecodeSampleArray(self, data_view_reader, sample.children);
return sample;
}
function DecodeSampleArray(self, data_view_reader, samples)
{
var nb_samples = data_view_reader.GetUInt32();
for (var i = 0; i < nb_samples; i++)
{
var sample = DecodeSample(self, data_view_reader);
samples.push(sample)
}
}
function DecodeSamples(self, data_view_reader)
{
// Message-specific header
let message = { };
message.sample_tree_bytes = data_view_reader.GetUInt32();
message.thread_name = data_view_reader.GetString();
message.nb_samples = data_view_reader.GetUInt32();
message.sample_digest = data_view_reader.GetUInt32();
message.partial_tree = data_view_reader.GetUInt32();
// Read samples
message.samples = [];
message.samples.push(DecodeSample(self, data_view_reader));
return message;
}
function OnSamples(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;
// Binary decode incoming sample data
var message = DecodeSamples(self, data_view_reader);
var name = message.thread_name;
// 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.SampleWindows))
{
self.SampleWindows[name] = new SampleWindow(self.WindowManager, name, self.NbSampleWindows);
self.SampleWindows[name].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
self.NbSampleWindows++;
MoveSampleWindows(this);
}
// 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.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples);
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.sampleNames.Set(name_hash, name_string);
}
function OnProcessorThreads(self, socket, data_view_reader)
{
let nb_processors = data_view_reader.GetUInt32();
let message_index = data_view_reader.GetUInt64();
// 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
let us_length = sample_time - last_thread_frame.usLastStart;
last_thread_frame.usLastStart = sample_time;
last_thread_frame.EndTime_us += us_length;
last_thread_frame.Samples[0].us_length += us_length;
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 [ _, thread_name ] = self.threadNames.Get(thread_name_hash);
// Make a pastel-y colour from the thread name hash
let hash = thread_name.hash;
let r = 127 + (hash & 255) / 2;
let g = 127 + ((hash >> 4) & 255) / 2;
let b = 127 + ((hash >> 8) & 255) / 2;
// 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 = {
nb_samples: 1,
sample_digest: 0,
samples : [
{
name_hash: thread_name_hash,
name: thread_name,
id: thread_id,
colour: "#FFFFFF",
us_start: sample_time,
us_length: 250,
rgbColour: [ r, g, b ],
children: []
}
]
};
// 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 OnThreadNames(self, socket, data_view_reader)
{
let name_hash = data_view_reader.GetUInt32();
let name_length = data_view_reader.GetUInt32();
let name_string = data_view_reader.GetStringOfLength(name_length);
self.threadNames.Set(name_hash, name_string);
}
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.SampleWindows[name].SetVisible(target.checked);
MoveSampleWindows(self);
}
function MoveSampleWindows(self)
{
// Stack all windows next to each other
var xpos = 0;
for (var i in self.SampleWindows)
{
var sample_window = self.SampleWindows[i];
if (sample_window.Visible)
{
sample_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window);
}
}
}
function OnSampleHover(self, thread_name, hover)
{
if (!self.Settings.IsPaused)
{
return;
}
for (let window_thread_name in self.SampleWindows)
{
let sample_window = self.SampleWindows[window_thread_name];
if (window_thread_name == thread_name && hover != null)
{
// Populate with sample under hover
let frame = hover[0];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
else
{
// When there's no hover, go back to the selected frame
if (self.SelectedFrames[window_thread_name])
{
const frame = self.SelectedFrames[window_thread_name];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
}
}
}
function OnSampleSelected(self, thread_name, select)
{
// Lookup sample window set the frame samples on it
if (select && thread_name in self.SampleWindows)
{
var sample_window = self.SampleWindows[thread_name];
var frame = select[0];
self.SelectedFrames[thread_name] = frame;
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
}
function OnResizeWindow(self)
{
// Resize windows
var w = window.innerWidth;
var h = window.innerHeight;
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.SampleWindows)
{
self.SampleWindows[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);
other_timeline.DrawAllRows();
}
}
return Remotery;
})();

221
vis/Code/SampleWindow.js Normal file
View file

@ -0,0 +1,221 @@
SampleWindow = (function()
{
function SampleWindow(wm, name, offset)
{
// Sample digest for checking if grid needs to be repopulated
this.NbSamples = 0;
this.SampleDigest = null;
// Source sample reference to reduce repopulation
this.Samples = null;
this.XPos = 10 + offset * 410;
this.Window = wm.AddWindow(name, 100, 100, 100, 100);
this.Window.ShowNoAnim();
this.Visible = true;
// Create a grid that's indexed by the unique sample ID
this.Grid = this.Window.AddControlNew(new WM.Grid());
var cell_data =
{
Name: "Samples",
Length: "Time (ms)",
Self: "Self (ms)",
Calls: "Calls",
Recurse: "Recurse",
};
var cell_classes =
{
Name: "SampleTitleNameCell",
Length: "SampleTitleTimeCell",
Self: "SampleTitleTimeCell",
Calls: "SampleTitleCountCell",
Recurse: "SampleTitleCountCell",
};
this.RootRow = this.Grid.Rows.Add(cell_data, "GridGroup", cell_classes);
this.RootRow.Rows.AddIndex("_ID");
}
SampleWindow.prototype.Close = function()
{
this.Window.Close();
}
SampleWindow.prototype.SetXPos = function(xpos, top_window, bottom_window)
{
Anim.Animate(
Bind(AnimatedMove, this, top_window, bottom_window),
this.XPos, 10 + xpos * 410, 0.25);
}
function AnimatedMove(self, top_window, bottom_window, val)
{
self.XPos = val;
self.WindowResized(top_window, bottom_window);
}
SampleWindow.prototype.SetVisible = function(visible)
{
if (visible != this.Visible)
{
if (visible == true)
this.Window.ShowNoAnim();
else
this.Window.HideNoAnim();
this.Visible = visible;
}
}
SampleWindow.prototype.WindowResized = function(top_window, bottom_window)
{
var 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);
}
SampleWindow.prototype.OnSamples = function(nb_samples, sample_digest, samples)
{
if (!this.Visible)
return;
// If the source hasn't changed, don't repopulate
if (this.Samples == samples)
return;
this.Samples = samples;
// Recreate all the HTML if the number of samples gets bigger
if (nb_samples > this.NbSamples)
{
GrowGrid(this.RootRow, nb_samples);
this.NbSamples = nb_samples;
}
// If the content of the samples changes from previous update, update them all
if (this.SampleDigest != sample_digest)
{
this.RootRow.Rows.ClearIndex("_ID");
var index = UpdateAllSampleFields(this.RootRow, samples, 0, "");
this.SampleDigest = sample_digest;
// Clear out any left-over rows
for (var i = index; i < this.RootRow.Rows.Rows.length; i++)
{
var row = this.RootRow.Rows.Rows[i];
DOM.Node.Hide(row.Node);
}
}
else if (this.Visible)
{
// Otherwise just update the existing sample fields
UpdateChangedSampleFields(this.RootRow, samples, "");
}
}
function GrowGrid(parent_row, nb_samples)
{
parent_row.Rows.Clear();
for (var i = 0; i < nb_samples; i++)
{
var cell_data =
{
_ID: i,
Name: "",
Length: "",
Self: "",
Calls: "",
Recurse: "",
};
var cell_classes =
{
Name: "SampleNameCell",
Length: "SampleTimeCell",
Self: "SampleTimeCell",
Calls: "SampleCountCell",
Recurse: "SampleCountCell",
};
parent_row.Rows.Add(cell_data, null, cell_classes);
}
}
function UpdateAllSampleFields(parent_row, samples, index, indent)
{
for (var i in samples)
{
var sample = samples[i];
// Match row allocation in GrowGrid
var row = parent_row.Rows.Rows[index++];
// Sample row may have been hidden previously
DOM.Node.Show(row.Node);
// Assign unique ID so that the common fast path of updating sample times only
// can lookup target samples in the grid
row.CellData._ID = sample.id;
parent_row.Rows.AddRowToIndex("_ID", sample.id, row);
// Record sample name for later comparison
row.CellData.Name = sample.name.string;
// Set sample name and colour
var name_node = row.CellNodes["Name"];
name_node.innerHTML = indent + sample.name.string;
DOM.Node.SetColour(name_node, sample.colour);
row.CellNodes["Length"].innerHTML = sample.ms_length;
row.CellNodes["Self"].innerHTML = sample.ms_self;
row.CellNodes["Calls"].innerHTML = sample.call_count;
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
index = UpdateAllSampleFields(parent_row, sample.children, index, indent + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
}
return index;
}
function UpdateChangedSampleFields(parent_row, samples, indent)
{
for (var i in samples)
{
var sample = samples[i];
var row = parent_row.Rows.GetBy("_ID", sample.id);
if (row)
{
row.CellNodes["Length"].innerHTML = sample.ms_length;
row.CellNodes["Self"].innerHTML = sample.ms_self;
row.CellNodes["Calls"].innerHTML = sample.call_count;
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
// Sample name will change when it switches from hash ID to network-retrieved
// name. Quickly check that before re-applying the HTML for the name.
if (row.CellData.Name != sample.name.string)
{
var name_node = row.CellNodes["Name"];
row.CellData.Name = sample.name.string;
name_node.innerHTML = indent + sample.name.string;
}
}
UpdateChangedSampleFields(parent_row, sample.children, indent + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
}
}
return SampleWindow;
})();

275
vis/Code/Shaders.js Normal file
View file

@ -0,0 +1,275 @@
const TimelineVShader =`#version 300 es
#define CANVAS_BORDER 1.0
#define SAMPLE_HEIGHT 16.0
#define SAMPLE_BORDER 1.0
#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0)
#define SAMPLE_Y_OFFSET (CANVAS_BORDER + 1.0)
struct Viewport
{
float width;
float height;
};
struct TimeRange
{
float usStart;
float usPerPixel;
};
struct Row
{
float yOffset;
};
uniform Viewport inViewport;
uniform TimeRange inTimeRange;
uniform Row inRow;
in vec4 inSample_TextOffset;
in vec4 inColour_TextLength;
out vec4 varColour_TimeMs;
out vec4 varPosInBoxPx_TextEntry;
out float varTimeChars;
//#define PIXEL_ROUNDED_OFFSETS
float PixelOffset(float time_us)
{
float offset = (time_us - inTimeRange.usStart) * inTimeRange.usPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(offset);
#else
return offset;
#endif
}
float PixelSize(float time_us)
{
float size = time_us * inTimeRange.usPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(size);
#else
return size;
#endif
}
void main()
{
// Unpack input data
float us_start = inSample_TextOffset.x;
float us_length = inSample_TextOffset.y;
float depth = inSample_TextOffset.z;
float text_buffer_offset = inSample_TextOffset.w;
vec3 box_colour = inColour_TextLength.rgb;
float text_length_chars = inColour_TextLength.w;
// Determine pixel range of the sample
float x0 = PixelOffset(us_start);
float x1 = x0 + PixelSize(us_length);
// Calculate box to render
float offset_x = x0;
float offset_y = inRow.yOffset + SAMPLE_Y_OFFSET + (depth - 1.0) * SAMPLE_Y_SPACING;
float size_x = max(x1 - x0, 1.0);
float size_y = SAMPLE_HEIGHT;
// Box range
float min_x = offset_x;
float min_y = offset_y;
float max_x = offset_x + size_x;
float max_y = offset_y + size_y;
// Quad indices are:
//
// 2 3
// +----+
// | |
// +----+
// 0 1
//
vec2 position;
position.x = (gl_VertexID & 1) == 0 ? min_x : max_x;
position.y = (gl_VertexID & 2) == 0 ? min_y : max_y;
//
// NDC is:
// -1 to 1, left to right
// -1 to 1, bottom to top
//
vec4 ndc_pos;
ndc_pos.x = (position.x / inViewport.width) * 2.0 - 1.0;
ndc_pos.y = 1.0 - (position.y / inViewport.height) * 2.0;
ndc_pos.z = 0.0;
ndc_pos.w = 1.0;
// Calculate number of characters required to display the millisecond time
float time_ms = us_length / 1000.0;
float time_ms_int = floor(time_ms);
float time_chars = time_ms_int == 0.0 ? 1.0 : floor(log(time_ms_int) / 2.302585092994046) + 1.0;
gl_Position = ndc_pos;
varColour_TimeMs = vec4(box_colour / 255.0, time_ms);
varPosInBoxPx_TextEntry = vec4(position.x - offset_x, position.y - offset_y, text_buffer_offset, text_length_chars);
varTimeChars = time_chars;
}
`;
const TimelineFShader = `#version 300 es
precision mediump float;
#define SAMPLE_HEIGHT 16.0
struct TextBufferDesc
{
float fontWidth;
float fontHeight;
float textBufferLength;
};
uniform sampler2D inFontAtlasTextre;
uniform sampler2D inTextBuffer;
uniform TextBufferDesc inTextBufferDesc;
in vec4 varColour_TimeMs;
in vec4 varPosInBoxPx_TextEntry;
in float varTimeChars;
out vec4 outColour;
vec4 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;
// Apply colour to the text in premultiplied alpha space
vec4 t = texture(inFontAtlasTextre, uv);
vec3 colour = vec3(1.0, 1.0, 1.0) * 0.25;
return vec4(colour * t.a, t.a);
}
void main()
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
// 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_offset_px = vec2(4.0, 3.0);
vec4 box_colour = vec4(varColour_TimeMs.rgb, 1.0);
// 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.rgb *= (0.8 + both * 0.1);
vec4 text_colour = vec4(0.0);
float text_end_px = text_length_px + text_offset_px.x + font_width_px;
float time_length_px = (varTimeChars + 4.0) * font_width_px;
if (pos_in_box_px.x > text_end_px && pos_in_box_px.x < text_end_px + time_length_px)
{
float time_ms = varColour_TimeMs.w;
vec2 time_pixel_pos;
time_pixel_pos.x = max(min(pos_in_box_px.x - text_end_px, time_length_px), 0.0);
time_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0);
float time_index = floor(time_pixel_pos.x / font_width_px);
if (time_index < varTimeChars)
{
// Use base-10 integer digit counting to calculate the divisor needed to bring this digit below 10
float time_divisor = 1.0;
for (int i = 0; i < int(varTimeChars - time_index - 1.0); i++)
{
time_divisor *= 10.0;
}
// Calculate digit
float time_shifted_int = floor(time_ms / time_divisor);
float time_digit = floor(mod(time_shifted_int, 10.0));
text_colour = LookupCharacter(48.0 + time_digit,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars)
{
text_colour = LookupCharacter(46.0,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 1.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(48.0 + time_digit,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 2.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(109.0,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 3.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(115.0, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px);
}
}
else
{
// 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(pos_in_box_px.x - text_offset_px.x, text_length_px), -1.0);
text_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_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
vec2 char_uv = vec2((text_index + 0.5) / text_buffer_length, 0.5);
float char_ascii = texture(inTextBuffer, char_uv).a * 255.0;
text_colour = 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);
}
// Bring out of premultiplied alpha space and lerp with the box colour
float inv_alpha = text_colour.a == 0.0 ? 1.0 : 1.0 / text_colour.a;
outColour = mix(box_colour, vec4(text_colour.rgb * inv_alpha, 1.0), text_colour.a);
}
`;

29
vis/Code/ThreadFrame.js Normal file
View file

@ -0,0 +1,29 @@
ThreadFrame = (function()
{
function ThreadFrame(message)
{
// Persist the required message data
this.NbSamples = message.nb_samples;
this.SampleDigest = message.sample_digest;
this.Samples = message.samples;
this.PartialTree = message.partial_tree > 0 ? true : false;
// Calculate the frame start/end times
this.StartTime_us = 0;
this.EndTime_us = 0;
var nb_root_samples = this.Samples.length;
if (nb_root_samples > 0)
{
var last_sample = this.Samples[nb_root_samples - 1];
this.StartTime_us = this.Samples[0].us_start;
this.EndTime_us = last_sample.us_start + last_sample.us_length;
}
this.Length_us = this.EndTime_us - this.StartTime_us;
}
return ThreadFrame;
})();

186
vis/Code/TimelineMarkers.js 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;
}
}

389
vis/Code/TimelineRow.js Normal file
View file

@ -0,0 +1,389 @@
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 CANVAS_BORDER = 1;
var SAMPLE_HEIGHT = 16;
var SAMPLE_BORDER = 1;
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, gl.FLOAT, 4, 8);
this.colourBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8);
// Create a vertex array for these buffers
this.vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this.vertexArrayObject);
this.sampleBuffer.BindAsInstanceAttribute(timeline.Program, "inSample_TextOffset");
this.colourBuffer.BindAsInstanceAttribute(timeline.Program, "inColour_TextLength");
// 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 = CANVAS_BORDER + SAMPLE_BORDER + 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(sample, depth, colour, y_scroll_offset)
{
if (depth <= this.Depth)
{
// Determine pixel range of the sample
var x0 = this.VisibleTimeRange.PixelOffset(sample.us_start);
var x1 = x0 + this.VisibleTimeRange.PixelSize(sample.us_length);
var offset_x = x0;
var offset_y = this.LabelContainerNode.offsetTop + 2 + (depth - 1) * SAMPLE_Y_SPACING + y_scroll_offset;
var size_x = x1 - x0;
var size_y = SAMPLE_HEIGHT;
// Normal rendering
var ctx = this.timeline.drawContext;
ctx.lineWidth = 2;
ctx.strokeStyle = colour;
ctx.strokeRect(offset_x + 2.5, offset_y - 0.5, size_x - 3, size_y + 1);
}
}
TimelineRow.prototype.DisplayHeight = function()
{
return this.LabelContainerNode.clientHeight;
}
TimelineRow.prototype.YOffset = function()
{
return this.LabelContainerNode.offsetTop;
}
TimelineRow.prototype.DrawBackground = function(hover_sample_info, y_scroll_offset)
{
// Fill box that shows the boundary between thread rows
this.timeline.drawContext.fillStyle = "#444"
var b = CANVAS_BORDER;
this.timeline.drawContext.fillRect(b, this.YOffset() + y_scroll_offset + b, this.timeline.drawCanvas.width - b * 2, this.DisplayHeight() - b * 2);
// Draw the selected sample for this row
if (this.SelectSampleInfo != null)
{
const sample = this.SelectSampleInfo[1];
const depth = this.SelectSampleInfo[2];
this.DrawSampleHighlight(sample, depth, "#FF0000", y_scroll_offset);
}
// Draw the current hover sample if it's over this row
if (hover_sample_info != null && hover_sample_info[3] == this)
{
const sample = hover_sample_info[1];
const depth = hover_sample_info[2];
const thread_row = hover_sample_info[3];
this.DrawSampleHighlight(sample, depth, "#FFFFFF", y_scroll_offset);
}
}
TimelineRow.prototype.Draw = function(gl, draw_text, y_scroll_offset)
{
let samples_per_depth = [];
// Gather all root samples in the visible frame set
for (var i in this.VisibleFrames)
{
var frame = this.VisibleFrames[i];
GatherSamples(this, frame.Samples, 1, draw_text, 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
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(this.timeline.Program, "inSample_TextOffset");
this.colourBuffer.BindAsInstanceAttribute(this.timeline.Program, "inColour_TextLength");
}
// CPU write destination for samples
let cpu_samples = this.sampleBuffer.cpuArray;
let cpu_colours = this.colourBuffer.cpuArray;
let sample_pos = 0;
const empty_text_entry = {
offset: 0,
length: 1,
};
// 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 sample of samples_this_depth)
{
const text_entry = sample.name.textEntry != null ? sample.name.textEntry : empty_text_entry;
cpu_samples[sample_pos + 0] = sample.us_start;
cpu_samples[sample_pos + 1] = sample.us_length;
cpu_samples[sample_pos + 2] = depth;
cpu_samples[sample_pos + 3] = text_entry.offset;
cpu_colours[sample_pos + 0] = sample.rgbColour[0];
cpu_colours[sample_pos + 1] = sample.rgbColour[1];
cpu_colours[sample_pos + 2] = sample.rgbColour[2];
cpu_colours[sample_pos + 3] = text_entry.length;
sample_pos += 4;
}
}
// Upload to GPU
this.sampleBuffer.UploadData();
this.colourBuffer.UploadData();
this.timeline.textBuffer.UploadData();
// Set row parameters
glSetUniform(gl, this.timeline.Program, "inRow.yOffset", this.YOffset() + y_scroll_offset);
gl.bindVertexArray(this.vertexArrayObject);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, nb_samples);
}
function GatherSamples(self, samples, depth, draw_text, samples_per_depth)
{
// 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];
for (var i in samples)
{
var sample = samples[i];
samples_this_depth.push(sample);
if (depth < self.Depth && sample.children != null)
GatherSamples(self, sample.children, depth + 1, draw_text, samples_per_depth);
}
}
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();
this.timeline.DrawAllRows();
}
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.ScrollVertically(0);
this.timeline.DrawAllRows();
}
}
TimelineRow.prototype.GetSampleAtPosition = function(time_us, mouse_y)
{
// Calculate depth of the mouse cursor
var depth = Math.min(Math.floor(mouse_y / SAMPLE_Y_SPACING) + 1, this.Depth);
// Search for the first frame to intersect this time
for (var 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.
var frame = this.VisibleFrames[i];
if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us)
{
var found_sample = FindSample(this, frame.Samples, time_us, depth, 1);
if (found_sample != null)
return [ frame, found_sample[0], found_sample[1], this ];
}
}
return null;
}
function FindSample(self, samples, time_us, target_depth, depth)
{
for (var i in samples)
{
var sample = samples[i];
if (depth == target_depth)
{
if (time_us >= sample.us_start && time_us < sample.us_start + sample.us_length)
return [ sample, depth ];
}
else if (depth < target_depth && sample.children != null)
{
var found_sample = FindSample(self, sample.children, time_us, target_depth, depth + 1);
if (found_sample != null)
return found_sample;
}
}
return null;
}
return TimelineRow;
})();

494
vis/Code/TimelineWindow.js Normal file
View file

@ -0,0 +1,494 @@
// TODO(don): Separate all knowledge of threads from this timeline
TimelineWindow = (function()
{
var BORDER = 10;
function TimelineWindow(wm, name, settings, check_handler)
{
this.Settings = settings;
// Create timeline window
this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
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");
var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this));
// Setup timeline manipulation
this.MouseDown = false;
this.LastMouseState = null;
this.TimelineMoved = false;
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseleave", Bind(OnMouseLeave, this));
// Create a canvas for timeline 2D rendering
// TODO(don): Port this to shaders
this.drawCanvas = document.createElement("canvas");
this.drawCanvas.width = this.TimelineContainer.Node.clientWidth;
this.drawCanvas.height = this.TimelineContainer.Node.clientHeight;
this.TimelineContainer.Node.appendChild(this.drawCanvas);
this.drawContext = this.drawCanvas.getContext("2d");
// Create a canvas for timeline 3D accelerated rendering
this.glCanvas = document.createElement("canvas");
this.glCanvas.width = this.TimelineContainer.Node.clientWidth;
this.glCanvas.height = this.TimelineContainer.Node.clientHeight;
this.TimelineContainer.Node.appendChild(this.glCanvas);
// OVERLAY - add to CSS
this.glCanvas.style.position = "absolute";
this.glCanvas.style.top = 0;
this.glCanvas.style.left = 0;
// For now a gl context per timeline
let gl = this.glCanvas.getContext("webgl2");
this.gl = gl;
const vshader = glCompileShader(gl, gl.VERTEX_SHADER, "TimelineVShader", TimelineVShader);
const fshader = glCompileShader(gl, gl.FRAGMENT_SHADER, "TimelineFShader", TimelineFShader);
this.Program = glCreateProgram(gl, vshader, fshader);
this.font = new glFont(gl);
this.textBuffer = new glTextBuffer(gl, this.font);
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;
}
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
// As multiple threads come through here with different end frames, only do this for the latest
var last_frame = frame_history[frame_history.length - 1];
if (last_frame.EndTime_us > this.TimeRange.End_us)
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 and re-sort
if (thread_index == -1)
{
var row = new TimelineRow(this.gl, thread_name, this, frame_history, this.CheckHandler);
this.ThreadRows.push(row);
}
}
TimelineWindow.prototype.DrawBackground = function()
{
// TODO(don): Port all this lot to shader, maybe... it's not performance sensitive
this.drawContext.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
// Draw thread row backgrounds
for (let thread_row of this.ThreadRows)
{
thread_row.DrawBackground(this.HoverSampleInfo, this.yScrollOffset);
}
this.timelineMarkers.Draw(this.TimeRange);
}
TimelineWindow.prototype.DrawAllRows = function()
{
let gl = this.gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(this.Program);
// Set viewport parameters
glSetUniform(gl, this.Program, "inViewport.width", gl.canvas.width);
glSetUniform(gl, this.Program, "inViewport.height", gl.canvas.height);
// Set time range parameters
const time_range = this.TimeRange;
time_range.SetAsUniform(gl, this.Program);
// Set text rendering resources
// Note it might not be loaded yet so we need the null check
if (this.font.atlasTexture != null)
{
glSetUniform(gl, this.Program, "inFontAtlasTextre", this.font.atlasTexture, 0);
this.textBuffer.SetAsUniform(gl, this.Program, "inTextBuffer", 1);
}
const draw_text = this.Settings.IsPaused;
for (let i in this.ThreadRows)
{
var thread_row = this.ThreadRows[i];
thread_row.SetVisibleFrames(time_range);
thread_row.Draw(gl, draw_text, this.yScrollOffset);
}
// Render last so that each thread row uses any new time ranges
this.DrawBackground();
}
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);
// Match canvas size to container
const width = self.TimelineContainer.Node.clientWidth;
const height = self.TimelineContainer.Node.clientHeight;
self.drawCanvas.width = width;
self.drawCanvas.height = height;
self.glCanvas.width = width;
self.glCanvas.height = height;
// Adjust time range to new width
self.TimeRange.SetPixelSpan(width);
self.DrawAllRows();
}
function OnMouseScroll(self, evt)
{
let mouse_state = new Mouse.State(evt);
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);
self.DrawAllRows();
if (self.OnMovedHandler)
{
self.OnMovedHandler(self);
}
// Prevent vertical scrolling on mouse-wheel
DOM.Event.StopDefaultAction(evt);
}
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.ScrollVertically = 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 OnMouseDown(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
self.MouseDown = true;
self.LastMouseState = new Mouse.State(evt);
self.TimelineMoved = false;
DOM.Event.StopDefaultAction(evt);
}
function OnMouseUp(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
var mouse_state = new Mouse.State(evt);
self.MouseDown = false;
if (!self.TimelineMoved)
{
// Are we hovering over a thread row?
let mouse_pos = self.TimelineMousePosition(mouse_state);
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
if (hover_thread_row != null)
{
// Are we hovering over a sample?
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
let sample_info = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
if (sample_info != null)
{
// Redraw with new select sample
hover_thread_row.SetSelectSample(sample_info);
self.DrawBackground();
// Call any selection handlers
if (self.OnSelectedHandler)
{
self.OnSelectedHandler(hover_thread_row.Name, sample_info);
}
}
}
}
}
function OnMouseMove(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
var mouse_state = new Mouse.State(evt);
if (self.MouseDown)
{
let movement = false;
// Shift the visible time range with mouse movement
let time_offset_us = (mouse_state.Position[0] - self.LastMouseState.Position[0]) / self.TimeRange.usPerPixel;
if (time_offset_us != 0)
{
self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);
movement = true;
}
// Control vertical movement
let y_offset_px = mouse_state.Position[1] - self.LastMouseState.Position[1];
if (y_offset_px != 0)
{
self.ScrollVertically(y_offset_px);
movement = true;
}
// Redraw everything if there is movement
if (movement)
{
self.DrawAllRows();
self.TimelineMoved = true;
if (self.OnMovedHandler)
{
self.OnMovedHandler(self);
}
}
}
else
{
// Are we hovering over a thread row?
let mouse_pos = self.TimelineMousePosition(mouse_state);
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
if (hover_thread_row != null)
{
// Are we hovering over a sample?
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
self.HoverSampleInfo = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
// Tell listeners which sample we're hovering over
if (self.OnHoverHandler != null)
{
self.OnHoverHandler(hover_thread_row.Name, self.HoverSampleInfo);
}
}
else
{
self.HoverSampleInfo = null;
}
// Redraw to update highlights
self.DrawBackground();
}
self.LastMouseState = mouse_state;
}
function OnMouseLeave(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
// Cancel scrolling
self.MouseDown = false;
// Cancel hovering
if (self.OnHoverHandler != null)
{
self.OnHoverHandler(null, null);
}
}
return TimelineWindow;
})();

105
vis/Code/TitleWindow.js 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;
})();

134
vis/Code/TraceDrop.js Normal file
View file

@ -0,0 +1,134 @@
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())
{
remotery.Server.CallMessageHandlers(data_view_reader);
}
}
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);
}
remotery.SampleTimelineWindow.DrawAllRows();
for (let name in remotery.ProcessorFrameHistory)
{
let frame_history = remotery.ProcessorFrameHistory[name];
remotery.ProcessorTimelineWindow.OnSamples(name, frame_history);
}
remotery.ProcessorTimelineWindow.DrawAllRows();
// Set the last frame of each thread sample history on the sample windows
for (let name in remotery.SampleWindows)
{
let sample_window = remotery.SampleWindows[name];
let frame_history = remotery.FrameHistory[name];
let frame = frame_history[frame_history.length - 1];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
// Pause for viewing
remotery.TitleWindow.Pause();
};
file_reader.readAsArrayBuffer(file);
}
}

238
vis/Code/WebGL.js Normal file
View file

@ -0,0 +1,238 @@
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 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, element_type, nb_elements, nb_entries, buffer_type)
{
this.gl = gl;
this.elementType = element_type;
this.nbElements = nb_elements;
this.bufferType = buffer_type == undefined ? glDynamicBufferType.Buffer : 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:
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, 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)
{
let gl = this.gl;
this.nbEntries = nb_entries;
// 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.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);
console.log(`glDynamicBuffer: Resizing to ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
}
else
{
console.log(`glDynamicBuffer: Creating ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
}
// 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);
// Fixed-format for now
assert(this.elementType == gl.BYTE);
assert(this.nbElements == 1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray);
break;
default:
assert(false, "Unsupported dynamic buffer type");
}
}
};

119
vis/Code/WebGLFont.js Normal file
View file

@ -0,0 +1,119 @@
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 = 12;
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);
// Render the text
this.charContext.font = font;
this.charContext.textAlign = "left";
this.charContext.textBaseline = "top";
this.charContext.fillText(text, 0, 1.5);
}
}
class glTextBuffer
{
constructor(gl, font)
{
this.font = font;
this.textMap = {};
this.textBuffer = new glDynamicBuffer(gl, gl.BYTE, 1, 8, glDynamicBufferType.Texture);
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.fontWidth", this.font.fontWidth);
glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight);
glSetUniform(gl, program, "inTextBufferDesc.textBufferLength", this.textBuffer.nbEntries);
}
}

View file

@ -0,0 +1,137 @@
WebSocketConnection = (function()
{
function WebSocketConnection()
{
this.MessageHandlers = { };
this.Socket = null;
this.Console = null;
}
WebSocketConnection.prototype.SetConsole = function(console)
{
this.Console = console;
}
WebSocketConnection.prototype.Connected = function()
{
// Will return true if the socket is also in the process of connecting
return this.Socket != null;
}
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)
{
// Disconnect if already connected
if (this.Connected())
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.Connected())
this.Socket.close();
}
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)
{
if (message_name in self.MessageHandlers)
{
var handlers = self.MessageHandlers[message_name];
for (var i in handlers)
handlers[i](self, data_view);
}
}
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)
{
let id = data_view_reader.GetStringOfLength(4);
CallMessageHandlers(this, id, data_view_reader);
}
return WebSocketConnection;
})();

Binary file not shown.

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.

274
vis/Styles/Remotery.css Normal file
View file

@ -0,0 +1,274 @@
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;
}
.SampleNameCell
{
width:243px;
}
.SampleTimeCell
{
width:52px;
}
.SampleCountCell
{
width:43px;
}
.SampleTitleNameCell
{
width:238px;
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;
}
.SampleTitleTimeCell
{
width:47px;
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;
}
.SampleTitleCountCell
{
width:38px;
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;
}
.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);
}

92
vis/extern/BrowserLib/Core/Code/Bind.js vendored Normal file
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) + ';';
});
};
})();

26
vis/extern/BrowserLib/Core/Code/Core.js vendored Normal file
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");
}

526
vis/extern/BrowserLib/Core/Code/DOM.js vendored Normal file
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,314 @@
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)
{
this.Manager = manager;
this.ParentNode = parent_node || document.body;
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);
}
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)
{
// Create the window and add it to the list of windows
var wnd = new WM.Window(this, title, x, y, width, height, parent_node);
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:#303030;
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;
}

61
vis/index.html Normal file
View file

@ -0,0 +1,61 @@
<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" />
<!-- 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/Shaders.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/NameMap.js"></script>
<script type="text/javascript" src="Code/WebSocketConnection.js"></script>
<script type="text/javascript" src="Code/TitleWindow.js"></script>
<script type="text/javascript" src="Code/SampleWindow.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>
</head>
<body>
<script type="text/javascript">
var remotery = new Remotery();
</script>
</body>
</html>