Browse Source

Added Remotery.

Branimir Karadžić 10 năm trước cách đây
mục cha
commit
f10b16117d
37 tập tin đã thay đổi với 11790 bổ sung0 xóa
  1. 175 0
      3rdparty/remotery/LICENSE
  2. 5837 0
      3rdparty/remotery/lib/Remotery.c
  3. 534 0
      3rdparty/remotery/lib/Remotery.h
  4. 197 0
      3rdparty/remotery/readme.md
  5. 33 0
      3rdparty/remotery/sample/sample.c
  6. BIN
      3rdparty/remotery/screenshot.png
  7. 117 0
      3rdparty/remotery/vis/Code/Console.js
  8. 61 0
      3rdparty/remotery/vis/Code/PixelTimeRange.js
  9. 234 0
      3rdparty/remotery/vis/Code/Remotery.js
  10. 164 0
      3rdparty/remotery/vis/Code/SampleWindow.js
  11. 28 0
      3rdparty/remotery/vis/Code/ThreadFrame.js
  12. 375 0
      3rdparty/remotery/vis/Code/TimelineRow.js
  13. 270 0
      3rdparty/remotery/vis/Code/TimelineWindow.js
  14. 59 0
      3rdparty/remotery/vis/Code/TitleWindow.js
  15. 130 0
      3rdparty/remotery/vis/Code/WebSocketConnection.js
  16. 211 0
      3rdparty/remotery/vis/Styles/Remotery.css
  17. 65 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Animation.js
  18. 92 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Bind.js
  19. 204 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Convert.js
  20. 20 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Core.js
  21. 499 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/DOM.js
  22. 149 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Keyboard.js
  23. 26 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/LocalStore.js
  24. 83 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Mouse.js
  25. 68 0
      3rdparty/remotery/vis/extern/BrowserLib/Core/Code/MurmurHash3.js
  26. 131 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Button.js
  27. 237 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js
  28. 34 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Container.js
  29. 117 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/EditBox.js
  30. 252 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Grid.js
  31. 31 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Label.js
  32. 352 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Treeview.js
  33. 109 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js
  34. 243 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Window.js
  35. 54 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js
  36. 546 0
      3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css
  37. 53 0
      3rdparty/remotery/vis/index.html

+ 175 - 0
3rdparty/remotery/LICENSE

@@ -0,0 +1,175 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.

+ 5837 - 0
3rdparty/remotery/lib/Remotery.c

@@ -0,0 +1,5837 @@
+//
+// Copyright 2014 Celtoys Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+/*
+@Contents:
+
+    @DEPS:          External Dependencies
+    @TIMERS:        Platform-specific timers
+    @TLS:           Thread-Local Storage
+    @ATOMIC:        Atomic Operations
+    @VMBUFFER:      Mirror Buffer using Virtual Memory for auto-wrap
+    @NEW:           New/Delete operators with error values for simplifying object create/destroy
+    @THREADS:       Threads
+    @SAFEC:         Safe C Library excerpts
+    @OBJALLOC:      Reusable Object Allocator
+    @DYNBUF:        Dynamic Buffer
+    @SOCKETS:       Sockets TCP/IP Wrapper
+    @SHA1:          SHA-1 Cryptographic Hash Function
+    @BASE64:        Base-64 encoder
+    @MURMURHASH:    Murmur-Hash 3
+    @WEBSOCKETS:    WebSockets
+    @MESSAGEQ:      Multiple producer, single consumer message queue
+    @NETWORK:       Network Server
+    @JSON:          Basic, text-based JSON serialisation
+    @SAMPLE:        Base Sample Description (CPU by default)
+    @SAMPLETREE:    A tree of samples with their allocator
+    @TSAMPLER:      Per-Thread Sampler
+    @REMOTERY:      Remotery
+    @CUDA:          CUDA event sampling
+    @D3D11:         Direct3D 11 event sampling
+    @OPENGL:        OpenGL event sampling
+*/
+
+#define RMT_IMPL
+#include "Remotery.h"
+
+#ifdef RMT_PLATFORM_WINDOWS
+  #pragma comment(lib, "ws2_32.lib")
+#endif
+
+#ifdef __ANDROID__
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <linux/ashmem.h>
+#include <fcntl.h>
+#include <string.h>
+#define ASHMEM_DEVICE	"/dev/ashmem"
+
+/*
+ * ashmem_create_region - creates a new ashmem region and returns the file
+ * descriptor, or <0 on error
+ *
+ * `name' is an optional label to give the region (visible in /proc/pid/maps)
+ * `size' is the size of the region, in page-aligned bytes
+ */
+int ashmem_create_region(const char *name, size_t size)
+{
+        int fd, ret;
+
+        fd = open(ASHMEM_DEVICE, O_RDWR);
+        if (fd < 0)
+                return fd;
+
+        if (name) {
+                char buf[ASHMEM_NAME_LEN] = {0};
+
+                strncpy(buf, name, sizeof(buf));
+                buf[sizeof(buf)-1] = 0;
+                ret = ioctl(fd, ASHMEM_SET_NAME, buf);
+                if (ret < 0)
+                        goto error;
+        }
+
+        ret = ioctl(fd, ASHMEM_SET_SIZE, size);
+        if (ret < 0)
+                goto error;
+
+        return fd;
+
+error:
+        close(fd);
+        return ret;
+}
+#endif // __ANDROID__
+
+#ifdef RMT_ENABLED
+
+// Global settings
+static rmtSettings g_Settings;
+static rmtBool g_SettingsInitialized = RMT_FALSE;
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @DEPS: External Dependencies
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+//
+// Required CRT dependencies
+//
+#ifdef RMT_USE_TINYCRT
+
+    #include <TinyCRT/TinyCRT.h>
+    #include <TinyCRT/TinyWinsock.h>
+
+    #define CreateFileMapping CreateFileMappingA
+
+#else
+
+    #ifdef RMT_PLATFORM_MACOS
+        #include <mach/mach_time.h>
+        #include <mach/vm_map.h>
+        #include <mach/mach.h>
+        #include <sys/time.h>
+    #else
+        #include <malloc.h>
+    #endif
+
+    #include <assert.h>
+
+    #ifdef RMT_PLATFORM_WINDOWS
+        #include <winsock2.h>
+        #ifndef __MINGW32__
+            #include <intrin.h>
+        #endif
+        #undef min
+        #undef max
+    #endif
+
+    #ifdef RMT_PLATFORM_LINUX
+        #include <time.h>
+    #endif
+
+    #if defined(RMT_PLATFORM_POSIX)
+        #include <stdlib.h>
+        #include <pthread.h>
+        #include <unistd.h>
+        #include <string.h>
+        #include <sys/socket.h>
+        #include <sys/mman.h>
+        #include <netinet/in.h>
+        #include <fcntl.h>
+        #include <errno.h>
+    #endif
+
+    #ifdef __MINGW32__
+        #include <pthread.h>
+    #endif
+
+#endif
+
+
+#define RMT_UNREFERENCED_PARAMETER(i) (void)(1 ? (void)0 : ((void)i))
+
+
+#ifdef RMT_USE_CUDA
+    #include <cuda.h>
+#endif
+
+
+rmtU8 minU8(rmtU8 a, rmtU8 b)
+{
+    return a < b ? a : b;
+}
+rmtS64 minS64(rmtS64 a, rmtS64 b)
+{
+    return a < b ? a : b;
+}
+
+
+rmtU8 maxU8(rmtU8 a, rmtU8 b)
+{
+    return a > b ? a : b;
+}
+rmtS64 maxS64(rmtS64 a, rmtS64 b)
+{
+    return a > b ? a : b;
+}
+
+
+// Memory management functions
+static void* rmtMalloc( rmtU32 size )
+{
+    return g_Settings.malloc( g_Settings.mm_context, size );
+}
+
+static void* rmtRealloc( void* ptr, rmtU32 size)
+{
+    return g_Settings.realloc( g_Settings.mm_context, ptr, size );
+}
+
+static void rmtFree( void* ptr )
+{
+    g_Settings.free( g_Settings.mm_context, ptr );
+}
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @TIMERS: Platform-specific timers
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+//
+// Get millisecond timer value that has only one guarantee: multiple calls are consistently comparable.
+// On some platforms, even though this returns milliseconds, the timer may be far less accurate.
+//
+static rmtU32 msTimer_Get()
+{
+    #ifdef RMT_PLATFORM_WINDOWS
+        return (rmtU32)GetTickCount();
+    #else
+        clock_t time = clock();
+        rmtU32 msTime = (rmtU32) (time / (CLOCKS_PER_SEC / 1000));
+        return msTime;
+    #endif
+}
+
+
+//
+// Micro-second accuracy high performance counter
+//
+#ifndef RMT_PLATFORM_WINDOWS
+    typedef rmtU64 LARGE_INTEGER;
+#endif
+typedef struct
+{
+    LARGE_INTEGER counter_start;
+    double counter_scale;
+} usTimer;
+
+
+static void usTimer_Init(usTimer* timer)
+{
+    #if defined(RMT_PLATFORM_WINDOWS)
+        LARGE_INTEGER performance_frequency;
+
+        assert(timer != NULL);
+
+        // Calculate the scale from performance counter to microseconds
+        QueryPerformanceFrequency(&performance_frequency);
+        timer->counter_scale = 1000000.0 / performance_frequency.QuadPart;
+
+        // Record the offset for each read of the counter
+        QueryPerformanceCounter(&timer->counter_start);
+
+    #elif defined(RMT_PLATFORM_MACOS)
+
+        mach_timebase_info_data_t nsScale;
+        mach_timebase_info( &nsScale );
+        const double ns_per_us = 1.0e3;
+        timer->counter_scale = (double)(nsScale.numer) / ((double)nsScale.denom * ns_per_us);
+
+        timer->counter_start = mach_absolute_time();
+
+    #elif defined(RMT_PLATFORM_LINUX)
+
+        struct timespec tv;
+        clock_gettime(CLOCK_REALTIME, &tv);
+        timer->counter_start = (rmtU64)(tv.tv_sec * 1000000) + (rmtU64)(tv.tv_nsec * 0.001);
+
+    #endif
+}
+
+
+static rmtU64 usTimer_Get(usTimer* timer)
+{
+    #if defined(RMT_PLATFORM_WINDOWS)
+        LARGE_INTEGER performance_count;
+
+        assert(timer != NULL);
+
+        // Read counter and convert to microseconds
+        QueryPerformanceCounter(&performance_count);
+        return (rmtU64)((performance_count.QuadPart - timer->counter_start.QuadPart) * timer->counter_scale);
+
+    #elif defined(RMT_PLATFORM_MACOS)
+
+        rmtU64 curr_time = mach_absolute_time();
+        return (rmtU64)((curr_time - timer->counter_start) * timer->counter_scale);
+
+    #elif defined(RMT_PLATFORM_LINUX)
+
+        struct timespec tv;
+        clock_gettime(CLOCK_REALTIME, &tv);
+        return  ((rmtU64)(tv.tv_sec * 1000000) + (rmtU64)(tv.tv_nsec * 0.001)) - timer->counter_start;
+
+    #endif
+}
+
+
+static void msSleep(rmtU32 time_ms)
+{
+    #ifdef RMT_PLATFORM_WINDOWS
+        Sleep(time_ms);
+    #elif defined(RMT_PLATFORM_POSIX)
+        usleep(time_ms * 1000);
+    #endif
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @TLS: Thread-Local Storage
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#define TLS_INVALID_HANDLE 0xFFFFFFFF
+
+#if defined(RMT_PLATFORM_WINDOWS)
+    typedef rmtU32 rmtTLS;
+#else
+    typedef pthread_key_t rmtTLS;
+#endif
+
+static rmtError tlsAlloc(rmtTLS* handle)
+{
+    assert(handle != NULL);
+
+#if defined(RMT_PLATFORM_WINDOWS)
+
+    *handle = (rmtTLS)TlsAlloc();
+    if (*handle == TLS_OUT_OF_INDEXES)
+    {
+        *handle = TLS_INVALID_HANDLE;
+        return RMT_ERROR_TLS_ALLOC_FAIL;
+    }
+
+#elif defined(RMT_PLATFORM_POSIX)
+
+    if (pthread_key_create(handle, NULL) != 0)
+    {
+        *handle = TLS_INVALID_HANDLE;
+        return RMT_ERROR_TLS_ALLOC_FAIL;
+    }
+
+#endif
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void tlsFree(rmtTLS handle)
+{
+    assert(handle != TLS_INVALID_HANDLE);
+
+#if defined(RMT_PLATFORM_WINDOWS)
+
+    TlsFree(handle);
+
+#elif defined(RMT_PLATFORM_POSIX)
+
+    pthread_key_delete((pthread_key_t)handle);
+
+#endif
+}
+
+
+static void tlsSet(rmtTLS handle, void* value)
+{
+    assert(handle != TLS_INVALID_HANDLE);
+
+#if defined(RMT_PLATFORM_WINDOWS)
+
+    TlsSetValue(handle, value);
+
+#elif defined(RMT_PLATFORM_POSIX)
+
+    pthread_setspecific((pthread_key_t)handle, value);
+
+#endif
+}
+
+
+static void* tlsGet(rmtTLS handle)
+{
+    assert(handle != TLS_INVALID_HANDLE);
+
+#if defined(RMT_PLATFORM_WINDOWS)
+
+    return TlsGetValue(handle);
+
+#elif defined(RMT_PLATFORM_POSIX)
+
+    return pthread_getspecific((pthread_key_t)handle);
+
+#endif
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @ATOMIC: Atomic Operations
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+static rmtBool AtomicCompareAndSwap(rmtU32 volatile* val, long old_val, long new_val)
+{
+    #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__)
+        return _InterlockedCompareExchange((long volatile*)val, new_val, old_val) == old_val ? RMT_TRUE : RMT_FALSE;
+    #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__)
+        return __sync_bool_compare_and_swap(val, old_val, new_val) ? RMT_TRUE : RMT_FALSE;
+    #endif
+}
+
+
+static rmtBool AtomicCompareAndSwapPointer(long* volatile* ptr, long* old_ptr, long* new_ptr)
+{
+    #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__)
+        #ifdef _WIN64
+            return _InterlockedCompareExchange64((__int64 volatile*)ptr, (__int64)new_ptr, (__int64)old_ptr) == (__int64)old_ptr ? RMT_TRUE : RMT_FALSE;
+        #else
+            return _InterlockedCompareExchange((long volatile*)ptr, (long)new_ptr, (long)old_ptr) == (long)old_ptr ? RMT_TRUE : RMT_FALSE;
+        #endif
+    #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__)
+        return __sync_bool_compare_and_swap(ptr, old_ptr, new_ptr) ? RMT_TRUE : RMT_FALSE;
+    #endif
+}
+
+
+//
+// NOTE: Does not guarantee a memory barrier
+// TODO: Make sure all platforms don't insert a memory barrier as this is only for stats
+//       Alternatively, add strong/weak memory order equivalents
+//
+static rmtS32 AtomicAdd(rmtS32 volatile* value, rmtS32 add)
+{
+    #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__)
+        return _InterlockedExchangeAdd((long volatile*)value, (long)add);
+    #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__)
+        return __sync_fetch_and_add(value, add);
+    #endif
+}
+
+
+static void AtomicSub(rmtS32 volatile* value, rmtS32 sub)
+{
+    // Not all platforms have an implementation so just negate and add
+    AtomicAdd(value, -sub);
+}
+
+
+// Compiler read/write fences (windows implementation)
+static void ReadFence()
+{
+#if defined(RMT_PLATFORM_WINDOWS)
+    _ReadBarrier();
+#else
+    asm volatile ("" : : : "memory");
+#endif
+}
+static void WriteFence()
+{
+#if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__)
+    _WriteBarrier();
+#else
+    asm volatile ("" : : : "memory");
+#endif
+}
+
+
+// Get a shared value with acquire semantics, ensuring the read is complete
+// before the function returns.
+static void* LoadAcquire(void* volatile const* addr)
+{
+    // Hardware fence is implicit on x86 so only need the compiler fence
+    void* v = *addr;
+    ReadFence();
+    return v;
+}
+
+
+// Set a shared value with release semantics, ensuring any prior writes
+// are complete before the value is set.
+static void StoreRelease(void* volatile*  addr, void* v)
+{
+    // Hardware fence is implicit on x86 so only need the compiler fence
+    WriteFence();
+    *addr = v;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @NEW: New/Delete operators with error values for simplifying object create/destroy
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+// Ensures the pointer is non-NULL, calls the destructor, frees memory and sets the pointer to NULL
+#define Delete(type, obj)           \
+    if (obj != NULL)                \
+    {                               \
+        type##_Destructor(obj);     \
+        rmtFree(obj);               \
+        obj = NULL;                 \
+    }
+
+
+// New is implemented in terms of two begin/end macros
+// New will allocate enough space for the object and call the constructor
+// If allocation fails the constructor won't be called
+// If the constructor fails, the destructor is called and memory is released
+// NOTE: Use of sizeof() requires that the type be defined at the point of call
+// This is a disadvantage over requiring only a custom Create function
+#define BeginNew(type, obj)                 \
+    {                                       \
+        obj = (type*)rmtMalloc(sizeof(type));  \
+        if (obj == NULL)                    \
+        {                                   \
+            error = RMT_ERROR_MALLOC_FAIL;  \
+        }                                   \
+        else                                \
+        {                                   \
+
+
+#define EndNew(type, obj)                   \
+            if (error != RMT_ERROR_NONE)    \
+                Delete(type, obj);          \
+        }                                   \
+    }
+
+
+// Specialisations for New with varying constructor parameter counts
+#define New_0(type, obj)    \
+    BeginNew(type, obj); error = type##_Constructor(obj); EndNew(type, obj)
+#define New_1(type, obj, a0)    \
+    BeginNew(type, obj); error = type##_Constructor(obj, a0); EndNew(type, obj)
+#define New_2(type, obj, a0, a1)    \
+    BeginNew(type, obj); error = type##_Constructor(obj, a0, a1); EndNew(type, obj)
+#define New_3(type, obj, a0, a1, a2)    \
+    BeginNew(type, obj); error = type##_Constructor(obj, a0, a1, a2); EndNew(type, obj)
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @VMBUFFER: Mirror Buffer using Virtual Memory for auto-wrap
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+typedef struct VirtualMirrorBuffer
+{
+    // Page-rounded size of the buffer without mirroring
+    rmtU32 size;
+
+    // Pointer to the first part of the mirror
+    // The second part comes directly after at ptr+size bytes
+    rmtU8* ptr;
+
+#ifdef RMT_PLATFORM_WINDOWS
+    HANDLE file_map_handle;
+#endif
+
+} VirtualMirrorBuffer;
+
+
+static rmtError VirtualMirrorBuffer_Constructor(VirtualMirrorBuffer* buffer, rmtU32 size, int nb_attempts)
+{
+    static const rmtU32 k_64 = 64 * 1024;
+
+#ifdef RMT_PLATFORM_LINUX
+    char path[] = "/dev/shm/ring-buffer-XXXXXX";
+    int file_descriptor;
+#endif
+
+    // Round up to page-granulation; the nearest 64k boundary for now
+    size = (size + k_64 - 1) / k_64 * k_64;
+
+    // Set defaults
+    buffer->size = size;
+    buffer->ptr = NULL;
+#ifdef RMT_PLATFORM_WINDOWS
+    buffer->file_map_handle = INVALID_HANDLE_VALUE;
+#endif
+
+#ifdef RMT_PLATFORM_WINDOWS
+
+    // Windows version based on https://gist.github.com/rygorous/3158316
+
+    while (nb_attempts-- > 0)
+    {
+        rmtU8* desired_addr;
+
+        // Create a file mapping for pointing to its physical address with multiple virtual pages
+        buffer->file_map_handle = CreateFileMapping(
+            INVALID_HANDLE_VALUE,
+            0,
+            PAGE_READWRITE,
+            0,
+            size,
+            0);
+        if (buffer->file_map_handle == NULL)
+            break;
+
+        // Reserve two contiguous pages of virtual memory
+        desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS);
+        if (desired_addr == NULL)
+            break;
+
+        // Release the range immediately but retain the address for the next sequence of code to
+        // try and map to it. In the mean-time some other OS thread may come along and allocate this
+        // address range from underneath us so multiple attempts need to be made.
+        VirtualFree(desired_addr, 0, MEM_RELEASE);
+
+        // Immediately try to point both pages at the file mapping
+        if (MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr) == desired_addr &&
+            MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr + size) == desired_addr + size)
+        {
+            buffer->ptr = desired_addr;
+            break;
+        }
+
+        // Failed to map the virtual pages; cleanup and try again
+        CloseHandle(buffer->file_map_handle);
+        buffer->file_map_handle = NULL;
+    }
+
+#endif
+
+#ifdef RMT_PLATFORM_MACOS
+
+    //
+    // Mac version based on https://github.com/mikeash/MAMirroredQueue
+    //
+    // Copyright (c) 2010, Michael Ash
+    // All rights reserved.
+    //
+    // Redistribution and use in source and binary forms, with or without modification, are permitted provided that
+    // the following conditions are met:
+    //
+    // Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+    // disclaimer.
+    //
+    // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
+    // following disclaimer in the documentation and/or other materials provided with the distribution.
+    // Neither the name of Michael Ash nor the names of its contributors may be used to endorse or promote products
+    // derived from this software without specific prior written permission.
+    //
+    // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+    // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+    // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+    // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+    // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+    // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+    // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+    //
+
+    while (nb_attempts-- > 0)
+    {
+        vm_prot_t cur_prot, max_prot;
+        kern_return_t mach_error;
+        rmtU8* ptr = NULL;
+        rmtU8* target = NULL;
+
+        // Allocate 2 contiguous pages of virtual memory
+        if (vm_allocate(mach_task_self(), (vm_address_t*)&ptr, size * 2, VM_FLAGS_ANYWHERE) != KERN_SUCCESS)
+            break;
+
+        // Try to deallocate the last page, leaving its virtual memory address free
+        target = ptr + size;
+        if (vm_deallocate(mach_task_self(), (vm_address_t)target, size) != KERN_SUCCESS)
+        {
+            vm_deallocate(mach_task_self(), (vm_address_t)ptr, size * 2);
+            break;
+        }
+
+        // Attempt to remap the page just deallocated to the buffer again
+        mach_error = vm_remap(
+            mach_task_self(),
+            (vm_address_t*)&target,
+            size,
+            0,  // mask
+            0,  // anywhere
+            mach_task_self(),
+            (vm_address_t)ptr,
+            0,  //copy
+            &cur_prot,
+            &max_prot,
+            VM_INHERIT_COPY);
+
+        if (mach_error == KERN_NO_SPACE)
+        {
+            // Failed on this pass, cleanup and make another attempt
+            if (vm_deallocate(mach_task_self(), (vm_address_t)ptr, size) != KERN_SUCCESS)
+                break;
+        }
+
+        else if (mach_error == KERN_SUCCESS)
+        {
+            // Leave the loop on success
+            buffer->ptr = ptr;
+            break;
+        }
+
+        else
+        {
+            // Unknown error, can't recover
+            vm_deallocate(mach_task_self(), (vm_address_t)ptr, size);
+            break;
+        }
+    }
+
+#endif
+
+#ifdef RMT_PLATFORM_LINUX
+
+    // Linux version based on now-defunct Wikipedia section http://en.wikipedia.org/w/index.php?title=Circular_buffer&oldid=600431497
+
+
+#ifdef __ANDROID__
+    file_descriptor = ashmem_create_region("remotery_shm", size * 2);
+    if (file_descriptor < 0) {
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+    }
+#else
+    // Create a unique temporary filename in the shared memory folder
+    file_descriptor = mkstemp(path);
+    if (file_descriptor < 0)
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+
+    // Delete the name
+    if (unlink(path))
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+
+    // Set the file size to twice the buffer size
+    // TODO: this 2x behaviour can be avoided with similar solution to Win/Mac
+    if (ftruncate (file_descriptor, size * 2))
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+
+#endif
+    // Map 2 contiguous pages
+    buffer->ptr = mmap(NULL, size * 2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+    if (buffer->ptr == MAP_FAILED)
+    {
+        buffer->ptr = NULL;
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+    }
+
+    // Point both pages to the same memory file
+    if (mmap(buffer->ptr, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != buffer->ptr ||
+        mmap(buffer->ptr + size, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != buffer->ptr + size)
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+
+#endif
+
+    // Cleanup if exceeded number of attempts or failed
+    if (buffer->ptr == NULL)
+        return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void VirtualMirrorBuffer_Destructor(VirtualMirrorBuffer* buffer)
+{
+    assert(buffer != 0);
+
+#ifdef RMT_PLATFORM_WINDOWS
+    if (buffer->file_map_handle != NULL)
+    {
+        CloseHandle(buffer->file_map_handle);
+        buffer->file_map_handle = NULL;
+    }
+#endif
+
+#ifdef RMT_PLATFORM_MACOS
+    if (buffer->ptr != NULL)
+        vm_deallocate(mach_task_self(), (vm_address_t)buffer->ptr, buffer->size * 2);
+#endif
+
+#ifdef RMT_PLATFORM_LINUX
+    if (buffer->ptr != NULL)
+        munmap(buffer->ptr, buffer->size * 2);
+#endif
+
+    buffer->ptr = NULL;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @THREADS: Threads
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+// comp.lang.c FAQ 4.13 : http://c-faq.com/ptrs/generic.html
+// Should not store a funcptr in a void*, but any funcptr type will do
+typedef int(*FuncPtr)();
+
+typedef struct
+{
+    // OS-specific data
+    #if defined(RMT_PLATFORM_WINDOWS)
+        HANDLE handle;
+    #else
+        pthread_t handle;
+    #endif
+
+    // Callback executed when the thread is created
+    FuncPtr callback;
+
+    // Caller-specified parameter passed to Thread_Create
+    void* param;
+
+    // Error state returned from callback
+    rmtError error;
+
+    // External threads can set this to request an exit
+    volatile rmtBool request_exit;
+
+} Thread;
+
+
+typedef rmtError (*ThreadProc)(Thread* thread);
+
+
+#if defined(RMT_PLATFORM_WINDOWS)
+
+    static DWORD WINAPI ThreadProcWindows(LPVOID lpParameter)
+    {
+        Thread* thread = (Thread*)lpParameter;
+        assert(thread != NULL);
+        thread->error = ((ThreadProc)thread->callback)(thread);
+        return thread->error == RMT_ERROR_NONE ? 1 : 0;
+    }
+
+#else
+    static void* StartFunc( void* pArgs )
+    {
+        Thread* thread = (Thread*)pArgs;
+        assert(thread != NULL);
+        thread->error = ((ThreadProc)thread->callback)(thread);
+        return NULL; // returned error not use, check thread->error.
+    }
+#endif
+
+
+static int Thread_Valid(Thread* thread)
+{
+    assert(thread != NULL);
+
+    #if defined(RMT_PLATFORM_WINDOWS)
+        return thread->handle != NULL;
+    #else
+        return !pthread_equal(thread->handle, pthread_self());
+    #endif
+}
+
+
+static rmtError Thread_Constructor(Thread* thread, ThreadProc callback, void* param)
+{
+    assert(thread != NULL);
+
+    thread->callback = (FuncPtr)callback;
+    thread->param = param;
+    thread->error = RMT_ERROR_NONE;
+    thread->request_exit = RMT_FALSE;
+
+    // OS-specific thread creation
+
+    #if defined (RMT_PLATFORM_WINDOWS)
+
+        thread->handle = CreateThread(
+            NULL,                               // lpThreadAttributes
+            0,                                  // dwStackSize
+            ThreadProcWindows,                  // lpStartAddress
+            thread,                            // lpParameter
+            0,                                  // dwCreationFlags
+            NULL);                              // lpThreadId
+
+        if (thread->handle == NULL)
+            return RMT_ERROR_CREATE_THREAD_FAIL;
+
+    #else
+
+        int32_t error = pthread_create( &thread->handle, NULL, StartFunc, thread );
+        if (error)
+        {
+            // Contents of 'thread' parameter to pthread_create() are undefined after
+            // failure call so can't pre-set to invalid value before hand.
+            thread->handle = pthread_self();
+            return RMT_ERROR_CREATE_THREAD_FAIL;
+        }
+
+    #endif
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void Thread_RequestExit(Thread* thread)
+{
+    // Not really worried about memory barriers or delayed visibility to the target thread
+    assert(thread != NULL);
+    thread->request_exit = RMT_TRUE;
+}
+
+
+static void Thread_Join(Thread* thread)
+{
+    assert(Thread_Valid(thread));
+
+    #if defined(RMT_PLATFORM_WINDOWS)
+    WaitForSingleObject(thread->handle, INFINITE);
+    #else
+    pthread_join(thread->handle, NULL);
+    #endif
+}
+
+
+static void Thread_Destructor(Thread* thread)
+{
+    assert(thread != NULL);
+
+    if (Thread_Valid(thread))
+    {
+        // Shutdown the thread
+        Thread_RequestExit(thread);
+        Thread_Join(thread);
+
+        // OS-specific release of thread resources
+
+        #if defined(RMT_PLATFORM_WINDOWS)
+        CloseHandle(thread->handle);
+        thread->handle = NULL;
+        #endif
+    }
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @SAFEC: Safe C Library excerpts
+   http://sourceforge.net/projects/safeclib/
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+/*------------------------------------------------------------------
+ *
+ * November 2008, Bo Berry
+ *
+ * Copyright (c) 2008-2011 by Cisco Systems, Inc
+ * All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *------------------------------------------------------------------
+ */
+
+
+// NOTE: Microsoft also has its own version of these functions so I'm do some hacky PP to remove them
+#define strnlen_s strnlen_s_safe_c
+#define strncat_s strncat_s_safe_c
+
+
+#define RSIZE_MAX_STR (4UL << 10)   /* 4KB */
+#define RCNEGATE(x) x
+
+
+#define EOK             ( 0 )
+#define ESNULLP         ( 400 )       /* null ptr                    */
+#define ESZEROL         ( 401 )       /* length is zero              */
+#define ESLEMAX         ( 403 )       /* length exceeds max          */
+#define ESOVRLP         ( 404 )       /* overlap undefined           */
+#define ESNOSPC         ( 406 )       /* not enough space for s2     */
+#define ESUNTERM        ( 407 )       /* unterminated string         */
+#define ESNOTFND        ( 409 )       /* not found                   */
+
+#ifndef _ERRNO_T_DEFINED
+#define _ERRNO_T_DEFINED
+typedef int errno_t;
+#endif
+
+#if !defined(_WIN64) && !defined(__APPLE__) || defined(__MINGW32__)
+typedef unsigned int rsize_t;
+#endif
+
+#if defined(RMT_PLATFORM_MACOS) && !defined(_RSIZE_T)
+typedef __darwin_size_t rsize_t;
+#endif
+
+static rsize_t
+strnlen_s (const char *dest, rsize_t dmax)
+{
+    rsize_t count;
+
+    if (dest == NULL) {
+        return RCNEGATE(0);
+    }
+
+    if (dmax == 0) {
+        return RCNEGATE(0);
+    }
+
+    if (dmax > RSIZE_MAX_STR) {
+        return RCNEGATE(0);
+    }
+
+    count = 0;
+    while (*dest && dmax) {
+        count++;
+        dmax--;
+        dest++;
+    }
+
+    return RCNEGATE(count);
+}
+
+
+static errno_t
+strstr_s (char *dest, rsize_t dmax,
+          const char *src, rsize_t slen, char **substring)
+{
+    rsize_t len;
+    rsize_t dlen;
+    int i;
+
+    if (substring == NULL) {
+        return RCNEGATE(ESNULLP);
+    }
+    *substring = NULL;
+
+    if (dest == NULL) {
+        return RCNEGATE(ESNULLP);
+    }
+
+    if (dmax == 0) {
+        return RCNEGATE(ESZEROL);
+    }
+
+    if (dmax > RSIZE_MAX_STR) {
+        return RCNEGATE(ESLEMAX);
+    }
+
+    if (src == NULL) {
+        return RCNEGATE(ESNULLP);
+    }
+
+    if (slen == 0) {
+        return RCNEGATE(ESZEROL);
+    }
+
+    if (slen > RSIZE_MAX_STR) {
+        return RCNEGATE(ESLEMAX);
+    }
+
+    /*
+     * src points to a string with zero length, or
+     * src equals dest, return dest
+     */
+    if (*src == '\0' || dest == src) {
+        *substring = dest;
+        return RCNEGATE(EOK);
+    }
+
+    while (*dest && dmax) {
+        i = 0;
+        len = slen;
+        dlen = dmax;
+
+        while (src[i] && dlen) {
+
+            /* not a match, not a substring */
+            if (dest[i] != src[i]) {
+                break;
+            }
+
+            /* move to the next char */
+            i++;
+            len--;
+            dlen--;
+
+            if (src[i] == '\0' || !len) {
+                *substring = dest;
+                return RCNEGATE(EOK);
+            }
+        }
+        dest++;
+        dmax--;
+    }
+
+    /*
+     * substring was not found, return NULL
+     */
+    *substring = NULL;
+    return RCNEGATE(ESNOTFND);
+}
+
+
+static errno_t
+strncat_s (char *dest, rsize_t dmax, const char *src, rsize_t slen)
+{
+    rsize_t orig_dmax;
+    char *orig_dest;
+    const char *overlap_bumper;
+
+    if (dest == NULL) {
+        return RCNEGATE(ESNULLP);
+    }
+
+    if (src == NULL) {
+        return RCNEGATE(ESNULLP);
+    }
+
+    if (slen > RSIZE_MAX_STR) {
+        return RCNEGATE(ESLEMAX);
+    }
+
+    if (dmax == 0) {
+        return RCNEGATE(ESZEROL);
+    }
+
+    if (dmax > RSIZE_MAX_STR) {
+        return RCNEGATE(ESLEMAX);
+    }
+
+    /* hold base of dest in case src was not copied */
+    orig_dmax = dmax;
+    orig_dest = dest;
+
+    if (dest < src) {
+        overlap_bumper = src;
+
+        /* Find the end of dest */
+        while (*dest != '\0') {
+
+            if (dest == overlap_bumper) {
+                return RCNEGATE(ESOVRLP);
+            }
+
+            dest++;
+            dmax--;
+            if (dmax == 0) {
+                return RCNEGATE(ESUNTERM);
+            }
+        }
+
+        while (dmax > 0) {
+            if (dest == overlap_bumper) {
+                return RCNEGATE(ESOVRLP);
+            }
+
+            /*
+             * Copying truncated before the source null is encountered
+             */
+            if (slen == 0) {
+                *dest = '\0';
+                return RCNEGATE(EOK);
+            }
+
+            *dest = *src;
+            if (*dest == '\0') {
+                return RCNEGATE(EOK);
+            }
+
+            dmax--;
+            slen--;
+            dest++;
+            src++;
+        }
+
+    } else {
+        overlap_bumper = dest;
+
+        /* Find the end of dest */
+        while (*dest != '\0') {
+
+            /*
+             * NOTE: no need to check for overlap here since src comes first
+             * in memory and we're not incrementing src here.
+             */
+            dest++;
+            dmax--;
+            if (dmax == 0) {
+                return RCNEGATE(ESUNTERM);
+            }
+        }
+
+        while (dmax > 0) {
+            if (src == overlap_bumper) {
+                return RCNEGATE(ESOVRLP);
+            }
+
+            /*
+             * Copying truncated
+             */
+            if (slen == 0) {
+                *dest = '\0';
+                return RCNEGATE(EOK);
+            }
+
+            *dest = *src;
+            if (*dest == '\0') {
+                return RCNEGATE(EOK);
+            }
+
+            dmax--;
+            slen--;
+            dest++;
+            src++;
+        }
+    }
+
+    /*
+     * the entire src was not copied, so the string will be nulled.
+     */
+    return RCNEGATE(ESNOSPC);
+}
+
+
+
+/* very simple integer to hex */
+static const char* hex_encoding_table = "0123456789ABCDEF";
+
+static void itoahex_s( char *dest, rsize_t dmax, rmtS32 value )
+{
+    rsize_t len;
+    rmtS32	halfbytepos;
+
+    halfbytepos = 8;
+
+    /* strip leading 0's */
+    while (halfbytepos > 1)
+    {
+        --halfbytepos;
+        if (value >> (4 * halfbytepos) & 0xF)
+        {
+            ++halfbytepos;
+            break;
+        }
+    }
+
+    len = 0;
+    while(len + 1 < dmax && halfbytepos > 0)
+    {
+        --halfbytepos;
+        dest[len] = hex_encoding_table[value >> (4 * halfbytepos) & 0xF];
+        ++len;
+    }
+
+    if (len < dmax)
+    {
+        dest[len] = 0;
+    }
+}
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @OBJALLOC: Reusable Object Allocator
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+//
+// All objects that require free-list-backed allocation need to inherit from this type.
+//
+typedef struct ObjectLink
+{
+    struct ObjectLink* volatile next;
+} ObjectLink;
+
+
+static void ObjectLink_Constructor(ObjectLink* link)
+{
+    assert(link != NULL);
+    link->next = NULL;
+}
+
+
+typedef rmtError (*ObjConstructor)(void*);
+typedef void (*ObjDestructor)(void*);
+
+
+typedef struct
+{
+    // Object create/destroy parameters
+    rmtU32 object_size;
+    ObjConstructor constructor;
+    ObjDestructor destructor;
+
+    // Number of objects in the free list
+    volatile rmtS32 nb_free;
+
+    // Number of objects used by callers
+    volatile rmtS32 nb_inuse;
+
+    // Total allocation count
+    volatile rmtS32 nb_allocated;
+
+    ObjectLink* first_free;
+} ObjectAllocator;
+
+
+static rmtError ObjectAllocator_Constructor(ObjectAllocator* allocator, rmtU32 object_size, ObjConstructor constructor, ObjDestructor destructor)
+{
+    allocator->object_size = object_size;
+    allocator->constructor = constructor;
+    allocator->destructor = destructor;
+    allocator->nb_free = 0;
+    allocator->nb_inuse = 0;
+    allocator->nb_allocated = 0;
+    allocator->first_free = NULL;
+    return RMT_ERROR_NONE;
+}
+
+
+static void ObjectAllocator_Destructor(ObjectAllocator* allocator)
+{
+    // Ensure everything has been released to the allocator
+    assert(allocator != NULL);
+    assert(allocator->nb_inuse == 0);
+
+    // Destroy all objects released to the allocator
+    while (allocator->first_free != NULL)
+    {
+        ObjectLink* next = allocator->first_free->next;
+        assert(allocator->destructor != NULL);
+        allocator->destructor(allocator->first_free);
+        rmtFree(allocator->first_free);
+        allocator->first_free = next;
+    }
+}
+
+
+static void ObjectAllocator_Push(ObjectAllocator* allocator, ObjectLink* start, ObjectLink* end)
+{
+    assert(allocator != NULL);
+    assert(start != NULL);
+    assert(end != NULL);
+
+    // CAS pop add range to the front of the list
+    while (1)
+    {
+        ObjectLink* old_link = (ObjectLink*)allocator->first_free;
+        end->next = old_link;
+        if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)start) == RMT_TRUE)
+            break;
+    }
+}
+
+
+static ObjectLink* ObjectAllocator_Pop(ObjectAllocator* allocator)
+{
+    ObjectLink* link;
+
+    assert(allocator != NULL);
+    assert(allocator->first_free != NULL);
+
+    // CAS pop from the front of the list
+    while (1)
+    {
+        ObjectLink* old_link = (ObjectLink*)allocator->first_free;
+        ObjectLink* next_link = old_link->next;
+        if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)next_link) == RMT_TRUE)
+        {
+            link = old_link;
+            break;
+        }
+    }
+
+    link->next = NULL;
+
+    return link;
+}
+
+
+static rmtError ObjectAllocator_Alloc(ObjectAllocator* allocator, void** object)
+{
+    // This function only calls the object constructor on initial malloc of an object
+
+    assert(allocator != NULL);
+    assert(object != NULL);
+
+    // Has the free list run out?
+    if (allocator->first_free == NULL)
+    {
+        rmtError error;
+
+        // Allocate/construct a new object
+        void* free_object = rmtMalloc( allocator->object_size );
+        if (free_object == NULL)
+            return RMT_ERROR_MALLOC_FAIL;
+        assert(allocator->constructor != NULL);
+        error = allocator->constructor(free_object);
+        if (error != RMT_ERROR_NONE)
+        {
+            // Auto-teardown on failure
+            assert(allocator->destructor != NULL);
+            allocator->destructor(free_object);
+            rmtFree(free_object);
+            return error;
+        }
+
+        // Add to the free list
+        ObjectAllocator_Push(allocator, (ObjectLink*)free_object, (ObjectLink*)free_object);
+        AtomicAdd(&allocator->nb_allocated, 1);
+        AtomicAdd(&allocator->nb_free, 1);
+    }
+
+    // Pull available objects from the free list
+    *object = ObjectAllocator_Pop(allocator);
+    AtomicSub(&allocator->nb_free, 1);
+    AtomicAdd(&allocator->nb_inuse, 1);
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void ObjectAllocator_Free(ObjectAllocator* allocator, void* object)
+{
+    // Add back to the free-list
+    assert(allocator != NULL);
+    ObjectAllocator_Push(allocator, (ObjectLink*)object, (ObjectLink*)object);
+    AtomicSub(&allocator->nb_inuse, 1);
+    AtomicAdd(&allocator->nb_free, 1);
+}
+
+
+static void ObjectAllocator_FreeRange(ObjectAllocator* allocator, void* start, void* end, rmtU32 count)
+{
+    assert(allocator != NULL);
+    ObjectAllocator_Push(allocator, (ObjectLink*)start, (ObjectLink*)end);
+    AtomicSub(&allocator->nb_inuse, count);
+    AtomicAdd(&allocator->nb_free, count);
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @DYNBUF: Dynamic Buffer
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+typedef struct
+{
+    rmtU32 alloc_granularity;
+
+    rmtU32 bytes_allocated;
+    rmtU32 bytes_used;
+
+    rmtU8* data;
+} Buffer;
+
+
+static rmtError Buffer_Constructor(Buffer* buffer, rmtU32 alloc_granularity)
+{
+    assert(buffer != NULL);
+    buffer->alloc_granularity = alloc_granularity;
+    buffer->bytes_allocated = 0;
+    buffer->bytes_used = 0;
+    buffer->data = NULL;
+    return RMT_ERROR_NONE;
+}
+
+
+static void Buffer_Destructor(Buffer* buffer)
+{
+    assert(buffer != NULL);
+
+    if (buffer->data != NULL)
+    {
+        rmtFree(buffer->data);
+        buffer->data = NULL;
+    }
+}
+
+
+static rmtError Buffer_Write(Buffer* buffer, void* data, rmtU32 length)
+{
+    assert(buffer != NULL);
+
+    // Reallocate the buffer on overflow
+    if (buffer->bytes_used + length > buffer->bytes_allocated)
+    {
+        // Calculate size increase rounded up to the requested allocation granularity
+        rmtU32 g = buffer->alloc_granularity;
+        rmtU32 a = buffer->bytes_allocated + length;
+        a = a + ((g - 1) - ((a - 1) % g));
+        buffer->bytes_allocated = a;
+        buffer->data = (rmtU8*)rmtRealloc(buffer->data, buffer->bytes_allocated);
+        if (buffer->data == NULL)
+            return RMT_ERROR_MALLOC_FAIL;
+    }
+
+    // Copy all bytes
+    memcpy(buffer->data + buffer->bytes_used, data, length);
+    buffer->bytes_used += length;
+
+    // NULL terminate (if possible) for viewing in debug
+    if (buffer->bytes_used < buffer->bytes_allocated)
+        buffer->data[buffer->bytes_used] = 0;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static rmtError Buffer_WriteString(Buffer* buffer, rmtPStr string)
+{
+    assert(string != NULL);
+    return Buffer_Write(buffer, (void*)string, (rmtU32)strnlen_s(string, 2048));
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @SOCKETS: Sockets TCP/IP Wrapper
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+#ifndef RMT_PLATFORM_WINDOWS
+    typedef int SOCKET;
+    #define INVALID_SOCKET -1
+    #define SOCKET_ERROR   -1
+    #define SD_SEND SHUT_WR
+    #define closesocket close
+#endif
+typedef struct
+{
+    SOCKET socket;
+} TCPSocket;
+
+
+typedef struct
+{
+    rmtBool can_read;
+    rmtBool can_write;
+    rmtError error_state;
+} SocketStatus;
+
+
+//
+// Function prototypes
+//
+static void TCPSocket_Close(TCPSocket* tcp_socket);
+
+
+static rmtError InitialiseNetwork()
+{
+    #ifdef RMT_PLATFORM_WINDOWS
+
+        WSADATA wsa_data;
+        if (WSAStartup(MAKEWORD(2, 2), &wsa_data))
+            return RMT_ERROR_SOCKET_INIT_NETWORK_FAIL;
+        if (LOBYTE(wsa_data.wVersion) != 2 || HIBYTE(wsa_data.wVersion) != 2)
+            return RMT_ERROR_SOCKET_INIT_NETWORK_FAIL;
+
+        return RMT_ERROR_NONE;
+
+    #else
+
+        return RMT_ERROR_NONE;
+
+    #endif
+}
+
+
+static void ShutdownNetwork()
+{
+    #ifdef RMT_PLATFORM_WINDOWS
+        WSACleanup();
+    #endif
+}
+
+
+static rmtError TCPSocket_Constructor(TCPSocket* tcp_socket)
+{
+    assert(tcp_socket != NULL);
+    tcp_socket->socket = INVALID_SOCKET;
+    return InitialiseNetwork();
+}
+
+
+static void TCPSocket_Destructor(TCPSocket* tcp_socket)
+{
+    assert(tcp_socket != NULL);
+    TCPSocket_Close(tcp_socket);
+    ShutdownNetwork();
+}
+
+
+static rmtError TCPSocket_RunServer(TCPSocket* tcp_socket, rmtU16 port)
+{
+    SOCKET s = INVALID_SOCKET;
+    struct sockaddr_in sin = { 0 };
+    #ifdef RMT_PLATFORM_WINDOWS
+        u_long nonblock = 1;
+    #endif
+
+    assert(tcp_socket != NULL);
+
+    // Try to create the socket
+    s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+    if (s == SOCKET_ERROR)
+        return RMT_ERROR_SOCKET_CREATE_FAIL;
+
+    // Bind the socket to the incoming port
+    sin.sin_family = AF_INET;
+    sin.sin_addr.s_addr = INADDR_ANY;
+    sin.sin_port = htons(port);
+    if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
+        return RMT_ERROR_SOCKET_BIND_FAIL;
+
+    // Connection is valid, remaining code is socket state modification
+    tcp_socket->socket = s;
+
+    // Enter a listening state with a backlog of 1 connection
+    if (listen(s, 1) == SOCKET_ERROR)
+        return RMT_ERROR_SOCKET_LISTEN_FAIL;
+
+    // Set as non-blocking
+    #ifdef RMT_PLATFORM_WINDOWS
+        if (ioctlsocket(tcp_socket->socket, FIONBIO, &nonblock) == SOCKET_ERROR)
+            return RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL;
+    #else
+        if (fcntl(tcp_socket->socket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR)
+            return RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL;
+    #endif
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void TCPSocket_Close(TCPSocket* tcp_socket)
+{
+    assert(tcp_socket != NULL);
+
+    if (tcp_socket->socket != INVALID_SOCKET)
+    {
+        // Shutdown the connection, stopping all sends
+        int result = shutdown(tcp_socket->socket, SD_SEND);
+        if (result != SOCKET_ERROR)
+        {
+            // Keep receiving until the peer closes the connection
+            int total = 0;
+            char temp_buf[128];
+            while (result > 0)
+            {
+                result = (int)recv(tcp_socket->socket, temp_buf, sizeof(temp_buf), 0);
+                total += result;
+            }
+        }
+
+        // Close the socket and issue a network shutdown request
+        closesocket(tcp_socket->socket);
+        tcp_socket->socket = INVALID_SOCKET;
+    }
+}
+
+
+static SocketStatus TCPSocket_PollStatus(TCPSocket* tcp_socket)
+{
+    SocketStatus status;
+    fd_set fd_read, fd_write, fd_errors;
+    struct timeval tv;
+
+    status.can_read = RMT_FALSE;
+    status.can_write = RMT_FALSE;
+    status.error_state = RMT_ERROR_NONE;
+
+    assert(tcp_socket != NULL);
+    if (tcp_socket->socket == INVALID_SOCKET)
+    {
+        status.error_state = RMT_ERROR_SOCKET_INVALID_POLL;
+        return status;
+    }
+
+    // Set read/write/error markers for the socket
+    FD_ZERO(&fd_read);
+    FD_ZERO(&fd_write);
+    FD_ZERO(&fd_errors);
+    FD_SET(tcp_socket->socket, &fd_read);
+    FD_SET(tcp_socket->socket, &fd_write);
+    FD_SET(tcp_socket->socket, &fd_errors);
+
+    // Poll socket status without blocking
+    tv.tv_sec = 0;
+    tv.tv_usec = 0;
+    if (select(((int)tcp_socket->socket)+1, &fd_read, &fd_write, &fd_errors, &tv) == SOCKET_ERROR)
+    {
+        status.error_state = RMT_ERROR_SOCKET_SELECT_FAIL;
+        return status;
+    }
+
+    status.can_read = FD_ISSET(tcp_socket->socket, &fd_read) != 0 ? RMT_TRUE : RMT_FALSE;
+    status.can_write = FD_ISSET(tcp_socket->socket, &fd_write) != 0 ? RMT_TRUE : RMT_FALSE;
+    status.error_state = FD_ISSET(tcp_socket->socket, &fd_errors) != 0 ? RMT_ERROR_SOCKET_POLL_ERRORS : RMT_ERROR_NONE;
+    return status;
+}
+
+
+static rmtError TCPSocket_AcceptConnection(TCPSocket* tcp_socket, TCPSocket** client_socket)
+{
+    SocketStatus status;
+    SOCKET s;
+    rmtError error;
+
+    // Ensure there is an incoming connection
+    assert(tcp_socket != NULL);
+    status = TCPSocket_PollStatus(tcp_socket);
+    if (status.error_state != RMT_ERROR_NONE || !status.can_read)
+        return status.error_state;
+
+    // Accept the connection
+    s = accept(tcp_socket->socket, 0, 0);
+    if (s == SOCKET_ERROR)
+        return RMT_ERROR_SOCKET_ACCEPT_FAIL;
+
+    // Create a client socket for the new connection
+    assert(client_socket != NULL);
+    New_0(TCPSocket, *client_socket);
+    if (error != RMT_ERROR_NONE)
+        return error;
+    (*client_socket)->socket = s;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static int TCPSocketWouldBlock()
+{
+#ifdef RMT_PLATFORM_WINDOWS
+    DWORD error = WSAGetLastError();
+    return (error == WSAEWOULDBLOCK);
+ #else
+    int error = errno;
+    return (error == EAGAIN || error == EWOULDBLOCK);
+#endif
+
+}
+
+
+static rmtError TCPSocket_Send(TCPSocket* tcp_socket, const void* data, rmtU32 length, rmtU32 timeout_ms)
+{
+    SocketStatus status;
+    char* cur_data = NULL;
+    char* end_data = NULL;
+    rmtU32 start_ms = 0;
+    rmtU32 cur_ms = 0;
+
+    assert(tcp_socket != NULL);
+
+    start_ms = msTimer_Get();
+
+    // Loop until timeout checking whether data can be written
+    status.can_write = RMT_FALSE;
+    while (!status.can_write)
+    {
+        status = TCPSocket_PollStatus(tcp_socket);
+        if (status.error_state != RMT_ERROR_NONE)
+            return status.error_state;
+
+        cur_ms = msTimer_Get();
+        if (cur_ms - start_ms > timeout_ms)
+            return RMT_ERROR_SOCKET_SEND_TIMEOUT;
+    }
+
+    cur_data = (char*)data;
+    end_data = cur_data + length;
+
+    while (cur_data < end_data)
+    {
+        // Attempt to send the remaining chunk of data
+        int bytes_sent = (int)send(tcp_socket->socket, cur_data, (int)(end_data - cur_data), 0);
+
+        if (bytes_sent == SOCKET_ERROR || bytes_sent == 0)
+        {
+            // Close the connection if sending fails for any other reason other than blocking
+            if (bytes_sent != 0 && !TCPSocketWouldBlock())
+                return RMT_ERROR_SOCKET_SEND_FAIL;
+
+            // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days
+            cur_ms = msTimer_Get();
+            if (cur_ms < start_ms)
+            {
+                start_ms = cur_ms;
+                continue;
+            }
+
+            //
+            // Timeout can happen when:
+            //
+            //    1) endpoint is no longer there
+            //    2) endpoint can't consume quick enough
+            //    3) local buffers overflow
+            //
+            // As none of these are actually errors, we have to pass this timeout back to the caller.
+            //
+            // TODO: This strategy breaks down if a send partially completes and then times out!
+            //
+            if (cur_ms - start_ms > timeout_ms)
+            {
+                return RMT_ERROR_SOCKET_SEND_TIMEOUT;
+            }
+        }
+        else
+        {
+            // Jump over the data sent
+            cur_data += bytes_sent;
+        }
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+static rmtError TCPSocket_Receive(TCPSocket* tcp_socket, void* data, rmtU32 length, rmtU32 timeout_ms)
+{
+    SocketStatus status;
+    char* cur_data = NULL;
+    char* end_data = NULL;
+    rmtU32 start_ms = 0;
+    rmtU32 cur_ms = 0;
+
+    assert(tcp_socket != NULL);
+
+    // Ensure there is data to receive
+    status = TCPSocket_PollStatus(tcp_socket);
+    if (status.error_state != RMT_ERROR_NONE)
+        return status.error_state;
+    if (!status.can_read)
+        return RMT_ERROR_SOCKET_RECV_NO_DATA;
+
+    cur_data = (char*)data;
+    end_data = cur_data + length;
+
+    // Loop until all data has been received
+    start_ms = msTimer_Get();
+    while (cur_data < end_data)
+    {
+        int bytes_received = (int)recv(tcp_socket->socket, cur_data, (int)(end_data - cur_data), 0);
+
+        if (bytes_received == SOCKET_ERROR || bytes_received == 0)
+        {
+            // Close the connection if receiving fails for any other reason other than blocking
+            if (bytes_received != 0 && !TCPSocketWouldBlock())
+                return RMT_ERROR_SOCKET_RECV_FAILED;
+
+            // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days
+            cur_ms = msTimer_Get();
+            if (cur_ms < start_ms)
+            {
+                start_ms = cur_ms;
+                continue;
+            }
+
+            //
+            // Timeout can happen when:
+            //
+            //    1) data is delayed by sender
+            //    2) sender fails to send a complete set of packets
+            //
+            // As not all of these scenarios are errors, we need to pass this information back to the caller.
+            //
+            // TODO: This strategy breaks down if a receive partially completes and then times out!
+            //
+            if (cur_ms - start_ms > timeout_ms)
+            {
+                return RMT_ERROR_SOCKET_RECV_TIMEOUT;
+            }
+        }
+        else
+        {
+            // Jump over the data received
+            cur_data += bytes_received;
+        }
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @SHA1: SHA-1 Cryptographic Hash Function
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+//
+// Typed to allow enforced data size specification
+//
+typedef struct
+{
+    rmtU8 data[20];
+} SHA1;
+
+
+/*
+ Copyright (c) 2011, Micael Hildenborg
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Micael Hildenborg nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ Contributors:
+ Gustav
+ Several members in the gamedev.se forum.
+ Gregory Petrosyan
+ */
+
+
+// Rotate an integer value to left.
+static unsigned int rol(const unsigned int value, const unsigned int steps)
+{
+    return ((value << steps) | (value >> (32 - steps)));
+}
+
+
+// Sets the first 16 integers in the buffert to zero.
+// Used for clearing the W buffert.
+static void clearWBuffert(unsigned int* buffert)
+{
+    int pos;
+    for (pos = 16; --pos >= 0;)
+    {
+        buffert[pos] = 0;
+    }
+}
+
+static void innerHash(unsigned int* result, unsigned int* w)
+{
+    unsigned int a = result[0];
+    unsigned int b = result[1];
+    unsigned int c = result[2];
+    unsigned int d = result[3];
+    unsigned int e = result[4];
+
+    int round = 0;
+
+    #define sha1macro(func,val) \
+    { \
+        const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \
+        e = d; \
+        d = c; \
+        c = rol(b, 30); \
+        b = a; \
+        a = t; \
+    }
+
+    while (round < 16)
+    {
+        sha1macro((b & c) | (~b & d), 0x5a827999)
+        ++round;
+    }
+    while (round < 20)
+    {
+        w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+        sha1macro((b & c) | (~b & d), 0x5a827999)
+        ++round;
+    }
+    while (round < 40)
+    {
+        w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+        sha1macro(b ^ c ^ d, 0x6ed9eba1)
+        ++round;
+    }
+    while (round < 60)
+    {
+        w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+        sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc)
+        ++round;
+    }
+    while (round < 80)
+    {
+        w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1);
+        sha1macro(b ^ c ^ d, 0xca62c1d6)
+        ++round;
+    }
+
+    #undef sha1macro
+
+    result[0] += a;
+    result[1] += b;
+    result[2] += c;
+    result[3] += d;
+    result[4] += e;
+}
+
+
+static void calc(const void* src, const int bytelength, unsigned char* hash)
+{
+    int roundPos;
+    int lastBlockBytes;
+    int hashByte;
+
+    // Init the result array.
+    unsigned int result[5] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 };
+
+    // Cast the void src pointer to be the byte array we can work with.
+    const unsigned char* sarray = (const unsigned char*) src;
+
+    // The reusable round buffer
+    unsigned int w[80];
+
+    // Loop through all complete 64byte blocks.
+    const int endOfFullBlocks = bytelength - 64;
+    int endCurrentBlock;
+    int currentBlock = 0;
+
+    while (currentBlock <= endOfFullBlocks)
+    {
+        endCurrentBlock = currentBlock + 64;
+
+        // Init the round buffer with the 64 byte block data.
+        for (roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4)
+        {
+            // This line will swap endian on big endian and keep endian on little endian.
+            w[roundPos++] = (unsigned int) sarray[currentBlock + 3]
+                | (((unsigned int) sarray[currentBlock + 2]) << 8)
+                | (((unsigned int) sarray[currentBlock + 1]) << 16)
+                | (((unsigned int) sarray[currentBlock]) << 24);
+        }
+        innerHash(result, w);
+    }
+
+    // Handle the last and not full 64 byte block if existing.
+    endCurrentBlock = bytelength - currentBlock;
+    clearWBuffert(w);
+    lastBlockBytes = 0;
+    for (;lastBlockBytes < endCurrentBlock; ++lastBlockBytes)
+    {
+        w[lastBlockBytes >> 2] |= (unsigned int) sarray[lastBlockBytes + currentBlock] << ((3 - (lastBlockBytes & 3)) << 3);
+    }
+    w[lastBlockBytes >> 2] |= 0x80 << ((3 - (lastBlockBytes & 3)) << 3);
+    if (endCurrentBlock >= 56)
+    {
+        innerHash(result, w);
+        clearWBuffert(w);
+    }
+    w[15] = bytelength << 3;
+    innerHash(result, w);
+
+    // Store hash in result pointer, and make sure we get in in the correct order on both endian models.
+    for (hashByte = 20; --hashByte >= 0;)
+    {
+        hash[hashByte] = (result[hashByte >> 2] >> (((3 - hashByte) & 0x3) << 3)) & 0xff;
+    }
+}
+
+
+static SHA1 SHA1_Calculate(const void* src, unsigned int length)
+{
+    SHA1 hash;
+    assert((int)length >= 0);
+    calc(src, length, hash.data);
+    return hash;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @BASE64: Base-64 encoder
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+static const char* b64_encoding_table =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+        "abcdefghijklmnopqrstuvwxyz"
+        "0123456789+/";
+
+
+static rmtU32 Base64_CalculateEncodedLength(rmtU32 length)
+{
+    // ceil(l * 4/3)
+    return 4 * ((length + 2) / 3);
+}
+
+
+static void Base64_Encode(const rmtU8* in_bytes, rmtU32 length, rmtU8* out_bytes)
+{
+    rmtU32 i;
+    rmtU32 encoded_length;
+    rmtU32 remaining_bytes;
+
+    rmtU8* optr = out_bytes;
+
+    for (i = 0; i < length; )
+    {
+        // Read input 3 values at a time, null terminating
+        rmtU32 c0 = i < length ? in_bytes[i++] : 0;
+        rmtU32 c1 = i < length ? in_bytes[i++] : 0;
+        rmtU32 c2 = i < length ? in_bytes[i++] : 0;
+
+        // Encode 4 bytes for ever 3 input bytes
+        rmtU32 triple = (c0 << 0x10) + (c1 << 0x08) + c2;
+        *optr++ = b64_encoding_table[(triple >> 3 * 6) & 0x3F];
+        *optr++ = b64_encoding_table[(triple >> 2 * 6) & 0x3F];
+        *optr++ = b64_encoding_table[(triple >> 1 * 6) & 0x3F];
+        *optr++ = b64_encoding_table[(triple >> 0 * 6) & 0x3F];
+    }
+
+    // Pad output to multiple of 3 bytes with terminating '='
+    encoded_length = Base64_CalculateEncodedLength(length);
+    remaining_bytes = (3 - ((length + 2) % 3)) - 1;
+    for (i = 0; i < remaining_bytes; i++)
+        out_bytes[encoded_length - 1 - i] = '=';
+
+    // Null terminate
+    out_bytes[encoded_length] = 0;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @MURMURHASH: MurmurHash3
+   https://code.google.com/p/smhasher
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+//-----------------------------------------------------------------------------
+
+
+static rmtU32 rotl32(rmtU32 x, rmtS8 r)
+{
+    return (x << r) | (x >> (32 - r));
+}
+
+
+// Block read - if your platform needs to do endian-swapping or can only
+// handle aligned reads, do the conversion here
+static rmtU32 getblock32(const rmtU32* p, int i)
+{
+    return p[i];
+}
+
+
+// Finalization mix - force all bits of a hash block to avalanche
+static rmtU32 fmix32(rmtU32 h)
+{
+    h ^= h >> 16;
+    h *= 0x85ebca6b;
+    h ^= h >> 13;
+    h *= 0xc2b2ae35;
+    h ^= h >> 16;
+    return h;
+}
+
+
+static rmtU32 MurmurHash3_x86_32(const void* key, int len, rmtU32 seed)
+{
+    const rmtU8* data = (const rmtU8*)key;
+    const int nblocks = len / 4;
+
+    rmtU32 h1 = seed;
+
+    const rmtU32 c1 = 0xcc9e2d51;
+    const rmtU32 c2 = 0x1b873593;
+
+    int i;
+
+    const rmtU32 * blocks = (const rmtU32 *)(data + nblocks*4);
+    const rmtU8 * tail = (const rmtU8*)(data + nblocks*4);
+
+    rmtU32 k1 = 0;
+
+    //----------
+    // body
+
+    for (i = -nblocks; i; i++)
+    {
+        rmtU32 k2 = getblock32(blocks,i);
+
+        k2 *= c1;
+        k2 = rotl32(k2,15);
+        k2 *= c2;
+
+        h1 ^= k2;
+        h1 = rotl32(h1,13); 
+        h1 = h1*5+0xe6546b64;
+    }
+
+    //----------
+    // tail
+
+    switch(len & 3)
+    {
+    case 3: k1 ^= tail[2] << 16;
+    case 2: k1 ^= tail[1] << 8;
+    case 1: k1 ^= tail[0];
+        k1 *= c1;
+        k1 = rotl32(k1,15);
+        k1 *= c2;
+        h1 ^= k1;
+    };
+
+    //----------
+    // finalization
+
+    h1 ^= len;
+
+    h1 = fmix32(h1);
+
+    return h1;
+} 
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @WEBSOCKETS: WebSockets
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+enum WebSocketMode
+{
+    WEBSOCKET_NONE = 0,
+    WEBSOCKET_TEXT = 1,
+    WEBSOCKET_BINARY = 2,
+};
+
+
+typedef struct
+{
+    TCPSocket* tcp_socket;
+
+    enum WebSocketMode mode;
+
+    rmtU32 frame_bytes_remaining;
+    rmtU32 mask_offset;
+
+    rmtU8 data_mask[4];
+} WebSocket;
+
+
+static void WebSocket_Close(WebSocket* web_socket);
+
+
+static char* GetField(char* buffer, rsize_t buffer_length, rmtPStr field_name)
+{
+    char* field = NULL;
+    char* buffer_end = buffer + buffer_length - 1;
+
+    rsize_t field_length = strnlen_s(field_name, buffer_length);
+    if (field_length == 0)
+        return NULL;
+
+    // Search for the start of the field
+    if (strstr_s(buffer, buffer_length, field_name, field_length, &field) != EOK)
+        return NULL;
+
+    // Field name is now guaranteed to be in the buffer so its safe to jump over it without hitting the bounds
+    field += strlen(field_name);
+
+    // Skip any trailing whitespace
+    while (*field == ' ')
+    {
+        if (field >= buffer_end)
+            return NULL;
+        field++;
+    }
+
+    return field;
+}
+
+
+static const char websocket_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+static const char websocket_response[] =
+    "HTTP/1.1 101 Switching Protocols\r\n"
+    "Upgrade: websocket\r\n"
+    "Connection: Upgrade\r\n"
+    "Sec-WebSocket-Accept: ";
+
+
+static rmtError WebSocketHandshake(TCPSocket* tcp_socket, rmtPStr limit_host)
+{
+    rmtU32 start_ms, now_ms;
+
+    // Parsing scratchpad
+    char buffer[1024];
+    char* buffer_ptr = buffer;
+    int buffer_len = sizeof(buffer) - 1;
+    char* buffer_end = buffer + buffer_len;
+
+    char response_buffer[256];
+    int response_buffer_len = sizeof(response_buffer) - 1;
+
+    char* version;
+    char* host;
+    char* key;
+    char* key_end;
+    SHA1 hash;
+
+    assert(tcp_socket != NULL);
+
+    start_ms = msTimer_Get();
+
+    // Really inefficient way of receiving the handshake data from the browser
+    // Not really sure how to do this any better, as the termination requirement is \r\n\r\n
+    while (buffer_ptr - buffer < buffer_len)
+    {
+        rmtError error = TCPSocket_Receive(tcp_socket, buffer_ptr, 1, 20);
+        if (error == RMT_ERROR_SOCKET_RECV_FAILED)
+            return error;
+
+        // If there's a stall receiving the data, check for a handshake timeout
+        if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT)
+        {
+            now_ms = msTimer_Get();
+            if (now_ms - start_ms > 1000)
+                return RMT_ERROR_SOCKET_RECV_TIMEOUT;
+
+            continue;
+        }
+
+        // Just in case new enums are added...
+        assert(error == RMT_ERROR_NONE);
+
+        if (buffer_ptr - buffer >= 4)
+        {
+            if (*(buffer_ptr - 3) == '\r' &&
+                *(buffer_ptr - 2) == '\n' &&
+                *(buffer_ptr - 1) == '\r' &&
+                *(buffer_ptr - 0) == '\n')
+                break;
+        }
+
+        buffer_ptr++;
+    }
+    *buffer_ptr = 0;
+
+    // HTTP GET instruction
+    if (memcmp(buffer, "GET", 3) != 0)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET;
+
+    // Look for the version number and verify that it's supported
+    version = GetField(buffer, buffer_len, "Sec-WebSocket-Version:");
+    if (version == NULL)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION;
+    if (buffer_end - version < 2 || (version[0] != '8' && (version[0] != '1' || version[1] != '3')))
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION;
+
+    // Make sure this connection comes from a known host
+    host = GetField(buffer, buffer_len, "Host:");
+    if (host == NULL)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST;
+    if (limit_host != NULL)
+    {
+        rsize_t limit_host_len = strnlen_s(limit_host, 128);
+        char* found = NULL;
+        if (strstr_s(host, buffer_end - host, limit_host, limit_host_len, &found) != EOK)
+            return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST;
+    }
+
+    // Look for the key start and null-terminate it within the receive buffer
+    key = GetField(buffer, buffer_len, "Sec-WebSocket-Key:");
+    if (key == NULL)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY;
+    if (strstr_s(key, buffer_end - key, "\r\n", 2, &key_end) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY;
+    *key_end = 0;
+
+    // Concatenate the browser's key with the WebSocket Protocol GUID and base64 encode
+    // the hash, to prove to the browser that this is a bonafide WebSocket server
+    buffer[0] = 0;
+    if (strncat_s(buffer, buffer_len, key, key_end - key) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL;
+    if (strncat_s(buffer, buffer_len, websocket_guid, sizeof(websocket_guid)) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL;
+    hash = SHA1_Calculate(buffer, (rmtU32)strnlen_s(buffer, buffer_len));
+    Base64_Encode(hash.data, sizeof(hash.data), (rmtU8*)buffer);
+
+    // Send the response back to the server with a longer timeout than usual
+    response_buffer[0] = 0;
+    if (strncat_s(response_buffer, response_buffer_len, websocket_response, sizeof(websocket_response)) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL;
+    if (strncat_s(response_buffer, response_buffer_len, buffer, buffer_len) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL;
+    if (strncat_s(response_buffer, response_buffer_len, "\r\n\r\n", 4) != EOK)
+        return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL;
+
+    return TCPSocket_Send(tcp_socket, response_buffer, (rmtU32)strnlen_s(response_buffer, response_buffer_len), 1000);
+}
+
+
+static rmtError WebSocket_Constructor(WebSocket* web_socket, TCPSocket* tcp_socket)
+{
+    rmtError error = RMT_ERROR_NONE;
+
+    assert(web_socket != NULL);
+    web_socket->tcp_socket = tcp_socket;
+    web_socket->mode = WEBSOCKET_NONE;
+    web_socket->frame_bytes_remaining = 0;
+    web_socket->mask_offset = 0;
+    web_socket->data_mask[0] = 0;
+    web_socket->data_mask[1] = 0;
+    web_socket->data_mask[2] = 0;
+    web_socket->data_mask[3] = 0;
+
+    // Caller can optionally specify which TCP socket to use
+    if (web_socket->tcp_socket == NULL)
+        New_0(TCPSocket, web_socket->tcp_socket);
+
+    return error;
+}
+
+
+static void WebSocket_Destructor(WebSocket* web_socket)
+{
+    WebSocket_Close(web_socket);
+}
+
+
+static rmtError WebSocket_RunServer(WebSocket* web_socket, rmtU32 port, enum WebSocketMode mode)
+{
+    // Create the server's listening socket
+    assert(web_socket != NULL);
+    web_socket->mode = mode;
+    return TCPSocket_RunServer(web_socket->tcp_socket, (rmtU16)port);
+}
+
+
+static void WebSocket_Close(WebSocket* web_socket)
+{
+    assert(web_socket != NULL);
+    Delete(TCPSocket, web_socket->tcp_socket);
+}
+
+
+static SocketStatus WebSocket_PollStatus(WebSocket* web_socket)
+{
+    assert(web_socket != NULL);
+    return TCPSocket_PollStatus(web_socket->tcp_socket);
+}
+
+
+static rmtError WebSocket_AcceptConnection(WebSocket* web_socket, WebSocket** client_socket)
+{
+    TCPSocket* tcp_socket = NULL;
+    rmtError error;
+
+    // Is there a waiting connection?
+    assert(web_socket != NULL);
+    error = TCPSocket_AcceptConnection(web_socket->tcp_socket, &tcp_socket);
+    if (error != RMT_ERROR_NONE || tcp_socket == NULL)
+        return error;
+
+    // Need a successful handshake between client/server before allowing the connection
+    // TODO: Specify limit_host
+    error = WebSocketHandshake(tcp_socket, NULL);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Allocate and return a new client socket
+    assert(client_socket != NULL);
+    New_1(WebSocket, *client_socket, tcp_socket);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    (*client_socket)->mode = web_socket->mode;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void WriteSize(rmtU32 size, rmtU8* dest, rmtU32 dest_size, rmtU32 dest_offset)
+{
+    int size_size = dest_size - dest_offset;
+    rmtU32 i;
+    for (i = 0; i < dest_size; i++)
+    {
+        int j = i - dest_offset;
+        dest[i] = (j < 0) ? 0 : (size >> ((size_size - j - 1) * 8)) & 0xFF;
+    }
+}
+
+
+static rmtError WebSocket_Send(WebSocket* web_socket, const void* data, rmtU32 length, rmtU32 timeout_ms)
+{
+    rmtError error;
+    SocketStatus status;
+    rmtU8 final_fragment, frame_type, frame_header[10];
+    rmtU32 frame_header_size;
+
+    assert(web_socket != NULL);
+
+    // Can't send if there are socket errors
+    status = WebSocket_PollStatus(web_socket);
+    if (status.error_state != RMT_ERROR_NONE)
+        return status.error_state;
+
+    final_fragment = 0x1 << 7;
+    frame_type = (rmtU8)web_socket->mode;
+    frame_header[0] = final_fragment | frame_type;
+
+    // Construct the frame header, correctly applying the narrowest size
+    frame_header_size = 0;
+    if (length <= 125)
+    {
+        frame_header_size = 2;
+        frame_header[1] = (rmtU8)length;
+    }
+    else if (length <= 65535)
+    {
+        frame_header_size = 2 + 2;
+        frame_header[1] = 126;
+        WriteSize(length, frame_header + 2, 2, 0);
+    }
+    else
+    {
+        frame_header_size = 2 + 8;
+        frame_header[1] = 127;
+        WriteSize(length, frame_header + 2, 8, 4);
+    }
+
+    // Send frame header
+    assert(data != NULL);
+    error = TCPSocket_Send(web_socket->tcp_socket, frame_header, frame_header_size, timeout_ms);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Send frame data separately so that we don't have to allocate memory or memcpy it into
+    // the same buffer as the header.
+    // If this step times out then the frame data will be discarded and the browser will receive
+    // an invalid frame without its data, forcing a disconnect error.
+    // Before things get that far, flag this as a send fail and let the server schedule a graceful
+    // disconnect.
+    error = TCPSocket_Send(web_socket->tcp_socket, data, length, timeout_ms);
+    if (error == RMT_ERROR_SOCKET_SEND_TIMEOUT)
+        error = RMT_ERROR_SOCKET_SEND_FAIL;
+
+    return error;
+}
+
+
+static rmtError ReceiveFrameHeader(WebSocket* web_socket)
+{
+    // TODO: Specify infinite timeout?
+
+    rmtError error;
+    rmtU8 msg_header[2] = { 0, 0 };
+    int msg_length, size_bytes_remaining, i;
+    rmtBool mask_present;
+
+    assert(web_socket != NULL);
+
+    // Get message header
+    error = TCPSocket_Receive(web_socket->tcp_socket, msg_header, 2, 20);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Check for WebSocket Protocol disconnect
+    if (msg_header[0] == 0x88)
+        return RMT_ERROR_WEBSOCKET_DISCONNECTED;
+
+    // Check that the client isn't sending messages we don't understand
+    if (msg_header[0] != 0x81 && msg_header[0] != 0x82)
+        return RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER;
+
+    // Get message length and check to see if it's a marker for a wider length
+    msg_length = msg_header[1] & 0x7F;
+    size_bytes_remaining = 0;
+    switch (msg_length)
+    {
+        case 126: size_bytes_remaining = 2; break;
+        case 127: size_bytes_remaining = 8; break;
+    }
+
+    if (size_bytes_remaining > 0)
+    {
+        // Receive the wider bytes of the length
+        rmtU8 size_bytes[4];
+        error = TCPSocket_Receive(web_socket->tcp_socket, size_bytes, size_bytes_remaining, 20);
+        if (error != RMT_ERROR_NONE)
+            return RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE;
+
+        // Calculate new length, MSB first
+        msg_length = 0;
+        for (i = 0; i < size_bytes_remaining; i++)
+            msg_length |= size_bytes[i] << ((size_bytes_remaining - 1 - i) * 8);
+    }
+
+    // Receive any message data masks
+    mask_present = (msg_header[1] & 0x80) != 0 ? RMT_TRUE : RMT_FALSE;
+    if (mask_present)
+    {
+        error = TCPSocket_Receive(web_socket->tcp_socket, web_socket->data_mask, 4, 20);
+        if (error != RMT_ERROR_NONE)
+            return error;
+    }
+
+    web_socket->frame_bytes_remaining = msg_length;
+    web_socket->mask_offset = 0;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static rmtError WebSocket_Receive(WebSocket* web_socket, void* data, rmtU32* msg_len, rmtU32 length, rmtU32 timeout_ms)
+{
+    SocketStatus status;
+    char* cur_data;
+    char* end_data;
+    rmtU32 start_ms, now_ms;
+    rmtU32 bytes_to_read;
+    rmtError error;
+
+    assert(web_socket != NULL);
+
+    // Can't read with any socket errors
+    status = WebSocket_PollStatus(web_socket);
+    if (status.error_state != RMT_ERROR_NONE)
+        return status.error_state;
+
+    cur_data = (char*)data;
+    end_data = cur_data + length;
+
+    start_ms = msTimer_Get();
+    while (cur_data < end_data)
+    {
+        // Get next WebSocket frame if we've run out of data to read from the socket
+        if (web_socket->frame_bytes_remaining == 0)
+        {
+            error = ReceiveFrameHeader(web_socket);
+            if (error != RMT_ERROR_NONE)
+                return error;
+
+            // Set output message length only on initial receive
+            if (msg_len != NULL)
+                *msg_len = web_socket->frame_bytes_remaining;
+        }
+
+        // Read as much required data as possible
+        bytes_to_read = web_socket->frame_bytes_remaining < length ? web_socket->frame_bytes_remaining : length;
+        error = TCPSocket_Receive(web_socket->tcp_socket, cur_data, bytes_to_read, 20);
+        if (error == RMT_ERROR_SOCKET_RECV_FAILED)
+            return error;
+
+        // If there's a stall receiving the data, check for timeout
+        if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT)
+        {
+            now_ms = msTimer_Get();
+            if (now_ms - start_ms > timeout_ms)
+                return RMT_ERROR_SOCKET_RECV_TIMEOUT;
+            continue;
+        }
+
+        // Apply data mask
+        if (*(rmtU32*)web_socket->data_mask != 0)
+        {
+            rmtU32 i;
+            for (i = 0; i < bytes_to_read; i++)
+            {
+                *((rmtU8*)cur_data + i) ^= web_socket->data_mask[web_socket->mask_offset & 3];
+                web_socket->mask_offset++;
+            }
+        }
+
+        cur_data += bytes_to_read;
+        web_socket->frame_bytes_remaining -= bytes_to_read;
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @MESSAGEQ: Multiple producer, single consumer message queue
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+typedef enum MessageID
+{
+    MsgID_NotReady,
+    MsgID_LogText,
+    MsgID_SampleTree,
+} MessageID;
+
+
+typedef struct Message
+{
+    MessageID id;
+
+    rmtU32 payload_size;
+
+    // For telling which thread the message came from in the debugger
+    struct ThreadSampler* thread_sampler;
+
+    rmtU8 payload[1];
+} Message;
+
+
+// Multiple producer, single consumer message queue that uses its own data buffer
+// to store the message data. 
+typedef struct MessageQueue
+{
+    rmtU32 size;
+
+    // The physical address of this data buffer is pointed to by two sequential
+    // virtual memory pages, allowing automatic wrap-around of any reads or writes
+    // that exceed the limits of the buffer.
+    VirtualMirrorBuffer* data;
+
+    // Read/write position never wrap allowing trivial overflow checks
+    // with easier debugging
+    rmtU32 read_pos;
+    rmtU32 write_pos;
+
+} MessageQueue;
+
+
+static rmtError MessageQueue_Constructor(MessageQueue* queue, rmtU32 size)
+{
+    rmtError error;
+
+    assert(queue != NULL);
+
+    // Set defaults
+    queue->size = 0;
+    queue->data = NULL;
+    queue->read_pos = 0;
+    queue->write_pos = 0;
+
+    New_2(VirtualMirrorBuffer, queue->data, size, 10);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // The mirror buffer needs to be page-aligned and will change the requested
+    // size to match that.
+    queue->size = queue->data->size;
+
+    // Set the entire buffer to not ready message
+    memset(queue->data->ptr, MsgID_NotReady, queue->size);
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void MessageQueue_Destructor(MessageQueue* queue)
+{
+    assert(queue != NULL);
+    Delete(VirtualMirrorBuffer, queue->data);
+}
+
+
+static Message* MessageQueue_AllocMessage(MessageQueue* queue, rmtU32 payload_size, struct ThreadSampler* thread_sampler)
+{
+    Message* msg;
+
+    rmtU32 write_size = sizeof(Message) + payload_size;
+
+    assert(queue != NULL);
+
+    while (1)
+    {
+        // Check for potential overflow
+        rmtU32 s = queue->size;
+        rmtU32 r = queue->read_pos;
+        rmtU32 w = queue->write_pos;
+        if ((int)(w - r) > ((int)(s - write_size)))
+            return NULL;
+
+        // Point to the newly allocated space
+        msg = (Message*)(queue->data->ptr + (w & (s - 1)));
+
+        // Increment the write position, leaving the loop if this is the thread that succeeded
+        if (AtomicCompareAndSwap(&queue->write_pos, w, w + write_size) == RMT_TRUE)
+        {
+            // Safe to set payload size after thread claims ownership of this allocated range
+            msg->payload_size = payload_size;
+            msg->thread_sampler = thread_sampler;
+            break;
+        }
+    }
+
+    return msg;
+}
+
+
+static void MessageQueue_CommitMessage(MessageQueue* queue, Message* message, MessageID id)
+{
+    assert(queue != NULL);
+    assert(message != NULL);
+
+    // Ensure message writes complete before commit
+    WriteFence();
+
+    // Setting the message ID signals to the consumer that the message is ready
+    assert(message->id == MsgID_NotReady);
+    message->id = id;
+}
+
+
+Message* MessageQueue_PeekNextMessage(MessageQueue* queue)
+{
+    Message* ptr;
+    rmtU32 r;
+
+    assert(queue != NULL);
+
+    // First check that there are bytes queued
+    if (queue->write_pos - queue->read_pos == 0)
+        return NULL;
+
+    // Messages are in the queue but may not have been commit yet
+    // Messages behind this one may have been commit but it's not reachable until
+    // the next one in the queue is ready.
+    r = queue->read_pos & (queue->size - 1);
+    ptr = (Message*)(queue->data->ptr + r);
+    if (ptr->id != MsgID_NotReady)
+        return ptr;
+
+    return NULL;
+}
+
+
+static void MessageQueue_ConsumeNextMessage(MessageQueue* queue, Message* message)
+{
+    rmtU32 message_size;
+
+    assert(queue != NULL);
+    assert(message != NULL);
+
+    // Setting the message ID to "not ready" serves as a marker to the consumer that even though
+    // space has been allocated for a message, the message isn't ready to be consumed
+    // yet.
+    //
+    // We can't do that when allocating the message because multiple threads will be fighting for
+    // the same location. Instead, clear out any messages just read by the consumer before advancing
+    // the read position so that a winning thread's allocation will inherit the "not ready" state.
+    //
+    // This costs some write bandwidth and has the potential to flush cache to other cores.
+    message_size = sizeof(Message) + message->payload_size;
+    memset(message, MsgID_NotReady, message_size);
+
+    // Ensure clear completes before advancing the read position
+    WriteFence();
+    queue->read_pos += message_size;
+}
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @NETWORK: Network Server
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+typedef struct
+{
+    WebSocket* listen_socket;
+
+    WebSocket* client_socket;
+
+    rmtU32 last_ping_time;
+
+    rmtU16 port;
+} Server;
+
+
+static rmtError Server_CreateListenSocket(Server* server, rmtU16 port)
+{
+    rmtError error = RMT_ERROR_NONE;
+
+    New_1(WebSocket, server->listen_socket, NULL);
+    if (error == RMT_ERROR_NONE)
+        error = WebSocket_RunServer(server->listen_socket, port, WEBSOCKET_TEXT);
+
+    return error;
+}
+
+
+static rmtError Server_Constructor(Server* server, rmtU16 port)
+{
+    assert(server != NULL);
+    server->listen_socket = NULL;
+    server->client_socket = NULL;
+    server->last_ping_time = 0;
+    server->port = port;
+
+    // Create the listening WebSocket
+    return Server_CreateListenSocket(server, port);
+}
+
+
+static void Server_Destructor(Server* server)
+{
+    assert(server != NULL);
+    Delete(WebSocket, server->client_socket);
+    Delete(WebSocket, server->listen_socket);
+}
+
+
+static rmtBool Server_IsClientConnected(Server* server)
+{
+    assert(server != NULL);
+    return server->client_socket != NULL ? RMT_TRUE : RMT_FALSE;
+}
+
+
+static void Server_DisconnectClient(Server* server)
+{
+    WebSocket* client_socket;
+
+    assert(server != NULL);
+
+    // NULL the variable before destroying the socket
+    client_socket = server->client_socket;
+    server->client_socket = NULL;
+    WriteFence();
+    Delete(WebSocket, client_socket);
+}
+
+
+static rmtError Server_Send(Server* server, const void* data, rmtU32 length, rmtU32 timeout)
+{
+    assert(server != NULL);
+    if (Server_IsClientConnected(server))
+    {
+        rmtError error = WebSocket_Send(server->client_socket, data, length, timeout);
+        if (error == RMT_ERROR_SOCKET_SEND_FAIL)
+            Server_DisconnectClient(server);
+
+        return error;
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+static rmtError Server_ReceiveMessage(Server* server, char message_first_byte, rmtU32 message_length)
+{
+    char message_data[1024];
+    rmtError error;
+
+    // Check for potential message data overflow
+    if (message_length >= sizeof(message_data) - 1)
+    {
+        rmt_LogText("Ignoring console input bigger than internal receive buffer (1024 bytes)");
+        return RMT_ERROR_NONE;
+    }
+
+    // Receive the rest of the message
+    message_data[0] = message_first_byte;
+    error = WebSocket_Receive(server->client_socket, message_data + 1, NULL, message_length - 1, 100);
+    if (error != RMT_ERROR_NONE)
+        return error;
+    message_data[message_length] = 0;
+
+    // Each message must have a descriptive 4 byte header
+    if (message_length < 4)
+        return RMT_ERROR_NONE;
+
+    // Silly check for console input message ('CONI')
+    // (don't want to add safe strcmp to lib yet)
+    if (message_data[0] == 'C' && message_data[1] == 'O' && message_data[2] == 'N' && message_data[3] == 'I')
+    {
+        // Pass on to any registered handler 
+        if (g_Settings.input_handler != NULL)
+            g_Settings.input_handler(message_data + 4, g_Settings.input_handler_context);
+
+        rmt_LogText("Console message received...");
+        rmt_LogText(message_data + 4);
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void Server_Update(Server* server)
+{
+    rmtU32 cur_time;
+
+    assert(server != NULL);
+
+    // Recreate the listening socket if it's been destroyed earlier
+    if (server->listen_socket == NULL)
+        Server_CreateListenSocket(server, server->port);
+
+    if (server->listen_socket != NULL && server->client_socket == NULL)
+    {
+        // Accept connections as long as there is no client connected
+        WebSocket* client_socket = NULL;
+        rmtError error = WebSocket_AcceptConnection(server->listen_socket, &client_socket);
+        if (error == RMT_ERROR_NONE)
+        {
+            server->client_socket = client_socket;
+        }
+        else
+        {
+            // Destroy the listen socket on failure to accept
+            // It will get recreated in another update
+            Delete(WebSocket, server->listen_socket);
+        }
+    }
+
+    else
+    {
+        // Check for any incoming messages
+        char message_first_byte;
+        rmtU32 message_length;
+        rmtError error = WebSocket_Receive(server->client_socket, &message_first_byte, &message_length, 1, 0);
+        if (error == RMT_ERROR_NONE)
+        {
+            // Parse remaining message
+            error = Server_ReceiveMessage(server, message_first_byte, message_length);
+            if (error != RMT_ERROR_NONE)
+                Server_DisconnectClient(server);
+        }
+        else if (error == RMT_ERROR_SOCKET_RECV_NO_DATA)
+        {
+            // no data available
+        }
+        else if (error == RMT_ERROR_SOCKET_RECV_TIMEOUT)
+        {
+            // data not available yet, can afford to ignore as we're only reading the first byte
+        }
+        else
+        {
+            // Anything else is an error that may have closed the connection
+            Server_DisconnectClient(server);
+        }
+    }
+
+    // Send pings to the client every second
+    cur_time = msTimer_Get();
+    if (cur_time - server->last_ping_time > 1000)
+    {
+        rmtPStr ping_message = "{ \"id\": \"PING\" }";
+        Server_Send(server, ping_message, (rmtU32)strlen(ping_message), 20);
+        server->last_ping_time = cur_time;
+    }
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @JSON: Basic, text-based JSON serialisation
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+//
+// Simple macro for hopefully making the serialisation a little clearer by hiding the error handling
+//
+#define JSON_ERROR_CHECK(stmt) { error = stmt; if (error != RMT_ERROR_NONE) return error; }
+
+
+
+static rmtError json_OpenObject(Buffer* buffer)
+{
+    return Buffer_Write(buffer, (void*)"{", 1);
+}
+
+
+static rmtError json_CloseObject(Buffer* buffer)
+{
+    return Buffer_Write(buffer, (void*)"}", 1);
+}
+
+
+static rmtError json_Comma(Buffer* buffer)
+{
+    return Buffer_Write(buffer, (void*)",", 1);
+}
+
+
+static rmtError json_Colon(Buffer* buffer)
+{
+    return Buffer_Write(buffer, (void*)":", 1);
+}
+
+
+static rmtError json_String(Buffer* buffer, rmtPStr string)
+{
+    rmtError error;
+    JSON_ERROR_CHECK(Buffer_Write(buffer, (void*)"\"", 1));
+    JSON_ERROR_CHECK(Buffer_WriteString(buffer, string));
+    return Buffer_Write(buffer, (void*)"\"", 1);
+}
+
+
+static rmtError json_FieldStr(Buffer* buffer, rmtPStr name, rmtPStr value)
+{
+    rmtError error;
+    JSON_ERROR_CHECK(json_String(buffer, name));
+    JSON_ERROR_CHECK(json_Colon(buffer));
+    return json_String(buffer, value);
+}
+
+
+static rmtError json_FieldU64(Buffer* buffer, rmtPStr name, rmtU64 value)
+{
+    static char temp_buf[32];
+
+    char* end;
+    char* tptr;
+
+    json_String(buffer, name);
+    json_Colon(buffer);
+
+    if (value == 0)
+        return Buffer_Write(buffer, (void*)"0", 1);
+
+    // Null terminate and start at the end
+    end = temp_buf + sizeof(temp_buf) - 1;
+    *end = 0;
+    tptr = end;
+
+    // Loop through the value with radix 10
+    do
+    {
+        rmtU64 next_value = value / 10;
+        *--tptr = (char)('0' + (value - next_value * 10));
+        value = next_value;
+    } while (value);
+
+    return Buffer_Write(buffer, tptr, (rmtU32)(end - tptr));
+}
+
+
+static rmtError json_OpenArray(Buffer* buffer, rmtPStr name)
+{
+    rmtError error;
+    JSON_ERROR_CHECK(json_String(buffer, name));
+    JSON_ERROR_CHECK(json_Colon(buffer));
+    return Buffer_Write(buffer, (void*)"[", 1);
+}
+
+
+static rmtError json_CloseArray(Buffer* buffer)
+{
+    return Buffer_Write(buffer, (void*)"]", 1);
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @SAMPLE: Base Sample Description for CPU by default
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+enum SampleType
+{
+    SampleType_CPU,
+    SampleType_CUDA,
+    SampleType_D3D11,
+    SampleType_OpenGL,
+    SampleType_Count,
+};
+
+
+typedef struct Sample
+{
+    // Inherit so that samples can be quickly allocated
+    ObjectLink ObjectLink;
+
+    enum SampleType type;
+
+    // Used to anonymously copy sample data without knowning its type
+    rmtU32 size_bytes;
+
+    // Sample name and unique hash
+    rmtPStr name;
+    rmtU32 name_hash;
+
+    // Unique, persistent ID among all samples
+    rmtU32 unique_id;
+
+    // Null-terminated string storing the hash-prefixed 6-digit colour
+    rmtU8 unique_id_html_colour[8];
+
+    // Links to related samples in the tree
+    struct Sample* parent;
+    struct Sample* first_child;
+    struct Sample* last_child;
+    struct Sample* next_sibling;
+
+    // Keep track of child count to distinguish from repeated calls to the same function at the same stack level
+    // This is also mixed with the callstack hash to allow consistent addressing of any point in the tree
+    rmtU32 nb_children;
+
+    // Start and end of the sample in microseconds
+    rmtU64 us_start;
+    rmtU64 us_end;
+
+} Sample;
+
+
+static rmtError Sample_Constructor(Sample* sample)
+{
+    assert(sample != NULL);
+
+    ObjectLink_Constructor((ObjectLink*)sample);
+
+    sample->type = SampleType_CPU;
+    sample->size_bytes = sizeof(Sample);
+    sample->name = NULL;
+    sample->name_hash = 0;
+    sample->unique_id = 0;
+    sample->unique_id_html_colour[0] = '#';
+    sample->unique_id_html_colour[1] = 0;
+    sample->unique_id_html_colour[7] = 0;
+    sample->parent = NULL;
+    sample->first_child = NULL;
+    sample->last_child = NULL;
+    sample->next_sibling = NULL;
+    sample->nb_children = 0;
+    sample->us_start = 0;
+    sample->us_end = 0;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void Sample_Destructor(Sample* sample)
+{
+    RMT_UNREFERENCED_PARAMETER(sample);
+}
+
+
+static void Sample_Prepare(Sample* sample, rmtPStr name, rmtU32 name_hash, Sample* parent)
+{
+    sample->name = name;
+    sample->name_hash = name_hash;
+    sample->unique_id = 0;
+    sample->parent = parent;
+    sample->first_child = NULL;
+    sample->last_child = NULL;
+    sample->next_sibling = NULL;
+    sample->nb_children = 0;
+    sample->us_start = 0;
+    sample->us_end = 0;
+}
+
+
+static rmtError json_SampleArray(Buffer* buffer, Sample* first_sample, rmtPStr name);
+
+
+static rmtError json_Sample(Buffer* buffer, Sample* sample)
+{
+    rmtError error;
+
+    assert(sample != NULL);
+
+    JSON_ERROR_CHECK(json_OpenObject(buffer));
+
+        JSON_ERROR_CHECK(json_FieldStr(buffer, "name", sample->name));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldU64(buffer, "id", sample->unique_id));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldStr(buffer, "colour", (rmtPStr)sample->unique_id_html_colour));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldU64(buffer, "us_start", sample->us_start));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldU64(buffer, "us_length", maxS64(sample->us_end - sample->us_start, 0)));
+
+        if (sample->first_child != NULL)
+        {
+            JSON_ERROR_CHECK(json_Comma(buffer));
+            JSON_ERROR_CHECK(json_SampleArray(buffer, sample->first_child, "children"));
+        }
+
+    return json_CloseObject(buffer);
+}
+
+
+static rmtError json_SampleArray(Buffer* buffer, Sample* first_sample, rmtPStr name)
+{
+    rmtError error;
+
+    Sample* sample;
+
+    JSON_ERROR_CHECK(json_OpenArray(buffer, name));
+
+    for (sample = first_sample; sample != NULL; sample = sample->next_sibling)
+    {
+        JSON_ERROR_CHECK(json_Sample(buffer, sample));
+        if (sample->next_sibling != NULL)
+            JSON_ERROR_CHECK(json_Comma(buffer));
+    }
+
+    return json_CloseArray(buffer);
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @SAMPLETREE: A tree of samples with their allocator
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+typedef struct SampleTree
+{
+    // Allocator for all samples 
+    ObjectAllocator* allocator;
+
+    // Root sample for all samples created by this thread
+    Sample* root;
+
+    // Most recently pushed sample
+    Sample* current_parent;
+
+} SampleTree;
+
+
+static rmtError SampleTree_Constructor(SampleTree* tree, rmtU32 sample_size, ObjConstructor constructor, ObjDestructor destructor)
+{
+    rmtError error;
+
+    assert(tree != NULL);
+
+    tree->allocator = NULL;
+    tree->root = NULL;
+    tree->current_parent = NULL;
+
+    // Create the sample allocator
+    New_3(ObjectAllocator, tree->allocator, sample_size, constructor, destructor);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Create a root sample that's around for the lifetime of the thread
+    error = ObjectAllocator_Alloc(tree->allocator, (void**)&tree->root);
+    if (error != RMT_ERROR_NONE)
+        return error;
+    Sample_Prepare(tree->root, "<Root Sample>", 0, NULL);
+    tree->current_parent = tree->root;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void SampleTree_Destructor(SampleTree* tree)
+{
+    assert(tree != NULL);
+
+    if (tree->root != NULL)
+    {
+        ObjectAllocator_Free(tree->allocator, tree->root);
+        tree->root = NULL;
+    }
+
+    Delete(ObjectAllocator, tree->allocator);
+}
+
+
+static rmtU32 HashCombine(rmtU32 hash_a, rmtU32 hash_b)
+{
+    // A sequence of 32 uniformly random bits so that each bit of the combined hash is changed on application
+    // Derived from the golden ratio: UINT_MAX / ((1 + sqrt(5)) / 2)
+    // In reality it's just an arbitrary value which happens to work well, avoiding mapping all zeros to zeros.
+    // http://burtleburtle.net/bob/hash/doobs.html
+    static rmtU32 random_bits = 0x9E3779B9;
+    hash_a ^= hash_b + random_bits + (hash_a << 6) + (hash_a >> 2);
+    return hash_a;
+}
+
+
+static rmtError SampleTree_Push(SampleTree* tree, rmtPStr name, rmtU32 name_hash, Sample** sample)
+{
+    Sample* parent;
+    rmtError error;
+    rmtU32 unique_id;
+
+    // As each tree has a root sample node allocated, a parent must always be present
+    assert(tree != NULL);
+    assert(tree->current_parent != NULL);
+    parent = tree->current_parent;
+
+    if (parent->last_child != NULL && parent->last_child->name_hash == name_hash)
+    {
+        // TODO: Collapse siblings with flag exception?
+        //       Note that above check is not enough - requires a linear search
+    }
+    if (parent->name_hash == name_hash)
+    {
+        // TODO: Collapse recursion on flag?
+    }
+
+    // Allocate a new sample
+    error = ObjectAllocator_Alloc(tree->allocator, (void**)sample);
+    if (error != RMT_ERROR_NONE)
+        return error;
+    Sample_Prepare(*sample, name, name_hash, parent);
+
+    // Generate a unique ID for this sample in the tree
+    unique_id = parent->unique_id;
+    unique_id = HashCombine(unique_id, (*sample)->name_hash);
+    unique_id = HashCombine(unique_id, parent->nb_children);
+    (*sample)->unique_id = unique_id;
+
+    // Add sample to its parent
+    parent->nb_children++;
+    if (parent->first_child == NULL)
+    {
+        parent->first_child = *sample;
+        parent->last_child = *sample;
+    }
+    else
+    {
+        assert(parent->last_child != NULL);
+        parent->last_child->next_sibling = *sample;
+        parent->last_child = *sample;
+    }
+
+    // Make this sample the new parent of any newly created samples
+    tree->current_parent = *sample;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void SampleTree_Pop(SampleTree* tree, Sample* sample)
+{
+    assert(tree != NULL);
+    assert(sample != NULL);
+    assert(sample != tree->root);
+    tree->current_parent = sample->parent;
+}
+
+
+static ObjectLink* FlattenSampleTree(Sample* sample, rmtU32* nb_samples)
+{
+    Sample* child;
+    ObjectLink* cur_link = &sample->ObjectLink;
+
+    assert(sample != NULL);
+    assert(nb_samples != NULL);
+
+    *nb_samples += 1;
+    sample->ObjectLink.next = (ObjectLink*)sample->first_child;
+
+    // Link all children together
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+    {
+        ObjectLink* last_link = FlattenSampleTree(child, nb_samples);
+        last_link->next = (ObjectLink*)child->next_sibling;
+        cur_link = last_link;
+    }
+
+    // Clear child info
+    sample->first_child = NULL;
+    sample->last_child = NULL;
+    sample->nb_children = 0;
+
+    return cur_link;
+}
+
+
+static void FreeSampleTree(Sample* sample, ObjectAllocator* allocator)
+{
+    // Chain all samples together in a flat list
+    rmtU32 nb_cleared_samples = 0;
+    ObjectLink* last_link = FlattenSampleTree(sample, &nb_cleared_samples);
+
+    // Release the complete sample memory range
+    if (sample->ObjectLink.next != NULL)
+        ObjectAllocator_FreeRange(allocator, sample, last_link, nb_cleared_samples);
+    else
+        ObjectAllocator_Free(allocator, sample);
+}
+
+
+typedef struct Msg_SampleTree
+{
+    Sample* root_sample;
+
+    ObjectAllocator* allocator;
+
+    rmtPStr thread_name;
+} Msg_SampleTree;
+
+
+static void AddSampleTreeMessage(MessageQueue* queue, Sample* sample, ObjectAllocator* allocator, rmtPStr thread_name, struct ThreadSampler* thread_sampler)
+{
+    Msg_SampleTree* payload;
+
+    // Attempt to allocate a message for sending the tree to the viewer
+    Message* message = MessageQueue_AllocMessage(queue, sizeof(Msg_SampleTree), thread_sampler);
+    if (message == NULL)
+    {
+        // Discard the tree on failure
+        FreeSampleTree(sample, allocator);
+        return;
+    }
+
+    // Populate and commit
+    payload = (Msg_SampleTree*)message->payload;
+    payload->root_sample = sample;
+    payload->allocator = allocator;
+    payload->thread_name = thread_name;
+    MessageQueue_CommitMessage(queue, message, MsgID_SampleTree);
+}
+
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @TSAMPLER: Per-Thread Sampler
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+typedef struct ThreadSampler
+{
+    // Name to assign to the thread in the viewer
+    rmtS8 name[64];
+
+    // Store a unique sample tree for each type
+    SampleTree* sample_trees[SampleType_Count];
+
+    // Next in the global list of active thread samplers
+    struct ThreadSampler* volatile next;
+
+} ThreadSampler;
+
+static rmtS32 countThreads = 0;
+
+static rmtError ThreadSampler_Constructor(ThreadSampler* thread_sampler)
+{
+    rmtError error;
+    int i;
+
+    assert(thread_sampler != NULL);
+
+    // Set defaults
+    for (i = 0; i < SampleType_Count; i++)
+        thread_sampler->sample_trees[i] = NULL; 
+    thread_sampler->next = NULL;
+
+    // Set the initial name to Thread0 etc.
+    thread_sampler->name[0] = 0;
+    strncat_s(thread_sampler->name, sizeof(thread_sampler->name), "Thread", 6);
+    itoahex_s(thread_sampler->name + 6, sizeof(thread_sampler->name) - 6, AtomicAdd(&countThreads, 1));
+
+    // Create the CPU sample tree only - the rest are created on-demand as they need
+    // extra context information to function correctly.
+    New_3(SampleTree, thread_sampler->sample_trees[SampleType_CPU], sizeof(Sample), (ObjConstructor)Sample_Constructor, (ObjDestructor)Sample_Destructor);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void ThreadSampler_Destructor(ThreadSampler* ts)
+{
+    int i;
+
+    assert(ts != NULL);
+    for (i = 0; i < SampleType_Count; i++)
+        Delete(SampleTree, ts->sample_trees[i]);
+}
+
+
+static rmtError ThreadSampler_Push(ThreadSampler* ts, SampleTree* tree, rmtPStr name, rmtU32 name_hash, Sample** sample)
+{
+    RMT_UNREFERENCED_PARAMETER(ts);
+    return SampleTree_Push(tree, name, name_hash, sample);
+}
+
+
+static rmtBool ThreadSampler_Pop(ThreadSampler* ts, MessageQueue* queue, Sample* sample)
+{
+    SampleTree* tree = ts->sample_trees[sample->type];
+    SampleTree_Pop(tree, sample);
+
+    // Are we back at the root?
+    if (tree->current_parent == tree->root)
+    {
+        // Disconnect all samples from the root and pack in the chosen message queue
+        Sample* root = tree->root;
+        root->first_child = NULL;
+        root->last_child = NULL;
+        root->nb_children = 0;
+        AddSampleTreeMessage(queue, sample, tree->allocator, ts->name, ts);
+
+        return RMT_TRUE;
+    }
+
+    return RMT_FALSE;
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @REMOTERY: Remotery
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef RMT_USE_D3D11
+typedef struct D3D11 D3D11;
+static rmtError D3D11_Create(D3D11** d3d11);
+static void D3D11_Destructor(D3D11* d3d11);
+#endif
+
+
+#ifdef RMT_USE_OPENGL
+typedef struct OpenGL OpenGL;
+static rmtError OpenGL_Create(OpenGL** opengl);
+static void OpenGL_Destructor(OpenGL* opengl);
+#endif
+
+
+struct Remotery
+{
+    Server* server;
+
+    // Microsecond accuracy timer for CPU timestamps
+    usTimer timer;
+
+    rmtTLS thread_sampler_tls_handle;
+
+    // Linked list of all known threads being sampled
+    ThreadSampler* volatile first_thread_sampler;
+
+    // Queue between clients and main remotery thread
+    MessageQueue* mq_to_rmt_thread;
+
+    // A dynamically-sized buffer used for encoding the sample tree as JSON and sending to the client
+    Buffer* json_buf;
+
+    // The main server thread
+    Thread* thread;
+
+#ifdef RMT_USE_CUDA
+    rmtCUDABind cuda;
+#endif
+
+#ifdef RMT_USE_D3D11
+    D3D11* d3d11;
+#endif
+
+#ifdef RMT_USE_OPENGL
+    OpenGL* opengl;
+#endif
+};
+
+
+//
+// Global remotery context
+//
+static Remotery* g_Remotery = NULL;
+
+
+//
+// This flag marks the EXE/DLL that created the global remotery instance. We want to allow
+// only the creating EXE/DLL to destroy the remotery instance.
+//
+static rmtBool g_RemoteryCreated = RMT_FALSE;
+
+
+static void Remotery_DestroyThreadSamplers(Remotery* rmt);
+
+
+static const rmtU8 g_DecimalToHex[17] = "0123456789abcdef";
+
+
+static void GetSampleDigest(Sample* sample, rmtU32* digest_hash, rmtU32* nb_samples)
+{
+    Sample* child;
+
+    assert(sample != NULL);
+    assert(digest_hash != NULL);
+    assert(nb_samples != NULL);
+
+    // Concatenate this sample
+    (*nb_samples)++;
+    *digest_hash = MurmurHash3_x86_32(&sample->unique_id, sizeof(sample->unique_id), *digest_hash);
+
+    {
+        rmtU8 shift = 4;
+
+        // Get 6 nibbles for lower 3 bytes of the unique sample ID
+        rmtU8* sample_id = (rmtU8*)&sample->unique_id;
+        rmtU8 hex_sample_id[6];
+        hex_sample_id[0] = sample_id[0] & 15;
+        hex_sample_id[1] = sample_id[0] >> 4;
+        hex_sample_id[2] = sample_id[1] & 15;
+        hex_sample_id[3] = sample_id[1] >> 4;
+        hex_sample_id[4] = sample_id[2] & 15;
+        hex_sample_id[5] = sample_id[2] >> 4;
+
+        // As the nibbles will be used as hex colour digits, shift them up to make pastel colours
+        hex_sample_id[0] = minU8(hex_sample_id[0] + shift, 15);
+        hex_sample_id[1] = minU8(hex_sample_id[1] + shift, 15);
+        hex_sample_id[2] = minU8(hex_sample_id[2] + shift, 15);
+        hex_sample_id[3] = minU8(hex_sample_id[3] + shift, 15);
+        hex_sample_id[4] = minU8(hex_sample_id[4] + shift, 15);
+        hex_sample_id[5] = minU8(hex_sample_id[5] + shift, 15);
+
+        // Convert the nibbles to hex for the final colour
+        sample->unique_id_html_colour[1] = g_DecimalToHex[hex_sample_id[0]];
+        sample->unique_id_html_colour[2] = g_DecimalToHex[hex_sample_id[1]];
+        sample->unique_id_html_colour[3] = g_DecimalToHex[hex_sample_id[2]];
+        sample->unique_id_html_colour[4] = g_DecimalToHex[hex_sample_id[3]];
+        sample->unique_id_html_colour[5] = g_DecimalToHex[hex_sample_id[4]];
+        sample->unique_id_html_colour[6] = g_DecimalToHex[hex_sample_id[5]];
+    }
+
+    // Concatenate children
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+        GetSampleDigest(child, digest_hash, nb_samples);
+}
+
+
+static rmtError Remotery_SendLogTextMessage(Remotery* rmt, Message* message)
+{
+    assert(rmt != NULL);
+    assert(message != NULL);
+    return Server_Send(rmt->server, message->payload, message->payload_size, 20);
+}
+
+
+static rmtError json_SampleTree(Buffer* buffer, Msg_SampleTree* msg)
+{
+    Sample* root_sample;
+    char thread_name[64];
+    rmtU32 digest_hash = 0, nb_samples = 0;
+    rmtError error;
+
+    assert(buffer != NULL);
+    assert(msg != NULL);
+
+    // Get the message root sample
+    root_sample = msg->root_sample;
+    assert(root_sample != NULL);
+
+    // Reset the buffer position to the start
+    buffer->bytes_used = 0;
+
+    // Add any sample types as a thread name post-fix to ensure they get their own viewer
+    thread_name[0] = 0;
+    strncat_s(thread_name, sizeof(thread_name), msg->thread_name, strnlen_s(msg->thread_name, 64));
+    if (root_sample->type == SampleType_CUDA)
+        strncat_s(thread_name, sizeof(thread_name), " (CUDA)", 7);
+    if (root_sample->type == SampleType_D3D11)
+        strncat_s(thread_name, sizeof(thread_name), " (D3D11)", 8);
+    if (root_sample->type == SampleType_OpenGL)
+        strncat_s(thread_name, sizeof(thread_name), " (OpenGL)", 9);
+
+    // Get digest hash of samples so that viewer can efficiently rebuild its tables
+    GetSampleDigest(root_sample, &digest_hash, &nb_samples);
+
+    // Build the sample data
+    JSON_ERROR_CHECK(json_OpenObject(buffer));
+
+        JSON_ERROR_CHECK(json_FieldStr(buffer, "id", "SAMPLES"));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldStr(buffer, "thread_name", thread_name));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldU64(buffer, "nb_samples", nb_samples));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_FieldU64(buffer, "sample_digest", digest_hash));
+        JSON_ERROR_CHECK(json_Comma(buffer));
+        JSON_ERROR_CHECK(json_SampleArray(buffer, root_sample, "samples"));
+
+    JSON_ERROR_CHECK(json_CloseObject(buffer));
+
+    return RMT_ERROR_NONE;
+}
+
+
+#ifdef RMT_USE_CUDA
+static rmtBool AreCUDASamplesReady(Sample* sample);
+static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample);
+#endif
+
+
+static rmtError Remotery_SendSampleTreeMessage(Remotery* rmt, Message* message)
+{
+    Msg_SampleTree* sample_tree;
+    rmtError error = RMT_ERROR_NONE;
+    Sample* sample;
+
+    assert(rmt != NULL);
+    assert(message != NULL);
+
+    // Get the message root sample
+    sample_tree = (Msg_SampleTree*)message->payload;
+    sample = sample_tree->root_sample;
+    assert(sample != NULL);
+
+    #ifdef RMT_USE_CUDA
+    if (sample->type == SampleType_CUDA)
+    {
+        // If these CUDA samples aren't ready yet, stick them to the back of the queue and continue
+        rmtBool are_samples_ready;
+        rmt_BeginCPUSample(AreCUDASamplesReady);
+        are_samples_ready = AreCUDASamplesReady(sample);
+        rmt_EndCPUSample();
+        if (!are_samples_ready)
+        {
+            AddSampleTreeMessage(rmt->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler);
+            return RMT_ERROR_NONE;
+        }
+
+        // Retrieve timing of all CUDA samples
+        rmt_BeginCPUSample(GetCUDASampleTimes);
+        GetCUDASampleTimes(sample->parent, sample);
+        rmt_EndCPUSample();
+    }
+    #endif
+
+    // Serialise the sample tree and send to the viewer with a reasonably long timeout as the size
+    // of the sample data may be large
+    error = json_SampleTree(rmt->json_buf, sample_tree);
+    if (error == RMT_ERROR_NONE)
+        error = Server_Send(rmt->server, rmt->json_buf->data, rmt->json_buf->bytes_used, 5000);
+
+    // Release the sample tree back to its allocator
+    FreeSampleTree(sample, sample_tree->allocator);
+
+    return error;
+}
+
+
+static rmtError Remotery_ConsumeMessageQueue(Remotery* rmt)
+{
+    rmtU32 nb_messages_sent = 0;
+    const rmtU32 maxNbMessagesPerUpdate = g_Settings.maxNbMessagesPerUpdate;
+
+    assert(rmt != NULL);
+
+    // Absorb as many messages in the queue while disconnected
+    if (Server_IsClientConnected(rmt->server) == RMT_FALSE)
+        return RMT_ERROR_NONE;
+
+    // Loop reading the max number of messages for this update
+    while( nb_messages_sent++ < maxNbMessagesPerUpdate )
+    {
+        rmtError error = RMT_ERROR_NONE;
+        Message* message = MessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread);
+        if (message == NULL)
+            break;
+
+        switch (message->id)
+        {
+            // This shouldn't be possible
+            case MsgID_NotReady:
+                assert(RMT_FALSE); 
+                break;
+
+            // Dispatch to message handler
+            case MsgID_LogText:
+                error = Remotery_SendLogTextMessage(rmt, message);
+                break;
+            case MsgID_SampleTree:
+                error = Remotery_SendSampleTreeMessage(rmt, message);
+                break;
+        }
+
+        // Consume the message before reacting to any errors
+        MessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message);
+        if (error != RMT_ERROR_NONE)
+            return error;
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void Remotery_FlushMessageQueue(Remotery* rmt)
+{
+    assert(rmt != NULL);
+
+    // Loop reading all remaining messages
+    while (1)
+    {
+        Message* message = MessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread);
+        if (message == NULL)
+            break;
+
+        switch (message->id)
+        {
+            // These can be safely ignored
+            case MsgID_NotReady:
+            case MsgID_LogText:
+                break;
+
+            // Release all samples back to their allocators
+            case MsgID_SampleTree:
+            {
+                Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload;
+                FreeSampleTree(sample_tree->root_sample, sample_tree->allocator);
+                break;
+            }
+        }
+
+        MessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message);
+    }
+}
+
+
+static rmtError Remotery_ThreadMain(Thread* thread)
+{
+    Remotery* rmt = (Remotery*)thread->param;
+    assert(rmt != NULL);
+
+    rmt_SetCurrentThreadName("Remotery");
+
+    while (thread->request_exit == RMT_FALSE)
+    {
+        rmt_BeginCPUSample(Wakeup);
+
+            rmt_BeginCPUSample(ServerUpdate);
+            Server_Update(rmt->server);
+            rmt_EndCPUSample();
+
+            rmt_BeginCPUSample(ConsumeMessageQueue);
+            Remotery_ConsumeMessageQueue(rmt);
+            rmt_EndCPUSample();
+
+        rmt_EndCPUSample();
+
+        //
+        // [NOTE-A]
+        //
+        // Possible sequence of user events at this point:
+        //
+        //    1. Add samples to the queue.
+        //    2. Shutdown remotery.
+        //
+        // This loop will exit with unrelease samples.
+        //
+
+        msSleep(g_Settings.msSleepBetweenServerUpdates);
+    }
+
+    // Release all samples to their allocators as a consequence of [NOTE-A]
+    Remotery_FlushMessageQueue(rmt);
+
+    return RMT_ERROR_NONE;
+}
+
+
+static rmtError Remotery_Constructor(Remotery* rmt)
+{
+    rmtError error;
+
+    assert(rmt != NULL);
+
+    // Set default state
+    rmt->server = NULL;
+    rmt->thread_sampler_tls_handle = TLS_INVALID_HANDLE;
+    rmt->first_thread_sampler = NULL;
+    rmt->mq_to_rmt_thread = NULL;
+    rmt->json_buf = NULL;
+    rmt->thread = NULL;
+
+    // Kick-off the timer
+    usTimer_Init(&rmt->timer);
+
+    // Allocate a TLS handle for the thread sampler
+    error = tlsAlloc(&rmt->thread_sampler_tls_handle);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Create the server
+    New_1( Server, rmt->server, g_Settings.port );
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Create the main message thread with only one page
+    New_1(MessageQueue, rmt->mq_to_rmt_thread, g_Settings.messageQueueSizeInBytes);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    // Create the JSON serialisation buffer
+    New_1(Buffer, rmt->json_buf, 4096);
+    if (error != RMT_ERROR_NONE)
+        return error;
+
+    #ifdef RMT_USE_CUDA
+
+        rmt->cuda.CtxSetCurrent = NULL;
+        rmt->cuda.EventCreate = NULL;
+        rmt->cuda.EventDestroy = NULL;
+        rmt->cuda.EventElapsedTime = NULL;
+        rmt->cuda.EventQuery = NULL;
+        rmt->cuda.EventRecord = NULL;
+
+    #endif
+
+    #ifdef RMT_USE_D3D11
+        rmt->d3d11 = NULL;
+        error = D3D11_Create(&rmt->d3d11);
+        if (error != RMT_ERROR_NONE)
+            return error;
+    #endif
+
+    #ifdef RMT_USE_OPENGL
+        rmt->opengl = NULL;
+        error = OpenGL_Create(&rmt->opengl);
+        if (error != RMT_ERROR_NONE)
+            return error;
+    #endif
+
+    // Set as the global instance before creating any threads that uses it for sampling itself
+    assert(g_Remotery == NULL);
+    g_Remotery = rmt;
+    g_RemoteryCreated = RMT_TRUE;
+
+    // Ensure global instance writes complete before other threads get a chance to use it
+    WriteFence();
+
+    // Create the main update thread once everything has been defined for the global remotery object
+    New_2(Thread, rmt->thread, Remotery_ThreadMain, rmt);
+    return error;
+}
+
+
+static void Remotery_Destructor(Remotery* rmt)
+{
+    assert(rmt != NULL);
+
+    // Join the remotery thread before clearing the global object as the thread is profiling itself
+    Delete(Thread, rmt->thread);
+
+    // Ensure this is the module that created it
+    assert(g_RemoteryCreated == RMT_TRUE);
+    assert(g_Remotery == rmt);
+    g_Remotery = NULL;
+    g_RemoteryCreated = RMT_FALSE;
+
+    #ifdef RMT_USE_D3D11
+        Delete(D3D11, rmt->d3d11);
+    #endif
+
+    #ifdef RMT_USE_OPENGL
+        Delete(OpenGL, rmt->opengl);
+    #endif
+
+    Delete(Buffer, rmt->json_buf);
+    Delete(MessageQueue, rmt->mq_to_rmt_thread);
+
+    Remotery_DestroyThreadSamplers(rmt);
+
+    Delete(Server, rmt->server);
+
+    if (rmt->thread_sampler_tls_handle != TLS_INVALID_HANDLE)
+    {
+        tlsFree(rmt->thread_sampler_tls_handle);
+        rmt->thread_sampler_tls_handle = 0;
+    }
+}
+
+
+static rmtError Remotery_GetThreadSampler(Remotery* rmt, ThreadSampler** thread_sampler)
+{
+    ThreadSampler* ts;
+
+    // Is there a thread sampler associated with this thread yet?
+    assert(rmt != NULL);
+    ts = (ThreadSampler*)tlsGet(rmt->thread_sampler_tls_handle);
+    if (ts == NULL)
+    {
+        // Allocate on-demand
+        rmtError error;
+        New_0(ThreadSampler, *thread_sampler);
+        if (error != RMT_ERROR_NONE)
+            return error;
+        ts = *thread_sampler;
+ 
+        // Add to the beginning of the global linked list of thread samplers
+        while (1)
+        {
+            ThreadSampler* old_ts = rmt->first_thread_sampler;
+            ts->next = old_ts;
+
+            // If the old value is what we expect it to be then no other thread has
+            // changed it since this thread sampler was used as a candidate first list item
+            if (AtomicCompareAndSwapPointer((long* volatile*)&rmt->first_thread_sampler, (long*)old_ts, (long*)ts) == RMT_TRUE)
+                break;
+        }
+
+        tlsSet(rmt->thread_sampler_tls_handle, ts);
+    }
+
+    assert(thread_sampler != NULL);
+    *thread_sampler = ts;
+    return RMT_ERROR_NONE;
+}
+
+
+static void Remotery_DestroyThreadSamplers(Remotery* rmt)
+{
+    // If the handle failed to create in the first place then it shouldn't be possible to create thread samplers
+    assert(rmt != NULL);
+    if (rmt->thread_sampler_tls_handle == TLS_INVALID_HANDLE)
+    {
+        assert(rmt->first_thread_sampler == NULL);
+        return;
+    }
+
+    // Keep popping thread samplers off the linked list until they're all gone
+    // This does not make any assumptions, making it possible for thread samplers to be created while they're all
+    // deleted. While this is erroneous calling code, this will prevent a confusing crash.
+    while (rmt->first_thread_sampler != NULL)
+    {
+        ThreadSampler* ts;
+
+        while (1)
+        {
+            ThreadSampler* old_ts = rmt->first_thread_sampler;
+            ThreadSampler* next_ts = old_ts->next;
+
+            if (AtomicCompareAndSwapPointer((long* volatile*)&rmt->first_thread_sampler, (long*)old_ts, (long*)next_ts) == RMT_TRUE)
+            {
+                ts = old_ts;
+                break;
+            }
+        }
+
+        Delete(ThreadSampler, ts);
+    }
+}
+
+
+static void* CRTMalloc(void* mm_context, rmtU32 size)
+{
+    return malloc((size_t)size);
+}
+
+
+static void CRTFree(void* mm_context, void* ptr)
+{
+    free(ptr);
+}
+
+static void* CRTRealloc(void* mm_context, void* ptr, rmtU32 size)
+{
+    return realloc(ptr, size);
+}
+
+
+RMT_API rmtSettings* _rmt_Settings(void)
+{
+    // Default-initialize on first call
+    if( g_SettingsInitialized == RMT_FALSE )
+    {
+        g_Settings.port = 0x4597;
+        g_Settings.msSleepBetweenServerUpdates = 10;
+        g_Settings.messageQueueSizeInBytes = 64 * 1024;
+        g_Settings.maxNbMessagesPerUpdate = 100;
+        g_Settings.malloc = CRTMalloc;
+        g_Settings.free = CRTFree;
+        g_Settings.realloc = CRTRealloc;
+        g_Settings.input_handler = NULL;
+        g_Settings.input_handler_context = NULL;
+        g_Settings.logFilename = "rmtLog.txt";
+
+        g_SettingsInitialized = RMT_TRUE;
+    }
+
+    return &g_Settings;
+}
+
+
+RMT_API rmtError _rmt_CreateGlobalInstance(Remotery** remotery)
+{
+    rmtError error;
+
+    // Default-initialise if user has not set values
+    rmt_Settings();
+
+    // Creating the Remotery instance also records it as the global instance
+    assert(remotery != NULL);
+    New_0(Remotery, *remotery);
+    return error;
+}
+
+
+RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery)
+{
+    Delete(Remotery, remotery);
+}
+
+
+RMT_API void _rmt_SetGlobalInstance(Remotery* remotery)
+{
+    // Default-initialise if user has not set values
+    rmt_Settings();
+
+    g_Remotery = remotery;
+}
+
+
+RMT_API Remotery* _rmt_GetGlobalInstance(void)
+{
+    return g_Remotery;
+}
+
+
+#ifdef RMT_PLATFORM_WINDOWS
+    #pragma pack(push,8)
+    typedef struct tagTHREADNAME_INFO
+    {
+       DWORD dwType; // Must be 0x1000.
+       LPCSTR szName; // Pointer to name (in user addr space).
+       DWORD dwThreadID; // Thread ID (-1=caller thread).
+       DWORD dwFlags; // Reserved for future use, must be zero.
+    } THREADNAME_INFO;
+    #pragma pack(pop)
+#endif
+
+static void SetDebuggerThreadName(const char* name)
+{
+    #ifdef RMT_PLATFORM_WINDOWS
+        THREADNAME_INFO info;
+        info.dwType = 0x1000;
+        info.szName = name;
+        info.dwThreadID = (DWORD)-1;
+        info.dwFlags = 0;
+
+        #ifndef __MINGW32__
+        __try
+        {
+            RaiseException(0x406D1388, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info);
+        }
+        __except(1 /* EXCEPTION_EXECUTE_HANDLER */)
+        {
+        }
+        #endif
+    #endif
+}
+
+
+RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name)
+{
+    ThreadSampler* ts;
+    rsize_t slen;
+
+    if (g_Remotery == NULL)
+        return;
+
+    // Get data for this thread
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) != RMT_ERROR_NONE)
+        return;
+
+    // Use strcat to strcpy the thread name over
+    slen = strnlen_s(thread_name, sizeof(ts->name));
+    ts->name[0] = 0;
+    strncat_s(ts->name, sizeof(ts->name), thread_name, slen);
+
+    // Apply to the debugger
+    SetDebuggerThreadName(thread_name);
+}
+
+
+static rmtBool QueueLine(MessageQueue* queue, char* text, rmtU32 size, struct ThreadSampler* thread_sampler)
+{
+    Message* message;
+
+    assert(queue != NULL);
+
+    // String/JSON block/null terminate
+    text[size++] = '\"';
+    text[size++] = '}';
+    text[size] = 0;
+
+    // Allocate some space for the line
+    message = MessageQueue_AllocMessage(queue, size, thread_sampler);
+    if (message == NULL)
+        return RMT_FALSE;
+
+    // Copy the text and commit the message
+    memcpy(message->payload, text, size);
+    MessageQueue_CommitMessage(queue, message, MsgID_LogText);
+
+    return RMT_TRUE;
+}
+
+
+static const char log_message[] = "{ \"id\": \"LOG\", \"text\": \"";
+
+
+RMT_API void _rmt_LogText(rmtPStr text)
+{
+    int start_offset, prev_offset, i;
+    char line_buffer[1024] = { 0 };
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    Remotery_GetThreadSampler(g_Remotery, &ts);
+
+    // Start the line buffer off with the JSON message markup
+    strncat_s(line_buffer, sizeof(line_buffer), log_message, sizeof(log_message));
+    start_offset = (int)strnlen_s(line_buffer, sizeof(line_buffer) - 1);
+
+    // There might be newlines in the buffer, so split them into multiple network calls
+    prev_offset = start_offset;
+    for (i = 0; text[i] != 0; i++)
+    {
+        char c = text[i];
+
+        // Line wrap when too long or newline encountered
+        if (prev_offset == sizeof(line_buffer) - 3 || c == '\n')
+        {
+            if (QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, prev_offset, ts) == RMT_FALSE)
+                return;
+
+            // Restart line
+            prev_offset = start_offset;
+        }
+
+        // Safe to insert 2 characters here as previous check would split lines if not enough space left
+        switch (c)
+        {
+            // Skip newline, dealt with above
+            case '\n':
+                break;
+
+            // Escape these
+            case '\\':
+                line_buffer[prev_offset++] = '\\';
+                line_buffer[prev_offset++] = '\\';
+                break;
+
+            case '\"':
+                line_buffer[prev_offset++] = '\\';
+                line_buffer[prev_offset++] = '\"';
+                break;
+
+            // Add the rest
+            default:
+                line_buffer[prev_offset++] = c;
+                break;
+        }
+    }
+
+    // Send the last line
+    if (prev_offset > start_offset)
+    {
+        assert(prev_offset < ((int)sizeof(line_buffer) - 3));
+        QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, prev_offset, ts);
+    }
+}
+
+
+static rmtU32 GetNameHash(rmtPStr name, rmtU32* hash_cache)
+{
+    // Hash cache provided?
+    if (hash_cache != NULL)
+    {
+        // Calculate the hash first time round only
+        if (*hash_cache == 0)
+        {
+            assert(name != NULL);
+            *hash_cache = MurmurHash3_x86_32(name, (int)strnlen_s(name, 256), 0);
+        }
+
+        return *hash_cache;
+    }
+
+    // Have to recalculate every time when no cache storage exists
+    return MurmurHash3_x86_32(name, (int)strnlen_s(name, 256), 0);
+}
+
+
+RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32* hash_cache)
+{
+    // 'hash_cache' stores a pointer to a sample name's hash value. Internally this is used to identify unique callstacks and it
+    // would be ideal that it's not recalculated each time the sample is used. This can be statically cached at the point
+    // of call or stored elsewhere when dynamic names are required.
+    //
+    // If 'hash_cache' is NULL then this call becomes more expensive, as it has to recalculate the hash of the name.
+    
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    // TODO: Time how long the bits outside here cost and subtract them from the parent
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        Sample* sample;
+        rmtU32 name_hash = GetNameHash(name, hash_cache);
+        if (ThreadSampler_Push(ts, ts->sample_trees[SampleType_CPU], name, name_hash, &sample) == RMT_ERROR_NONE)
+            sample->us_start = usTimer_Get(&g_Remotery->timer);
+    }
+}
+
+
+RMT_API void _rmt_EndCPUSample(void)
+{
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        Sample* sample = ts->sample_trees[SampleType_CPU]->current_parent;
+        sample->us_end = usTimer_Get(&g_Remotery->timer);
+        ThreadSampler_Pop(ts, g_Remotery->mq_to_rmt_thread, sample);
+    }
+}
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @CUDA: CUDA event sampling
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef RMT_USE_CUDA
+
+
+typedef struct CUDASample
+{
+    // IS-A inheritance relationship
+    Sample Sample;
+
+    // Pair of events that wrap the sample
+    CUevent event_start;
+    CUevent event_end;
+
+} CUDASample;
+
+
+static rmtError MapCUDAResult(CUresult result)
+{
+    switch (result)
+    {
+        case CUDA_SUCCESS: return RMT_ERROR_NONE;
+        case CUDA_ERROR_DEINITIALIZED: return RMT_ERROR_CUDA_DEINITIALIZED;
+        case CUDA_ERROR_NOT_INITIALIZED: return RMT_ERROR_CUDA_NOT_INITIALIZED;
+        case CUDA_ERROR_INVALID_CONTEXT: return RMT_ERROR_CUDA_INVALID_CONTEXT;
+        case CUDA_ERROR_INVALID_VALUE: return RMT_ERROR_CUDA_INVALID_VALUE;
+        case CUDA_ERROR_INVALID_HANDLE: return RMT_ERROR_CUDA_INVALID_HANDLE;
+        case CUDA_ERROR_OUT_OF_MEMORY: return RMT_ERROR_CUDA_OUT_OF_MEMORY;
+        case CUDA_ERROR_NOT_READY: return RMT_ERROR_ERROR_NOT_READY;
+        default: return RMT_ERROR_CUDA_UNKNOWN;
+    }
+}
+
+
+#define CUDA_MAKE_FUNCTION(name, params)                    \
+    typedef CUresult (CUDAAPI *name##Ptr) params;           \
+    name##Ptr name = (name##Ptr)g_Remotery->cuda.name;
+
+
+#define CUDA_GUARD(call)                \
+    {                                   \
+        rmtError error = call;     \
+        if (error != RMT_ERROR_NONE)    \
+            return error;               \
+    }
+
+
+// Wrappers around CUDA driver functions that manage the active context.
+static rmtError CUDASetContext(void* context)
+{
+    CUDA_MAKE_FUNCTION(CtxSetCurrent, (CUcontext ctx));
+    assert(CtxSetCurrent != NULL);
+    return MapCUDAResult(CtxSetCurrent((CUcontext)context));
+}
+static rmtError CUDAGetContext(void** context)
+{
+    CUDA_MAKE_FUNCTION(CtxGetCurrent, (CUcontext* ctx));
+    assert(CtxGetCurrent != NULL);
+    return MapCUDAResult(CtxGetCurrent((CUcontext*)context));
+}
+static rmtError CUDAEnsureContext()
+{
+    void* current_context;
+    CUDA_GUARD(CUDAGetContext(&current_context));
+
+    assert(g_Remotery != NULL);
+    if (current_context != g_Remotery->cuda.context)
+        CUDA_GUARD(CUDASetContext(g_Remotery->cuda.context));
+
+    return RMT_ERROR_NONE;
+}
+
+
+// Wrappers around CUDA driver functions that manage events
+static rmtError CUDAEventCreate(CUevent* phEvent, unsigned int Flags)
+{
+    CUDA_MAKE_FUNCTION(EventCreate, (CUevent *phEvent, unsigned int Flags));
+    CUDA_GUARD(CUDAEnsureContext());
+    return MapCUDAResult(EventCreate(phEvent, Flags));
+}
+static rmtError CUDAEventDestroy(CUevent hEvent)
+{
+    CUDA_MAKE_FUNCTION(EventDestroy, (CUevent hEvent));
+    CUDA_GUARD(CUDAEnsureContext());
+    return MapCUDAResult(EventDestroy(hEvent));
+}
+static rmtError CUDAEventRecord(CUevent hEvent, void* hStream)
+{
+    CUDA_MAKE_FUNCTION(EventRecord, (CUevent hEvent, CUstream hStream));
+    CUDA_GUARD(CUDAEnsureContext());
+    return MapCUDAResult(EventRecord(hEvent, (CUstream)hStream));
+}
+static rmtError CUDAEventQuery(CUevent hEvent)
+{
+    CUDA_MAKE_FUNCTION(EventQuery,  (CUevent hEvent));
+    CUDA_GUARD(CUDAEnsureContext());
+    return MapCUDAResult(EventQuery(hEvent));
+}
+static rmtError CUDAEventElapsedTime(float* pMilliseconds, CUevent hStart, CUevent hEnd)
+{
+    CUDA_MAKE_FUNCTION(EventElapsedTime, (float *pMilliseconds, CUevent hStart, CUevent hEnd));
+    CUDA_GUARD(CUDAEnsureContext());
+    return MapCUDAResult(EventElapsedTime(pMilliseconds, hStart, hEnd));
+}
+
+
+static rmtError CUDASample_Constructor(CUDASample* sample)
+{
+    rmtError error;
+
+    assert(sample != NULL);
+
+    // Chain to sample constructor
+    Sample_Constructor((Sample*)sample);
+    sample->Sample.type = SampleType_CUDA;
+    sample->Sample.size_bytes = sizeof(CUDASample);
+    sample->event_start = NULL;
+    sample->event_end = NULL;
+
+    // Create non-blocking events with timing
+    assert(g_Remotery != NULL);
+    error = CUDAEventCreate(&sample->event_start, CU_EVENT_DEFAULT);
+    if (error == RMT_ERROR_NONE)
+        error = CUDAEventCreate(&sample->event_end, CU_EVENT_DEFAULT);
+    return error;
+}
+
+
+static void CUDASample_Destructor(CUDASample* sample)
+{
+    assert(sample != NULL);
+
+    // Destroy events
+    if (sample->event_start != NULL)
+        CUDAEventDestroy(sample->event_start);
+    if (sample->event_end != NULL)
+        CUDAEventDestroy(sample->event_end);
+
+    Sample_Destructor((Sample*)sample);
+}
+
+
+static rmtBool AreCUDASamplesReady(Sample* sample)
+{
+    rmtError error;
+    Sample* child;
+
+    CUDASample* cuda_sample = (CUDASample*)sample;
+    assert(sample->type == SampleType_CUDA);
+
+    // Check to see if both of the CUDA events have been processed
+    error = CUDAEventQuery(cuda_sample->event_start);
+    if (error != RMT_ERROR_NONE)
+        return RMT_FALSE;
+    error = CUDAEventQuery(cuda_sample->event_end);
+    if (error != RMT_ERROR_NONE)
+        return RMT_FALSE;
+
+    // Check child sample events
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+    {
+        if (!AreCUDASamplesReady(child))
+            return RMT_FALSE;
+    }
+
+    return RMT_TRUE;
+}
+
+
+static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample)
+{
+    Sample* child;
+
+    CUDASample* cuda_root_sample = (CUDASample*)root_sample;
+    CUDASample* cuda_sample = (CUDASample*)sample;
+
+    float ms_start, ms_end;
+
+    assert(root_sample != NULL);
+    assert(sample != NULL);
+
+    // Get millisecond timing of each sample event, relative to initial root sample
+    if (CUDAEventElapsedTime(&ms_start, cuda_root_sample->event_start, cuda_sample->event_start) != RMT_ERROR_NONE)
+        return RMT_FALSE;
+    if (CUDAEventElapsedTime(&ms_end, cuda_root_sample->event_start, cuda_sample->event_end) != RMT_ERROR_NONE)
+        return RMT_FALSE;
+
+    // Convert to microseconds and add to the sample
+    sample->us_start = (rmtU64)(ms_start * 1000);
+    sample->us_end = (rmtU64)(ms_end * 1000);
+
+    // Get child sample times
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+    {
+        if (!GetCUDASampleTimes(root_sample, child))
+            return RMT_FALSE;
+    }
+
+    return RMT_TRUE;
+}
+
+
+RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind)
+{
+    assert(bind != NULL);
+    if (g_Remotery != NULL)
+        g_Remotery->cuda = *bind;
+}
+
+
+RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream)
+{
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        rmtError error;
+        Sample* sample;
+        rmtU32 name_hash = GetNameHash(name, hash_cache);
+
+        // Create the CUDA tree on-demand as the tree needs an up-front-created root.
+        // This is not possible to create on initialisation as a CUDA binding is not yet available.
+        SampleTree** cuda_tree = &ts->sample_trees[SampleType_CUDA];
+        if (*cuda_tree == NULL)
+        {
+            CUDASample* root_sample;
+            
+            New_3(SampleTree, *cuda_tree, sizeof(CUDASample), (ObjConstructor)CUDASample_Constructor, (ObjDestructor)CUDASample_Destructor);
+            if (error != RMT_ERROR_NONE)
+                return;
+
+            // Record an event once on the root sample, used to measure absolute sample
+            // times since this point
+            root_sample = (CUDASample*)(*cuda_tree)->root;
+            error = CUDAEventRecord(root_sample->event_start, stream);
+            if (error != RMT_ERROR_NONE)
+                return;
+        }
+
+        // Push the same and record its event
+        if (ThreadSampler_Push(ts, *cuda_tree, name, name_hash, &sample) == RMT_ERROR_NONE)
+        {
+            CUDASample* cuda_sample = (CUDASample*)sample;
+            CUDAEventRecord(cuda_sample->event_start, stream);
+        }
+    }
+}
+
+
+RMT_API void _rmt_EndCUDASample(void* stream)
+{
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        CUDASample* sample = (CUDASample*)ts->sample_trees[SampleType_CUDA]->current_parent;
+        CUDAEventRecord(sample->event_end, stream);
+        ThreadSampler_Pop(ts, g_Remotery->mq_to_rmt_thread, (Sample*)sample);
+    }
+}
+
+
+#endif  // RMT_USE_CUDA
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   @D3D11: Direct3D 11 event sampling
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef RMT_USE_D3D11
+
+
+// As clReflect has no way of disabling C++ compile mode, this forces C interfaces everywhere...
+#define CINTERFACE
+
+// ...unfortunately these C++ helpers aren't wrapped by the same macro but they can be disabled individually
+#define D3D11_NO_HELPERS
+
+// Allow use of the D3D11 helper macros for accessing the C-style vtable
+#define COBJMACROS
+
+#include <d3d11.h>
+
+
+typedef struct D3D11
+{
+    // Context set by user
+    ID3D11Device* device;
+    ID3D11DeviceContext* context;
+
+    HRESULT last_error;
+
+    // An allocator separate to the samples themselves so that D3D resource lifetime can be controlled
+    // outside of the Remotery thread.
+    ObjectAllocator* timestamp_allocator;
+
+    // Queue to the D3D 11 main update thread
+    // Given that BeginSample/EndSample need to be called from the same thread that does the update, there
+    // is really no need for this to be a thread-safe queue. I'm using it for its convenience.
+    MessageQueue* mq_to_d3d11_main;
+
+    // Mark the first time so that remaining timestamps are offset from this
+    rmtU64 first_timestamp;
+} D3D11;
+
+
+static rmtError D3D11_Create(D3D11** d3d11)
+{
+    rmtError error;
+
+    assert(d3d11 != NULL);
+
+    // Allocate space for the D3D11 data
+    *d3d11 = (D3D11*)rmtMalloc(sizeof(D3D11));
+    if (*d3d11 == NULL)
+        return RMT_ERROR_MALLOC_FAIL;
+
+    // Set defaults
+    (*d3d11)->device = NULL;
+    (*d3d11)->context = NULL;
+    (*d3d11)->last_error = S_OK;
+    (*d3d11)->timestamp_allocator = NULL;
+    (*d3d11)->mq_to_d3d11_main = NULL;
+    (*d3d11)->first_timestamp = 0;
+
+    New_1(MessageQueue, (*d3d11)->mq_to_d3d11_main, g_Settings.messageQueueSizeInBytes);
+    if (error != RMT_ERROR_NONE)
+    {
+        Delete(D3D11, *d3d11);
+        return error;
+    }
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void D3D11_Destructor(D3D11* d3d11)
+{
+    assert(d3d11 != NULL);
+
+    Delete(ObjectAllocator, d3d11->timestamp_allocator);
+    Delete(MessageQueue, d3d11->mq_to_d3d11_main);
+}
+
+
+typedef struct D3D11Timestamp
+{
+    // Inherit so that timestamps can be quickly allocated
+    ObjectLink ObjectLink;
+
+    // Pair of timestamp queries that wrap the sample
+    ID3D11Query* query_start;
+    ID3D11Query* query_end;
+
+    // A disjoint to measure frequency/stability
+    // TODO: Does *each* sample need one of these?
+    ID3D11Query* query_disjoint;
+} D3D11Timestamp;
+
+
+static rmtError D3D11Timestamp_Constructor(D3D11Timestamp* stamp)
+{
+    D3D11_QUERY_DESC timestamp_desc;
+    D3D11_QUERY_DESC disjoint_desc;
+    ID3D11Device* device;
+    HRESULT* last_error;
+
+    assert(stamp != NULL);
+
+    ObjectLink_Constructor((ObjectLink*)stamp);
+
+    // Set defaults
+    stamp->query_start = NULL;
+    stamp->query_end = NULL;
+    stamp->query_disjoint = NULL;
+
+    assert(g_Remotery != NULL);
+    assert(g_Remotery->d3d11 != NULL);
+    device = g_Remotery->d3d11->device;
+    last_error = &g_Remotery->d3d11->last_error;
+
+    // Create start/end timestamp queries
+    timestamp_desc.Query = D3D11_QUERY_TIMESTAMP;
+    timestamp_desc.MiscFlags = 0;
+    *last_error = ID3D11Device_CreateQuery(device, &timestamp_desc, &stamp->query_start);
+    if (*last_error != S_OK)
+        return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY;
+    *last_error = ID3D11Device_CreateQuery(device, &timestamp_desc, &stamp->query_end);
+    if (*last_error != S_OK)
+        return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY;
+
+    // Create disjoint query
+    disjoint_desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT;
+    disjoint_desc.MiscFlags = 0;
+    *last_error = ID3D11Device_CreateQuery(device, &disjoint_desc, &stamp->query_disjoint);
+    if (*last_error != S_OK)
+        return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void D3D11Timestamp_Destructor(D3D11Timestamp* stamp)
+{
+    assert(stamp != NULL);
+
+    // Destroy queries
+    if (stamp->query_disjoint != NULL)
+        ID3D11Query_Release(stamp->query_disjoint);
+    if (stamp->query_end != NULL)
+        ID3D11Query_Release(stamp->query_end);
+    if (stamp->query_start != NULL)
+        ID3D11Query_Release(stamp->query_start);
+}
+
+
+static void D3D11Timestamp_Begin(D3D11Timestamp* stamp, ID3D11DeviceContext* context)
+{
+    assert(stamp != NULL);
+
+    // Start of disjoint and first query
+    ID3D11DeviceContext_Begin(context, (ID3D11Asynchronous*)stamp->query_disjoint);
+    ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_start);
+}
+
+
+static void D3D11Timestamp_End(D3D11Timestamp* stamp, ID3D11DeviceContext* context)
+{
+    assert(stamp != NULL);
+
+    // End of disjoint and second query
+    ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_end);
+    ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_disjoint);
+}
+
+
+static HRESULT D3D11Timestamp_GetData(D3D11Timestamp* stamp, ID3D11DeviceContext* context, rmtU64* out_start, rmtU64* out_end, rmtU64* out_first_timestamp)
+{
+    ID3D11Asynchronous* query_start;
+    ID3D11Asynchronous* query_end;
+    ID3D11Asynchronous* query_disjoint;
+    HRESULT result;
+
+    UINT64 start;
+    UINT64 end;
+    D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint;
+
+    assert(stamp != NULL);
+    query_start = (ID3D11Asynchronous*)stamp->query_start;
+    query_end = (ID3D11Asynchronous*)stamp->query_end;
+    query_disjoint = (ID3D11Asynchronous*)stamp->query_disjoint;
+
+    // Check to see if all queries are ready
+    // If any fail to arrive, wait until later
+    result = ID3D11DeviceContext_GetData(context, query_start, &start, sizeof(start), D3D11_ASYNC_GETDATA_DONOTFLUSH);
+    if (result != S_OK)
+        return result;
+    result = ID3D11DeviceContext_GetData(context, query_end, &end, sizeof(end), D3D11_ASYNC_GETDATA_DONOTFLUSH);
+    if (result != S_OK)
+        return result;
+    result = ID3D11DeviceContext_GetData(context, query_disjoint, &disjoint, sizeof(disjoint), D3D11_ASYNC_GETDATA_DONOTFLUSH);
+    if (result != S_OK)
+        return result;
+
+    if (disjoint.Disjoint == FALSE)
+    {
+        double frequency = disjoint.Frequency / 1000000.0;
+
+        // Mark the first timestamp
+        assert(out_first_timestamp != NULL);
+        if (*out_first_timestamp == 0)
+            *out_first_timestamp = start;
+
+        // Calculate start and end timestamps from the disjoint info
+        *out_start = (rmtU64)((start - *out_first_timestamp) / frequency);
+        *out_end = (rmtU64)((end - *out_first_timestamp) / frequency);
+    }
+
+    return S_OK;
+}
+
+
+typedef struct D3D11Sample
+{
+    // IS-A inheritance relationship
+    Sample Sample;
+
+    D3D11Timestamp* timestamp;
+
+} D3D11Sample;
+
+
+static rmtError D3D11Sample_Constructor(D3D11Sample* sample)
+{
+    assert(sample != NULL);
+
+    // Chain to sample constructor
+    Sample_Constructor((Sample*)sample);
+    sample->Sample.type = SampleType_D3D11;
+    sample->Sample.size_bytes = sizeof(D3D11Sample);
+    sample->timestamp = NULL;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void D3D11Sample_Destructor(D3D11Sample* sample)
+{
+    Sample_Destructor((Sample*)sample);
+}
+
+
+RMT_API void _rmt_BindD3D11(void* device, void* context)
+{
+    if (g_Remotery != NULL)
+    {
+        assert(g_Remotery->d3d11 != NULL);
+
+        assert(device != NULL);
+        g_Remotery->d3d11->device = (ID3D11Device*)device;
+        assert(context != NULL);
+        g_Remotery->d3d11->context = (ID3D11DeviceContext*)context;
+    }
+}
+
+
+static void FreeD3D11TimeStamps(Sample* sample)
+{
+    Sample* child;
+
+    D3D11Sample* d3d_sample = (D3D11Sample*)sample;
+
+    assert(g_Remotery != NULL);
+    assert(g_Remotery->d3d11 != NULL);
+    assert(d3d_sample->timestamp != NULL);
+    ObjectAllocator_Free(g_Remotery->d3d11->timestamp_allocator, (void*)d3d_sample->timestamp);
+    d3d_sample->timestamp = NULL;
+
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+        FreeD3D11TimeStamps(child);
+}
+
+
+RMT_API void _rmt_UnbindD3D11(void)
+{
+    if (g_Remotery != NULL)
+    {
+        D3D11* d3d11 = g_Remotery->d3d11;
+        assert(d3d11 != NULL);
+
+        // Inform sampler to not add any more samples
+        d3d11->device = NULL;
+        d3d11->context = NULL;
+
+        // Flush the main queue of allocated D3D timestamps
+        while (1)
+        {
+            Msg_SampleTree* sample_tree;
+            Sample* sample;
+
+            Message* message = MessageQueue_PeekNextMessage(d3d11->mq_to_d3d11_main);
+            if (message == NULL)
+                break;
+
+            // There's only one valid message type in this queue
+            assert(message->id == MsgID_SampleTree);
+            sample_tree = (Msg_SampleTree*)message->payload;
+            sample = sample_tree->root_sample;
+            assert(sample->type == SampleType_D3D11);
+            FreeD3D11TimeStamps(sample);
+            FreeSampleTree(sample, sample_tree->allocator);
+
+            MessageQueue_ConsumeNextMessage(d3d11->mq_to_d3d11_main, message);
+        }
+
+        // Free all allocated D3D resources
+        Delete(ObjectAllocator, d3d11->timestamp_allocator);
+    }
+}
+
+
+RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache)
+{
+    ThreadSampler* ts;
+    D3D11* d3d11;
+
+    if (g_Remotery == NULL)
+        return;
+
+    // Has D3D11 been unbound?
+    d3d11 = g_Remotery->d3d11;
+    assert(d3d11 != NULL);
+    if (d3d11->device == NULL || d3d11->context == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        rmtError error;
+        Sample* sample;
+        rmtU32 name_hash = GetNameHash(name, hash_cache);
+
+        // Create the D3D11 tree on-demand as the tree needs an up-front-created root.
+        // This is not possible to create on initialisation as a D3D11 binding is not yet available.
+        SampleTree** d3d_tree = &ts->sample_trees[SampleType_D3D11];
+        if (*d3d_tree == NULL)
+        {
+            New_3(SampleTree, *d3d_tree, sizeof(D3D11Sample), (ObjConstructor)D3D11Sample_Constructor, (ObjDestructor)D3D11Sample_Destructor);
+            if (error != RMT_ERROR_NONE)
+                return;
+        }
+
+        // Also create the timestamp allocator on-demand to keep the D3D11 code localised to the same file section
+        if (d3d11->timestamp_allocator == NULL)
+            New_3(ObjectAllocator, d3d11->timestamp_allocator, sizeof(D3D11Timestamp), (ObjConstructor)D3D11Timestamp_Constructor, (ObjDestructor)D3D11Timestamp_Destructor);
+
+        // Push the sample
+        if (ThreadSampler_Push(ts, *d3d_tree, name, name_hash, &sample) == RMT_ERROR_NONE)
+        {
+            D3D11Sample* d3d_sample = (D3D11Sample*)sample;
+
+            // Allocate a timestamp for the sample and activate it
+            assert(d3d_sample->timestamp == NULL);
+            error = ObjectAllocator_Alloc(d3d11->timestamp_allocator, (void**)&d3d_sample->timestamp);
+            if (error == RMT_ERROR_NONE)
+                D3D11Timestamp_Begin(d3d_sample->timestamp, d3d11->context);
+        }
+    }
+}
+
+
+static rmtBool GetD3D11SampleTimes(Sample* sample, rmtU64* out_first_timestamp)
+{
+    Sample* child;
+
+    D3D11Sample* d3d_sample = (D3D11Sample*)sample;
+
+    assert(sample != NULL);
+    if (d3d_sample->timestamp != NULL)
+    {
+        HRESULT result;
+
+        D3D11* d3d11 = g_Remotery->d3d11;
+        assert(d3d11 != NULL);
+
+        result = D3D11Timestamp_GetData(
+            d3d_sample->timestamp,
+            d3d11->context,
+            &sample->us_start,
+            &sample->us_end,
+            out_first_timestamp);
+
+        if (result != S_OK)
+        {
+            d3d11->last_error = result;
+            return RMT_FALSE;
+        }
+    }
+
+    // Get child sample times
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+    {
+        if (!GetD3D11SampleTimes(child, out_first_timestamp))
+            return RMT_FALSE;
+    }
+
+    return RMT_TRUE;
+}
+
+
+static void UpdateD3D11Frame(void)
+{
+    D3D11* d3d11;
+
+    if (g_Remotery == NULL)
+        return;
+
+    d3d11 = g_Remotery->d3d11;
+    assert(d3d11 != NULL);
+
+    rmt_BeginCPUSample(rmt_UpdateD3D11Frame);
+
+    // Process all messages in the D3D queue
+    while (1)
+    {
+        Msg_SampleTree* sample_tree;
+        Sample* sample;
+
+        Message* message = MessageQueue_PeekNextMessage(d3d11->mq_to_d3d11_main);
+        if (message == NULL)
+            break;
+
+        // There's only one valid message type in this queue
+        assert(message->id == MsgID_SampleTree);
+        sample_tree = (Msg_SampleTree*)message->payload;
+        sample = sample_tree->root_sample;
+        assert(sample->type == SampleType_D3D11);
+
+        // Retrieve timing of all D3D11 samples
+        // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order
+        if (!GetD3D11SampleTimes(sample, &d3d11->first_timestamp))
+            break;
+
+        // Pass samples onto the remotery thread for sending to the viewer
+        FreeD3D11TimeStamps(sample);
+        AddSampleTreeMessage(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler);
+        MessageQueue_ConsumeNextMessage(d3d11->mq_to_d3d11_main, message);
+    }
+
+    rmt_EndCPUSample();
+}
+
+
+RMT_API void _rmt_EndD3D11Sample(void)
+{
+    ThreadSampler* ts;
+    D3D11* d3d11;
+
+    if (g_Remotery == NULL)
+        return;
+
+    // Has D3D11 been unbound?
+    d3d11 = g_Remotery->d3d11;
+    assert(d3d11 != NULL);
+    if (d3d11->device == NULL || d3d11->context == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        // Close the timestamp
+        D3D11Sample* d3d_sample = (D3D11Sample*)ts->sample_trees[SampleType_D3D11]->current_parent;
+        if (d3d_sample->timestamp != NULL)
+            D3D11Timestamp_End(d3d_sample->timestamp, d3d11->context);
+
+        // Send to the update loop for ready-polling
+        if (ThreadSampler_Pop(ts, d3d11->mq_to_d3d11_main, (Sample*)d3d_sample))
+            // Perform ready-polling on popping of the root sample
+            UpdateD3D11Frame();
+    }
+}
+
+
+#endif  // RMT_USE_D3D11
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+@OpenGL: OpenGL event sampling
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef RMT_USE_OPENGL
+
+
+#ifndef APIENTRY
+#  if defined(__MINGW32__) || defined(__CYGWIN__)
+#    define APIENTRY __stdcall
+#  elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED) || defined(__BORLANDC__)
+#    define APIENTRY __stdcall
+#  else
+#    define APIENTRY
+#  endif
+#endif
+
+#ifndef GLAPI
+#  if defined(__MINGW32__) || defined(__CYGWIN__)
+#    define GLAPI extern
+#  elif defined (_WIN32)
+#    define GLAPI WINGDIAPI
+#  else
+#    define GLAPI extern
+#  endif
+#endif
+
+#ifndef GLAPIENTRY
+#define GLAPIENTRY APIENTRY
+#endif
+
+typedef rmtU32 GLenum;
+typedef rmtU32 GLuint;
+typedef rmtS32 GLint;
+typedef rmtS32 GLsizei;
+typedef rmtU64 GLuint64;
+typedef rmtS64 GLint64;
+typedef unsigned char GLubyte;
+
+typedef void (GLAPIENTRY * PFNGLGENQUERIESPROC) (GLsizei n, GLuint* ids);
+typedef void (GLAPIENTRY * PFNGLDELETEQUERIESPROC) (GLsizei n, const GLuint* ids);
+typedef void (GLAPIENTRY * PFNGLBEGINQUERYPROC) (GLenum target, GLuint id);
+typedef void (GLAPIENTRY * PFNGLENDQUERYPROC) (GLenum target);
+typedef void (GLAPIENTRY * PFNGLGETQUERYOBJECTIVPROC) (GLuint id, GLenum pname, GLint* params);
+typedef void (GLAPIENTRY * PFNGLGETQUERYOBJECTUIVPROC) (GLuint id, GLenum pname, GLuint* params);
+typedef void (GLAPIENTRY * PFNGLGETQUERYOBJECTI64VPROC) (GLuint id, GLenum pname, GLint64* params);
+typedef void (GLAPIENTRY * PFNGLGETQUERYOBJECTUI64VPROC) (GLuint id, GLenum pname, GLuint64* params);
+typedef void (GLAPIENTRY * PFNGLQUERYCOUNTERPROC) (GLuint id, GLenum target);
+
+GLAPI GLenum GLAPIENTRY glGetError(void);
+
+#define GL_NO_ERROR 0
+#define GL_QUERY_RESULT 0x8866
+#define GL_QUERY_RESULT_AVAILABLE 0x8867
+#define GL_TIME_ELAPSED 0x88BF
+#define GL_TIMESTAMP 0x8E28
+
+// Not sure which platforms we need
+#if defined(_WIN32)
+#  define rmtGetProcAddress(name) wglGetProcAddress((LPCSTR)name)
+#elif defined(__APPLE__) && !defined(GLEW_APPLE_GLX)
+#  define rmtGetProcAddress(name) NSGLGetProcAddress(name)
+#elif defined(__sgi) || defined(__sun)
+#  define rmtGetProcAddress(name) dlGetProcAddress(name)
+#elif defined(__ANDROID__)
+#  define rmtGetProcAddress(name) NULL /* TODO */
+#elif defined(__native_client__)
+#  define rmtGetProcAddress(name) NULL /* TODO */
+#else /* __linux */
+extern void* glXGetProcAddressARB(const GLubyte*);
+#  define rmtGetProcAddress(name) (*glXGetProcAddressARB)(name)
+#endif
+
+#define RMT_GL_GET_FUN(x) assert(g_Remotery->opengl->x != NULL), g_Remotery->opengl->x
+
+#define glGenQueries RMT_GL_GET_FUN(__glGenQueries)
+#define glDeleteQueries RMT_GL_GET_FUN(__glDeleteQueries)
+#define glBeginQuery RMT_GL_GET_FUN(__glBeginQuery)
+#define glEndQuery RMT_GL_GET_FUN(__glEndQuery)
+#define glGetQueryObjectiv RMT_GL_GET_FUN(__glGetQueryObjectiv)
+#define glGetQueryObjectuiv RMT_GL_GET_FUN(__glGetQueryObjectuiv)
+#define glGetQueryObjecti64v RMT_GL_GET_FUN(__glGetQueryObjecti64v)
+#define glGetQueryObjectui64v RMT_GL_GET_FUN(__glGetQueryObjectui64v)
+#define glQueryCounter RMT_GL_GET_FUN(__glQueryCounter)
+
+
+typedef struct OpenGL
+{
+    PFNGLGENQUERIESPROC __glGenQueries;
+    PFNGLDELETEQUERIESPROC __glDeleteQueries;
+    PFNGLBEGINQUERYPROC __glBeginQuery;
+    PFNGLENDQUERYPROC __glEndQuery;
+    PFNGLGETQUERYOBJECTIVPROC __glGetQueryObjectiv;
+    PFNGLGETQUERYOBJECTUIVPROC __glGetQueryObjectuiv;
+    PFNGLGETQUERYOBJECTI64VPROC __glGetQueryObjecti64v;
+    PFNGLGETQUERYOBJECTUI64VPROC __glGetQueryObjectui64v;
+    PFNGLQUERYCOUNTERPROC __glQueryCounter;
+
+    // An allocator separate to the samples themselves so that OpenGL resource lifetime can be controlled
+    // outside of the Remotery thread.
+    ObjectAllocator* timestamp_allocator;
+
+    // Queue to the OpenGL main update thread
+    // Given that BeginSample/EndSample need to be called from the same thread that does the update, there
+    // is really no need for this to be a thread-safe queue. I'm using it for its convenience.
+    MessageQueue* mq_to_opengl_main;
+
+    // Mark the first time so that remaining timestamps are offset from this
+    rmtU64 first_timestamp;
+} OpenGL;
+
+
+static rmtError OpenGL_Create(OpenGL** opengl)
+{
+    rmtError error;
+
+    assert(opengl != NULL);
+
+    *opengl = (OpenGL*)rmtMalloc(sizeof(OpenGL));
+    if (*opengl == NULL)
+        return RMT_ERROR_MALLOC_FAIL;
+
+    (*opengl)->__glGenQueries = NULL;
+    (*opengl)->__glDeleteQueries = NULL;
+    (*opengl)->__glBeginQuery = NULL;
+    (*opengl)->__glEndQuery = NULL;
+    (*opengl)->__glGetQueryObjectiv = NULL;
+    (*opengl)->__glGetQueryObjectuiv = NULL;
+    (*opengl)->__glGetQueryObjecti64v = NULL;
+    (*opengl)->__glGetQueryObjectui64v = NULL;
+    (*opengl)->__glQueryCounter = NULL;
+
+    (*opengl)->timestamp_allocator = NULL;
+    (*opengl)->mq_to_opengl_main = NULL;
+    (*opengl)->first_timestamp = 0;
+
+    New_1(MessageQueue, (*opengl)->mq_to_opengl_main, g_Settings.messageQueueSizeInBytes);
+    return error;
+}
+
+
+static void OpenGL_Destructor(OpenGL* opengl)
+{
+    assert(opengl != NULL);
+    Delete(ObjectAllocator, opengl->timestamp_allocator);
+    Delete(MessageQueue, opengl->mq_to_opengl_main);
+}
+
+
+typedef struct OpenGLTimestamp
+{
+    // Inherit so that timestamps can be quickly allocated
+    ObjectLink ObjectLink;
+
+    // Pair of timestamp queries that wrap the sample
+    GLuint queries[2];
+} OpenGLTimestamp;
+
+
+static rmtError OpenGLTimestamp_Constructor(OpenGLTimestamp* stamp)
+{
+    int error;
+
+    assert(stamp != NULL);
+
+    ObjectLink_Constructor((ObjectLink*)stamp);
+
+    // Set defaults
+    stamp->queries[0] = stamp->queries[1] = 0;
+
+    // Create start/end timestamp queries
+    assert(g_Remotery != NULL);
+    glGenQueries(2, stamp->queries);
+    error = glGetError();
+    if (error != GL_NO_ERROR)
+        return RMT_ERROR_OPENGL_ERROR;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void OpenGLTimestamp_Destructor(OpenGLTimestamp* stamp)
+{
+    assert(stamp != NULL);
+
+    // Destroy queries
+    if (stamp->queries[0] != 0)
+    {
+        int error;
+        glDeleteQueries(2, stamp->queries);
+        error = glGetError();
+        assert(error == GL_NO_ERROR);
+    }
+}
+
+
+static void OpenGLTimestamp_Begin(OpenGLTimestamp* stamp)
+{
+    int error;
+
+    assert(stamp != NULL);
+
+    // Start of disjoint and first query
+    assert(g_Remotery != NULL);
+    glQueryCounter(stamp->queries[0], GL_TIMESTAMP);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+}
+
+
+static void OpenGLTimestamp_End(OpenGLTimestamp* stamp)
+{
+    int error;
+
+    assert(stamp != NULL);
+
+    // End of disjoint and second query
+    assert(g_Remotery != NULL);
+    glQueryCounter(stamp->queries[1], GL_TIMESTAMP);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+}
+
+
+static rmtBool OpenGLTimestamp_GetData(OpenGLTimestamp* stamp, rmtU64* out_start, rmtU64* out_end, rmtU64* out_first_timestamp)
+{
+    GLuint64 start = 0, end = 0;
+    GLint startAvailable = 0, endAvailable = 0;
+    int error;
+
+    assert(g_Remotery != NULL);
+
+    assert(stamp != NULL);
+    assert(stamp->queries[0] != 0 && stamp->queries[1] != 0);
+
+    // Check to see if all queries are ready
+    // If any fail to arrive, wait until later
+    glGetQueryObjectiv(stamp->queries[0], GL_QUERY_RESULT_AVAILABLE, &startAvailable);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+    if (!startAvailable)
+        return RMT_FALSE;
+    glGetQueryObjectiv(stamp->queries[1], GL_QUERY_RESULT_AVAILABLE, &endAvailable);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+    if (!endAvailable)
+        return RMT_FALSE;
+
+    glGetQueryObjectui64v(stamp->queries[0], GL_QUERY_RESULT, &start);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+    glGetQueryObjectui64v(stamp->queries[1], GL_QUERY_RESULT, &end);
+    error = glGetError();
+    assert(error == GL_NO_ERROR);
+
+    // Mark the first timestamp
+    assert(out_first_timestamp != NULL);
+    if (*out_first_timestamp == 0)
+        *out_first_timestamp = start;
+
+    // Calculate start and end timestamps (we want us, the queries give us ns)
+    *out_start = (rmtU64)(start - *out_first_timestamp) / 1000ULL;
+    *out_end = (rmtU64)(end - *out_first_timestamp) / 1000ULL;
+
+    return RMT_TRUE;
+}
+
+
+typedef struct OpenGLSample
+{
+    // IS-A inheritance relationship
+    Sample Sample;
+
+    OpenGLTimestamp* timestamp;
+
+} OpenGLSample;
+
+
+static rmtError OpenGLSample_Constructor(OpenGLSample* sample)
+{
+    assert(sample != NULL);
+
+    // Chain to sample constructor
+    Sample_Constructor((Sample*)sample);
+    sample->Sample.type = SampleType_OpenGL;
+    sample->Sample.size_bytes = sizeof(OpenGLSample);
+    sample->timestamp = NULL;
+
+    return RMT_ERROR_NONE;
+}
+
+
+static void OpenGLSample_Destructor(OpenGLSample* sample)
+{
+    Sample_Destructor((Sample*)sample);
+}
+
+
+RMT_API void _rmt_BindOpenGL()
+{
+    if (g_Remotery != NULL)
+    {
+        OpenGL* opengl = g_Remotery->opengl;
+        assert(opengl != NULL);
+
+        opengl->__glGenQueries = (PFNGLGENQUERIESPROC)rmtGetProcAddress((const GLubyte*)"glGenQueries");
+        opengl->__glDeleteQueries = (PFNGLDELETEQUERIESPROC)rmtGetProcAddress((const GLubyte*)"glDeleteQueries");
+        opengl->__glBeginQuery = (PFNGLBEGINQUERYPROC)rmtGetProcAddress((const GLubyte*)"glBeginQuery");
+        opengl->__glEndQuery = (PFNGLENDQUERYPROC)rmtGetProcAddress((const GLubyte*)"glEndQuery");
+        opengl->__glGetQueryObjectiv = (PFNGLGETQUERYOBJECTIVPROC)rmtGetProcAddress((const GLubyte*)"glGetQueryObjectiv");
+        opengl->__glGetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC)rmtGetProcAddress((const GLubyte*)"glGetQueryObjectuiv");
+        opengl->__glGetQueryObjecti64v = (PFNGLGETQUERYOBJECTI64VPROC)rmtGetProcAddress((const GLubyte*)"glGetQueryObjecti64v");
+        opengl->__glGetQueryObjectui64v = (PFNGLGETQUERYOBJECTUI64VPROC)rmtGetProcAddress((const GLubyte*)"glGetQueryObjectui64v");
+        opengl->__glQueryCounter = (PFNGLQUERYCOUNTERPROC)rmtGetProcAddress((const GLubyte*)"glQueryCounter");
+    }
+}
+
+
+static void FreeOpenGLTimeStamps(Sample* sample)
+{
+    Sample* child;
+
+    OpenGLSample* ogl_sample = (OpenGLSample*)sample;
+
+    assert(ogl_sample->timestamp != NULL);
+    ObjectAllocator_Free(g_Remotery->opengl->timestamp_allocator, (void*)ogl_sample->timestamp);
+    ogl_sample->timestamp = NULL;
+
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+        FreeOpenGLTimeStamps(child);
+}
+
+
+RMT_API void _rmt_UnbindOpenGL(void)
+{
+    if (g_Remotery != NULL)
+    {
+        OpenGL* opengl = g_Remotery->opengl;
+        assert(opengl != NULL);
+
+        // Flush the main queue of allocated OpenGL timestamps
+        while (1)
+        {
+            Msg_SampleTree* sample_tree;
+            Sample* sample;
+
+            Message* message = MessageQueue_PeekNextMessage(opengl->mq_to_opengl_main);
+            if (message == NULL)
+                break;
+
+            // There's only one valid message type in this queue
+            assert(message->id == MsgID_SampleTree);
+            sample_tree = (Msg_SampleTree*)message->payload;
+            sample = sample_tree->root_sample;
+            assert(sample->type == SampleType_OpenGL);
+            FreeOpenGLTimeStamps(sample);
+            FreeSampleTree(sample, sample_tree->allocator);
+
+            MessageQueue_ConsumeNextMessage(opengl->mq_to_opengl_main, message);
+        }
+
+        // Free all allocated OpenGL resources
+        Delete(ObjectAllocator, opengl->timestamp_allocator);
+    }
+}
+
+
+RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache)
+{
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        rmtError error;
+        Sample* sample;
+        rmtU32 name_hash = GetNameHash(name, hash_cache);
+
+        OpenGL* opengl = g_Remotery->opengl;
+
+        // Create the OpenGL tree on-demand as the tree needs an up-front-created root.
+        // This is not possible to create on initialisation as a OpenGL binding is not yet available.
+        SampleTree** ogl_tree = &ts->sample_trees[SampleType_OpenGL];
+        if (*ogl_tree == NULL)
+        {
+            New_3(SampleTree, *ogl_tree, sizeof(OpenGLSample), (ObjConstructor)OpenGLSample_Constructor, (ObjDestructor)OpenGLSample_Destructor);
+            if (error != RMT_ERROR_NONE)
+                return;
+        }
+
+        // Also create the timestamp allocator on-demand to keep the OpenGL code localised to the same file section
+        assert(opengl != NULL);
+        if (opengl->timestamp_allocator == NULL)
+            New_3(ObjectAllocator, opengl->timestamp_allocator, sizeof(OpenGLTimestamp), (ObjConstructor)OpenGLTimestamp_Constructor, (ObjDestructor)OpenGLTimestamp_Destructor);
+
+        // Push the sample
+        if (ThreadSampler_Push(ts, *ogl_tree, name, name_hash, &sample) == RMT_ERROR_NONE)
+        {
+            OpenGLSample* ogl_sample = (OpenGLSample*)sample;
+
+            // Allocate a timestamp for the sample and activate it
+            assert(ogl_sample->timestamp == NULL);
+            error = ObjectAllocator_Alloc(opengl->timestamp_allocator, (void**)&ogl_sample->timestamp);
+            if (error == RMT_ERROR_NONE)
+                OpenGLTimestamp_Begin(ogl_sample->timestamp);
+        }
+    }
+}
+
+
+static rmtBool GetOpenGLSampleTimes(Sample* sample, rmtU64* out_first_timestamp)
+{
+    Sample* child;
+
+    OpenGLSample* ogl_sample = (OpenGLSample*)sample;
+
+    assert(sample != NULL);
+    if (ogl_sample->timestamp != NULL)
+    {
+        if (!OpenGLTimestamp_GetData(ogl_sample->timestamp, &sample->us_start, &sample->us_end, out_first_timestamp))
+            return RMT_FALSE;
+    }
+
+    // Get child sample times
+    for (child = sample->first_child; child != NULL; child = child->next_sibling)
+    {
+        if (!GetOpenGLSampleTimes(child, out_first_timestamp))
+            return RMT_FALSE;
+    }
+
+    return RMT_TRUE;
+}
+
+
+static void UpdateOpenGLFrame(void)
+{
+    OpenGL* opengl;
+
+    if (g_Remotery == NULL)
+        return;
+
+    opengl = g_Remotery->opengl;
+    assert(opengl != NULL);
+
+    rmt_BeginCPUSample(rmt_UpdateOpenGLFrame);
+
+    // Process all messages in the OpenGL queue
+    while (1)
+    {
+        Msg_SampleTree* sample_tree;
+        Sample* sample;
+
+        Message* message = MessageQueue_PeekNextMessage(opengl->mq_to_opengl_main);
+        if (message == NULL)
+            break;
+
+        // There's only one valid message type in this queue
+        assert(message->id == MsgID_SampleTree);
+        sample_tree = (Msg_SampleTree*)message->payload;
+        sample = sample_tree->root_sample;
+        assert(sample->type == SampleType_OpenGL);
+
+        // Retrieve timing of all OpenGL samples
+        // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order
+        if (!GetOpenGLSampleTimes(sample, &opengl->first_timestamp))
+            break;
+
+        // Pass samples onto the remotery thread for sending to the viewer
+        FreeOpenGLTimeStamps(sample);
+        AddSampleTreeMessage(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler);
+        MessageQueue_ConsumeNextMessage(opengl->mq_to_opengl_main, message);
+    }
+
+    rmt_EndCPUSample();
+}
+
+
+RMT_API void _rmt_EndOpenGLSample(void)
+{
+    ThreadSampler* ts;
+
+    if (g_Remotery == NULL)
+        return;
+
+    if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE)
+    {
+        // Close the timestamp
+        OpenGLSample* ogl_sample = (OpenGLSample*)ts->sample_trees[SampleType_OpenGL]->current_parent;
+        if (ogl_sample->timestamp != NULL)
+            OpenGLTimestamp_End(ogl_sample->timestamp);
+
+        // Send to the update loop for ready-polling
+        if (ThreadSampler_Pop(ts, g_Remotery->opengl->mq_to_opengl_main, (Sample*)ogl_sample))
+            // Perform ready-polling on popping of the root sample
+            UpdateOpenGLFrame();
+    }
+}
+
+
+
+#endif  // RMT_USE_OPENGL
+
+
+#endif // RMT_ENABLED

+ 534 - 0
3rdparty/remotery/lib/Remotery.h

@@ -0,0 +1,534 @@
+
+
+/*
+Copyright 2014 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
+
+
+// Disable this to not include any bits of Remotery in your build
+#define RMT_ENABLED
+
+// Used by the Celtoys TinyCRT library (not released yet)
+//#define RMT_USE_TINYCRT
+
+// Assuming CUDA headers/libs are setup, allow CUDA profiling
+//#define RMT_USE_CUDA
+
+// Assuming Direct3D 11 headers/libs are setup, allow D3D11 profiling
+//#define RMT_USE_D3D11
+
+// Allow OpenGL profiling
+//#define RMT_USE_OPENGL
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   Compiler/Platform Detection and Preprocessor Utilities
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+// Compiler identification
+#if defined(_MSC_VER)
+    #define RMT_COMPILER_MSVC
+#elif defined(__GNUC__)
+    #define RMT_COMPILER_GNUC
+#elif defined(__clang__)
+    #define RMT_COMPILER_CLANG
+#endif
+
+
+// Platform identification
+#if defined(_WINDOWS) || defined(_WIN32)
+    #define RMT_PLATFORM_WINDOWS
+#elif defined(__linux__)
+    #define RMT_PLATFORM_LINUX
+    #define RMT_PLATFORM_POSIX
+#elif defined(__APPLE__)
+    #define RMT_PLATFORM_MACOS
+    #define RMT_PLATFORM_POSIX
+#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.
+#ifdef RMT_ENABLED
+    #define IFDEF_RMT_ENABLED(t, f) t
+#else
+    #define IFDEF_RMT_ENABLED(t, f) f
+#endif
+#if defined(RMT_ENABLED) && defined(RMT_USE_CUDA)
+    #define IFDEF_RMT_USE_CUDA(t, f) t
+#else
+    #define IFDEF_RMT_USE_CUDA(t, f) f
+#endif
+#if defined(RMT_ENABLED) && defined(RMT_USE_D3D11)
+    #define IFDEF_RMT_USE_D3D11(t, f) t
+#else
+    #define IFDEF_RMT_USE_D3D11(t, f) f
+#endif
+#if defined(RMT_ENABLED) && defined(RMT_USE_OPENGL)
+#define IFDEF_RMT_USE_OPENGL(t, f) t
+#else
+#define IFDEF_RMT_USE_OPENGL(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
+typedef enum rmtError
+{
+    RMT_ERROR_NONE,
+
+    // 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
+
+    // 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 real need to expose more detail since app will probably have an OpenGL error callback registered
+
+    RMT_ERROR_CUDA_UNKNOWN,
+} rmtError;
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   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)                                                    \
+    RMT_OPTIONAL(RMT_ENABLED, {                                                     \
+        static rmtU32 rmt_sample_hash_##name = 0;                                   \
+        _rmt_BeginCPUSample(#name, &rmt_sample_hash_##name);                        \
+    })
+
+#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
+{
+    rmtU32 port;
+
+    // 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 logFilename;
+} 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_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_EndOpenGLSample()                                               \
+    RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_EndOpenGLSample())
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   C++ Public Interface Extensions
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef __cplusplus
+
+
+#ifdef RMT_ENABLED
+
+// Types that end samples in their destructors
+extern "C" RMT_API void _rmt_EndCPUSample(void);
+struct rmt_EndCPUSampleOnScopeExit
+{
+    ~rmt_EndCPUSampleOnScopeExit()
+    {
+        _rmt_EndCPUSample();
+    }
+};
+#ifdef 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
+#ifdef RMT_USE_D3D11
+extern "C" RMT_API void _rmt_EndD3D11Sample(void);
+struct rmt_EndD3D11SampleOnScopeExit
+{
+    ~rmt_EndD3D11SampleOnScopeExit()
+    {
+        _rmt_EndD3D11Sample();
+    }
+};
+#endif
+
+#ifdef RMT_USE_OPENGL
+extern "C" RMT_API void _rmt_EndOpenGLSample(void);
+struct rmt_EndOpenGLSampleOnScopeExit
+{
+    ~rmt_EndOpenGLSampleOnScopeExit()
+    {
+        _rmt_EndOpenGLSample();
+    }
+};
+#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)                                                                       \
+        RMT_OPTIONAL(RMT_ENABLED, rmt_BeginCPUSample(name));                                            \
+        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);
+
+#endif
+
+
+
+/*
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+   Private Interface - don't directly call these
+------------------------------------------------------------------------------------------------------------------------
+------------------------------------------------------------------------------------------------------------------------
+*/
+
+
+
+#ifdef 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* hash_cache);
+RMT_API void _rmt_EndCPUSample(void);
+
+#ifdef 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
+
+#ifdef 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
+
+#ifdef 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
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // RMT_ENABLED
+
+
+#endif

+ 197 - 0
3rdparty/remotery/readme.md

@@ -0,0 +1,197 @@
+Remotery
+--------
+
+A realtime CPU/GPU profiler hosted in a single C file with a viewer that runs in a web browser.
+
+![screenshot](screenshot.png?raw=true)
+
+Supported features:
+
+* Lightweight instrumentation of multiple threads running on the CPU.
+* Web viewer that runs in Chrome, Firefox and Safari. Custom WebSockets server
+  transmits sample data to the browser on a latent thread.
+* Profiles itself and shows how it's performing in the viewer.
+* Can optionally sample CUDA/D3D11 GPU activity.
+* Console output for logging text.
+* Console input for sending commands to your game.
+
+
+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:
+
+    Macro               Default             Description
+
+    RMT_ENABLED         <defined>           Disable this to not include any bits of Remotery in your build
+    RMT_USE_TINYCRT     <not defined>       Used by the Celtoys TinyCRT library (not released yet)
+    RMT_USE_CUDA        <not defined>       Assuming CUDA headers/libs are setup, allow CUDA profiling
+    RMT_USE_D3D11       <not defined>       Assuming Direct3D 11 headers/libs are setup, allow D3D11 GPU profiling
+    RMT_USE_OPENGL      <not defined>       Allow OpenGL GPU profiling (standalone except you must link to OpenGL which you already do if you use it)
+
+
+Basic Use
+---------
+
+See the sample directory for further examples. A quick example:
+
+    int main()
+    {
+        // Create the main instance of Remotery.
+        // You need only do this once per program.
+        Remotery* rmt;
+        rmt_CreateGlobalInstance(&rmt);
+
+        // Explicit begin/end for C
+        {
+            rmt_BeginCPUSample(LogText);
+            rmt_LogText("Time me, please!");
+            rmt_EndCPUSample();
+        }
+
+        // Scoped begin/end for C++
+        {
+            rmt_ScopedCPUSample(LogText);
+            rmt_LogText("Time me, too!");
+        }
+
+        // Destroy the main instance of Remotery.
+        rmt_DestroyGlobalInstance(rmt);
+    }
+
+
+Running the Viewer
+------------------
+
+Double-click or launch `vis/index.html` from the browser.
+
+
+Sampling CUDA GPU activity
+--------------------------
+
+Remotery allows for profiling multiple threads of CUDA execution using different asynchronous streams
+that must all share the same context. After initialising both Remotery and CUDA you need to bind the
+two together using the call:
+
+    rmtCUDABind bind;
+    bind.context = m_Context;
+    bind.CtxSetCurrent = &cuCtxSetCurrent;
+    bind.CtxGetCurrent = &cuCtxGetCurrent;
+    bind.EventCreate = &cuEventCreate;
+    bind.EventDestroy = &cuEventDestroy;
+    bind.EventRecord = &cuEventRecord;
+    bind.EventQuery = &cuEventQuery;
+    bind.EventElapsedTime = &cuEventElapsedTime;
+    rmt_BindCUDA(&bind);
+
+Explicitly pointing to the CUDA interface allows Remotery to be included anywhere in your project without
+need for you to link with the required CUDA libraries. After the bind completes you can safely sample any
+CUDA activity:
+
+    CUstream stream;
+
+    // Explicit begin/end for C
+    {
+        rmt_BeginCUDASample(UnscopedSample, stream);
+        // ... CUDA code ...
+        rmt_EndCUDASample(stream);
+    }
+
+    // Scoped begin/end for C++
+    {
+        rmt_ScopedCUDASample(ScopedSample, stream);
+        // ... CUDA code ...
+    }
+
+Remotery supports only one context for all threads and will use cuCtxGetCurrent and cuCtxSetCurrent to
+ensure the current thread has the context you specify in rmtCUDABind.context.
+
+
+Sampling Direct3D 11 GPU activity
+---------------------------------
+
+Remotery allows sampling of GPU activity on your main D3D11 context. After initialising Remotery, you need
+to bind it to D3D11 with a single call from the thread that owns the device context:
+
+    // Parameters are ID3D11Device* and ID3D11DeviceContext*
+    rmt_BindD3D11(d3d11_device, d3d11_context);
+
+Sampling is then a simple case of:
+
+    // Explicit begin/end for C
+    {
+        rmt_BeginD3D11Sample(UnscopedSample);
+        // ... D3D code ...
+        rmt_EndD3D11Sample();
+    }
+
+    // Scoped begin/end for C++
+    {
+        rmt_ScopedD3D11Sample(ScopedSample);
+        // ... D3D code ...
+    }
+
+Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown
+your D3D11 device and context, ensure you notify Remotery before shutting down Remotery itself:
+
+    rmt_UnbindD3D11();
+
+
+Sampling OpenGL GPU activity
+----------------------------
+
+Remotery allows sampling of GPU activity on your main OpenGL context. After initialising Remotery, you need
+to bind it to OpenGL with the single call:
+
+    rmt_BindOpenGL();
+
+Sampling is then a simple case of:
+
+    // Explicit begin/end for C
+    {
+        rmt_BeginOpenGLSample(UnscopedSample);
+        // ... OpenGL code ...
+        rmt_EndOpenGLSample();
+    }
+
+    // Scoped begin/end for C++
+    {
+        rmt_ScopedOpenGLSample(ScopedSample);
+        // ... OpenGL code ...
+    }
+
+Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown
+your OpenGL device and context, ensure you notify Remotery before shutting down Remotery itself:
+
+    rmt_UnbindOpenGL();
+
+
+Applying Configuration Settings
+-------------------------------
+
+Before creating your Remotery instance, you can configure its behaviour by retrieving its settings object:
+
+    rmtSettings* settings = rmt_Settings();
+
+Some important settings are:
+
+    // Redirect any Remotery allocations to your own malloc/free, with an additional context pointer
+    // that gets passed to your callbacks.
+    settings->malloc;
+    settings->free;
+    settings->mm_context;
+
+    // Specify an input handler that receives text input from the Remotery console, with an additional
+    // context pointer that gets passed to your callback.
+    settings->input_handler;
+    settings->input_handler_context;

+ 33 - 0
3rdparty/remotery/sample/sample.c

@@ -0,0 +1,33 @@
+#include <stdlib.h>
+#include <math.h>
+#include "Remotery.h"
+
+double delay() {
+    int i, end;
+    double j = 0;
+
+    rmt_BeginCPUSample(delay);
+    for( i = 0, end = rand()/100; i < end; ++i ) {
+        j += sin(i);
+    }
+    rmt_EndCPUSample();
+    return j;
+}
+
+
+int main( int argc, const char **argv ) {
+    Remotery *rmt;
+
+    if( RMT_ERROR_NONE != rmt_CreateGlobalInstance(&rmt) ) {
+        return -1;
+    }
+
+    for(;;) {
+        rmt_LogText("start profiling");
+        delay();
+        rmt_LogText("end profiling");
+    }
+
+    rmt_DestroyGlobalInstance(rmt);
+    return 0;
+}

BIN
3rdparty/remotery/screenshot.png


+ 117 - 0
3rdparty/remotery/vis/Code/Console.js

@@ -0,0 +1,117 @@
+
+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.AppTextBuffer = "";
+
+		// 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("LOG", Bind(OnLog, this));
+	}
+
+
+	Console.prototype.Log = function(text)
+	{
+		this.PageTextBuffer = LogText(this.PageTextBuffer, text);
+	}
+
+
+	Console.prototype.WindowResized = function(width, height)
+	{
+		// Place window
+		this.Window.SetPosition(BORDER, height - BORDER - 200);
+		this.Window.SetSize(width - 2 * BORDER, HEIGHT);
+
+		// Place controls
+		var parent_size = this.Window.Size;
+		var mid_w = parent_size[0] / 3;
+		this.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30);
+		this.UserInput.SetSize(parent_size[0] - 100, 18);
+		var output_height = this.UserInput.Position[1] - 2 * BORDER;
+		this.PageContainer.SetPosition(BORDER, BORDER);
+		this.PageContainer.SetSize(mid_w - 2 * BORDER, output_height);
+		this.AppContainer.SetPosition(mid_w, BORDER);
+		this.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height);
+	}
+
+
+	function OnLog(self, socket, message)
+	{
+		self.AppTextBuffer = LogText(self.AppTextBuffer, message.text);
+	}
+
+
+	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 = 10 * 1024;
+		var len = existing_text.length;
+		if (len > max_len)
+			existing_text = existing_text.substr(len - max_len, max_len);
+
+		return existing_text;
+	}
+
+
+	function UpdateHTML(self)
+	{
+		// Reset the current text buffer as html
+
+		var page_node = self.PageContainer.Node;
+		page_node.innerHTML = self.PageTextBuffer;
+		page_node.scrollTop = page_node.scrollHeight;
+
+		var app_node = self.AppContainer.Node;
+		app_node.innerHTML = self.AppTextBuffer;
+		app_node.scrollTop = app_node.scrollHeight;
+	}
+
+
+	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("");
+	}
+
+
+	return Console;
+})();

+ 61 - 0
3rdparty/remotery/vis/Code/PixelTimeRange.js

@@ -0,0 +1,61 @@
+
+
+PixelTimeRange = (function()
+{
+	function PixelTimeRange(start_us, span_us, span_px)
+	{
+		this.Span_px = span_px;
+		this.Set(start_us, span_us);
+	}
+
+
+	PixelTimeRange.prototype.Set = function(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;
+	}
+
+
+	PixelTimeRange.prototype.SetStart = function(start_us)
+	{
+		this.Start_us = start_us;
+		this.End_us = start_us + this.Span_us;
+	}
+
+
+	PixelTimeRange.prototype.SetEnd = function(end_us)
+	{
+		this.End_us = end_us;
+		this.Start_us = end_us - this.Span_us;
+	}
+
+
+	PixelTimeRange.prototype.SetPixelSpan = function(span_px)
+	{
+		this.Span_px = span_px;
+		this.usPerPixel = this.Span_px / this.Span_us;
+	}
+
+
+	PixelTimeRange.prototype.PixelOffset = function(time_us)
+	{
+		return Math.floor((time_us - this.Start_us) * this.usPerPixel);
+	}
+
+
+	PixelTimeRange.prototype.PixelSize = function(time_us)
+	{
+		return Math.floor(time_us * this.usPerPixel);
+	}
+
+
+	PixelTimeRange.prototype.Clone = function()
+	{
+		return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px);
+	}
+
+
+	return PixelTimeRange;
+})();

+ 234 - 0
3rdparty/remotery/vis/Code/Remotery.js

@@ -0,0 +1,234 @@
+
+//
+// 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;
+	}
+
+	return Settings;
+
+})();
+
+
+Remotery = (function()
+{
+	function Remotery()
+	{
+		this.WindowManager = new WM.WindowManager();
+		this.Settings = new Settings();
+
+		this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt");
+		this.Server = new WebSocketConnection();
+		this.Server.AddConnectHandler(Bind(OnConnect, 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.TimelineWindow = new TimelineWindow(this.WindowManager, this.Settings, this.Server, Bind(OnTimelineCheck, this));
+		this.TimelineWindow.SetOnHover(Bind(OnSampleHover, this));
+		this.TimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
+
+		this.NbSampleWindows = 0;
+		this.SampleWindows = { };
+		this.FrameHistory = { };
+		this.SelectedFrames = { };
+
+		this.Server.AddMessageHandler("SAMPLES", Bind(OnSamples, 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);
+		})();
+	}
+
+
+	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.TimelineWindow.ResetTimeRange();
+		self.FrameHistory = { };
+		self.SelectedFrames = { };
+	}
+
+
+	function OnAddressChanged(self, node)
+	{
+		// Update and disconnect, relying on auto-connect to reconnect
+		self.ConnectionAddress = node.value;
+		self.Server.Disconnect();
+	}
+
+
+	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.TimelineWindow.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)
+		if ((self.DisplayFrame % 10) == 0)
+			self.TimelineWindow.DrawAllRows();
+
+		self.DisplayFrame++;
+	}
+
+
+	function OnSamples(self, socket, message)
+	{
+		var name = message.thread_name;
+
+		// Discard any new samples while paused
+		if (self.Settings.IsPaused)
+			return;
+
+		// 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];
+		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.TimelineWindow.Window, self.Console.Window);
+			self.NbSampleWindows++;
+			MoveSampleWindows(this);
+		}
+
+		// Set on the window and timeline
+		self.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples);
+		self.TimelineWindow.OnSamples(name, frame_history);
+	}
+
+
+	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.TimelineWindow.Window, self.Console.Window);
+		}
+	}
+
+
+	function OnSampleHover(self, thread_name, hover)
+	{
+		// Hover only changes sample window contents when paused
+		var sample_window = self.SampleWindows[thread_name];
+		if (sample_window && self.Settings.IsPaused)
+		{
+			if (hover == null)
+			{
+				// When there's no hover, go back to the selected frame
+				if (self.SelectedFrames[thread_name])
+				{
+					var frame = self.SelectedFrames[thread_name];
+					sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
+				}
+			}
+
+			else
+			{
+				// Populate with sample under hover
+				var frame = hover[0];
+				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.TimelineWindow.WindowResized(w, h, self.TitleWindow.Window);
+		for (var i in self.SampleWindows)
+			self.SampleWindows[i].WindowResized(self.TimelineWindow.Window, self.Console.Window);
+	}
+
+
+	return Remotery;
+})();

+ 164 - 0
3rdparty/remotery/vis/Code/SampleWindow.js

@@ -0,0 +1,164 @@
+
+SampleWindow = (function()
+{
+	function SampleWindow(wm, name, offset)
+	{
+		// Sample digest for checking if grid needs to be repopulated
+		this.NbSamples = 0;
+		this.SampleDigest = null;
+
+		this.XPos = 10 + offset * 410;
+		this.Window = wm.AddWindow(name, 100, 100, 100, 100);
+		this.Window.Show();
+		this.Visible = true;
+
+		// Create a grid that's indexed by the unique sample ID
+		this.Grid = this.Window.AddControlNew(new WM.Grid(0, 0, 380, 400));
+		this.RootRow = this.Grid.Rows.Add({ "Name": "Samples" }, "GridGroup", { "Name": "GridGroup" });
+		this.RootRow.Rows.AddIndex("_ID");
+	}
+
+
+	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.Show();
+			else
+				this.Window.Hide();
+
+			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;
+
+		// 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 = UpdateSamples(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 times
+			UpdateSampleTimes(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: "",
+				Control: new WM.Label()
+			};
+
+			var cell_classes =
+			{
+				Name: "SampleNameCell",
+			};
+
+			parent_row.Rows.Add(cell_data, null, cell_classes);
+		}
+	}
+
+
+	function UpdateSamples(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);
+
+			// Set sample name and colour
+			var name_node = row.CellNodes["Name"];
+			name_node.innerHTML = indent + sample.name;
+			DOM.Node.SetColour(name_node, sample.colour);
+
+			row.CellData.Control.SetText(sample.us_length);
+
+			index = UpdateSamples(parent_row, sample.children, index, indent + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
+		}
+
+		return index;
+	}
+
+
+	function UpdateSampleTimes(parent_row, samples)
+	{
+		for (var i in samples)
+		{
+			var sample = samples[i];
+
+			var row = parent_row.Rows.GetBy("_ID", sample.id);
+			if (row)
+				row.CellData.Control.SetText(sample.us_length);
+
+			UpdateSampleTimes(parent_row, sample.children);
+		}
+	}
+
+
+	return SampleWindow;
+})();

+ 28 - 0
3rdparty/remotery/vis/Code/ThreadFrame.js

@@ -0,0 +1,28 @@
+
+
+ThreadFrame = (function()
+{
+	function ThreadFrame(message)
+	{
+		// Persist the required message data
+		this.NbSamples = message.nb_samples;
+		this.SampleDigest = message.sample_digest;
+		this.Samples = message.samples;
+
+		// 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;
+})();

+ 375 - 0
3rdparty/remotery/vis/Code/TimelineRow.js

@@ -0,0 +1,375 @@
+
+
+TimelineRow = (function()
+{
+	var row_template = function(){/*
+		<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>
+			<canvas class='TimelineRowCanvas'></canvas>
+			<div style="clear:left"></div>
+		</div>
+*/}.toString().split(/\n/).slice(1, -1).join("\n");
+
+
+	var CANVAS_Y_OFFSET = 0;
+	var CANVAS_BORDER = 1;
+	var SAMPLE_HEIGHT = 16;
+	var SAMPLE_BORDER = 1;
+	var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2;
+	var SAMPLE_Y_OFFSET = CANVAS_Y_OFFSET + CANVAS_BORDER + 1;
+
+
+	function TimelineRow(name, width, parent_node, frame_history, check_handler)
+	{
+		this.Name = name;
+
+		// Create the row HTML and add to the parent
+		this.ContainerNode = DOM.Node.CreateHTML(row_template);
+		this.Node = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowData");
+		this.LabelNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowLabel");
+		this.LabelNode.innerHTML = name;
+		this.CheckboxNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCheckbox");
+		var expand_node_0 = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowExpand", 0);
+		var expand_node_1 = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowExpand", 1);
+		this.IncNode = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton");
+		this.DecNode = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton");
+		this.CanvasNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCanvas");
+		parent_node.appendChild(this.ContainerNode);
+
+		// All sample view windows visible by default
+		this.CheckboxNode.checked = true;
+		DOM.Event.AddHandler(this.CheckboxNode, "change", function(evt) { check_handler(name, evt); });
+
+		// Manually hook-up events to simulate div:active
+		// I can't get the equivalent CSS to work in Firefox, so...
+		DOM.Event.AddHandler(this.IncNode, "mousedown", ExpandButtonDown);
+		DOM.Event.AddHandler(this.IncNode, "mouseup", ExpandButtonUp);
+		DOM.Event.AddHandler(this.IncNode, "mouseleave", ExpandButtonUp);
+		DOM.Event.AddHandler(this.DecNode, "mousedown", ExpandButtonDown);
+		DOM.Event.AddHandler(this.DecNode, "mouseup", ExpandButtonUp);
+		DOM.Event.AddHandler(this.DecNode, "mouseleave", ExpandButtonUp);
+
+		// Pressing +/i increases/decreases depth
+		DOM.Event.AddHandler(this.IncNode, "click", Bind(IncDepth, this));
+		DOM.Event.AddHandler(this.DecNode, "click", Bind(DecDepth, this));
+
+		// Setup the canvas
+		this.Depth = 1;
+		this.Ctx = this.CanvasNode.getContext("2d");
+		this.SetSize(width);
+		this.Clear();
+
+		// Frame index to start at when looking for first visible sample
+		this.StartFrameIndex = 0;
+
+		this.FrameHistory = frame_history;
+		this.VisibleFrames = [ ];
+		this.VisibleTimeRange = null;
+
+		// Sample the mouse is currently hovering over
+		this.HoverSample = null;
+		this.HoverSampleDepth = 0;
+
+		// Currently selected sample
+		this.SelectedSample = null;
+		this.SelectedSampleDepth = 0;
+	}
+
+
+	TimelineRow.prototype.SetSize = function(width)
+	{
+		// Must ALWAYS set the width/height properties together. Setting one on its own has weird side-effects.
+		this.CanvasNode.width = width;
+		this.CanvasNode.height = CANVAS_BORDER + SAMPLE_BORDER + SAMPLE_Y_SPACING * this.Depth;
+		this.Draw(true);
+	}
+
+
+	TimelineRow.prototype.Clear = function()
+	{
+		// Fill box that shows the boundary between thread rows
+		this.Ctx.fillStyle = "#666"
+		var b = CANVAS_BORDER;
+		this.Ctx.fillRect(b, b, this.CanvasNode.width - b * 2, this.CanvasNode.height - b * 2);
+	}
+
+
+	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.Draw = function(draw_text)
+	{
+		this.Clear();
+
+		// Draw all root samples in the visible frame set
+		for (var i in this.VisibleFrames)
+		{
+			var frame = this.VisibleFrames[i];
+			DrawSamples(this, frame.Samples, 1, draw_text);
+		}
+	}
+
+
+	function DrawSamples(self, samples, depth, draw_text)
+	{
+		for (var i in samples)
+		{
+			var sample = samples[i];
+			DrawSample(self, sample, depth, draw_text);
+
+			if (depth < self.Depth && sample.children != null)
+				DrawSamples(self, sample.children, depth + 1, draw_text);
+		}
+	}
+
+
+	TimelineRow.prototype.UpdateHoverSample = function(mouse_state, x_offset)
+	{
+		var hover = GetSampleAtPosition(this, mouse_state, x_offset);
+		if (hover)
+			this.SetHoverSample(hover[1], hover[2]);
+		return hover;
+	}
+
+
+	TimelineRow.prototype.UpdateSelectedSample = function(mouse_state, x_offset)
+	{
+		var select = GetSampleAtPosition(this, mouse_state, x_offset);
+		if (select)
+			this.SetSelectedSample(select[1], select[2]);
+		return select;
+	}
+
+
+	TimelineRow.prototype.SetHoverSample = function(sample, sample_depth)
+	{
+		if (sample != this.HoverSample)
+		{
+			// Discard old highlight
+			// TODO: When zoomed right out, tiny samples are anti-aliased and this becomes inaccurate
+			var old_sample = this.HoverSample;
+			var old_sample_depth = this.HoverSampleDepth;
+			this.HoverSample = null;
+			this.HoverSampleDepth = 0;
+			DrawSample(this, old_sample, old_sample_depth, true);
+
+			// Add new highlight
+			this.HoverSample = sample;
+			this.HoverSampleDepth = sample_depth;
+			DrawSample(this, sample, sample_depth, true);
+		}
+	}
+
+
+	TimelineRow.prototype.SetSelectedSample = function(sample, sample_depth)
+	{
+		if (sample != this.SelectedSample)
+		{
+			// Discard old highlight
+			// TODO: When zoomed right out, tiny samples are anti-aliased and this becomes inaccurate
+			var old_sample = this.SelectedSample;
+			var old_sample_depth = this.SelectedSampleDepth;
+			this.SelectedSample = null;
+			this.SelectedSampleDepth = 0;
+			DrawSample(this, old_sample, old_sample_depth, true);
+
+			// Add new highlight
+			this.SelectedSample = sample;
+			this.SelectedSampleDepth = sample_depth;
+			DrawSample(this, sample, sample_depth, true);
+		}
+	}
+
+
+	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");
+	}
+
+
+	function IncDepth(self)
+	{
+		self.Depth++;
+		self.SetSize(self.CanvasNode.width);
+	}
+
+
+	function DecDepth(self)
+	{
+		if (self.Depth > 1)
+		{
+			self.Depth--;
+			self.SetSize(self.CanvasNode.width);
+		}
+	}
+
+
+	function GetSampleAtPosition(self, mouse_state, x_offset)
+	{
+		// Mouse movement can occur before any data is sent to a timeline row
+		var time_range = self.VisibleTimeRange;
+		if (time_range == null)
+			return;
+
+		// Get the time the mouse is over
+		var x = mouse_state.Position[0] - x_offset;
+		var time_us = time_range.Start_us + x / time_range.usPerPixel;
+
+		var canvas_y_offset = DOM.Node.GetPosition(self.CanvasNode)[1];
+		var mouse_y_offset = mouse_state.Position[1] - canvas_y_offset;
+		mouse_y_offset = Math.min(Math.max(mouse_y_offset, 0), self.CanvasNode.height);
+		var depth = Math.floor(mouse_y_offset / SAMPLE_Y_SPACING) + 1;
+		
+		// Search for the first frame to intersect this time
+		for (var i in self.VisibleFrames)
+		{
+			var frame = self.VisibleFrames[i];
+			if (time_us >= frame.StartTime_us && time_us < frame.EndTime_us)
+			{
+				var found_sample = FindSample(self, frame.Samples, time_us, depth, 1);
+				if (found_sample != null)
+					return [ frame, found_sample[0], found_sample[1] ];
+			}
+		}
+
+		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;
+	}
+
+
+	function DrawSample(self, sample, depth, draw_text)
+	{
+		if (sample == null)
+			return;
+
+		// Determine pixel range of the sample
+		var time_range = self.VisibleTimeRange;
+		var x0 = time_range.PixelOffset(sample.us_start);
+		var x1 = x0 + time_range.PixelSize(sample.us_length);
+
+		// Clip to padded timeline row
+		var min_x = 3;
+		var max_x = self.CanvasNode.width - 5;
+		x0 = Math.min(Math.max(x0, min_x), max_x);
+		x1 = Math.min(Math.max(x1, min_x), max_x);
+
+		var offset_x = x0;
+		var offset_y = SAMPLE_Y_OFFSET + (depth - 1) * SAMPLE_Y_SPACING;
+		var size_x = x1 - x0;
+		var size_y = SAMPLE_HEIGHT;
+
+		// Normal rendering
+		var ctx = self.Ctx;
+		ctx.fillStyle = sample.colour;
+		ctx.fillRect(offset_x, offset_y, size_x, size_y);
+
+		// Highlight rendering
+		var b = (sample == self.HoverSample) ? 255 : 0;
+		var r = (sample == self.SelectedSample) ? 255 : 0;
+		if (b + r > 0)
+		{
+			ctx.lineWidth = 1;
+			ctx.strokeStyle = "rgb(" + r + ", 0, " + b + ")";
+			ctx.strokeRect(offset_x + 0.5, offset_y + 0.5, size_x - 1, size_y - 1);
+		}
+
+		// Draw sample names clipped to the bounds of the sample
+		if (draw_text)
+		{
+			ctx.save();
+			ctx.beginPath();
+			ctx.rect(offset_x + 2.5, offset_y + 1.5, size_x - 5, size_y - 3);
+			ctx.clip();
+			ctx.font = "9px verdana";
+			ctx.fillStyle = "black";
+			ctx.fillText(sample.name, offset_x + 5.5, offset_y + 1.5 + 9);
+			ctx.restore();
+		}
+	}
+
+
+	return TimelineRow;
+})();

+ 270 - 0
3rdparty/remotery/vis/Code/TimelineWindow.js

@@ -0,0 +1,270 @@
+
+//
+// TODO: Use WebGL and instancing for quicker renders
+//
+
+
+TimelineWindow = (function()
+{
+	var BORDER = 10;
+
+	var ROW_START_SIZE = 210;
+
+	var ROW_END_SIZE = 20;  // make room for scrollbar
+
+	var box_template = "<div class='TimelineBox'></div>";
+
+
+	function TimelineWindow(wm, settings, server, check_handler)
+	{
+		this.Settings = settings;
+
+		// Ordered list of thread rows on the timeline
+		this.ThreadRows = [ ];
+
+		// Create window and containers
+		this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
+		this.Window.ShowNoAnim();
+		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.TimelineMoved = false;
+		this.OnHoverHandler = null;
+		this.OnSelectedHandler = null;
+		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));		
+
+		// Set time range AFTER the window has been created, as it uses the window to determine pixel coverage
+		this.TimeRange = new PixelTimeRange(0, 200 * 1000, RowWidth(this));
+
+		this.CheckHandler = check_handler;
+	}
+
+
+	TimelineWindow.prototype.SetOnHover = function(handler)
+	{
+		this.OnHoverHandler = handler;
+	}
+
+
+	TimelineWindow.prototype.SetOnSelected = function(handler)
+	{
+		this.OnSelectedHandler = handler;
+	}
+
+
+	TimelineWindow.prototype.WindowResized = function(width, height, top_window)
+	{
+		// Resize window
+		var top = top_window.Position[1] + top_window.Size[1] + 10;
+		this.Window.SetPosition(10, top);
+		this.Window.SetSize(width - 2 * 10, 200);
+
+		// Resize controls
+		var parent_size = this.Window.Size;
+		this.TimelineContainer.SetPosition(BORDER, 10);
+		this.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER, 160);
+
+		// Resize rows
+		var row_width = RowWidth(this);
+		for (var i in this.ThreadRows)
+		{
+			var row = this.ThreadRows[i];
+			row.SetSize(row_width);
+		}
+
+		// Adjust time range to new width
+		this.TimeRange.SetPixelSpan(row_width);
+		this.DrawAllRows();
+	}
+
+
+	TimelineWindow.prototype.ResetTimeRange = function()
+	{
+		this.TimeRange.SetStart(0);
+	}
+
+
+	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(thread_name, RowWidth(this), this.TimelineContainer.Node, frame_history, this.CheckHandler);
+			this.ThreadRows.push(row);
+			this.ThreadRows.sort(function(a, b) { return b.Name.localeCompare(a.Name); });
+		}
+	}
+
+
+	TimelineWindow.prototype.DrawAllRows = function()
+	{
+		var time_range = this.TimeRange;
+		var draw_text = this.Settings.IsPaused;
+		for (var i in this.ThreadRows)
+		{
+			var thread_row = this.ThreadRows[i];
+			thread_row.SetVisibleFrames(time_range);
+			thread_row.Draw(draw_text);
+		}
+	}
+
+
+	function RowXOffset(self)
+	{
+		// Add sizing of the label
+		// TODO: Use computed size
+		return DOM.Node.GetPosition(self.TimelineContainer.Node)[0] + ROW_START_SIZE;
+	}
+
+
+	function RowWidth(self)
+	{
+		// Subtract sizing of the label
+		// TODO: Use computed size
+		return self.TimelineContainer.Size[0] - (ROW_START_SIZE + ROW_END_SIZE);
+	}
+
+
+	function OnMouseScroll(self, evt)
+	{
+		var mouse_state = new Mouse.State(evt);
+		var scale = 1.11;
+			if (mouse_state.WheelDelta > 0)
+				scale = 1 / scale;
+
+		// What time is the mouse hovering over?
+		var x = mouse_state.Position[0] - RowXOffset(self);
+		var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;
+
+		// 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();
+
+		// Prevent vertical scrolling on mouse-wheel
+		DOM.Event.StopDefaultAction(evt);
+	}
+
+
+	function OnMouseDown(self, evt)
+	{
+		// Only manipulate the timelime when paused
+		if (!self.Settings.IsPaused)
+			return;
+
+		self.MouseDown = true;
+		self.TimelineMoved = false;
+		DOM.Event.StopDefaultAction(evt);
+	}
+
+
+	function OnMouseUp(self, evt)
+	{
+		// Only manipulate the timelime when paused
+		if (!self.Settings.IsPaused)
+			return;
+
+		var mouse_state = new Mouse.State(evt);
+
+		self.MouseDown = false;
+
+		if (!self.TimelineMoved)
+		{
+			// Search for the row being clicked and update its selection
+			var row_node = DOM.Event.GetNode(evt);
+			for (var i in self.ThreadRows)
+			{
+				var thread_row = self.ThreadRows[i];
+				if (thread_row.CanvasNode == row_node)
+				{
+					var select = thread_row.UpdateSelectedSample(mouse_state, RowXOffset(self));
+
+					// Call any selection handlers
+					if (self.OnSelectedHandler)
+						self.OnSelectedHandler(thread_row.Name, select);
+
+					break;
+				}
+			}
+		}
+	}
+
+
+	function OnMouseMove(self, evt)
+	{
+		// Only manipulate the timelime when paused
+		if (!self.Settings.IsPaused)
+			return;
+
+		var mouse_state = new Mouse.State(evt);
+
+		if (self.MouseDown)
+		{
+			// Get the time the mouse is over
+			var x = mouse_state.Position[0] - RowXOffset(self);
+			var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel;
+
+			// Shift the visible time range with mouse movement
+			var time_offset_us = mouse_state.PositionDelta[0] / self.TimeRange.usPerPixel;
+			if (time_offset_us)
+			{
+				self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);
+				self.DrawAllRows();
+				self.TimelineMoved = true;
+			}
+		}
+
+		else
+		{
+			// Highlight any samples the mouse moves over
+			var row_node = DOM.Event.GetNode(evt);
+			for (var i in self.ThreadRows)
+			{
+				var thread_row = self.ThreadRows[i];
+				if (thread_row.CanvasNode == row_node)
+				{
+					var hover = thread_row.UpdateHoverSample(mouse_state, RowXOffset(self));
+
+					if (self.OnHoverHandler)
+						self.OnHoverHandler(thread_row.Name, hover);
+				}
+				else
+				{
+					thread_row.SetHoverSample(null, 0);
+					if (self.OnHoverHandler)
+						self.OnHoverHandler(thread_row.Name, null);
+				}
+			}
+		}
+	}
+
+
+	return TimelineWindow;
+})();
+

+ 59 - 0
3rdparty/remotery/vis/Code/TitleWindow.js

@@ -0,0 +1,59 @@
+
+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));
+
+		server.AddMessageHandler("PING", Bind(OnPing, this));
+	}
+
+
+	TitleWindow.prototype.SetConnectionAddressChanged = function(handler)
+	{
+		this.EditBox.SetChangeHandler(handler);
+	}
+
+
+	TitleWindow.prototype.WindowResized = function(width, height)
+	{
+		this.Window.SetSize(width - 2 * 10, 50);
+		this.PauseButton.SetPosition(width - 80, 5);
+	}
+
+
+	function OnPausePressed(self)
+	{
+		self.Settings.IsPaused = self.PauseButton.IsPressed();
+		if (self.Settings.IsPaused)
+			self.PauseButton.SetText("Paused");
+		else
+			self.PauseButton.SetText("Pause");
+	}
+
+
+	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;
+})();

+ 130 - 0
3rdparty/remotery/vis/Code/WebSocketConnection.js

@@ -0,0 +1,130 @@
+
+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.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, message)
+	{
+		if (message_name in self.MessageHandlers)
+		{
+			var handlers = self.MessageHandlers[message_name];
+			for (var i in handlers)
+				handlers[i](self, message);
+		}
+	}
+
+
+	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)
+	{
+		var message = JSON.parse(event.data);
+		if ("id" in message)
+			CallMessageHandlers(self, message.id, message);
+	}
+
+
+	return WebSocketConnection;
+})();

+ 211 - 0
3rdparty/remotery/vis/Styles/Remotery.css

@@ -0,0 +1,211 @@
+
+body
+{
+    /* Take up the full page */
+    width: 100%;
+    height: 100%;
+    margin: 0px;
+
+    background-color: #AAA;
+}
+
+
+.NoSelect
+{
+    /* Disable text selection so that it doesn't interfere with faux-button clicking */
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+
+    /* Stops the text cursor over the label */
+    cursor:default;
+}
+
+
+/* Override default window styles to remove 3D effect */
+.Window
+{
+    background: #555;
+    box-shadow: none;
+    border-radius: 3px;
+}
+/*.WindowTitleBar
+{
+    border-bottom: none;
+    border-radius: 0px;
+}
+.WindowBody
+{
+    border-top: 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;
+}
+
+
+
+.ConsoleText
+{
+    overflow:auto;
+    color: #BBB;
+    font: 9px Verdana;
+    margin: 2px;
+}
+
+
+.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:300px;
+}
+
+
+.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
+{
+    /* Pull .TimelineRowCanvas to the right of the label */
+    float:left;
+
+    width: 140px;
+    height: 14px;
+}
+.TimelineRowCanvas
+{
+}
+
+/* enable vertical scrollbar in TimelineContainer (useful for many threads) */
+.TimelineContainer
+{
+    overflow-y: scroll;
+}

+ 65 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Animation.js

@@ -0,0 +1,65 @@
+
+// 
+// Very basic linear value animation system, for now.
+//
+
+
+namespace("Anim");
+
+
+Anim.Animation = (function()
+{
+	var anim_hz = 60;
+
+	
+	function Animation(anim_func, start_value, end_value, time, end_callback)
+	{
+		// Setup initial parameters
+		this.StartValue = start_value;
+		this.EndValue = end_value;
+		this.ValueInc = (end_value - start_value) / (time * anim_hz);
+		this.Value = start_value;
+		this.Complete = false;
+		this.EndCallback = end_callback;
+
+		// Cache the update function to prevent recreating the closure
+		var self = this;
+		this.AnimFunc = anim_func;
+		this.AnimUpdate = function() { Update(self); }
+
+		// Call for the start value
+		this.AnimUpdate();
+	}
+
+
+	function Update(self)
+	{
+		// Queue up the next frame immediately
+		var id = window.setTimeout(self.AnimUpdate, 1000 / anim_hz);
+
+		// Linear step the value and check for completion
+		self.Value += self.ValueInc;
+		if (Math.abs(self.Value - self.EndValue) < 0.01)
+		{
+			self.Value = self.EndValue;
+			self.Complete = true;
+
+			if (self.EndCallback)
+				self.EndCallback();
+
+			window.clearTimeout(id);
+		}
+
+		// Pass to the animation function
+		self.AnimFunc(self.Value);
+	}
+
+
+	return Animation;
+})();
+
+
+Anim.Animate = function(anim_func, start_value, end_value, time, end_callback)
+{
+	return new Anim.Animation(anim_func, start_value, end_value, time, end_callback);
+}

+ 92 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Bind.js

@@ -0,0 +1,92 @@
+//
+// This will generate a closure for the given function and optionally bind an arbitrary number of
+// its initial arguments to specific values.
+//
+// Parameters:
+//
+//    0: Either the function scope or the function.
+//    1: If 0 is the function scope, this is the function.
+//       Otherwise it's the start of the optional bound argument list.
+//    2: Start of the optional bound argument list if 1 is the function.
+//
+// Examples:
+//
+//    function GlobalFunction(p0, p1, p2) { }
+//    function ThisFunction(p0, p1, p2) { }
+//
+//    var a = Bind("GlobalFunction");
+//    var b = Bind(this, "ThisFunction");
+//    var c = Bind("GlobalFunction", BoundParam0, BoundParam1);
+//    var d = Bind(this, "ThisFunction", BoundParam0, BoundParam1);
+//    var e = Bind(GlobalFunction);
+//    var f = Bind(this, ThisFunction);
+//    var g = Bind(GlobalFunction, BoundParam0, BoundParam1);
+//    var h = Bind(this, ThisFunction, BoundParam0, BoundParam1);
+//
+//    a(0, 1, 2);
+//    b(0, 1, 2);
+//    c(2);
+//    d(2);
+//    e(0, 1, 2);
+//    f(0, 1, 2);
+//    g(2);
+//    h(2);
+//
+function Bind()
+{
+	// No closure to define?
+	if (arguments.length == 0)
+		return null;
+
+	// Figure out which of the 4 call types is being used to bind
+	// Locate scope, function and bound parameter start index
+
+	if (typeof(arguments[0]) == "string")
+	{
+		var scope = window;
+		var func = window[arguments[0]];
+		var start = 1;
+	}
+
+	else if (typeof(arguments[0]) == "function")
+	{
+		var scope = window;
+		var func = arguments[0];
+		var start = 1;
+	}
+
+	else if (typeof(arguments[1]) == "string")
+	{
+		var scope = arguments[0];
+		var func = scope[arguments[1]];
+		var start = 2;
+	}
+
+	else if (typeof(arguments[1]) == "function")
+	{
+		var scope = arguments[0];
+		var func = arguments[1];
+		var start = 2;
+	}
+
+	else
+	{
+		// unknown
+		console.log("Bind() ERROR: Unknown bind parameter configuration");
+		return;
+	}
+
+	// Convert the arguments list to an array
+	var arg_array = Array.prototype.slice.call(arguments, start);
+	start = arg_array.length;
+
+	return function()
+	{
+		// Concatenate incoming arguments
+		for (var i = 0; i < arguments.length; i++)
+			arg_array[start + i] = arguments[i];
+
+		// Call the function in the given scope with the new arguments
+		return func.apply(scope, arg_array);
+	}
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 204 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Convert.js


+ 20 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Core.js

@@ -0,0 +1,20 @@
+
+// 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];
+	}
+}

+ 499 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/DOM.js

@@ -0,0 +1,499 @@
+
+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];
+}
+
+
+
+//
+// =====================================================================================================================
+// ----- 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";
+}

+ 149 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Keyboard.js

@@ -0,0 +1,149 @@
+
+namespace("Keyboard")
+
+
+// =====================================================================================================================
+// Key codes copied from closure-library
+// https://code.google.com/p/closure-library/source/browse/closure/goog/events/keycodes.js
+// ---------------------------------------------------------------------------------------------------------------------
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+Keyboard.Codes = {
+	WIN_KEY_FF_LINUX	: 0,
+	MAC_ENTER			: 3,
+	BACKSPACE			: 8,
+	TAB					: 9,
+	NUM_CENTER			: 12,	// NUMLOCK on FF/Safari Mac
+	ENTER				: 13,
+	SHIFT				: 16,
+	CTRL				: 17,
+	ALT					: 18,
+	PAUSE				: 19,
+	CAPS_LOCK			: 20,
+	ESC					: 27,
+	SPACE				: 32,
+	PAGE_UP				: 33,	// also NUM_NORTH_EAST
+	PAGE_DOWN			: 34,	// also NUM_SOUTH_EAST
+	END					: 35,	// also NUM_SOUTH_WEST
+	HOME				: 36,	// also NUM_NORTH_WEST
+	LEFT				: 37,	// also NUM_WEST
+	UP					: 38,	// also NUM_NORTH
+	RIGHT				: 39,	// also NUM_EAST
+	DOWN				: 40,	// also NUM_SOUTH
+	PRINT_SCREEN		: 44,
+	INSERT				: 45,	// also NUM_INSERT
+	DELETE				: 46,	// also NUM_DELETE
+	ZERO				: 48,
+	ONE					: 49,
+	TWO					: 50,
+	THREE				: 51,
+	FOUR				: 52,
+	FIVE				: 53,
+	SIX					: 54,
+	SEVEN				: 55,
+	EIGHT				: 56,
+	NINE				: 57,
+	FF_SEMICOLON		: 59,	// Firefox (Gecko) fires this for semicolon instead of 186
+	FF_EQUALS			: 61,	// Firefox (Gecko) fires this for equals instead of 187
+	FF_DASH				: 173,	// Firefox (Gecko) fires this for dash instead of 189
+	QUESTION_MARK		: 63,	// needs localization
+	A					: 65,
+	B					: 66,
+	C					: 67,
+	D					: 68,
+	E					: 69,
+	F					: 70,
+	G					: 71,
+	H					: 72,
+	I					: 73,
+	J					: 74,
+	K					: 75,
+	L					: 76,
+	M					: 77,
+	N					: 78,
+	O					: 79,
+	P					: 80,
+	Q					: 81,
+	R					: 82,
+	S					: 83,
+	T					: 84,
+	U					: 85,
+	V					: 86,
+	W					: 87,
+	X					: 88,
+	Y					: 89,
+	Z					: 90,
+	META				: 91,	// WIN_KEY_LEFT
+	WIN_KEY_RIGHT		: 92,
+	CONTEXT_MENU		: 93,
+	NUM_ZERO			: 96,
+	NUM_ONE				: 97,
+	NUM_TWO				: 98,
+	NUM_THREE			: 99,
+	NUM_FOUR			: 100,
+	NUM_FIVE			: 101,
+	NUM_SIX				: 102,
+	NUM_SEVEN			: 103,
+	NUM_EIGHT			: 104,
+	NUM_NINE			: 105,
+	NUM_MULTIPLY		: 106,
+	NUM_PLUS			: 107,
+	NUM_MINUS			: 109,
+	NUM_PERIOD			: 110,
+	NUM_DIVISION		: 111,
+	F1					: 112,
+	F2					: 113,
+	F3					: 114,
+	F4					: 115,
+	F5					: 116,
+	F6					: 117,
+	F7					: 118,
+	F8					: 119,
+	F9					: 120,
+	F10					: 121,
+	F11					: 122,
+	F12					: 123,
+	NUMLOCK				: 144,
+	SCROLL_LOCK			: 145,
+
+	// OS-specific media keys like volume controls and browser controls.
+	FIRST_MEDIA_KEY		: 166,
+	LAST_MEDIA_KEY		: 183,
+
+	SEMICOLON			: 186,	// needs localization
+	DASH				: 189,	// needs localization
+	EQUALS				: 187,	// needs localization
+	COMMA				: 188,	// needs localization
+	PERIOD				: 190,	// needs localization
+	SLASH				: 191,	// needs localization
+	APOSTROPHE			: 192,	// needs localization
+	TILDE				: 192,	// needs localization
+	SINGLE_QUOTE		: 222,	// needs localization
+	OPEN_SQUARE_BRACKET	: 219,	// needs localization
+	BACKSLASH			: 220,	// needs localization
+	CLOSE_SQUARE_BRACKET: 221,	// needs localization
+	WIN_KEY				: 224,
+	MAC_FF_META			: 224,	// Firefox (Gecko) fires this for the meta key instead of 91
+	MAC_WK_CMD_LEFT		: 91,	// WebKit Left Command key fired, same as META
+	MAC_WK_CMD_RIGHT	: 93,	// WebKit Right Command key fired, different from META
+	WIN_IME				: 229,
+
+	// We've seen users whose machines fire this keycode at regular one
+	// second intervals. The common thread among these users is that
+	// they're all using Dell Inspiron laptops, so we suspect that this
+	// indicates a hardware/bios problem.
+	// http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
+	PHANTOM				: 255
+};
+// =====================================================================================================================

+ 26 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/LocalStore.js

@@ -0,0 +1,26 @@
+
+namespace("LocalStore");
+
+
+LocalStore.Set = function(class_name, class_id, variable_id, data)
+{
+	if (typeof(Storage) != "undefined")
+	{
+		var name = class_name + "_" + class_id + "_" + variable_id;
+		localStorage[name] = JSON.stringify(data);
+	}
+}
+
+
+LocalStore.Get = function(class_name, class_id, variable_id, default_data)
+{
+	if (typeof(Storage) != "undefined")
+	{
+		var name = class_name + "_" + class_id + "_" + variable_id;
+		var data = localStorage[name]
+		if (data)
+			return JSON.parse(data);
+	}
+
+	return default_data;
+}

+ 83 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/Mouse.js

@@ -0,0 +1,83 @@
+
+namespace("Mouse");
+
+
+Mouse.State =(function()
+{
+	function State(event)
+	{
+		// Get button press states
+		if (typeof event.buttons != "undefined")
+		{
+			// Firefox
+			this.Left = (event.buttons & 1) != 0;
+			this.Right = (event.buttons & 2) != 0;
+			this.Middle = (event.buttons & 4) != 0;
+		}
+		else
+		{
+			// Chrome
+			this.Left = (event.button == 0);
+			this.Middle = (event.button == 1);
+			this.Right = (event.button == 2);
+		}
+
+		// Get page-relative mouse position
+		this.Position = DOM.Event.GetMousePosition(event);
+
+		// Get wheel delta
+		var delta = 0;
+		if (event.wheelDelta)
+			delta = event.wheelDelta / 120;		// IE/Opera
+		else if (event.detail)
+			delta = -event.detail / 3;			// Mozilla
+		this.WheelDelta = delta;
+
+		// Get the mouse position delta
+		// Requires Pointer Lock API support
+		this.PositionDelta = [
+			event.movementX || event.mozMovementX || event.webkitMovementX || 0,
+			event.movementY || event.mozMovementY || event.webkitMovementY || 0
+		];
+	}
+
+	return State;
+})();
+
+
+//
+// Basic Pointer Lock API support
+// https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock
+// http://www.chromium.org/developers/design-documents/mouse-lock
+//
+// Note that API has not been standardised yet so browsers can implement functions with prefixes
+//
+
+
+Mouse.PointerLockSupported = function()
+{
+	return 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document;
+}
+
+
+Mouse.RequestPointerLock = function(element)
+{
+	element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock;
+	if (element.requestPointerLock)
+		element.requestPointerLock();
+}
+
+
+Mouse.ExitPointerLock = function()
+{
+	document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock;
+	if (document.exitPointerLock)
+		document.exitPointerLock();
+}
+
+
+// Can use this element to detect whether pointer lock is enabled (returns non-null)
+Mouse.PointerLockElement = function()
+{
+	return document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement;
+}

+ 68 - 0
3rdparty/remotery/vis/extern/BrowserLib/Core/Code/MurmurHash3.js

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

+ 131 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Button.js

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

+ 237 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js

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

+ 34 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Container.js

@@ -0,0 +1,34 @@
+
+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);
+	}
+
+
+	return Container;
+})();

+ 117 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/EditBox.js

@@ -0,0 +1,117 @@
+
+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);
+
+		// 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));
+		DOM.Event.AddHandler(this.EditNode, "change", Bind(OnChange, 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.EditNode.blur();
+		}
+	}
+
+
+	function OnKeyDown(self, evt)
+	{
+		// Allow escape to cancel any text changes
+		if (evt.keyCode == 27)
+		{
+			self.EditNode.value = self.PreviousValue;
+			self.EditNode.blur();
+		}
+	}
+
+
+	function OnChange(self, evt)
+	{
+		if (self.ChangeHandler)
+			self.ChangeHandler(self.EditNode);
+	}
+
+
+	return EditBox;
+})();

+ 252 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Grid.js

@@ -0,0 +1,252 @@
+
+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
+		DOM.Node.AppendClearFloat(this.Node);
+		this.BodyNode = DOM.Node.AppendHTML(this.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(x, y, width, height)
+	{
+		this.Rows = new WM.GridRows(this);
+
+		this.Node = DOM.Node.CreateHTML(template_html);
+		this.BodyNode = DOM.Node.FindWithClass(this.Node, "GridBody");
+
+		DOM.Node.SetPosition(this.Node, [ x, y ]);
+		DOM.Node.SetSize(this.Node, [ width, height ]);
+
+		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;
+})();

+ 31 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Label.js

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

+ 352 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Treeview.js

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

+ 109 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js

@@ -0,0 +1,109 @@
+
+namespace("WM");
+
+
+WM.TreeviewItem = (function()
+{
+	function TreeviewItem(treeview, name, data, open_image, close_image)
+	{
+		// Assign members
+		this.Treeview = treeview;
+		this.Label = name;
+		this.Data = data;
+		this.OpenImage = open_image;
+		this.CloseImage = close_image;
+
+		this.Children = [ ];
+
+		// The HTML node wrapping the item and its children
+		this.Node = null;
+
+		// The HTML node storing the image for the open/close state feedback
+		this.ImageNode = null;
+
+		// The HTML node storing just the children
+		this.ChildrenNode = null;
+
+		// Animation handle for opening and closing the child nodes, only used
+		// if the tree view item as children
+		this.AnimHandle = null;
+
+		// Open state of the item
+		this.IsOpen = true;
+	}
+
+
+	TreeviewItem.prototype.AddItem = function(name, data, open_image, close_image)
+	{
+		var item = new WM.TreeviewItem(this.Treeview, name, data, open_image, close_image);
+		this.Children.push(item);
+		return item;
+	}
+
+
+	TreeviewItem.prototype.Open = function()
+	{
+		if (this.AnimHandle == null || this.AnimHandle.Complete)
+		{
+			// Swap to the open state
+			this.IsOpen = true;
+			if (this.ImageNode != null && this.OpenImage != null)
+				this.ImageNode.src = this.OpenImage.src;
+			
+			// Cache for closure binding
+			var child_node = this.ChildrenNode;
+			var end_height = this.StartHeight;
+			var treeview = this.Treeview;
+
+			// Reveal the children and animate their height to max
+			this.ChildrenNode.style.display = "block";
+			this.AnimHandle = Anim.Animate(
+				function (val) { DOM.Node.SetHeight(child_node, val) },
+				0, end_height, 0.2,
+				function() { treeview.UpdateScrollbar(); });
+
+			// Fade the children in
+			Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 0, 1, 0.2);
+		}
+	}
+
+
+	TreeviewItem.prototype.Close = function()
+	{
+		if (this.AnimHandle == null || this.AnimHandle.Complete)
+		{
+			// Swap to the close state
+			this.IsOpen = false;
+			if (this.ImageNode != null && this.CloseImage != null)
+				this.ImageNode.src = this.CloseImage.src;
+
+			// Cache for closure binding
+			var child_node = this.ChildrenNode;
+			var treeview = this.Treeview;
+
+			// Mark the height of the item for reload later
+			this.StartHeight = child_node.offsetHeight;
+
+			// Shrink the height of the children and hide them upon completion
+			this.AnimHandle = Anim.Animate(
+				function (val) { DOM.Node.SetHeight(child_node, val) },
+				this.ChildrenNode.offsetHeight, 0, 0.2,
+				function() { child_node.style.display = "none"; treeview.UpdateScrollbar(); });
+
+			// Fade the children out
+			Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 1, 0, 0.2);
+		}
+	}
+
+
+	TreeviewItem.prototype.Toggle = function()
+	{
+		if (this.IsOpen)
+			this.Close();
+		else
+			this.Open();
+	}
+
+
+	return TreeviewItem;
+})();

+ 243 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/Window.js

@@ -0,0 +1,243 @@
+
+namespace("WM");
+
+
+WM.Window = (function()
+{
+	var template_html = "																								\
+		<div class='Window'>																							\
+			<div class='WindowTitleBar'>																				\
+				<div class='WindowTitleBarText notextsel' style='float:left'>Window Title Bar</div>						\
+				<div class='WindowTitleBarClose notextsel' style='float:right'>O</div>									\
+			</div>																										\
+			<div class='WindowBody'>																					\
+			</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.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.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.TitleBarCloseNode, "mousedown", Bind(this, "Hide"));
+
+		// Create delegates for removable handlers
+		this.MoveDelegate = Bind(this, "Move");
+		this.EndMoveDelegate = Bind(this, "EndMove");		
+	}
+
+
+	Window.prototype.SetOnMove = function(on_move)
+	{
+		this.OnMove = on_move;
+	}
+
+
+	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()
+	{
+		if (this.Node.parentNode == this.ParentNode)
+		{
+			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()
+	{
+		// Remove node
+		this.ParentNode.removeChild(this.Node);
+		this.Visible = false;
+	}
+
+
+	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.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.SetPosition = function(x, y)
+	{
+		this.Position = [ x, y ];
+		DOM.Node.SetPosition(this.Node, this.Position);
+	}
+
+
+	Window.prototype.SetSize = function(w, h)
+	{
+		this.Size = [ w, h ];
+		DOM.Node.SetSize(this.Node, this.Size);
+	}
+
+
+	Window.prototype.GetZIndex = function()
+	{
+		return parseInt(this.Node.style.zIndex);
+	}
+
+
+	return Window;
+})();

+ 54 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js

@@ -0,0 +1,54 @@
+
+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.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;
+
+})();

+ 546 - 0
3rdparty/remotery/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css

@@ -0,0 +1,546 @@
+
+
+.notextsel
+{
+	/* Disable text selection so that it doesn't interfere with button-clicking */
+	user-select:none;
+
+	/* Stops the text cursor over the label */
+	cursor:default;
+}
+
+
+
+/* ------------------------------------------------------------------------------------------------------------------ */
+/* Window Styles                                                                                                      */
+/* ------------------------------------------------------------------------------------------------------------------ */
+
+
+
+.Window
+{
+	/* Allows movement of the window to exceed browser region without triggering scroll bars */
+	position:fixed;
+
+	/* Clip all contents to the window border */
+	overflow: hidden;
+
+	background: #404040;
+
+	border-radius: 5px;
+	-moz-border-radius: 5px;
+
+	-webkit-box-shadow: 3px 3px 3px #111, 1px 1px 1px #606060 inset;
+	box-shadow: 3px 3px 3px #111, 1px 1px 1px #606060 inset;
+}
+
+.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;
+
+	border-bottom: 1px solid #303030;
+	border-radius: 5px;
+}
+
+.WindowTitleBarText
+{
+	color: #BBB;
+	font: 9px Verdana;
+
+	padding: 3px;
+	cursor: move;
+}
+
+.WindowTitleBarClose
+{
+	color: #999999;
+	font: 9px Verdana;
+
+	padding: 3px;
+	cursor: default;
+}
+
+.WindowBody
+{
+	/* Turns this node into a "positioned node" so that its children can be placed relative to it */
+	position: absolute;
+
+	/* Fill the parent window node */
+	width: 100%;
+	height: 100%;
+
+	padding:10px;
+	border-top: 1px solid #606060;
+}
+
+
+
+/* ------------------------------------------------------------------------------------------------------------------ */
+/* Container Styles                                                                                                   */
+/* ------------------------------------------------------------------------------------------------------------------ */
+
+
+
+.Container
+{
+	/* Position relative to the parent window */
+	position: absolute;
+
+	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;
+}
+
+
+
+/* ------------------------------------------------------------------------------------------------------------------ */
+/* 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;
+}
+
+
+ 
+/* ------------------------------------------------------------------------------------------------------------------ */
+/* Treeview 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
+{
+	/* Clip contents */
+	overflow: hidden;
+
+	position: relative;
+
+	background: #333;
+
+	border-radius: 2px;
+}
+
+.GridBody
+{
+}
+
+.GridRow
+{
+	background:#303030;
+
+	color: #BBB;
+	font: 9px Verdana;
+
+	padding: 2px;
+}
+
+.GridRow.GridGroup
+{
+	padding: 0px;
+}
+
+.GridRow:nth-child(odd)
+{
+	background:#333;
+}
+
+.GridRowCell
+{
+	float:left;
+}
+.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:6px;
+	border-color:#000;
+	border-width:1px;
+	border-style:solid;
+
+	/* 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;
+
+	/* 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;
+}
+
+.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;
+}

+ 53 - 0
3rdparty/remotery/vis/index.html

@@ -0,0 +1,53 @@
+
+<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>
+
+		<!-- 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/Console.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/TimelineWindow.js"></script>
+		<script type="text/javascript" src="Code/ThreadFrame.js"></script>
+		<script type="text/javascript" src="Code/Remotery.js"></script>
+
+	</head>
+
+	<body>
+
+		<script type="text/javascript">
+
+			var remotery = new Remotery();
+
+		</script>
+
+	</body>
+
+</html>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác