From 6331a2bf79f897927547e9ddba63e8ef25875941 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 24 Aug 2021 20:55:39 +0200 Subject: [PATCH] Add profiling with Remotery --- Makefile | 8 +- camera.hpp | 1 + hittable_list.hpp | 1 + include/Remotery.c | 8809 +++++++++++++++++ include/Remotery.h | 679 ++ include/RemoteryMetal.mm | 59 + main.cpp | 38 +- rtweekend.hpp | 9 + sphere.hpp | 12 + vec3.hpp | 7 +- vis/Code/Console.js | 218 + vis/Code/DataViewReader.js | 52 + vis/Code/NameMap.js | 53 + vis/Code/PixelTimeRange.js | 61 + vis/Code/Remotery.js | 540 + vis/Code/SampleWindow.js | 221 + vis/Code/Shaders.js | 275 + vis/Code/ThreadFrame.js | 29 + vis/Code/TimelineMarkers.js | 186 + vis/Code/TimelineRow.js | 389 + vis/Code/TimelineWindow.js | 494 + vis/Code/TitleWindow.js | 105 + vis/Code/TraceDrop.js | 134 + vis/Code/WebGL.js | 238 + vis/Code/WebGLFont.js | 119 + vis/Code/WebSocketConnection.js | 137 + .../Fonts/FiraCode/FiraCode-Regular.ttf | Bin 0 -> 299152 bytes vis/Styles/Fonts/FiraCode/LICENSE | 93 + vis/Styles/Remotery.css | 274 + vis/extern/BrowserLib/Core/Code/Animation.js | 65 + vis/extern/BrowserLib/Core/Code/Bind.js | 92 + vis/extern/BrowserLib/Core/Code/Convert.js | 218 + vis/extern/BrowserLib/Core/Code/Core.js | 26 + vis/extern/BrowserLib/Core/Code/DOM.js | 526 + vis/extern/BrowserLib/Core/Code/Keyboard.js | 149 + vis/extern/BrowserLib/Core/Code/LocalStore.js | 40 + vis/extern/BrowserLib/Core/Code/Mouse.js | 83 + .../BrowserLib/Core/Code/MurmurHash3.js | 68 + .../BrowserLib/WindowManager/Code/Button.js | 131 + .../BrowserLib/WindowManager/Code/ComboBox.js | 237 + .../WindowManager/Code/Container.js | 48 + .../BrowserLib/WindowManager/Code/EditBox.js | 119 + .../BrowserLib/WindowManager/Code/Grid.js | 248 + .../BrowserLib/WindowManager/Code/Label.js | 31 + .../BrowserLib/WindowManager/Code/Treeview.js | 352 + .../WindowManager/Code/TreeviewItem.js | 109 + .../BrowserLib/WindowManager/Code/Window.js | 314 + .../WindowManager/Code/WindowManager.js | 65 + .../WindowManager/Styles/WindowManager.css | 652 ++ vis/index.html | 61 + 50 files changed, 16864 insertions(+), 11 deletions(-) create mode 100644 include/Remotery.c create mode 100644 include/Remotery.h create mode 100644 include/RemoteryMetal.mm create mode 100644 vis/Code/Console.js create mode 100644 vis/Code/DataViewReader.js create mode 100644 vis/Code/NameMap.js create mode 100644 vis/Code/PixelTimeRange.js create mode 100644 vis/Code/Remotery.js create mode 100644 vis/Code/SampleWindow.js create mode 100644 vis/Code/Shaders.js create mode 100644 vis/Code/ThreadFrame.js create mode 100644 vis/Code/TimelineMarkers.js create mode 100644 vis/Code/TimelineRow.js create mode 100644 vis/Code/TimelineWindow.js create mode 100644 vis/Code/TitleWindow.js create mode 100644 vis/Code/TraceDrop.js create mode 100644 vis/Code/WebGL.js create mode 100644 vis/Code/WebGLFont.js create mode 100644 vis/Code/WebSocketConnection.js create mode 100644 vis/Styles/Fonts/FiraCode/FiraCode-Regular.ttf create mode 100644 vis/Styles/Fonts/FiraCode/LICENSE create mode 100644 vis/Styles/Remotery.css create mode 100644 vis/extern/BrowserLib/Core/Code/Animation.js create mode 100644 vis/extern/BrowserLib/Core/Code/Bind.js create mode 100644 vis/extern/BrowserLib/Core/Code/Convert.js create mode 100644 vis/extern/BrowserLib/Core/Code/Core.js create mode 100644 vis/extern/BrowserLib/Core/Code/DOM.js create mode 100644 vis/extern/BrowserLib/Core/Code/Keyboard.js create mode 100644 vis/extern/BrowserLib/Core/Code/LocalStore.js create mode 100644 vis/extern/BrowserLib/Core/Code/Mouse.js create mode 100644 vis/extern/BrowserLib/Core/Code/MurmurHash3.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Button.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/ComboBox.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Container.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/EditBox.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Grid.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Label.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Treeview.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/Window.js create mode 100644 vis/extern/BrowserLib/WindowManager/Code/WindowManager.js create mode 100644 vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css create mode 100644 vis/index.html diff --git a/Makefile b/Makefile index 2a8eb74..22930b9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ -raytracer: camera.hpp color.hpp hittable.hpp hittable_list.hpp main.cpp material.hpp ray.hpp rtweekend.hpp sphere.hpp vec3.hpp - @g++ -g -O2 -Wall -Wextra -Wpedantic main.cpp -o raytracer +INCLUDE=./include +LIBS=-pthread -lm +FLAGS=-Og -g -Wall -Wextra -Wpedantic + +raytracer: camera.hpp color.hpp hittable.hpp hittable_list.hpp main.cpp material.hpp ray.hpp rtweekend.hpp sphere.hpp vec3.hpp $(INCLUDE)/Remotery.c $(INCLUDE)/Remotery.h + @g++ $(FLAGS) -I$(INCLUDE) $(LIBS) main.cpp -o raytracer image: raytracer @./raytracer > image.ppm diff --git a/camera.hpp b/camera.hpp index 38e7ceb..970ffad 100644 --- a/camera.hpp +++ b/camera.hpp @@ -42,6 +42,7 @@ struct camera { ray get_ray(double s, double t) const { + rmt_ScopedCPUSample(GetRay, RMTSF_Aggregate); vec3 rd = lens_radius * random_in_unit_disk(); vec3 offset = u * rd.x + v * rd.y; diff --git a/hittable_list.hpp b/hittable_list.hpp index 4a7483e..3a1416b 100644 --- a/hittable_list.hpp +++ b/hittable_list.hpp @@ -26,6 +26,7 @@ struct hittable_list : hittable { bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { + rmt_ScopedCPUSample(HittableList_Hit, RMTSF_Aggregate); hit_record temp_rec; bool hit_anything = false; double closest_so_far = t_max; diff --git a/include/Remotery.c b/include/Remotery.c new file mode 100644 index 0000000..477ff38 --- /dev/null +++ b/include/Remotery.c @@ -0,0 +1,8809 @@ +// +// Copyright 2014-2018 Celtoys Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* +@Contents: + + @DEPS: External Dependencies + @TIMERS: Platform-specific timers + @TLS: Thread-Local Storage + @ATOMIC: Atomic Operations + @RNG: Random Number Generator + @LFSR: Galois Linear-feedback Shift Register + @VMBUFFER: Mirror Buffer using Virtual Memory for auto-wrap + @NEW: New/Delete operators with error values for simplifying object create/destroy + @SAFEC: Safe C Library excerpts + @OSTHREADS: Wrappers around OS-specific thread functions + @THREADS: Cross-platform thread object + @OBJALLOC: Reusable Object Allocator + @DYNBUF: Dynamic Buffer + @HASHTABLE: Integer pair hash map for inserts/finds. No removes for added simplicity. + @STRINGTABLE: Map from string hash to string offset in local buffer + @SOCKETS: Sockets TCP/IP Wrapper + @SHA1: SHA-1 Cryptographic Hash Function + @BASE64: Base-64 encoder + @MURMURHASH: Murmur-Hash 3 + @WEBSOCKETS: WebSockets + @MESSAGEQ: Multiple producer, single consumer message queue + @NETWORK: Network Server + @SAMPLE: Base Sample Description (CPU by default) + @SAMPLETREE: A tree of samples with their allocator + @TPROFILER: Thread Profiler data, storing both sampling and instrumentation results + @TGATHER: Thread Gatherer, periodically polling for newly created threads + @TSAMPLER: Sampling thread contexts + @REMOTERY: Remotery + @CUDA: CUDA event sampling + @D3D11: Direct3D 11 event sampling + @OPENGL: OpenGL event sampling + @METAL: Metal event sampling +*/ + +#define RMT_IMPL +#include "Remotery.h" + +#ifdef RMT_PLATFORM_WINDOWS +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "winmm.lib") +#endif + +#if RMT_ENABLED + +// Global settings +static rmtSettings g_Settings; +static rmtBool g_SettingsInitialized = RMT_FALSE; + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @DEPS: External Dependencies +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// clang-format off + +// +// Required CRT dependencies +// +#if RMT_USE_TINYCRT + + #include + #include + #include + + #define CreateFileMapping CreateFileMappingA + +#else + + #ifdef RMT_PLATFORM_MACOS + #include + #include + #include + #include + #else + #if !defined(__FreeBSD__) && !defined(__OpenBSD__) + #include + #endif + #endif + + #include + #include + #include + #include + #include + + #ifdef RMT_PLATFORM_WINDOWS + #include + #ifndef __MINGW32__ + #include + #endif + #undef min + #undef max + #include + #include + #include + #ifdef _XBOX_ONE + #include "xmem.h" + #endif + typedef long NTSTATUS; // winternl.h + #endif + + #ifdef RMT_PLATFORM_LINUX + #if defined(__FreeBSD__) || defined(__OpenBSD__) + #include + #else + #include + #endif + #endif + + #if defined(RMT_PLATFORM_POSIX) + #include + #include + #include + #include + #include + #include + #include + #include + #include + #endif + + #ifdef __MINGW32__ + #include + #endif + +#endif + +#if RMT_USE_CUDA + #include +#endif + +// clang-format on + +#if defined(_MSC_VER) && !defined(__clang__) + #define RMT_UNREFERENCED_PARAMETER(i) (i) +#else + #define RMT_UNREFERENCED_PARAMETER(i) (void)(1 ? (void)0 : ((void)i)) +#endif + +static rmtU8 minU8(rmtU8 a, rmtU8 b) +{ + return a < b ? a : b; +} +static rmtU16 maxU16(rmtU16 a, rmtU16 b) +{ + return a > b ? a : b; +} +static rmtU32 minU32(rmtU32 a, rmtU32 b) +{ + return a < b ? a : b; +} +static rmtS64 maxS64(rmtS64 a, rmtS64 b) +{ + return a > b ? a : b; +} + +// Memory management functions +static void* rmtMalloc(rmtU32 size) +{ + return g_Settings.malloc(g_Settings.mm_context, size); +} + +static void* rmtRealloc(void* ptr, rmtU32 size) +{ + return g_Settings.realloc(g_Settings.mm_context, ptr, size); +} + +static void rmtFree(void* ptr) +{ + g_Settings.free(g_Settings.mm_context, ptr); +} + +// File system functions +static FILE* rmtOpenFile(const char* filename, const char* mode) +{ +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + FILE* fp; + return fopen_s(&fp, filename, mode) == 0 ? fp : NULL; +#else + return fopen(filename, mode); +#endif +} + +void rmtCloseFile(FILE* fp) +{ + if (fp != NULL) + { + fclose(fp); + } +} + +rmtBool rmtWriteFile(FILE* fp, const void* data, rmtU32 size) +{ + assert(fp != NULL); + return fwrite(data, size, 1, fp) == size ? RMT_TRUE : RMT_FALSE; +} + +#if RMT_USE_OPENGL +// DLL/Shared Library functions + +static void* rmtLoadLibrary(const char* path) +{ +#if defined(RMT_PLATFORM_WINDOWS) + return (void*)LoadLibraryA(path); +#elif defined(RMT_PLATFORM_POSIX) + return dlopen(path, RTLD_LOCAL | RTLD_LAZY); +#else + return NULL; +#endif +} + +static void rmtFreeLibrary(void* handle) +{ +#if defined(RMT_PLATFORM_WINDOWS) + FreeLibrary((HMODULE)handle); +#elif defined(RMT_PLATFORM_POSIX) + dlclose(handle); +#endif +} + +#if defined(RMT_PLATFORM_WINDOWS) +typedef FARPROC ProcReturnType; +#else +typedef void* ProcReturnType; +#endif + +static ProcReturnType rmtGetProcAddress(void* handle, const char* symbol) +{ +#if defined(RMT_PLATFORM_WINDOWS) + return GetProcAddress((HMODULE)handle, (LPCSTR)symbol); +#elif defined(RMT_PLATFORM_POSIX) + return dlsym(handle, symbol); +#endif +} + +#endif + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TIMERS: Platform-specific timers +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// Get millisecond timer value that has only one guarantee: multiple calls are consistently comparable. +// On some platforms, even though this returns milliseconds, the timer may be far less accurate. +// +static rmtU32 msTimer_Get() +{ +#ifdef RMT_PLATFORM_WINDOWS + + return (rmtU32)GetTickCount(); + +#else + + clock_t time = clock(); + +// CLOCKS_PER_SEC is 128 on FreeBSD, causing div/0 +#if defined(__FreeBSD__) || defined(__OpenBSD__) + rmtU32 msTime = (rmtU32)(time * 1000 / CLOCKS_PER_SEC); +#else + rmtU32 msTime = (rmtU32)(time / (CLOCKS_PER_SEC / 1000)); +#endif + + return msTime; + +#endif +} + +// +// Micro-second accuracy high performance counter +// +#ifndef RMT_PLATFORM_WINDOWS +typedef rmtU64 LARGE_INTEGER; +#endif +typedef struct +{ + LARGE_INTEGER counter_start; + double counter_scale; +} usTimer; + +static void usTimer_Init(usTimer* timer) +{ +#if defined(RMT_PLATFORM_WINDOWS) + LARGE_INTEGER performance_frequency; + + assert(timer != NULL); + + // Calculate the scale from performance counter to microseconds + QueryPerformanceFrequency(&performance_frequency); + timer->counter_scale = 1000000.0 / performance_frequency.QuadPart; + + // Record the offset for each read of the counter + QueryPerformanceCounter(&timer->counter_start); + +#elif defined(RMT_PLATFORM_MACOS) + + mach_timebase_info_data_t nsScale; + mach_timebase_info(&nsScale); + const double ns_per_us = 1.0e3; + timer->counter_scale = (double)(nsScale.numer) / ((double)nsScale.denom * ns_per_us); + + timer->counter_start = mach_absolute_time(); + +#elif defined(RMT_PLATFORM_LINUX) + + struct timespec tv; + clock_gettime(CLOCK_REALTIME, &tv); + timer->counter_start = (rmtU64)(tv.tv_sec * (rmtU64)1000000) + (rmtU64)(tv.tv_nsec * 0.001); + +#endif +} + +static rmtU64 usTimer_Get(usTimer* timer) +{ +#if defined(RMT_PLATFORM_WINDOWS) + LARGE_INTEGER performance_count; + + assert(timer != NULL); + + // Read counter and convert to microseconds + QueryPerformanceCounter(&performance_count); + return (rmtU64)((performance_count.QuadPart - timer->counter_start.QuadPart) * timer->counter_scale); + +#elif defined(RMT_PLATFORM_MACOS) + + rmtU64 curr_time = mach_absolute_time(); + return (rmtU64)((curr_time - timer->counter_start) * timer->counter_scale); + +#elif defined(RMT_PLATFORM_LINUX) + + struct timespec tv; + clock_gettime(CLOCK_REALTIME, &tv); + return ((rmtU64)(tv.tv_sec * (rmtU64)1000000) + (rmtU64)(tv.tv_nsec * 0.001)) - timer->counter_start; + +#endif +} + +static void msSleep(rmtU32 time_ms) +{ +#ifdef RMT_PLATFORM_WINDOWS + Sleep(time_ms); +#elif defined(RMT_PLATFORM_POSIX) + usleep(time_ms * 1000); +#endif +} + +static struct tm* TimeDateNow() +{ + time_t time_now = time(NULL); + +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + // Discard the thread-safety benefit of gmtime_s + static struct tm tm_now; + gmtime_s(&tm_now, &time_now); + return &tm_now; +#else + return gmtime(&time_now); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TLS: Thread-Local Storage +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define TLS_INVALID_HANDLE 0xFFFFFFFF + +#if defined(RMT_PLATFORM_WINDOWS) +typedef rmtU32 rmtTLS; +#else +typedef pthread_key_t rmtTLS; +#endif + +static rmtError tlsAlloc(rmtTLS* handle) +{ + assert(handle != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + *handle = (rmtTLS)TlsAlloc(); + if (*handle == TLS_OUT_OF_INDEXES) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } +#elif defined(RMT_PLATFORM_POSIX) + if (pthread_key_create(handle, NULL) != 0) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } +#endif + + return RMT_ERROR_NONE; +} + +static void tlsFree(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + TlsFree(handle); +#elif defined(RMT_PLATFORM_POSIX) + pthread_key_delete((pthread_key_t)handle); +#endif +} + +static void tlsSet(rmtTLS handle, void* value) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + TlsSetValue(handle, value); +#elif defined(RMT_PLATFORM_POSIX) + pthread_setspecific((pthread_key_t)handle, value); +#endif +} + +static void* tlsGet(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); +#if defined(RMT_PLATFORM_WINDOWS) + return TlsGetValue(handle); +#elif defined(RMT_PLATFORM_POSIX) + return pthread_getspecific((pthread_key_t)handle); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MUTEX: Mutexes +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#ifdef RMT_PLATFORM_WINDOWS +typedef CRITICAL_SECTION rmtMutex; +#else +typedef pthread_mutex_t rmtMutex; +#endif + +static void mtxInit(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + InitializeCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_init(mutex, NULL); +#endif +} + +static void mtxLock(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + EnterCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_lock(mutex); +#endif +} + +static void mtxUnlock(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + LeaveCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_unlock(mutex); +#endif +} + +static void mtxDelete(rmtMutex* mutex) +{ + assert(mutex != NULL); +#if defined(RMT_PLATFORM_WINDOWS) + DeleteCriticalSection(mutex); +#elif defined(RMT_PLATFORM_POSIX) + pthread_mutex_destroy(mutex); +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @ATOMIC: Atomic Operations +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +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); +} + +static void CompilerWriteFence() +{ +#if defined(__clang__) + __asm__ volatile("" : : : "memory"); +#elif defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + _WriteBarrier(); +#else + asm volatile("" : : : "memory"); +#endif +} + +static void CompilerReadFence() +{ +#if defined(__clang__) + __asm__ volatile("" : : : "memory"); +#elif defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + _ReadBarrier(); +#else + asm volatile("" : : : "memory"); +#endif +} + +static rmtU32 LoadAcquire(rmtU32* volatile address) +{ + rmtU32 value = *address; + CompilerReadFence(); + return value; +} + +static long* LoadAcquirePointer(long* volatile* ptr) +{ + long* value = *ptr; + CompilerReadFence(); + return value; +} + +static void StoreRelease(rmtU32* volatile address, rmtU32 value) +{ + CompilerWriteFence(); + *address = value; +} + +static void StoreReleasePointer(long* volatile* ptr, long* value) +{ + CompilerWriteFence(); + *ptr = value; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @RNG: Random Number Generator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// WELL: Well Equidistributed Long-period Linear +// These algorithms produce numbers with better equidistrib ution than MT19937 and improve upon “bit-mixing” properties. They are +// fast, come in many sizes, and produce higher quality random numbers. +// +// This implementation has a period of 2^512, or 10^154. +// +// Implementation from: Game Programming Gems 7, Random Number Generation Chris Lomont +// Documentation: http://www.lomont.org/Math/Papers/2008/Lomont_PRNG_2008.pdf +// + +// Global RNG state for now +// Far better than interfering with the user's rand() +#define Well512_StateSize 16 +static rmtU32 Well512_State[Well512_StateSize]; +static rmtU32 Well512_Index; + +static void Well512_Init(rmtU32 seed) +{ + rmtU32 i; + + // Generate initial state from seed + Well512_State[0] = seed; + for (i = 1; i < Well512_StateSize; i++) + { + rmtU32 prev = Well512_State[i - 1]; + Well512_State[i] = (1812433253 * (prev ^ (prev >> 30)) + i); + } + Well512_Index = 0; +} + +static rmtU32 Well512_RandomU32() +{ + rmtU32 a, b, c, d; + + a = Well512_State[Well512_Index]; + c = Well512_State[(Well512_Index + 13) & 15]; + b = a ^ c ^ (a << 16) ^ (c << 15); + c = Well512_State[(Well512_Index + 9) & 15]; + c ^= (c >> 11); + a = Well512_State[Well512_Index] = b ^ c; + d = a ^ ((a << 5) & 0xDA442D24UL); + Well512_Index = (Well512_Index + 15) & 15; + a = Well512_State[Well512_Index]; + Well512_State[Well512_Index] = a ^ b ^ d ^ (a << 2) ^ (b << 18) ^ (c << 28); + return Well512_State[Well512_Index]; +} + +static rmtU32 Well512_RandomOpenLimit(rmtU32 limit) +{ + // Using % to modulo with range is just masking out the higher bits, leaving a result that's objectively biased. + // Dividing by RAND_MAX is better but leads to increased repetition at low ranges due to very large bucket sizes. + // Instead use multiple passes with smaller bucket sizes, rejecting results that don't fit into this smaller range. + rmtU32 bucket_size = UINT_MAX / limit; + rmtU32 bucket_limit = bucket_size * limit; + rmtU32 r; + do + { + r = Well512_RandomU32(); + } while(r >= bucket_limit); + + return r / bucket_size; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @LFSR: Galois Linear-feedback Shift Register +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static rmtU32 Log2i(rmtU32 x) +{ + static const rmtU32 MultiplyDeBruijnBitPosition[32] = + { + 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31 + }; + + // First round down to one less than a power of two + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + + return MultiplyDeBruijnBitPosition[(rmtU32)(x * 0x07C4ACDDU) >> 27]; +} + +static rmtU32 GaloisLFSRMask(rmtU32 table_size_log2) +{ + // Taps for 4 to 8 bit ranges + static const rmtU32 XORMasks[] = + { + ((1 << 0) | (1 << 1)), // 2 + ((1 << 1) | (1 << 2)), // 3 + ((1 << 2) | (1 << 3)), // 4 + ((1 << 2) | (1 << 4)), // 5 + ((1 << 4) | (1 << 5)), // 6 + ((1 << 5) | (1 << 6)), // 7 + ((1 << 3) | (1 << 4) | (1 << 5) | (1 << 7)), // 8 + }; + + // Map table size to required XOR mask + assert(table_size_log2 >= 2); + assert(table_size_log2 <= 8); + return XORMasks[table_size_log2 - 2]; +} + +static rmtU32 GaloisLFSRNext(rmtU32 value, rmtU32 xor_mask) +{ + // Output bit + rmtU32 lsb = value & 1; + + // Apply the register shift + value >>= 1; + + // Apply toggle mask if the output bit is set + if (lsb != 0) + { + value ^= xor_mask; + } + + return value; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @NEW: New/Delete operators with error values for simplifying object create/destroy +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// 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 +#ifdef _XBOX_ONE + size_t page_count; + size_t* page_mapping; +#else + HANDLE file_map_handle; +#endif +#endif + +} VirtualMirrorBuffer; + +#ifdef __ANDROID__ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#define ASHMEM_DEVICE "/dev/ashmem" + +/* + * ashmem_create_region - creates a new ashmem region and returns the file + * descriptor, or <0 on error + * + * `name' is an optional label to give the region (visible in /proc/pid/maps) + * `size' is the size of the region, in page-aligned bytes + */ +int ashmem_create_region(const char* name, size_t size) +{ + int fd, ret; + + fd = open(ASHMEM_DEVICE, O_RDWR); + if (fd < 0) + return fd; + + if (name) + { + char buf[ASHMEM_NAME_LEN] = {0}; + + strncpy(buf, name, sizeof(buf)); + buf[sizeof(buf) - 1] = 0; + ret = ioctl(fd, ASHMEM_SET_NAME, buf); + if (ret < 0) + goto error; + } + + ret = ioctl(fd, ASHMEM_SET_SIZE, size); + if (ret < 0) + goto error; + + return fd; + +error: + close(fd); + return ret; +} +#endif // __ANDROID__ + +static rmtError VirtualMirrorBuffer_Constructor(VirtualMirrorBuffer* buffer, rmtU32 size, int nb_attempts) +{ + static const rmtU32 k_64 = 64 * 1024; + RMT_UNREFERENCED_PARAMETER(nb_attempts); + +#ifdef RMT_PLATFORM_LINUX +#if defined(__FreeBSD__) || defined(__OpenBSD__) + char path[] = "/tmp/ring-buffer-XXXXXX"; +#else + char path[] = "/dev/shm/ring-buffer-XXXXXX"; +#endif + int file_descriptor; +#endif + + // Round up to page-granulation; the nearest 64k boundary for now + size = (size + k_64 - 1) / k_64 * k_64; + + // Set defaults + buffer->size = size; + buffer->ptr = NULL; +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _XBOX_ONE + buffer->page_count = 0; + buffer->page_mapping = NULL; +#else + buffer->file_map_handle = INVALID_HANDLE_VALUE; +#endif +#endif + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _XBOX_ONE + + // Xbox version based on Windows version and XDK reference + + buffer->page_count = size / k_64; + if (buffer->page_mapping) + { + free(buffer->page_mapping); + } + buffer->page_mapping = (size_t*)malloc(sizeof(ULONG) * buffer->page_count); + + while (nb_attempts-- > 0) + { + rmtU8* desired_addr; + + // Create a page mapping for pointing to its physical address with multiple virtual pages + if (!AllocateTitlePhysicalPages(GetCurrentProcess(), MEM_LARGE_PAGES, &buffer->page_count, + buffer->page_mapping)) + { + free(buffer->page_mapping); + buffer->page_mapping = NULL; + break; + } + + // Reserve two contiguous pages of virtual memory + desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS); + if (desired_addr == NULL) + break; + + // Release the range immediately but retain the address for the next sequence of code to + // try and map to it. In the mean-time some other OS thread may come along and allocate this + // address range from underneath us so multiple attempts need to be made. + VirtualFree(desired_addr, 0, MEM_RELEASE); + + // Immediately try to point both pages at the file mapping + if (MapTitlePhysicalPages(desired_addr, buffer->page_count, MEM_LARGE_PAGES, PAGE_READWRITE, + buffer->page_mapping) == desired_addr && + MapTitlePhysicalPages(desired_addr + size, buffer->page_count, MEM_LARGE_PAGES, PAGE_READWRITE, + buffer->page_mapping) == desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } + + // Failed to map the virtual pages; cleanup and try again + FreeTitlePhysicalPages(GetCurrentProcess(), buffer->page_count, buffer->page_mapping); + buffer->page_mapping = NULL; + } + +#else + + // Windows version based on https://gist.github.com/rygorous/3158316 + + while (nb_attempts-- > 0) + { + rmtU8* desired_addr; + + // Create a file mapping for pointing to its physical address with multiple virtual pages + buffer->file_map_handle = CreateFileMapping(INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, size, 0); + if (buffer->file_map_handle == NULL) + break; + +#ifndef _UWP // NON-UWP Windows Desktop Version + + // Reserve two contiguous pages of virtual memory + desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS); + if (desired_addr == NULL) + break; + + // Release the range immediately but retain the address for the next sequence of code to + // try and map to it. In the mean-time some other OS thread may come along and allocate this + // address range from underneath us so multiple attempts need to be made. + VirtualFree(desired_addr, 0, MEM_RELEASE); + + // Immediately try to point both pages at the file mapping + if (MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr) == desired_addr && + MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr + size) == + desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } + +#else // UWP + + // Implementation based on example from: + // https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-virtualalloc2 + // + // Notes + // - just replaced the non-uwp functions by the uwp variants. + // - Both versions could be rewritten to not need the try-loop, see the example mentioned above. I just keep it + // as is for now. + // - Successfully tested on Hololens + desired_addr = (rmtU8*)VirtualAlloc2FromApp(NULL, NULL, 2 * size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, + PAGE_NOACCESS, NULL, 0); + + // Split the placeholder region into two regions of equal size. + VirtualFree(desired_addr, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); + + // Immediately try to point both pages at the file mapping. + if (MapViewOfFile3FromApp(buffer->file_map_handle, NULL, desired_addr, 0, size, MEM_REPLACE_PLACEHOLDER, + PAGE_READWRITE, NULL, 0) == desired_addr && + MapViewOfFile3FromApp(buffer->file_map_handle, NULL, desired_addr + size, 0, size, MEM_REPLACE_PLACEHOLDER, + PAGE_READWRITE, NULL, 0) == desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } +#endif + // Failed to map the virtual pages; cleanup and try again + CloseHandle(buffer->file_map_handle); + buffer->file_map_handle = NULL; + } + +#endif // _XBOX_ONE + +#endif + +#ifdef RMT_PLATFORM_MACOS + + // + // Mac version based on https://github.com/mikeash/MAMirroredQueue + // + // Copyright (c) 2010, Michael Ash + // All rights reserved. + // + // Redistribution and use in source and binary forms, with or without modification, are permitted provided that + // the following conditions are met: + // + // Redistributions of source code must retain the above copyright notice, this list of conditions and the following + // disclaimer. + // + // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + // following disclaimer in the documentation and/or other materials provided with the distribution. + // Neither the name of Michael Ash nor the names of its contributors may be used to endorse or promote products + // derived from this software without specific prior written permission. + // + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + // IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + // + + while (nb_attempts-- > 0) + { + vm_prot_t cur_prot, max_prot; + kern_return_t mach_error; + rmtU8* ptr = NULL; + rmtU8* target = NULL; + + // Allocate 2 contiguous pages of virtual memory + if (vm_allocate(mach_task_self(), (vm_address_t*)&ptr, size * 2, VM_FLAGS_ANYWHERE) != KERN_SUCCESS) + break; + + // Try to deallocate the last page, leaving its virtual memory address free + target = ptr + size; + if (vm_deallocate(mach_task_self(), (vm_address_t)target, size) != KERN_SUCCESS) + { + vm_deallocate(mach_task_self(), (vm_address_t)ptr, size * 2); + break; + } + + // Attempt to remap the page just deallocated to the buffer again + mach_error = vm_remap(mach_task_self(), (vm_address_t*)&target, size, + 0, // mask + 0, // anywhere + mach_task_self(), (vm_address_t)ptr, + 0, // copy + &cur_prot, &max_prot, VM_INHERIT_COPY); + + if (mach_error == KERN_NO_SPACE) + { + // Failed on this pass, cleanup and make another attempt + if (vm_deallocate(mach_task_self(), (vm_address_t)ptr, size) != KERN_SUCCESS) + break; + } + + else if (mach_error == KERN_SUCCESS) + { + // Leave the loop on success + buffer->ptr = ptr; + break; + } + + else + { + // Unknown error, can't recover + vm_deallocate(mach_task_self(), (vm_address_t)ptr, size); + break; + } + } + +#endif + +#ifdef RMT_PLATFORM_LINUX + + // Linux version based on now-defunct Wikipedia section + // http://en.wikipedia.org/w/index.php?title=Circular_buffer&oldid=600431497 + +#ifdef __ANDROID__ + file_descriptor = ashmem_create_region("remotery_shm", size * 2); + if (file_descriptor < 0) + { + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + } +#else + // Create a unique temporary filename in the shared memory folder + file_descriptor = mkstemp(path); + if (file_descriptor < 0) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + // Delete the name + if (unlink(path)) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + // Set the file size to twice the buffer size + // TODO: this 2x behaviour can be avoided with similar solution to Win/Mac + if (ftruncate(file_descriptor, size * 2)) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + +#endif + // Map 2 contiguous pages + buffer->ptr = (rmtU8*)mmap(NULL, size * 2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (buffer->ptr == MAP_FAILED) + { + buffer->ptr = NULL; + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + } + + // Point both pages to the same memory file + if (mmap(buffer->ptr, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != buffer->ptr || + mmap(buffer->ptr + size, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, file_descriptor, 0) != + buffer->ptr + size) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + +#endif + + // Cleanup if exceeded number of attempts or failed + if (buffer->ptr == NULL) + return RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL; + + return RMT_ERROR_NONE; +} + +static void VirtualMirrorBuffer_Destructor(VirtualMirrorBuffer* buffer) +{ + assert(buffer != 0); + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _XBOX_ONE + if (buffer->page_mapping != NULL) + { + VirtualFree(buffer->ptr, 0, MEM_DECOMMIT); // needed in conjunction with FreeTitlePhysicalPages + FreeTitlePhysicalPages(GetCurrentProcess(), buffer->page_count, buffer->page_mapping); + free(buffer->page_mapping); + buffer->page_mapping = NULL; + } +#else + if (buffer->file_map_handle != NULL) + { + // FIXME, don't we need to unmap the file views obtained in VirtualMirrorBuffer_Constructor, both for + // uwp/non-uwp See example + // https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-virtualalloc2 + + CloseHandle(buffer->file_map_handle); + buffer->file_map_handle = NULL; + } +#endif +#endif + +#ifdef RMT_PLATFORM_MACOS + if (buffer->ptr != NULL) + vm_deallocate(mach_task_self(), (vm_address_t)buffer->ptr, buffer->size * 2); +#endif + +#ifdef RMT_PLATFORM_LINUX + if (buffer->ptr != NULL) + munmap(buffer->ptr, buffer->size * 2); +#endif + + buffer->ptr = NULL; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAFEC: Safe C Library excerpts + http://sourceforge.net/projects/safeclib/ +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +/*------------------------------------------------------------------ + * + * November 2008, Bo Berry + * + * Copyright (c) 2008-2011 by Cisco Systems, Inc + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + *------------------------------------------------------------------ + */ + +// NOTE: Microsoft also has its own version of these functions so I'm do some hacky PP to remove them +#define strnlen_s strnlen_s_safe_c +#define strncat_s strncat_s_safe_c +#define strcpy_s strcpy_s_safe_c + +#define RSIZE_MAX_STR (4UL << 10) /* 4KB */ +#define RCNEGATE(x) x + +#define EOK (0) +#define ESNULLP (400) /* null ptr */ +#define ESZEROL (401) /* length is zero */ +#define ESLEMAX (403) /* length exceeds max */ +#define ESOVRLP (404) /* overlap undefined */ +#define ESNOSPC (406) /* not enough space for s2 */ +#define ESUNTERM (407) /* unterminated string */ +#define ESNOTFND (409) /* not found */ + +#ifndef _ERRNO_T_DEFINED +#define _ERRNO_T_DEFINED +typedef int errno_t; +#endif + +// rsize_t equivalent without going to the hassle of detecting if a platform has implemented C11/K3.2 +typedef unsigned int r_size_t; + +static r_size_t strnlen_s(const char* dest, r_size_t dmax) +{ + r_size_t count; + + if (dest == NULL) + { + return RCNEGATE(0); + } + + if (dmax == 0) + { + return RCNEGATE(0); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(0); + } + + count = 0; + while (*dest && dmax) + { + count++; + dmax--; + dest++; + } + + return RCNEGATE(count); +} + +static errno_t strstr_s(char* dest, r_size_t dmax, const char* src, r_size_t slen, char** substring) +{ + r_size_t len; + r_size_t dlen; + int i; + + if (substring == NULL) + { + return RCNEGATE(ESNULLP); + } + *substring = NULL; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (src == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (slen == 0) + { + return RCNEGATE(ESZEROL); + } + + if (slen > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + /* + * src points to a string with zero length, or + * src equals dest, return dest + */ + if (*src == '\0' || dest == src) + { + *substring = dest; + return RCNEGATE(EOK); + } + + while (*dest && dmax) + { + i = 0; + len = slen; + dlen = dmax; + + while (src[i] && dlen) + { + + /* not a match, not a substring */ + if (dest[i] != src[i]) + { + break; + } + + /* move to the next char */ + i++; + len--; + dlen--; + + if (src[i] == '\0' || !len) + { + *substring = dest; + return RCNEGATE(EOK); + } + } + dest++; + dmax--; + } + + /* + * substring was not found, return NULL + */ + *substring = NULL; + return RCNEGATE(ESNOTFND); +} + +static errno_t strncat_s(char* dest, r_size_t dmax, const char* src, r_size_t slen) +{ + const char* overlap_bumper; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (src == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (slen > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + /* hold base of dest in case src was not copied */ + + if (dest < src) + { + overlap_bumper = src; + + /* Find the end of dest */ + while (*dest != '\0') + { + + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + dest++; + dmax--; + if (dmax == 0) + { + return RCNEGATE(ESUNTERM); + } + } + + while (dmax > 0) + { + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + /* + * Copying truncated before the source null is encountered + */ + if (slen == 0) + { + *dest = '\0'; + return RCNEGATE(EOK); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + slen--; + dest++; + src++; + } + } + else + { + overlap_bumper = dest; + + /* Find the end of dest */ + while (*dest != '\0') + { + + /* + * NOTE: no need to check for overlap here since src comes first + * in memory and we're not incrementing src here. + */ + dest++; + dmax--; + if (dmax == 0) + { + return RCNEGATE(ESUNTERM); + } + } + + while (dmax > 0) + { + if (src == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + /* + * Copying truncated + */ + if (slen == 0) + { + *dest = '\0'; + return RCNEGATE(EOK); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + slen--; + dest++; + src++; + } + } + + /* + * the entire src was not copied, so the string will be nulled. + */ + return RCNEGATE(ESNOSPC); +} + +errno_t strcpy_s(char* dest, r_size_t dmax, const char* src) +{ + const char* overlap_bumper; + + if (dest == NULL) + { + return RCNEGATE(ESNULLP); + } + + if (dmax == 0) + { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) + { + return RCNEGATE(ESLEMAX); + } + + if (src == NULL) + { + *dest = '\0'; + return RCNEGATE(ESNULLP); + } + + if (dest == src) + { + return RCNEGATE(EOK); + } + + if (dest < src) + { + overlap_bumper = src; + + while (dmax > 0) + { + if (dest == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + dest++; + src++; + } + } + else + { + overlap_bumper = dest; + + while (dmax > 0) + { + if (src == overlap_bumper) + { + return RCNEGATE(ESOVRLP); + } + + *dest = *src; + if (*dest == '\0') + { + return RCNEGATE(EOK); + } + + dmax--; + dest++; + src++; + } + } + + /* + * the entire src must have been copied, if not reset dest + * to null the string. + */ + return RCNEGATE(ESNOSPC); +} + +#if !(defined(RMT_PLATFORM_LINUX) && RMT_USE_POSIX_THREADNAMES) + +/* very simple integer to hex */ +static const char* hex_encoding_table = "0123456789ABCDEF"; + +static void itoahex_s(char* dest, r_size_t dmax, rmtS32 value) +{ + r_size_t len; + rmtS32 halfbytepos; + + halfbytepos = 8; + + /* strip leading 0's */ + while (halfbytepos > 1) + { + --halfbytepos; + if (value >> (4 * halfbytepos) & 0xF) + { + ++halfbytepos; + break; + } + } + + len = 0; + while (len + 1 < dmax && halfbytepos > 0) + { + --halfbytepos; + dest[len] = hex_encoding_table[value >> (4 * halfbytepos) & 0xF]; + ++len; + } + + if (len < dmax) + { + dest[len] = 0; + } +} + +static const char* itoa_s(rmtS32 value) +{ + static char temp_dest[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + int pos = 10; + + // Work back with the absolute value + rmtS32 abs_value = abs(value); + while (abs_value > 0) + { + temp_dest[pos--] = '0' + (abs_value % 10); + abs_value /= 10; + } + + // Place the negative + if (value < 0) + { + temp_dest[pos--] = '-'; + } + + return temp_dest + pos + 1; +} + +#endif + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @OSTHREADS: Wrappers around OS-specific thread functions +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef rmtU64 rmtThreadId; + +#ifdef RMT_PLATFORM_WINDOWS +typedef HANDLE rmtThreadHandle; +#else +typedef pthread_t rmtThreadHandle; +#endif + +#ifdef RMT_PLATFORM_WINDOWS +typedef CONTEXT rmtCpuContext; +#else +typedef int rmtCpuContext; +#endif + +static rmtU32 rmtGetNbProcessors() +{ +#ifdef RMT_PLATFORM_WINDOWS + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + return system_info.dwNumberOfProcessors; +#else + // TODO: get_nprocs_conf / get_nprocs + return 0; +#endif +} + +static rmtThreadId rmtGetCurrentThreadId() +{ +#ifdef RMT_PLATFORM_WINDOWS + return GetCurrentThreadId(); +#else + return (rmtThreadId)pthread_self(); +#endif +} + +static rmtBool rmtSuspendThread(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + // SuspendThread is an async call to the scheduler and upon return the thread is not guaranteed to be suspended. + // Calling GetThreadContext will serialise that. + // See: https://github.com/mono/mono/blob/master/mono/utils/mono-threads-windows.c#L203 + return SuspendThread(thread_handle) == 0 ? RMT_TRUE : RMT_FALSE; +#else + return RMT_FALSE; +#endif +} + +static void rmtResumeThread(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + ResumeThread(thread_handle); +#endif +} + +#ifdef RMT_PLATFORM_WINDOWS +#ifndef CONTEXT_EXCEPTION_REQUEST +// These seem to be guarded by a _AMD64_ macro in winnt.h, which doesn't seem to be defined in older MSVC compilers. +// Which makes sense given this was a post-Vista/Windows 7 patch around errors in the WoW64 context switch. +// This bug was never fixed in the OS so defining these will only get this code to compile on Old Windows systems, with no +// guarantee of being stable at runtime. +#define CONTEXT_EXCEPTION_ACTIVE 0x8000000L +#define CONTEXT_SERVICE_ACTIVE 0x10000000L +#define CONTEXT_EXCEPTION_REQUEST 0x40000000L +#define CONTEXT_EXCEPTION_REPORTING 0x80000000L +#endif +#endif + +static rmtBool rmtGetUserModeThreadContext(rmtThreadHandle thread, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD kernel_mode_mask; + + // Request thread context with exception reporting + context->ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_EXCEPTION_REQUEST; + if (GetThreadContext(thread, context) == 0) + { + return RMT_FALSE; + } + + // Context on WoW64 is only valid and can only be set if the thread isn't in kernel mode + // Typical reference to this appears to be: http://zachsaw.blogspot.com/2010/11/wow64-bug-getthreadcontext-may-return.html + // Confirmed by MS here: https://social.msdn.microsoft.com/Forums/vstudio/en-US/aa176c36-6624-4776-9380-1c9cf37a314e/getthreadcontext-returns-stale-register-values-on-wow64?forum=windowscompatibility + kernel_mode_mask = CONTEXT_EXCEPTION_REPORTING | CONTEXT_EXCEPTION_ACTIVE | CONTEXT_SERVICE_ACTIVE; + return (context->ContextFlags & kernel_mode_mask) == CONTEXT_EXCEPTION_REPORTING ? RMT_TRUE : RMT_FALSE; +#else + return RMT_FALSE; +#endif +} + +static void rmtSetThreadContext(rmtThreadHandle thread_handle, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS + SetThreadContext(thread_handle, context); +#endif +} + +static rmtError rmtOpenThreadHandle(rmtThreadId thread_id, rmtThreadHandle* out_thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + // Open the thread with required access rights to get the thread handle + *out_thread_handle = OpenThread(THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_SET_CONTEXT | THREAD_GET_CONTEXT, FALSE, thread_id); + if (*out_thread_handle == NULL) + { + return RMT_ERROR_OPEN_THREAD_HANDLE_FAIL; + } +#endif + + return RMT_ERROR_NONE; +} + +static void rmtCloseThreadHandle(rmtThreadHandle thread_handle) +{ +#ifdef RMT_PLATFORM_WINDOWS + if (thread_handle != NULL) + { + CloseHandle(thread_handle); + } +#endif +} + +#ifdef RMT_PLATFORM_WINDOWS +DWORD_PTR GetThreadStartAddress(rmtThreadHandle thread_handle) +{ + // Get NtQueryInformationThread from ntdll + HMODULE ntdll = GetModuleHandleA("ntdll.dll"); + if (ntdll != NULL) + { + typedef NTSTATUS (WINAPI *NTQUERYINFOMATIONTHREAD)(HANDLE, LONG, PVOID, ULONG, PULONG); + NTQUERYINFOMATIONTHREAD NtQueryInformationThread = (NTQUERYINFOMATIONTHREAD)GetProcAddress(ntdll, "NtQueryInformationThread"); + + // Use it to query the start address + DWORD_PTR start_address; + NTSTATUS status = NtQueryInformationThread(thread_handle, 9, &start_address, sizeof(DWORD), NULL); + if (status == 0) + { + return start_address; + } + } + + return 0; +} + +const char* GetStartAddressModuleName(DWORD_PTR start_address) +{ + BOOL success; + MODULEENTRY32 module_entry; + + // Snapshot the modules + HANDLE handle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0); + if (handle == INVALID_HANDLE_VALUE) + { + return NULL; + } + + module_entry.dwSize = sizeof(MODULEENTRY32); + module_entry.th32ModuleID = 1; + + // Enumerate modules checking start address against their loaded address range + success = Module32First(handle, &module_entry); + while (success == TRUE) + { + if (start_address >= (DWORD_PTR)module_entry.modBaseAddr && start_address <= ((DWORD_PTR)module_entry.modBaseAddr + module_entry.modBaseSize)) + { + static char name[MAX_MODULE_NAME32 + 1]; +#ifdef UNICODE + int size = WideCharToMultiByte(CP_ACP, 0, module_entry.szModule, -1, name, MAX_MODULE_NAME32, NULL, NULL); + if (size < 1) + { + name[0] = '\0'; + } +#else + strcpy_s(name, sizeof(name), module_entry.szModule); +#endif + CloseHandle(handle); + return name; + } + + success = Module32Next(handle, &module_entry); + } + + CloseHandle(handle); + + return NULL; +} +#endif + +static void rmtGetThreadNameFallback(char* out_thread_name, rmtU32 thread_name_size) +{ + // In cases where we can't get a thread name from the OS + static rmtS32 countThreads = 0; + out_thread_name[0] = 0; + strncat_s(out_thread_name, thread_name_size, "Thread", 6); + itoahex_s(out_thread_name + 6, thread_name_size - 6, AtomicAdd(&countThreads, 1)); +} + +static void rmtGetThreadName(rmtThreadId thread_id, rmtThreadHandle thread_handle, char* out_thread_name, rmtU32 thread_name_size) +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD_PTR address; + const char* module_name; + rmtU32 len; + + // Use the new Windows 10 GetThreadDescription function + HMODULE kernel32 = GetModuleHandleA("Kernel32.dll"); + if (kernel32 != NULL) + { + typedef HRESULT(WINAPI* GETTHREADDESCRIPTION)(HANDLE hThread, PWSTR *ppszThreadDescription); + GETTHREADDESCRIPTION GetThreadDescription = (GETTHREADDESCRIPTION)GetProcAddress(kernel32, "GetThreadDescription"); + if (GetThreadDescription != NULL) + { + int size; + + WCHAR* thread_name_w; + GetThreadDescription(thread_handle, &thread_name_w); + + // Returned size is the byte size, so will be 1 for a null-terminated strings + size = WideCharToMultiByte(CP_ACP, 0, thread_name_w, -1, out_thread_name, thread_name_size, NULL, NULL); + if (size > 1) + { + return; + } + } + } + + // At this point GetThreadDescription hasn't returned anything so let's get the thread module name and use that + address = GetThreadStartAddress(thread_handle); + if (address == 0) + { + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + return; + } + module_name = GetStartAddressModuleName(address); + if (module_name == NULL) + { + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + return; + } + + // Concatenate thread name with then thread ID as that will be unique, whereas the start address won't be + memset(out_thread_name, 0, thread_name_size); + strcpy_s(out_thread_name, thread_name_size, module_name); + strncat_s(out_thread_name, thread_name_size, "!", 1); + len = strnlen_s(out_thread_name, thread_name_size); + itoahex_s(out_thread_name + len, thread_name_size - len, thread_id); + +#elif defined(RMT_PLATFORM_LINUX) && RMT_USE_POSIX_THREADNAMES && !defined(__FreeBSD__) && !defined(__OpenBSD__) + + prctl(PR_GET_NAME, out_thread_name, 0, 0, 0); + +#else + + rmtGetThreadNameFallback(out_thread_name, thread_name_size); + +#endif +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @THREADS: Cross-platform thread object +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct Thread_t rmtThread; +typedef rmtError (*ThreadProc)(rmtThread* thread); + +struct Thread_t +{ + rmtThreadHandle handle; + + // Callback executed when the thread is created + ThreadProc callback; + + // Caller-specified parameter passed to Thread_Create + void* param; + + // Error state returned from callback + rmtError error; + + // External threads can set this to request an exit + volatile rmtBool request_exit; +}; + +#if defined(RMT_PLATFORM_WINDOWS) + +static DWORD WINAPI ThreadProcWindows(LPVOID lpParameter) +{ + rmtThread* thread = (rmtThread*)lpParameter; + assert(thread != NULL); + thread->error = thread->callback(thread); + return thread->error == RMT_ERROR_NONE ? 0 : 1; +} + +#else +static void* StartFunc(void* pArgs) +{ + rmtThread* thread = (rmtThread*)pArgs; + assert(thread != NULL); + thread->error = thread->callback(thread); + return NULL; // returned error not use, check thread->error. +} +#endif + +static int rmtThread_Valid(rmtThread* thread) +{ + assert(thread != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + return thread->handle != NULL; +#else + return !pthread_equal(thread->handle, pthread_self()); +#endif +} + +static rmtError rmtThread_Constructor(rmtThread* thread, ThreadProc callback, void* param) +{ + assert(thread != NULL); + + thread->callback = callback; + thread->param = param; + thread->error = RMT_ERROR_NONE; + thread->request_exit = RMT_FALSE; + + // OS-specific thread creation + +#if defined(RMT_PLATFORM_WINDOWS) + + thread->handle = CreateThread(NULL, // lpThreadAttributes + 0, // dwStackSize + ThreadProcWindows, // lpStartAddress + thread, // lpParameter + 0, // dwCreationFlags + NULL); // lpThreadId + + if (thread->handle == NULL) + return RMT_ERROR_CREATE_THREAD_FAIL; + +#else + + int32_t error = pthread_create(&thread->handle, NULL, StartFunc, thread); + if (error) + { + // Contents of 'thread' parameter to pthread_create() are undefined after + // failure call so can't pre-set to invalid value before hand. + thread->handle = pthread_self(); + return RMT_ERROR_CREATE_THREAD_FAIL; + } + +#endif + + return RMT_ERROR_NONE; +} + +static void rmtThread_RequestExit(rmtThread* thread) +{ + // Not really worried about memory barriers or delayed visibility to the target thread + assert(thread != NULL); + thread->request_exit = RMT_TRUE; +} + +static void rmtThread_Join(rmtThread* thread) +{ + assert(rmtThread_Valid(thread)); + +#if defined(RMT_PLATFORM_WINDOWS) + WaitForSingleObject(thread->handle, INFINITE); +#else + pthread_join(thread->handle, NULL); +#endif +} + +static void rmtThread_Destructor(rmtThread* thread) +{ + assert(thread != NULL); + + if (rmtThread_Valid(thread)) + { + // Shutdown the thread + rmtThread_RequestExit(thread); + rmtThread_Join(thread); + + // OS-specific release of thread resources + +#if defined(RMT_PLATFORM_WINDOWS) + CloseHandle(thread->handle); + thread->handle = NULL; +#endif + } +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @OBJALLOC: Reusable Object Allocator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// All objects that require free-list-backed allocation need to inherit from this type. +// +typedef struct ObjectLink_s +{ + struct ObjectLink_s* volatile next; +} ObjectLink; + +static void ObjectLink_Constructor(ObjectLink* link) +{ + assert(link != NULL); + link->next = NULL; +} + +typedef rmtError (*ObjConstructor)(void*); +typedef void (*ObjDestructor)(void*); + +typedef struct +{ + // Object create/destroy parameters + rmtU32 object_size; + ObjConstructor constructor; + ObjDestructor destructor; + + // Number of objects in the free list + 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 + for (;;) + { + ObjectLink* old_link = (ObjectLink*)allocator->first_free; + end->next = old_link; + if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)start) == + RMT_TRUE) + break; + } +} + +static ObjectLink* ObjectAllocator_Pop(ObjectAllocator* allocator) +{ + ObjectLink* link; + + assert(allocator != NULL); + + // CAS pop from the front of the list + for (;;) + { + ObjectLink* old_link = (ObjectLink*)allocator->first_free; + if (old_link == NULL) + { + return NULL; + } + ObjectLink* next_link = old_link->next; + if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)next_link) == + RMT_TRUE) + { + link = old_link; + break; + } + } + + link->next = NULL; + + return link; +} + +static rmtError ObjectAllocator_Alloc(ObjectAllocator* allocator, void** object) +{ + // This function only calls the object constructor on initial malloc of an object + + assert(allocator != NULL); + assert(object != NULL); + + // Pull available objects from the free list + *object = ObjectAllocator_Pop(allocator); + + // Has the free list run out? + if (*object == NULL) + { + rmtError error; + + // Allocate/construct a new object + *object = rmtMalloc(allocator->object_size); + if (*object == NULL) + return RMT_ERROR_MALLOC_FAIL; + assert(allocator->constructor != NULL); + error = allocator->constructor(*object); + if (error != RMT_ERROR_NONE) + { + // Auto-teardown on failure + assert(allocator->destructor != NULL); + allocator->destructor(*object); + rmtFree(*object); + return error; + } + + AtomicAdd(&allocator->nb_allocated, 1); + } + else + { + 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_Grow(Buffer* buffer, rmtU32 length) +{ + // Calculate size increase rounded up to the requested allocation granularity + rmtU32 granularity = buffer->alloc_granularity; + rmtU32 allocate = buffer->bytes_allocated + length; + allocate = allocate + ((granularity - 1) - ((allocate - 1) % granularity)); + + buffer->bytes_allocated = allocate; + buffer->data = (rmtU8*)rmtRealloc(buffer->data, buffer->bytes_allocated); + if (buffer->data == NULL) + return RMT_ERROR_MALLOC_FAIL; + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_Write(Buffer* buffer, const void* data, rmtU32 length) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + length > buffer->bytes_allocated) + { + rmtError error = Buffer_Grow(buffer, length); + if (error != RMT_ERROR_NONE) + return error; + } + + // Copy all bytes + memcpy(buffer->data + buffer->bytes_used, data, length); + buffer->bytes_used += length; + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_WriteStringZ(Buffer* buffer, rmtPStr string) +{ + assert(string != NULL); + return Buffer_Write(buffer, (void*)string, (rmtU32)strnlen_s(string, 2048) + 1); +} + +static void U32ToByteArray(rmtU8* dest, rmtU32 value) +{ + // Commit as little-endian + dest[0] = value & 255; + dest[1] = (value >> 8) & 255; + dest[2] = (value >> 16) & 255; + dest[3] = value >> 24; +} + +static rmtError Buffer_WriteU32(Buffer* buffer, rmtU32 value) +{ + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + sizeof(value) > buffer->bytes_allocated) + { + rmtError error = Buffer_Grow(buffer, sizeof(value)); + if (error != RMT_ERROR_NONE) + return error; + } + +// Copy all bytes +#if RMT_ASSUME_LITTLE_ENDIAN + *(rmtU32*)(buffer->data + buffer->bytes_used) = value; +#else + U32ToByteArray(buffer->data + buffer->bytes_used, value); +#endif + + buffer->bytes_used += sizeof(value); + + return RMT_ERROR_NONE; +} + +static rmtBool IsLittleEndian() +{ + // Not storing this in a global variable allows the compiler to more easily optimise + // this away altogether. + union { + unsigned int i; + unsigned char c[sizeof(unsigned int)]; + } u; + u.i = 1; + return u.c[0] == 1 ? RMT_TRUE : RMT_FALSE; +} + +static rmtError Buffer_WriteU64(Buffer* buffer, rmtU64 value) +{ + // Write as a double as Javascript DataView doesn't have a 64-bit integer read + + assert(buffer != NULL); + + // Reallocate the buffer on overflow + if (buffer->bytes_used + sizeof(value) > buffer->bytes_allocated) + { + rmtError error = Buffer_Grow(buffer, sizeof(value)); + if (error != RMT_ERROR_NONE) + return error; + } + +// Copy all bytes +#if RMT_ASSUME_LITTLE_ENDIAN + *(double*)(buffer->data + buffer->bytes_used) = (double)value; +#else + { + union { + double d; + unsigned char c[sizeof(double)]; + } u; + rmtU8* dest = buffer->data + buffer->bytes_used; + u.d = (double)value; + if (IsLittleEndian()) + { + dest[0] = u.c[0]; + dest[1] = u.c[1]; + dest[2] = u.c[2]; + dest[3] = u.c[3]; + dest[4] = u.c[4]; + dest[5] = u.c[5]; + dest[6] = u.c[6]; + dest[7] = u.c[7]; + } + else + { + dest[0] = u.c[7]; + dest[1] = u.c[6]; + dest[2] = u.c[5]; + dest[3] = u.c[4]; + dest[4] = u.c[3]; + dest[5] = u.c[2]; + dest[6] = u.c[1]; + dest[7] = u.c[0]; + } + } +#endif + + buffer->bytes_used += sizeof(value); + + return RMT_ERROR_NONE; +} + +static rmtError Buffer_WriteStringWithLength(Buffer* buffer, rmtPStr string) +{ + rmtU32 length = (rmtU32)strnlen_s(string, 2048); + rmtError error; + + error = Buffer_WriteU32(buffer, length); + if (error != RMT_ERROR_NONE) + return error; + + return Buffer_Write(buffer, (void*)string, length); +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @HASHTABLE: Integer pair hash map for inserts/finds. No removes for added simplicity. +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define RMT_NOT_FOUND 0xffffffffffffffff + +typedef struct +{ + // Non-zero, pre-hashed key + rmtU32 key; + + // Value that's not equal to RMT_NOT_FOUND + rmtU64 value; +} HashSlot; + +typedef struct +{ + // Stats + rmtU32 maxNbSlots; + rmtU32 nbSlots; + + // Data + HashSlot* slots; +} rmtHashTable; + +static rmtError rmtHashTable_Constructor(rmtHashTable* table, rmtU32 max_nb_slots) +{ + // Default initialise + assert(table != NULL); + table->maxNbSlots = max_nb_slots; + table->nbSlots = 0; + + // Allocate and clear the hash slots + table->slots = (HashSlot*)rmtMalloc(table->maxNbSlots * sizeof(HashSlot)); + if (table->slots == NULL) + { + return RMT_ERROR_MALLOC_FAIL; + } + memset(table->slots, 0, table->maxNbSlots * sizeof(HashSlot)); + + return RMT_ERROR_NONE; +} + +static void rmtHashTable_Destructor(rmtHashTable* table) +{ + assert(table != NULL); + + if (table->slots != NULL) + { + rmtFree(table->slots); + table->slots = NULL; + } +} + +static rmtError rmtHashTable_Resize(rmtHashTable* table); + +static rmtError rmtHashTable_Insert(rmtHashTable* table, rmtU32 key, rmtU64 value) +{ + HashSlot* slot = NULL; + rmtError error = RMT_ERROR_NONE; + + // Calculate initial slot location for this key + rmtU32 index_mask = table->maxNbSlots - 1; + rmtU32 index = key & index_mask; + + assert(key != 0); + assert(value != RMT_NOT_FOUND); + + // Linear probe for free slot, reusing any existing key matches + // There will always be at least one free slot due to load factor management + while (table->slots[index].key) + { + if (table->slots[index].key == key) + { + // Counter occupied slot increments below + table->nbSlots--; + break; + } + + index = (index + 1) & index_mask; + } + + // Just verify that I've got no errors in the code above + assert(index < table->maxNbSlots); + + // Add to the table + slot = table->slots + index; + slot->key = key; + slot->value = value; + table->nbSlots++; + + // Resize when load factor is greater than 2/3 + if (table->nbSlots > (table->maxNbSlots * 2) / 3) + { + error = rmtHashTable_Resize(table); + } + + return error; +} + +static rmtError rmtHashTable_Resize(rmtHashTable* table) +{ + rmtU32 old_max_nb_slots = table->maxNbSlots; + HashSlot* new_slots = NULL; + HashSlot* old_slots = table->slots; + rmtU32 i; + + // Increase the table size + rmtU32 new_max_nb_slots = table->maxNbSlots; + if (new_max_nb_slots < 8192 * 4) + { + new_max_nb_slots *= 4; + } + else + { + new_max_nb_slots *= 2; + } + + // Allocate and clear a new table + new_slots = (HashSlot*)rmtMalloc(new_max_nb_slots * sizeof(HashSlot)); + if (new_slots == NULL) + { + return RMT_ERROR_MALLOC_FAIL; + } + memset(new_slots, 0, new_max_nb_slots * sizeof(HashSlot)); + + // Update fields of the table after successful allocation only + table->slots = new_slots; + table->maxNbSlots = new_max_nb_slots; + table->nbSlots = 0; + + // Reinsert all objects into the new table + for (i = 0; i < old_max_nb_slots; i++) + { + HashSlot* slot = old_slots + i; + if (slot->key != 0) + { + rmtHashTable_Insert(table, slot->key, slot->value); + } + } + + rmtFree(old_slots); + + return RMT_ERROR_NONE; +} + +static rmtU64 rmtHashTable_Find(rmtHashTable* table, rmtU32 key) +{ + // Calculate initial slot location for this key + rmtU32 index_mask = table->maxNbSlots - 1; + rmtU32 index = key & index_mask; + + // Linear probe for matching hash + while (table->slots[index].key) + { + HashSlot* slot = table->slots + index; + + if (slot->key == key) + { + return slot->value; + } + + index = (index + 1) & index_mask; + } + + return RMT_NOT_FOUND; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @STRINGTABLE: Map from string hash to string offset in local buffer +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct +{ + // Growable dynamic array of strings added so far + Buffer* text; + + // Map from text hash to text location in the buffer + rmtHashTable* text_map; +} StringTable; + +static rmtError StringTable_Constructor(StringTable* table) +{ + rmtError error; + + // Default initialise + assert(table != NULL); + table->text = NULL; + table->text_map = NULL; + + // Allocate reasonably storage for initial sample names + + New_1(Buffer, table->text, 8 * 1024); + if (error != RMT_ERROR_NONE) + return error; + + New_1(rmtHashTable, table->text_map, 1 * 1024); + if (error != RMT_ERROR_NONE) + return error; + + return RMT_ERROR_NONE; +} + +static void StringTable_Destructor(StringTable* table) +{ + assert(table != NULL); + + Delete(rmtHashTable, table->text_map); + Delete(Buffer, table->text); +} + +static rmtPStr StringTable_Find(StringTable* table, rmtU32 name_hash) +{ + rmtU64 text_offset = rmtHashTable_Find(table->text_map, name_hash); + if (text_offset != RMT_NOT_FOUND) + { + return (rmtPStr)(table->text->data + text_offset); + } + return NULL; +} + +static void StringTable_Insert(StringTable* table, rmtU32 name_hash, rmtPStr name) +{ + // Only add to the buffer if the string isn't already there + rmtU64 text_offset = rmtHashTable_Find(table->text_map, name_hash); + if (text_offset == RMT_NOT_FOUND) + { + // TODO: Allocation errors aren't being passed on to the caller + text_offset = table->text->bytes_used; + Buffer_WriteStringZ(table->text, name); + rmtHashTable_Insert(table->text_map, name_hash, text_offset); + } +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @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, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + SOCKET s = INVALID_SOCKET; + struct sockaddr_in sin; +#ifdef RMT_PLATFORM_WINDOWS + u_long nonblock = 1; +#endif + + memset(&sin, 0, sizeof(sin)); + assert(tcp_socket != NULL); + + // Try to create the socket + s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == SOCKET_ERROR) + return RMT_ERROR_SOCKET_CREATE_FAIL; + + if (reuse_open_port) + { + int enable = 1; + +// set SO_REUSEADDR so binding doesn't fail when restarting the application +// (otherwise the same port can't be reused within TIME_WAIT) +// I'm not checking for errors because if this fails (unlikely) we might still +// be able to bind to the socket anyway +#ifdef RMT_PLATFORM_POSIX + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); +#elif defined(RMT_PLATFORM_WINDOWS) + // windows also needs SO_EXCLUSEIVEADDRUSE, + // see http://www.andy-pearce.com/blog/posts/2013/Feb/so_reuseaddr-on-windows/ + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable)); + enable = 1; + setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&enable, sizeof(enable)); +#endif + } + + // Bind the socket to the incoming port + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(limit_connections_to_localhost ? INADDR_LOOPBACK : INADDR_ANY); + sin.sin_port = htons(port); + if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) + return 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); +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4127) // warning C4127: conditional expression is constant +#endif // _MSC_VER + FD_SET(tcp_socket->socket, &fd_read); + FD_SET(tcp_socket->socket, &fd_write); + FD_SET(tcp_socket->socket, &fd_errors); +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + + // Poll socket status without blocking + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select(((int)tcp_socket->socket) + 1, &fd_read, &fd_write, &fd_errors, &tv) == SOCKET_ERROR) + { + status.error_state = RMT_ERROR_SOCKET_SELECT_FAIL; + return status; + } + + status.can_read = FD_ISSET(tcp_socket->socket, &fd_read) != 0 ? RMT_TRUE : RMT_FALSE; + status.can_write = FD_ISSET(tcp_socket->socket, &fd_write) != 0 ? RMT_TRUE : RMT_FALSE; + status.error_state = FD_ISSET(tcp_socket->socket, &fd_errors) != 0 ? RMT_ERROR_SOCKET_POLL_ERRORS : RMT_ERROR_NONE; + return status; +} + +static rmtError TCPSocket_AcceptConnection(TCPSocket* tcp_socket, TCPSocket** client_socket) +{ + SocketStatus status; + SOCKET s; + 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; + +#ifdef SO_NOSIGPIPE + // On POSIX systems, send() may send a SIGPIPE signal when writing to an + // already closed connection. By setting this option, we prevent the + // signal from being emitted and send will instead return an error and set + // errno to EPIPE. + // + // This is supported on BSD platforms and not on Linux. + { + int flag = 1; + setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(flag)); + } +#endif + // Create a client socket for the new connection + assert(client_socket != NULL); + New_0(TCPSocket, *client_socket); + if (error != RMT_ERROR_NONE) + return error; + (*client_socket)->socket = s; + + return RMT_ERROR_NONE; +} + +static int TCPTryAgain() +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD error = WSAGetLastError(); + return error == WSAEWOULDBLOCK; +#else +#if EAGAIN == EWOULDBLOCK + return errno == EAGAIN; +#else + return errno == EAGAIN || errno == EWOULDBLOCK; +#endif +#endif +} + +static rmtError TCPSocket_Send(TCPSocket* tcp_socket, const void* data, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data = NULL; + char* end_data = NULL; + rmtU32 start_ms = 0; + rmtU32 cur_ms = 0; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Loop until timeout checking whether data can be written + status.can_write = RMT_FALSE; + while (!status.can_write) + { + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + cur_ms = msTimer_Get(); + if (cur_ms - start_ms > timeout_ms) + return 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_flags = 0; +#ifdef MSG_NOSIGNAL + // On Linux this prevents send from emitting a SIGPIPE signal + // Equivalent on BSD to the SO_NOSIGPIPE option. + send_flags = MSG_NOSIGNAL; +#endif + bytes_sent = (int)send(tcp_socket->socket, cur_data, (int)(end_data - cur_data), send_flags); + + if (bytes_sent == SOCKET_ERROR || bytes_sent == 0) + { + // Close the connection if sending fails for any other reason other than blocking + if (bytes_sent != 0 && !TCPTryAgain()) + return RMT_ERROR_SOCKET_SEND_FAIL; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) endpoint is no longer there + // 2) endpoint can't consume quick enough + // 3) local buffers overflow + // + // As none of these are actually errors, we have to pass this timeout back to the caller. + // + // TODO: This strategy breaks down if a send partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return 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 && !TCPTryAgain()) + return RMT_ERROR_SOCKET_RECV_FAILED; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) data is delayed by sender + // 2) sender fails to send a complete set of packets + // + // As not all of these scenarios are errors, we need to pass this information back to the caller. + // + // TODO: This strategy breaks down if a receive partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + } + } + else + { + // Jump over the data received + cur_data += bytes_received; + } + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SHA1: SHA-1 Cryptographic Hash Function +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +// +// Typed to allow enforced data size specification +// +typedef struct +{ + rmtU8 data[20]; +} SHA1; + +/* + Copyright (c) 2011, Micael Hildenborg + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Micael Hildenborg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + Contributors: + Gustav + Several members in the gamedev.se forum. + Gregory Petrosyan + */ + +// Rotate an integer value to left. +static unsigned int rol(const unsigned int value, const unsigned int steps) +{ + return ((value << steps) | (value >> (32 - steps))); +} + +// Sets the first 16 integers in the buffert to zero. +// Used for clearing the W buffert. +static void clearWBuffert(unsigned int* buffert) +{ + int pos; + for (pos = 16; --pos >= 0;) + { + buffert[pos] = 0; + } +} + +static void innerHash(unsigned int* result, unsigned int* w) +{ + unsigned int a = result[0]; + unsigned int b = result[1]; + unsigned int c = result[2]; + unsigned int d = result[3]; + unsigned int e = result[4]; + + int round = 0; + +#define sha1macro(func, val) \ + { \ + const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \ + e = d; \ + d = c; \ + c = rol(b, 30); \ + b = a; \ + a = t; \ + } + + while (round < 16) + { + sha1macro((b & c) | (~b & d), 0x5a827999); + ++round; + } + while (round < 20) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (~b & d), 0x5a827999); + ++round; + } + while (round < 40) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0x6ed9eba1); + ++round; + } + while (round < 60) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc); + ++round; + } + while (round < 80) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0xca62c1d6); + ++round; + } + +#undef sha1macro + + result[0] += a; + result[1] += b; + result[2] += c; + result[3] += d; + result[4] += e; +} + +static void calc(const void* src, const int bytelength, unsigned char* hash) +{ + int roundPos; + int lastBlockBytes; + int hashByte; + + // Init the result array. + unsigned int result[5] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0}; + + // Cast the void src pointer to be the byte array we can work with. + const unsigned char* sarray = (const unsigned char*)src; + + // The reusable round buffer + unsigned int w[80]; + + // Loop through all complete 64byte blocks. + const int endOfFullBlocks = bytelength - 64; + int endCurrentBlock; + int currentBlock = 0; + + while (currentBlock <= endOfFullBlocks) + { + endCurrentBlock = currentBlock + 64; + + // Init the round buffer with the 64 byte block data. + for (roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4) + { + // This line will swap endian on big endian and keep endian on little endian. + w[roundPos++] = (unsigned int)sarray[currentBlock + 3] | (((unsigned int)sarray[currentBlock + 2]) << 8) | + (((unsigned int)sarray[currentBlock + 1]) << 16) | + (((unsigned int)sarray[currentBlock]) << 24); + } + innerHash(result, w); + } + + // Handle the last and not full 64 byte block if existing. + endCurrentBlock = bytelength - currentBlock; + clearWBuffert(w); + lastBlockBytes = 0; + for (; lastBlockBytes < endCurrentBlock; ++lastBlockBytes) + { + w[lastBlockBytes >> 2] |= (unsigned int)sarray[lastBlockBytes + currentBlock] + << ((3 - (lastBlockBytes & 3)) << 3); + } + w[lastBlockBytes >> 2] |= 0x80U << ((3 - (lastBlockBytes & 3)) << 3); + if (endCurrentBlock >= 56) + { + innerHash(result, w); + clearWBuffert(w); + } + w[15] = bytelength << 3; + innerHash(result, w); + + // Store hash in result pointer, and make sure we get in in the correct order on both endian models. + for (hashByte = 20; --hashByte >= 0;) + { + hash[hashByte] = (result[hashByte >> 2] >> (((3 - hashByte) & 0x3) << 3)) & 0xff; + } +} + +static SHA1 SHA1_Calculate(const void* src, unsigned int length) +{ + SHA1 hash; + assert((int)length >= 0); + calc(src, length, hash.data); + return hash; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @BASE64: Base-64 encoder +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static const char* b64_encoding_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static rmtU32 Base64_CalculateEncodedLength(rmtU32 length) +{ + // ceil(l * 4/3) + return 4 * ((length + 2) / 3); +} + +static void Base64_Encode(const rmtU8* in_bytes, rmtU32 length, rmtU8* out_bytes) +{ + rmtU32 i; + rmtU32 encoded_length; + rmtU32 remaining_bytes; + + rmtU8* optr = out_bytes; + + for (i = 0; i < length;) + { + // Read input 3 values at a time, null terminating + rmtU32 c0 = i < length ? in_bytes[i++] : 0; + rmtU32 c1 = i < length ? in_bytes[i++] : 0; + rmtU32 c2 = i < length ? in_bytes[i++] : 0; + + // Encode 4 bytes for ever 3 input bytes + rmtU32 triple = (c0 << 0x10) + (c1 << 0x08) + c2; + *optr++ = b64_encoding_table[(triple >> 3 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 2 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 1 * 6) & 0x3F]; + *optr++ = b64_encoding_table[(triple >> 0 * 6) & 0x3F]; + } + + // Pad output to multiple of 3 bytes with terminating '=' + encoded_length = Base64_CalculateEncodedLength(length); + remaining_bytes = (3 - ((length + 2) % 3)) - 1; + for (i = 0; i < remaining_bytes; i++) + out_bytes[encoded_length - 1 - i] = '='; + + // Null terminate + out_bytes[encoded_length] = 0; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MURMURHASH: MurmurHash3 + https://code.google.com/p/smhasher +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. +//----------------------------------------------------------------------------- + +static rmtU32 rotl32(rmtU32 x, rmtS8 r) +{ + return (x << r) | (x >> (32 - r)); +} + +// Block read - if your platform needs to do endian-swapping, do the conversion here +static rmtU32 getblock32(const rmtU32* p, int i) +{ + rmtU32 result; + const rmtU8* src = ((const rmtU8*)p) + i * sizeof(rmtU32); + memcpy(&result, src, sizeof(result)); + return result; +} + +// Finalization mix - force all bits of a hash block to avalanche +static rmtU32 fmix32(rmtU32 h) +{ + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + return h; +} + +static rmtU32 MurmurHash3_x86_32(const void* key, int len, rmtU32 seed) +{ + const rmtU8* data = (const rmtU8*)key; + const int nblocks = len / 4; + + rmtU32 h1 = seed; + + const rmtU32 c1 = 0xcc9e2d51; + const rmtU32 c2 = 0x1b873593; + + int i; + + const rmtU32* blocks = (const rmtU32*)(data + nblocks * 4); + const rmtU8* tail = (const rmtU8*)(data + nblocks * 4); + + rmtU32 k1 = 0; + + //---------- + // body + + for (i = -nblocks; i; i++) + { + rmtU32 k2 = getblock32(blocks, i); + + k2 *= c1; + k2 = rotl32(k2, 15); + k2 *= c2; + + h1 ^= k2; + h1 = rotl32(h1, 13); + h1 = h1 * 5 + 0xe6546b64; + } + + //---------- + // tail + + switch (len & 3) + { + case 3: + k1 ^= tail[2] << 16; // fallthrough + case 2: + k1 ^= tail[1] << 8; // fallthrough + case 1: + k1 ^= tail[0]; + k1 *= c1; + k1 = rotl32(k1, 15); + k1 *= c2; + h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + + h1 = fmix32(h1); + + return h1; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @WEBSOCKETS: WebSockets +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +enum WebSocketMode +{ + WEBSOCKET_NONE = 0, + WEBSOCKET_TEXT = 1, + WEBSOCKET_BINARY = 2, +}; + +typedef struct +{ + TCPSocket* tcp_socket; + + enum WebSocketMode mode; + + rmtU32 frame_bytes_remaining; + rmtU32 mask_offset; + + union { + rmtU8 mask[4]; + rmtU32 mask_u32; + } data; + +} WebSocket; + +static void WebSocket_Close(WebSocket* web_socket); + +static char* GetField(char* buffer, r_size_t buffer_length, rmtPStr field_name) +{ + char* field = NULL; + char* buffer_end = buffer + buffer_length - 1; + + r_size_t field_length = strnlen_s(field_name, buffer_length); + if (field_length == 0) + return NULL; + + // Search for the start of the field + if (strstr_s(buffer, buffer_length, field_name, field_length, &field) != EOK) + return NULL; + + // Field name is now guaranteed to be in the buffer so its safe to jump over it without hitting the bounds + field += strlen(field_name); + + // Skip any trailing whitespace + while (*field == ' ') + { + if (field >= buffer_end) + return NULL; + field++; + } + + return field; +} + +static const char websocket_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +static const char websocket_response[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: "; + +static rmtError WebSocketHandshake(TCPSocket* tcp_socket, rmtPStr limit_host) +{ + rmtU32 start_ms, now_ms; + + // Parsing scratchpad + char buffer[1024]; + char* buffer_ptr = buffer; + int buffer_len = sizeof(buffer) - 1; + char* buffer_end = buffer + buffer_len; + + char response_buffer[256]; + int response_buffer_len = sizeof(response_buffer) - 1; + + char* version; + char* host; + char* key; + char* key_end; + SHA1 hash; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Really inefficient way of receiving the handshake data from the browser + // Not really sure how to do this any better, as the termination requirement is \r\n\r\n + while (buffer_ptr - buffer < buffer_len) + { + rmtError error = TCPSocket_Receive(tcp_socket, buffer_ptr, 1, 20); + if (error == RMT_ERROR_SOCKET_RECV_FAILED) + return error; + + // If there's a stall receiving the data, check for a handshake timeout + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + now_ms = msTimer_Get(); + if (now_ms - start_ms > 1000) + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + + continue; + } + + // Just in case new enums are added... + assert(error == RMT_ERROR_NONE); + + if (buffer_ptr - buffer >= 4) + { + if (*(buffer_ptr - 3) == '\r' && *(buffer_ptr - 2) == '\n' && *(buffer_ptr - 1) == '\r' && + *(buffer_ptr - 0) == '\n') + break; + } + + buffer_ptr++; + } + *buffer_ptr = 0; + + // HTTP GET instruction + if (memcmp(buffer, "GET", 3) != 0) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET; + + // Look for the version number and verify that it's supported + version = GetField(buffer, buffer_len, "Sec-WebSocket-Version:"); + if (version == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION; + if (buffer_end - version < 2 || (version[0] != '8' && (version[0] != '1' || version[1] != '3'))) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION; + + // Make sure this connection comes from a known host + host = GetField(buffer, buffer_len, "Host:"); + if (host == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST; + if (limit_host != NULL) + { + r_size_t limit_host_len = strnlen_s(limit_host, 128); + char* found = NULL; + if (strstr_s(host, 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, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost, enum WebSocketMode mode) +{ + // Create the server's listening socket + assert(web_socket != NULL); + web_socket->mode = mode; + return TCPSocket_RunServer(web_socket->tcp_socket, port, reuse_open_port, limit_connections_to_localhost); +} + +static void WebSocket_Close(WebSocket* web_socket) +{ + assert(web_socket != NULL); + 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; + } +} + +// For send buffers to preallocate +#define WEBSOCKET_MAX_FRAME_HEADER_SIZE 10 + +static void WebSocket_PrepareBuffer(Buffer* buffer) +{ + char empty_frame_header[WEBSOCKET_MAX_FRAME_HEADER_SIZE]; + + assert(buffer != NULL); + + // Reset to start + buffer->bytes_used = 0; + + // Allocate enough space for a maximum-sized frame header + Buffer_Write(buffer, empty_frame_header, sizeof(empty_frame_header)); +} + +static rmtU32 WebSocket_FrameHeaderSize(rmtU32 length) +{ + if (length <= 125) + return 2; + if (length <= 65535) + return 4; + return 10; +} + +static void WebSocket_WriteFrameHeader(WebSocket* web_socket, rmtU8* dest, rmtU32 length) +{ + rmtU8 final_fragment = 0x1 << 7; + rmtU8 frame_type = (rmtU8)web_socket->mode; + + dest[0] = final_fragment | frame_type; + + // Construct the frame header, correctly applying the narrowest size + if (length <= 125) + { + dest[1] = (rmtU8)length; + } + else if (length <= 65535) + { + dest[1] = 126; + WriteSize(length, dest + 2, 2, 0); + } + else + { + dest[1] = 127; + WriteSize(length, dest + 2, 8, 4); + } +} + +static rmtError WebSocket_Send(WebSocket* web_socket, const void* data, rmtU32 length, rmtU32 timeout_ms) +{ + rmtError error; + SocketStatus status; + rmtU32 payload_length, frame_header_size, delta; + + assert(web_socket != NULL); + assert(data != NULL); + + // Can't send if there are socket errors + status = WebSocket_PollStatus(web_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + // Assume space for max frame header has been allocated in the incoming data + payload_length = length - WEBSOCKET_MAX_FRAME_HEADER_SIZE; + frame_header_size = WebSocket_FrameHeaderSize(payload_length); + delta = WEBSOCKET_MAX_FRAME_HEADER_SIZE - frame_header_size; + data = (void*)((rmtU8*)data + delta); + length -= delta; + WebSocket_WriteFrameHeader(web_socket, (rmtU8*)data, payload_length); + + // Send frame header and data together + error = TCPSocket_Send(web_socket->tcp_socket, data, length, timeout_ms); + return error; +} + +static rmtError ReceiveFrameHeader(WebSocket* web_socket) +{ + // TODO: Specify infinite timeout? + + 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[8]; + 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 (web_socket->data.mask_u32 != 0) + { + rmtU32 i; + for (i = 0; i < bytes_to_read; i++) + { + *((rmtU8*)cur_data + i) ^= web_socket->data.mask[web_socket->mask_offset & 3]; + web_socket->mask_offset++; + } + } + + cur_data += bytes_to_read; + web_socket->frame_bytes_remaining -= bytes_to_read; + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @MESSAGEQ: Multiple producer, single consumer message queue +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef enum MessageID +{ + MsgID_NotReady, + MsgID_AddToStringTable, + MsgID_LogText, + MsgID_SampleTree, + MsgID_ProcessorThreads, + MsgID_ThreadName, + MsgID_None, + MsgID_Force32Bits = 0xFFFFFFFF, +} MessageID; + +typedef struct Message +{ + MessageID id; + + rmtU32 payload_size; + + // For telling which thread the message came from in the debugger + struct ThreadProfiler* threadProfiler; + + rmtU8 payload[1]; +} Message; + +// Multiple producer, single consumer message queue that uses its own data buffer +// to store the message data. +typedef struct rmtMessageQueue +{ + rmtU32 size; + + // The physical address of this data buffer is pointed to by two sequential + // virtual memory pages, allowing automatic wrap-around of any reads or writes + // that exceed the limits of the buffer. + VirtualMirrorBuffer* data; + + // Read/write position never wrap allowing trivial overflow checks + // with easier debugging + rmtU32 read_pos; + rmtU32 write_pos; + +} rmtMessageQueue; + +static rmtError rmtMessageQueue_Constructor(rmtMessageQueue* 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 rmtMessageQueue_Destructor(rmtMessageQueue* queue) +{ + assert(queue != NULL); + Delete(VirtualMirrorBuffer, queue->data); +} + +static rmtU32 rmtMessageQueue_SizeForPayload(rmtU32 payload_size) +{ + // Add message header and align for ARM platforms + rmtU32 size = sizeof(Message) + payload_size; + size = (size + 3) & ~3U; + return size; +} + +static Message* rmtMessageQueue_AllocMessage(rmtMessageQueue* queue, rmtU32 payload_size, + struct ThreadProfiler* thread_profiler) +{ + Message* msg; + + rmtU32 write_size = rmtMessageQueue_SizeForPayload(payload_size); + + assert(queue != NULL); + + for (;;) + { + // Check for potential overflow + // Order of loads means allocation failure can happen when enough space has just been freed + // However, incorrect overflows are not possible + rmtU32 s = queue->size; + rmtU32 w = LoadAcquire(&queue->write_pos); + rmtU32 r = LoadAcquire(&queue->read_pos); + if ((int)(w - r) > ((int)(s - write_size))) + return NULL; + + // Point to the newly allocated space + msg = (Message*)(queue->data->ptr + (w & (s - 1))); + + // Increment the write position, leaving the loop if this is the thread that succeeded + if (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->threadProfiler = thread_profiler; + break; + } + } + + return msg; +} + +static void rmtMessageQueue_CommitMessage(Message* message, MessageID id) +{ + assert(message != NULL); + + // Setting the message ID signals to the consumer that the message is ready + assert(LoadAcquire((rmtU32*)&message->id) == MsgID_NotReady); + StoreRelease((rmtU32*)&message->id, id); +} + +Message* rmtMessageQueue_PeekNextMessage(rmtMessageQueue* queue) +{ + Message* ptr; + rmtU32 r, w; + MessageID id; + + assert(queue != NULL); + + // First check that there are bytes queued + w = LoadAcquire(&queue->write_pos); + r = queue->read_pos; + if (w - r == 0) + return NULL; + + // Messages are in the queue but may not have been commit yet + // Messages behind this one may have been commit but it's not reachable until + // the next one in the queue is ready. + r = r & (queue->size - 1); + ptr = (Message*)(queue->data->ptr + r); + id = (MessageID)LoadAcquire((rmtU32*)&ptr->id); + if (id != MsgID_NotReady) + return ptr; + + return NULL; +} + +static void rmtMessageQueue_ConsumeNextMessage(rmtMessageQueue* queue, Message* message) +{ + rmtU32 message_size, read_pos; + + assert(queue != NULL); + assert(message != NULL); + + // Setting the message ID to "not ready" serves as a marker to the consumer that even though + // space has been allocated for a message, the message isn't ready to be consumed + // yet. + // + // We can't do that when allocating the message because multiple threads will be fighting for + // the same location. Instead, clear out any messages just read by the consumer before advancing + // the read position so that a winning thread's allocation will inherit the "not ready" state. + // + // This costs some write bandwidth and has the potential to flush cache to other cores. + message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + memset(message, MsgID_NotReady, message_size); + + // Advance read position + read_pos = queue->read_pos + message_size; + StoreRelease(&queue->read_pos, read_pos); +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @NETWORK: Network Server +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef rmtError (*Server_ReceiveHandler)(void*, char*, rmtU32); + +typedef struct +{ + WebSocket* listen_socket; + + WebSocket* client_socket; + + rmtU32 last_ping_time; + + rmtU16 port; + + rmtBool reuse_open_port; + rmtBool limit_connections_to_localhost; + + // A dynamically-sized buffer used for binary-encoding messages and sending to the client + Buffer* bin_buf; + + // Handler for receiving messages from the client + Server_ReceiveHandler receive_handler; + void* receive_handler_context; +} Server; + +static rmtError Server_CreateListenSocket(Server* server, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + rmtError error = RMT_ERROR_NONE; + + New_1(WebSocket, server->listen_socket, NULL); + if (error == RMT_ERROR_NONE) + error = WebSocket_RunServer(server->listen_socket, port, reuse_open_port, limit_connections_to_localhost, + WEBSOCKET_BINARY); + + return error; +} + +static rmtError Server_Constructor(Server* server, rmtU16 port, rmtBool reuse_open_port, + rmtBool limit_connections_to_localhost) +{ + rmtError error; + + assert(server != NULL); + server->listen_socket = NULL; + server->client_socket = NULL; + server->last_ping_time = 0; + server->port = port; + server->reuse_open_port = reuse_open_port; + server->limit_connections_to_localhost = limit_connections_to_localhost; + server->bin_buf = NULL; + server->receive_handler = NULL; + server->receive_handler_context = NULL; + + // Create the binary serialisation buffer + New_1(Buffer, server->bin_buf, 4096); + if (error != RMT_ERROR_NONE) + return error; + + // Create the listening WebSocket + return Server_CreateListenSocket(server, port, reuse_open_port, limit_connections_to_localhost); +} + +static void Server_Destructor(Server* server) +{ + assert(server != NULL); + Delete(WebSocket, server->client_socket); + Delete(WebSocket, server->listen_socket); + Delete(Buffer, server->bin_buf); +} + +static rmtBool Server_IsClientConnected(Server* server) +{ + assert(server != NULL); + return server->client_socket != NULL ? RMT_TRUE : RMT_FALSE; +} + +static void Server_DisconnectClient(Server* server) +{ + WebSocket* client_socket; + + assert(server != NULL); + + // NULL the variable before destroying the socket + client_socket = server->client_socket; + server->client_socket = NULL; + CompilerWriteFence(); + 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; + + // Dispatch to handler + if (server->receive_handler) + error = server->receive_handler(server->receive_handler_context, message_data, message_length); + + return error; +} + +static void Server_Update(Server* server) +{ + rmtU32 cur_time; + + assert(server != NULL); + + // Recreate the listening socket if it's been destroyed earlier + if (server->listen_socket == NULL) + Server_CreateListenSocket(server, server->port, server->reuse_open_port, + server->limit_connections_to_localhost); + + if (server->listen_socket != NULL && server->client_socket == NULL) + { + // Accept connections as long as there is no client connected + WebSocket* client_socket = NULL; + rmtError error = WebSocket_AcceptConnection(server->listen_socket, &client_socket); + if (error == RMT_ERROR_NONE) + { + server->client_socket = client_socket; + } + else + { + // Destroy the listen socket on failure to accept + // It will get recreated in another update + Delete(WebSocket, server->listen_socket); + } + } + + else + { + // Loop checking for incoming messages + for (;;) + { + // Inspect first byte to see if a message is there + char message_first_byte; + rmtU32 message_length; + rmtError error = WebSocket_Receive(server->client_socket, &message_first_byte, &message_length, 1, 0); + if (error == RMT_ERROR_NONE) + { + // Parse remaining message + error = Server_ReceiveMessage(server, message_first_byte, message_length); + if (error != RMT_ERROR_NONE) + { + Server_DisconnectClient(server); + break; + } + + // Check for more... + continue; + } + + // Passable errors... + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA) + { + // No data available + break; + } + + if (error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + // Data not available yet, can afford to ignore as we're only reading the first byte + break; + } + + // Anything else is an error that may have closed the connection + Server_DisconnectClient(server); + break; + } + } + + // Send pings to the client every second + cur_time = msTimer_Get(); + if (cur_time - server->last_ping_time > 1000) + { + Buffer* bin_buf = server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + Buffer_WriteStringZ(bin_buf, "PING"); + Server_Send(server, bin_buf->data, bin_buf->bytes_used, 10); + server->last_ping_time = cur_time; + } +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAMPLE: Base Sample Description for CPU by default +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#define SAMPLE_NAME_LEN 128 + +typedef enum SampleType +{ + SampleType_CPU, + SampleType_CUDA, + SampleType_D3D11, + SampleType_OpenGL, + SampleType_Metal, + SampleType_Count, +} SampleType; + +typedef struct Sample +{ + // Inherit so that samples can be quickly allocated + ObjectLink Link; + + enum SampleType type; + + // Hash generated from sample 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; + + // Sample end points and length in microseconds + rmtU64 us_start; + rmtU64 us_end; + rmtU64 us_length; + + // Total sampled length of all children + rmtU64 us_sampled_length; + + // Number of times this sample was used in a call in aggregate mode, 1 otherwise + rmtU32 call_count; + + // Current and maximum sample recursion depths + rmtU16 recurse_depth; + rmtU16 max_recurse_depth; + +} Sample; + +static rmtError Sample_Constructor(Sample* sample) +{ + assert(sample != NULL); + + ObjectLink_Constructor((ObjectLink*)sample); + + sample->type = SampleType_CPU; + 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; + sample->us_length = 0; + sample->us_sampled_length = 0; + sample->call_count = 0; + sample->recurse_depth = 0; + sample->max_recurse_depth = 0; + + return RMT_ERROR_NONE; +} + +static void Sample_Destructor(Sample* sample) +{ + RMT_UNREFERENCED_PARAMETER(sample); +} + +static void Sample_Prepare(Sample* sample, rmtU32 name_hash, Sample* parent) +{ + sample->name_hash = name_hash; + sample->unique_id = 0; + sample->parent = parent; + sample->first_child = NULL; + sample->last_child = NULL; + sample->next_sibling = NULL; + sample->nb_children = 0; + sample->us_start = 0; + sample->us_end = 0; + sample->us_length = 0; + sample->us_sampled_length = 0; + sample->call_count = 1; + sample->recurse_depth = 0; + sample->max_recurse_depth = 0; +} + +static void Sample_Close(Sample* sample, rmtU64 us_end) +{ + // Aggregate samples use us_end to store start so that us_start is preserved + rmtU64 us_length = 0; + if (sample->call_count > 1 && sample->max_recurse_depth == 0) + { + us_length = (us_end - sample->us_end); + } + else + { + us_length = (us_end - sample->us_start); + } + + sample->us_length += us_length; + + // Sum length on the parent to track un-sampled time in the parent + if (sample->parent != NULL) + { + sample->parent->us_sampled_length += us_length; + } +} + +static void Sample_CopyState(Sample* dst_sample, const Sample* src_sample) +{ + // Copy fields that don't override destination allocator links or transfer source sample tree positioning + // Also ignoring unique_id_html_colour as that's calculated in the Remotery thread + dst_sample->type = src_sample->type; + dst_sample->name_hash = src_sample->name_hash; + dst_sample->unique_id = src_sample->unique_id; + dst_sample->nb_children = src_sample->nb_children; + dst_sample->us_start = src_sample->us_start; + dst_sample->us_end = src_sample->us_end; + dst_sample->us_length = src_sample->us_length; + dst_sample->us_sampled_length = src_sample->us_sampled_length; + dst_sample->call_count = src_sample->call_count; + dst_sample->recurse_depth = src_sample->recurse_depth; + dst_sample->max_recurse_depth = src_sample->max_recurse_depth; + + // Prepare empty tree links + dst_sample->parent = NULL; + dst_sample->first_child = NULL; + dst_sample->last_child = NULL; + dst_sample->next_sibling = NULL; +} + +#define BIN_ERROR_CHECK(stmt) \ + { \ + error = stmt; \ + if (error != RMT_ERROR_NONE) \ + return error; \ + } + +static rmtError bin_SampleArray(Buffer* buffer, Sample* parent_sample); + +static rmtError bin_Sample(Buffer* buffer, Sample* sample) +{ + rmtError error; + + assert(sample != NULL); + + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, sample->name_hash)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, sample->unique_id)); + BIN_ERROR_CHECK(Buffer_Write(buffer, sample->unique_id_html_colour, 7)); + BIN_ERROR_CHECK(Buffer_WriteU64(buffer, sample->us_start)); + BIN_ERROR_CHECK(Buffer_WriteU64(buffer, sample->us_length)); + BIN_ERROR_CHECK(Buffer_WriteU64(buffer, maxS64(sample->us_length - sample->us_sampled_length, 0))); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, sample->call_count)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, sample->max_recurse_depth)); + BIN_ERROR_CHECK(bin_SampleArray(buffer, sample)); + + return RMT_ERROR_NONE; +} + +static rmtError bin_SampleArray(Buffer* buffer, Sample* parent_sample) +{ + rmtError error; + Sample* sample; + + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, parent_sample->nb_children)); + for (sample = parent_sample->first_child; sample != NULL; sample = sample->next_sibling) + BIN_ERROR_CHECK(bin_Sample(buffer, sample)); + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SAMPLETREE: A tree of samples with their allocator +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct SampleTree +{ + // Allocator for all samples + ObjectAllocator* allocator; + + // Root sample for all samples created by this thread + Sample* root; + + // Most recently pushed sample + Sample* currentParent; + + // Last time this sample tree was completed and sent to listeners, for stall detection + rmtU32 msLastTreeSendTime; + + // Lightweight flag, changed with release/acquire semantics to inform the stall detector the state of the tree is unreliable + rmtU32 treeBeingModified; + +} SampleTree; + +// Notify tree watchers that its structure is in the process of being changed +#define ModifySampleTree(tree, statements) \ + StoreRelease(&tree->treeBeingModified, 1); \ + statements; \ + StoreRelease(&tree->treeBeingModified, 0); + +static rmtError SampleTree_Constructor(SampleTree* tree, rmtU32 sample_size, ObjConstructor constructor, + ObjDestructor destructor) +{ + rmtError error; + + assert(tree != NULL); + + tree->allocator = NULL; + tree->root = NULL; + tree->currentParent = NULL; + StoreRelease(&tree->msLastTreeSendTime, 0); + StoreRelease(&tree->treeBeingModified, 0); + + // 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, 0, NULL); + tree->currentParent = tree->root; + + return RMT_ERROR_NONE; +} + +static void SampleTree_Destructor(SampleTree* tree) +{ + assert(tree != NULL); + + if (tree->root != NULL) + { + ObjectAllocator_Free(tree->allocator, tree->root); + tree->root = NULL; + } + + 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, rmtU32 name_hash, rmtU32 flags, 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->currentParent != NULL); + parent = tree->currentParent; + + // Assume no flags is the common case and predicate branch checks + if (flags != 0) + { + // Check root status + if ((flags & RMTSF_Root) != 0) + { + assert(parent->parent == NULL); + } + + if ((flags & RMTSF_Aggregate) != 0) + { + // Linear search for previous instance of this sample name + Sample* sibling; + for (sibling = parent->first_child; sibling != NULL; sibling = sibling->next_sibling) + { + if (sibling->name_hash == name_hash) + { + tree->currentParent = sibling; + sibling->call_count++; + *sample = sibling; + return RMT_ERROR_NONE; + } + } + } + + // Collapse sample on recursion + if ((flags & RMTSF_Recursive) != 0 && parent->name_hash == name_hash) + { + parent->recurse_depth++; + parent->max_recurse_depth = maxU16(parent->max_recurse_depth, parent->recurse_depth); + parent->call_count++; + *sample = parent; + return RMT_ERROR_RECURSIVE_SAMPLE; + } + } + + // Allocate a new sample + error = ObjectAllocator_Alloc(tree->allocator, (void**)sample); + if (error != RMT_ERROR_NONE) + { + return error; + } + Sample_Prepare(*sample, name_hash, parent); + + // Generate a unique ID for this sample in the tree + unique_id = parent->unique_id; + unique_id = HashCombine(unique_id, (*sample)->name_hash); + unique_id = HashCombine(unique_id, parent->nb_children); + (*sample)->unique_id = unique_id; + + // Add sample to its parent + parent->nb_children++; + if (parent->first_child == NULL) + { + parent->first_child = *sample; + parent->last_child = *sample; + } + else + { + assert(parent->last_child != NULL); + parent->last_child->next_sibling = *sample; + parent->last_child = *sample; + } + + // Make this sample the new parent of any newly created samples + tree->currentParent = *sample; + + return RMT_ERROR_NONE; +} + +static void SampleTree_Pop(SampleTree* tree, Sample* sample) +{ + assert(tree != NULL); + assert(sample != NULL); + assert(sample != tree->root); + tree->currentParent = sample->parent; +} + +static ObjectLink* FlattenSamples(Sample* sample, rmtU32* nb_samples) +{ + Sample* child; + ObjectLink* cur_link = &sample->Link; + + assert(sample != NULL); + assert(nb_samples != NULL); + + *nb_samples += 1; + sample->Link.next = (ObjectLink*)sample->first_child; + + // Link all children together + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + ObjectLink* last_link = FlattenSamples(child, nb_samples); + last_link->next = (ObjectLink*)child->next_sibling; + cur_link = last_link; + } + + // Clear child info + sample->first_child = NULL; + sample->last_child = NULL; + sample->nb_children = 0; + + return cur_link; +} + +static void FreeSamples(Sample* sample, ObjectAllocator* allocator) +{ + // Chain all samples together in a flat list + rmtU32 nb_cleared_samples = 0; + ObjectLink* last_link = FlattenSamples(sample, &nb_cleared_samples); + + // Release the complete sample memory range + if (sample->Link.next != NULL) + { + ObjectAllocator_FreeRange(allocator, sample, last_link, nb_cleared_samples); + } + else + { + ObjectAllocator_Free(allocator, sample); + } +} + +static rmtError SampleTree_CopySample(Sample** out_dst_sample, Sample* dst_parent_sample, ObjectAllocator* allocator, const Sample* src_sample) +{ + Sample* src_child; + + // Allocate a copy of the sample + Sample* dst_sample; + rmtError error = ObjectAllocator_Alloc(allocator, (void**)&dst_sample); + if (error != RMT_ERROR_NONE) + { + return error; + } + Sample_CopyState(dst_sample, src_sample); + + // Link the newly created/copied sample to its parent + // Note that metrics including nb_children have already been copied by the Sample_CopyState call + if (dst_parent_sample != NULL) + { + if (dst_parent_sample->first_child == NULL) + { + dst_parent_sample->first_child = dst_sample; + dst_parent_sample->last_child = dst_sample; + } + else + { + assert(dst_parent_sample->last_child != NULL); + dst_parent_sample->last_child->next_sibling = dst_sample; + dst_parent_sample->last_child = dst_sample; + } + } + + // Copy all children + for (src_child = src_sample->first_child; src_child != NULL; src_child = src_child->next_sibling) + { + Sample* dst_child; + error = SampleTree_CopySample(&dst_child, dst_sample, allocator, src_child); + if (error != RMT_ERROR_NONE) + { + return error; + } + } + + *out_dst_sample = dst_sample; + + return RMT_ERROR_NONE; +} + +static rmtError SampleTree_Copy(SampleTree* dst_tree, const SampleTree* src_tree) +{ + rmtError error; + + // Sample trees are allocated at startup and their allocators are persistent for the lifetime of the Remotery object. + // It's safe to reference the allocator and use it for sample lifetime. + ObjectAllocator* allocator = src_tree->allocator; + dst_tree->allocator = allocator; + + // Copy from the root + error = SampleTree_CopySample(&dst_tree->root, NULL, allocator, src_tree->root); + if (error != RMT_ERROR_NONE) + { + return error; + } + dst_tree->currentParent = dst_tree->root; + + return RMT_ERROR_NONE; +} + +typedef struct Msg_SampleTree +{ + Sample* rootSample; + + ObjectAllocator* allocator; + + rmtPStr threadName; + + rmtBool partialTree; +} Msg_SampleTree; + +static void QueueSampleTree(rmtMessageQueue* queue, Sample* sample, ObjectAllocator* allocator, rmtPStr thread_name, + struct ThreadProfiler* thread_profiler, rmtBool partial_tree) +{ + Msg_SampleTree* payload; + + // Attempt to allocate a message for sending the tree to the viewer + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_SampleTree), thread_profiler); + if (message == NULL) + { + // Discard tree samples on failure + FreeSamples(sample, allocator); + return; + } + + // Populate and commit + payload = (Msg_SampleTree*)message->payload; + payload->rootSample = sample; + payload->allocator = allocator; + payload->threadName = thread_name; + payload->partialTree = partial_tree; + rmtMessageQueue_CommitMessage(message, MsgID_SampleTree); +} + +typedef struct Msg_AddToStringTable +{ + rmtU32 hash; + rmtU32 length; +} Msg_AddToStringTable; + +static rmtBool QueueAddToStringTable(rmtMessageQueue* queue, rmtU32 hash, const char* string, size_t length, struct ThreadProfiler* thread_profiler) +{ + Msg_AddToStringTable* payload; + + // Attempt to allocate a message om the queue + size_t nb_string_bytes = length + 1; + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_AddToStringTable) + nb_string_bytes, thread_profiler); + if (message == NULL) + { + return RMT_FALSE; + } + + // Populate and commit + payload = (Msg_AddToStringTable*)message->payload; + payload->hash = hash; + payload->length = length; + memcpy(payload + 1, string, nb_string_bytes); + rmtMessageQueue_CommitMessage(message, MsgID_AddToStringTable); + + return RMT_TRUE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TPROFILER: Thread Profiler data, storing both sampling and instrumentation results +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_D3D11 +typedef struct D3D11 D3D11; +static rmtError D3D11_Create(D3D11** d3d11); +static void D3D11_Destructor(D3D11* d3d11); +#endif + +typedef struct ThreadProfiler +{ + // Storage for backing up initial register values when modifying a thread's context + rmtU64 registerBackup0; // 0 + rmtU64 registerBackup1; // 8 + rmtU64 registerBackup2; // 16 + + // Used to schedule callbacks taking into account some threads may be sleeping + rmtS32 nbSamplesWithoutCallback; // 24 + + // Index of the processor the thread was last seen running on + rmtU32 processorIndex; // 28 + rmtU32 lastProcessorIndex; + + // OS thread ID/handle + rmtThreadId threadId; + rmtThreadHandle threadHandle; + + // Thread name stored for sending to the viewer + char threadName[64]; + rmtU32 threadNameHash; + + // Store a unique sample tree for each type + SampleTree* sampleTrees[SampleType_Count]; + +#if RMT_USE_D3D11 + D3D11* d3d11; +#endif +} ThreadProfiler; + +static rmtError QueueThreadName(rmtMessageQueue* queue, const char* name, ThreadProfiler* thread_profiler) +{ + Message* message; + rmtU32 name_length; + + assert(queue != NULL); + + // Allocate some space for the message + name_length = strnlen_s(name, 64); + message = rmtMessageQueue_AllocMessage(queue, sizeof(rmtU32) * 2 + name_length, thread_profiler); + if (message == NULL) + { + return RMT_ERROR_UNKNOWN; + } + + // Copy and commit + U32ToByteArray(message->payload, MurmurHash3_x86_32(name, name_length, 0)); + U32ToByteArray(message->payload + sizeof(rmtU32), name_length); + memcpy(message->payload + sizeof(rmtU32) * 2, name, name_length); + rmtMessageQueue_CommitMessage(message, MsgID_ThreadName); + + return RMT_ERROR_NONE; +} + +static rmtError ThreadProfiler_Constructor(rmtMessageQueue* mq_to_rmt, ThreadProfiler* thread_profiler, rmtThreadId thread_id) +{ + rmtU32 name_length; + rmtError error; + + // Set defaults + thread_profiler->nbSamplesWithoutCallback = 0; + thread_profiler->processorIndex = (rmtU32)-1; + thread_profiler->lastProcessorIndex = (rmtU32)-1; + thread_profiler->threadId = thread_id; + memset(thread_profiler->sampleTrees, 0, sizeof(thread_profiler->sampleTrees)); + +#if RMT_USE_D3D11 + thread_profiler->d3d11 = NULL; +#endif + + // Pre-open the thread handle + error = rmtOpenThreadHandle(thread_id, &thread_profiler->threadHandle); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Name the thread and send a thread name notification immediately + // Users can override this at a later point with the Remotery thread name API + rmtGetThreadName(thread_id, thread_profiler->threadHandle, thread_profiler->threadName, sizeof(thread_profiler->threadName)); + name_length = strnlen_s(thread_profiler->threadName, 64); + thread_profiler->threadNameHash = MurmurHash3_x86_32(thread_profiler->threadName, name_length, 0); + QueueThreadName(mq_to_rmt, thread_profiler->threadName, thread_profiler); + + // Create the CPU sample tree only. The rest are created on-demand as they need extra context to function correctly. + New_3(SampleTree, thread_profiler->sampleTrees[SampleType_CPU], sizeof(Sample), (ObjConstructor)Sample_Constructor, + (ObjDestructor)Sample_Destructor); + if (error != RMT_ERROR_NONE) + { + return error; + } + +#if RMT_USE_D3D11 + error = D3D11_Create(&thread_profiler->d3d11); + if (error != RMT_ERROR_NONE) + { + return error; + } +#endif + + return RMT_ERROR_NONE; +} + +static void ThreadProfiler_Destructor(ThreadProfiler* thread_profiler) +{ + rmtU32 index; + +#if RMT_USE_D3D11 + Delete(D3D11, thread_profiler->d3d11); +#endif + + for (index = 0; index < SampleType_Count; index++) + { + Delete(SampleTree, thread_profiler->sampleTrees[index]); + } + + rmtCloseThreadHandle(thread_profiler->threadHandle); +} + +static rmtError ThreadProfiler_Push(SampleTree* tree, rmtU32 name_hash, rmtU32 flags, Sample** sample) +{ + rmtError error; + ModifySampleTree(tree, + error = SampleTree_Push(tree, name_hash, flags, sample); + ); + return error; +} + +static rmtBool ThreadProfiler_Pop(ThreadProfiler* thread_profiler, rmtMessageQueue* queue, Sample* sample) +{ + SampleTree* tree = thread_profiler->sampleTrees[sample->type]; + SampleTree_Pop(tree, sample); + + // Are we back at the root? + if (tree->currentParent == tree->root) + { + Sample* root; + + // Disconnect all samples from the root and pack in the chosen message queue + ModifySampleTree(tree, + root = tree->root; + root->first_child = NULL; + root->last_child = NULL; + root->nb_children = 0; + ); + QueueSampleTree(queue, sample, tree->allocator, thread_profiler->threadName, thread_profiler, RMT_FALSE); + + // Update the last send time for this tree, for stall detection + StoreRelease(&tree->msLastTreeSendTime, (rmtU32)(sample->us_end / 1000)); + + return RMT_TRUE; + } + + return RMT_FALSE; +} + +static rmtU32 ThreadProfiler_GetNameHash(ThreadProfiler* thread_profiler, rmtMessageQueue* queue, rmtPStr name, rmtU32* hash_cache) +{ + size_t name_len; + rmtU32 name_hash; + + // Hash cache provided? + if (hash_cache != NULL) + { + // Calculate the hash first time round only + name_hash = *hash_cache; + if (name_hash == 0) + { + assert(name != NULL); + name_len = strnlen_s(name, 256); + name_hash = MurmurHash3_x86_32(name, name_len, 0); + + // Queue the string for the string table and only cache the hash if it succeeds + if (QueueAddToStringTable(queue, name_hash, name, name_len, thread_profiler) == RMT_TRUE) + { + *hash_cache = name_hash; + } + } + + return name_hash; + } + + // Have to recalculate and speculatively insert the name every time when no cache storage exists + name_len = strnlen_s(name, 256); + name_hash = MurmurHash3_x86_32(name, name_len, 0); + QueueAddToStringTable(queue, name_hash, name, name_len, thread_profiler); + return name_hash; +} + +typedef struct ThreadProfilers +{ + // Timer shared with Remotery threads + usTimer* timer; + + // Queue between clients and main remotery thread + rmtMessageQueue* mqToRmtThread; + + // On x64 machines this points to the sample function + void* compiledSampleFn; + rmtU32 compiledSampleFnSize; + + // Used to store thread profilers bound to an OS thread + rmtTLS threadProfilerTlsHandle; + + // Array of preallocated ThreadProfiler objects + // Read iteration is safe given that no incomplete ThreadProfiler objects will be encountered during iteration. + // The ThreadProfiler count is only incremented once a new ThreadProfiler is fully defined and ready to be used. + // Do not use this list to verify if a ThreadProfiler exists for a given thread. Use the mutex-guarded Get functions instead. + ThreadProfiler threadProfilers[256]; + rmtU32 nbThreadProfilers; + rmtU32 maxNbThreadProfilers; + + // Guards creation and existence-testing of the ThreadProfiler list + rmtMutex threadProfilerMutex; + + // Periodic thread sampling thread + rmtThread* threadSampleThread; + + // Periodic thread to processor gatherer + rmtThread* threadGatherThread; +} ThreadProfilers; + +static rmtError SampleThreadsLoop(rmtThread* rmt_thread); + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT +static void* CreateSampleCallback(rmtU32* out_size); +#endif +#endif + +static rmtError ThreadProfilers_Constructor(ThreadProfilers* thread_profilers, usTimer* timer, rmtMessageQueue* mq_to_rmt_thread) +{ + rmtError error; + + // Set to default + thread_profilers->timer = timer; + thread_profilers->mqToRmtThread = mq_to_rmt_thread; + thread_profilers->compiledSampleFn = NULL; + thread_profilers->compiledSampleFnSize = 0; + thread_profilers->threadProfilerTlsHandle = TLS_INVALID_HANDLE; + thread_profilers->nbThreadProfilers = 0; + thread_profilers->maxNbThreadProfilers = sizeof(thread_profilers->threadProfilers) / sizeof(thread_profilers->threadProfilers[0]); + mtxInit(&thread_profilers->threadProfilerMutex); + thread_profilers->threadSampleThread = NULL; + thread_profilers->threadGatherThread = NULL; + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT + thread_profilers->compiledSampleFn = CreateSampleCallback(&thread_profilers->compiledSampleFnSize); + if (thread_profilers->compiledSampleFn == NULL) + { + return RMT_ERROR_MALLOC_FAIL; + } +#endif +#endif + + // Allocate a TLS handle for the thread profilers + error = tlsAlloc(&thread_profilers->threadProfilerTlsHandle); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Kick-off the thread sampler + if (g_Settings.enableThreadSampler == RMT_TRUE) + { + New_2(rmtThread, thread_profilers->threadSampleThread, SampleThreadsLoop, thread_profilers); + if (error != RMT_ERROR_NONE) + { + return error; + } + } + + return RMT_ERROR_NONE; +} + +static void ThreadProfilers_Destructor(ThreadProfilers* thread_profilers) +{ + rmtU32 thread_index; + + Delete(rmtThread, thread_profilers->threadGatherThread); + Delete(rmtThread, thread_profilers->threadSampleThread); + + // Delete all profilers + for (thread_index = 0; thread_index < thread_profilers->nbThreadProfilers; thread_index++) + { + ThreadProfiler* thread_profiler = thread_profilers->threadProfilers + thread_index; + ThreadProfiler_Destructor(thread_profiler); + } + + if (thread_profilers->threadProfilerTlsHandle != TLS_INVALID_HANDLE) + { + tlsFree(thread_profilers->threadProfilerTlsHandle); + } + +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_64BIT + if (thread_profilers->compiledSampleFn != NULL) + { + VirtualFree(thread_profilers->compiledSampleFn, 0, MEM_RELEASE); + } +#endif +#endif + + mtxDelete(&thread_profilers->threadProfilerMutex); +} + +static rmtError ThreadProfilers_GetThreadProfiler(ThreadProfilers* thread_profilers, rmtThreadId thread_id, ThreadProfiler** out_thread_profiler) +{ + rmtU32 profiler_index; + ThreadProfiler* thread_profiler; + rmtError error; + + mtxLock(&thread_profilers->threadProfilerMutex); + + // Linear search for a matching thread id + for (profiler_index = 0; profiler_index < thread_profilers->nbThreadProfilers; profiler_index++) + { + thread_profiler = thread_profilers->threadProfilers + profiler_index; + if (thread_profiler->threadId == thread_id) + { + *out_thread_profiler = thread_profiler; + mtxUnlock(&thread_profilers->threadProfilerMutex); + return RMT_ERROR_NONE; + } + } + + // Thread info not found so create a new one at the end + thread_profiler = thread_profilers->threadProfilers + thread_profilers->nbThreadProfilers; + error = ThreadProfiler_Constructor(thread_profilers->mqToRmtThread, thread_profiler, thread_id); + if (error != RMT_ERROR_NONE) + { + ThreadProfiler_Destructor(thread_profiler); + mtxUnlock(&thread_profilers->threadProfilerMutex); + return error; + } + *out_thread_profiler = thread_profiler; + + // Increment count for consume by read iterators + // Within the mutex so that there are no race conditions creating thread profilers + // Using release semantics to ensure a memory barrier for read iterators + StoreRelease(&thread_profilers->nbThreadProfilers, thread_profilers->nbThreadProfilers + 1); + + mtxUnlock(&thread_profilers->threadProfilerMutex); + + return RMT_ERROR_NONE; +} + +static rmtError ThreadProfilers_GetCurrentThreadProfiler(ThreadProfilers* thread_profilers, ThreadProfiler** out_thread_profiler) +{ + // Is there a thread profiler associated with this thread yet? + *out_thread_profiler = (ThreadProfiler*)tlsGet(thread_profilers->threadProfilerTlsHandle); + if (*out_thread_profiler == NULL) + { + // Allocate on-demand + rmtError error = ThreadProfilers_GetThreadProfiler(thread_profilers, rmtGetCurrentThreadId(), out_thread_profiler); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Bind to the curren thread + tlsSet(thread_profilers->threadProfilerTlsHandle, *out_thread_profiler); + } + + return RMT_ERROR_NONE; +} + +static rmtBool ThreadProfilers_ThreadInCallback(ThreadProfilers* thread_profilers, rmtCpuContext* context) +{ +#ifdef RMT_PLATFORM_WINDOWS +#ifdef RMT_ARCH_32BIT + if (context->Eip >= (DWORD)thread_profilers->compiledSampleFn && + context->Eip < (DWORD)((char*)thread_profilers->compiledSampleFn + thread_profilers->compiledSampleFnSize)) + { + return RMT_TRUE; + } +#else + if (context->Rip >= (DWORD64)thread_profilers->compiledSampleFn && + context->Rip < (DWORD64)((char*)thread_profilers->compiledSampleFn + thread_profilers->compiledSampleFnSize)) + { + return RMT_TRUE; + } +#endif +#endif + return RMT_FALSE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TGATHER: Thread Gatherer, periodically polling for newly created threads +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +static void GatherThreads(ThreadProfilers* thread_profilers) +{ + rmtThreadHandle handle; + + assert(thread_profilers != NULL); + +#ifdef RMT_PLATFORM_WINDOWS + + // Create the snapshot - this is a slow call + handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (handle != INVALID_HANDLE_VALUE) + { + BOOL success; + + THREADENTRY32 thread_entry; + thread_entry.dwSize = sizeof(thread_entry); + + // Loop through all threads owned by this process + success = Thread32First(handle, &thread_entry); + while (success == TRUE) + { + if (thread_entry.th32OwnerProcessID == GetCurrentProcessId()) + { + // Create thread profilers on-demand if there're not already there + ThreadProfiler* thread_profiler; + rmtError error = ThreadProfilers_GetThreadProfiler(thread_profilers, thread_entry.th32ThreadID, &thread_profiler); + if (error != RMT_ERROR_NONE) + { + // Not really worth bringing the whole profiler down here + rmt_LogText("REMOTERY ERROR: Failed to create Thread Profiler"); + } + } + + success = Thread32Next(handle, &thread_entry); + } + + CloseHandle(handle); + } + +#endif +} + +static rmtError GatherThreadsLoop(rmtThread* thread) +{ + ThreadProfilers* thread_profilers = (ThreadProfilers*)thread->param; + rmtU32 sleep_time = 100; + + assert(thread_profilers != NULL); + + rmt_SetCurrentThreadName("RemoteryGatherThreads"); + + while (thread->request_exit == RMT_FALSE) + { + // We want a long period of time between scanning for new threads as the process is a little expensive (~30ms here). + // However not too long so as to miss potentially detailed process startup data. + // Use reduced sleep time at startup to catch as many early thread creations as possible. + // TODO(don): We could get processes to register themselves to ensure no startup data is lost but the scan must still + // be present, to catch threads in a process that the user doesn't create (e.g. graphics driver threads). + GatherThreads(thread_profilers); + msSleep(sleep_time); + sleep_time = minU32(sleep_time * 2, 2000); + } + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TSAMPLER: Sampling thread contexts +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +typedef struct Processor +{ + // Current thread profiler sampling this processor + ThreadProfiler* threadProfiler; + + rmtU32 sampleCount; + rmtU64 sampleTime; +} Processor; + +typedef struct Msg_ProcessorThreads +{ + // Running index of processor messages + rmtU64 messageIndex; + + // Processor array, leaking into the memory behind the struct + rmtU32 nbProcessors; + Processor processors[1]; +} Msg_ProcessorThreads; + +static void QueueProcessorThreads(rmtMessageQueue* queue, rmtU64 message_index, rmtU32 nb_processors, Processor* processors) +{ + Msg_ProcessorThreads* payload; + + // Attempt to allocate a message for sending processors to the viewer + rmtU32 array_size = (nb_processors - 1) * sizeof(Processor); + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_ProcessorThreads) + array_size, NULL); + if (message == NULL) + { + return; + } + + // Populate and commit + payload = (Msg_ProcessorThreads*)message->payload; + payload->messageIndex = message_index; + payload->nbProcessors = nb_processors; + memcpy(payload->processors, processors, nb_processors * sizeof(Processor)); + rmtMessageQueue_CommitMessage(message, MsgID_ProcessorThreads); +} + +#ifdef RMT_ARCH_32BIT +__declspec(naked) static void SampleCallback() +{ + // + // It's important to realise that this call can be pre-empted by the scheduler and shifted to another processor *while we are + // sampling which processor this thread is on*. + // + // This has two very important implications: + // + // * What we are sampling here is an *approximation* of the path of threads across processors. + // * These samples can't be used to "open" and "close" sample periods on a processor as it's highly likely you'll get many + // open events without a close, or vice versa. + // + // As such, we can only choose a sampling period and for each sample register which threads are on which processor. + // + // This is very different to hooking up the Event Tracing API (requiring Administrator elevation), which raises events for + // each context switch, directly from the kernel. + // + + __asm + { + // Push the EIP return address used by the final ret instruction + push ebx + + // We might be in the middle of something like a cmp/jmp instruction pair so preserve EFLAGS + // (Classic example which seems to pop up regularly is _RTC_CheckESP, with cmp/call/jne) + pushfd + + // Push all volatile registers as we don't know what the function calls below will destroy + push eax + push ecx + push edx + + // Retrieve and store the current processor index + call esi + mov [edi].processorIndex, eax + + // Mark as ready for scheduling another callback + // Intel x86 store release + mov [edi].nbSamplesWithoutCallback, 0 + + // Restore preserved register state + pop edx + pop ecx + pop eax + + // Restore registers used to provide parameters to the callback + mov ebx, dword ptr [edi].registerBackup0 + mov esi, dword ptr [edi].registerBackup1 + mov edi, dword ptr [edi].registerBackup2 + + // Restore EFLAGS + popfd + + // Pops the original EIP off the stack and jmps to origin suspend point in the thread + ret + } +} +#elif defined(RMT_ARCH_64BIT) +// Generated with https://defuse.ca/online-x86-assembler.htm +static rmtU8 SampleCallbackBytes[] = +{ + // Push the RIP return address used by the final ret instruction + 0x53, // push rbx + + // We might be in the middle of something like a cmp/jmp instruction pair so preserve RFLAGS + // (Classic example which seems to pop up regularly is _RTC_CheckESP, with cmp/call/jne) + 0x9C, // pushfq + + // Push all volatile registers as we don't know what the function calls below will destroy + 0x50, // push rax + 0x51, // push rcx + 0x52, // push rdx + 0x41, 0x50, // push r8 + 0x41, 0x51, // push r9 + 0x41, 0x52, // push r10 + 0x41, 0x53, // push r11 + + // Retrieve and store the current processor index + 0xFF, 0xD6, // call rsi + 0x89, 0x47, 0x1C, // mov dword ptr [rdi + 28], eax + + // Mark as ready for scheduling another callback + // Intel x64 store release + 0xC7, 0x47, 0x18, 0x00, 0x00, 0x00, 0x00, // mov dword ptr [rdi + 24], 0 + + // Restore preserved register state + 0x41, 0x5B, // pop r11 + 0x41, 0x5A, // pop r10 + 0x41, 0x59, // pop r9 + 0x41, 0x58, // pop r8 + 0x5A, // pop rdx + 0x59, // pop rcx + 0x58, // pop rax + + // Restore registers used to provide parameters to the callback + 0x48, 0x8B, 0x1F, // mov rbx, qword ptr [rdi + 0] + 0x48, 0x8B, 0x77, 0x08, // mov rsi, qword ptr [rdi + 8] + 0x48, 0x8B, 0x7F, 0x10, // mov rdi, qword ptr [rdi + 16] + + // Restore RFLAGS + 0x9D, // popfq + + // Pops the original EIP off the stack and jmps to origin suspend point in the thread + 0xC3 // ret +}; +#ifdef RMT_PLATFORM_WINDOWS +static void* CreateSampleCallback(rmtU32* out_size) +{ + // Allocate page for the generated code + DWORD size = 4096; + DWORD old_protect; + void* function = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (function == NULL) + { + return NULL; + } + + // Clear whole allocation to int 3h + memset(function, 0xCC, size); + + // Copy over the generated code + memcpy(function, SampleCallbackBytes, sizeof(SampleCallbackBytes)); + *out_size = sizeof(SampleCallbackBytes); + + // Enable execution + VirtualProtect(function, size, PAGE_EXECUTE_READ, &old_protect); + return function; +} +#endif +#endif + +#ifdef __cplusplus +static_assert(offsetof(ThreadProfiler, nbSamplesWithoutCallback) == 24, ""); +static_assert(offsetof(ThreadProfiler, processorIndex) == 28, ""); +#endif + +static void CloseOpenSamples(Sample* sample, rmtU64 sample_time_us, rmtU32 parents_are_last) +{ + Sample* child_sample; + + // Depth-first search into children as we want to close child samples before their parents + for (child_sample = sample->first_child; child_sample != NULL; child_sample = child_sample->next_sibling) + { + rmtU32 is_last = parents_are_last & (child_sample == sample->last_child ? 1 : 0); + CloseOpenSamples(child_sample, sample_time_us, is_last); + } + + // A chain of open samples will be linked from the root to the deepest, currently open sample + if (parents_are_last > 0) + { + Sample_Close(sample, sample_time_us); + } +} + +static rmtError CheckForStallingSamples(SampleTree* stalling_sample_tree, ThreadProfiler* thread_profiler, rmtU64 sample_time_us) +{ + SampleTree* sample_tree; + rmtU32 sample_time_s = (rmtU32)(sample_time_us / 1000); + + // Initialise to empty + stalling_sample_tree->root = NULL; + stalling_sample_tree->allocator = NULL; + + // Skip the stall check if the tree is being modified + sample_tree = thread_profiler->sampleTrees[SampleType_CPU]; + if (LoadAcquire(&sample_tree->treeBeingModified) != 0) + { + return RMT_ERROR_NONE; + } + + if (sample_tree != NULL) + { + // The root is a dummy root inserted on tree creation so check that for children + rmtBool send = RMT_FALSE; + Sample* root_sample = sample_tree->root; + if (root_sample != NULL && root_sample->nb_children > 0) + { + if (sample_time_s - LoadAcquire(&sample_tree->msLastTreeSendTime) > 1000) + { + send = RMT_TRUE; + StoreRelease(&sample_tree->msLastTreeSendTime, sample_time_s); + } + } + + if (send == RMT_TRUE) + { + // Make a local copy of the tree as we want to keep the current tree for active profiling + rmtError error = SampleTree_Copy(stalling_sample_tree, sample_tree); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Close all samples from the deepest open sample, right back to the root + CloseOpenSamples(stalling_sample_tree->root, sample_time_us, 1); + } + } + + return RMT_ERROR_NONE; +} + +static rmtError InitThreadSampling(ThreadProfilers* thread_profilers) +{ + rmtError error; + + rmt_SetCurrentThreadName("RemoterySampleThreads"); + + // Make an initial gather so that we have something to work with + GatherThreads(thread_profilers); + + // Kick-off the background thread that watches for new threads + New_2(rmtThread, thread_profilers->threadGatherThread, GatherThreadsLoop, thread_profilers); + if (error != RMT_ERROR_NONE) + { + return error; + } + +#ifdef RMT_PLATFORM_WINDOWS + // Ensure we can wake up every millisecond + if (timeBeginPeriod(1) != TIMERR_NOERROR) + { + return RMT_ERROR_UNKNOWN; + } +#endif + + // We're going to be shuffling thread visits to avoid the scheduler trying to predict a work-load based on sampling + // Use the global RNG with a random seed to start the shuffle + Well512_Init((rmtU32)time(NULL)); + + return RMT_ERROR_NONE; +} + +static rmtError SampleThreadsLoop(rmtThread* rmt_thread) +{ + rmtCpuContext context; + rmtU32 processor_message_index = 0; + rmtU32 nb_processors; + Processor* processors; + rmtU32 processor_index; + + ThreadProfilers* thread_profilers = (ThreadProfilers*)rmt_thread->param; + + rmtError error = InitThreadSampling(thread_profilers); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // If we can't figure out how many processors there are then we are running on an unsupported platform + nb_processors = rmtGetNbProcessors(); + if (nb_processors == 0) + { + return RMT_ERROR_UNKNOWN; + } + + // An array entry for each processor + processors = (Processor*)rmtMalloc(nb_processors * sizeof(Processor)); + for (processor_index = 0; processor_index < nb_processors; processor_index++) + { + processors[processor_index].threadProfiler = NULL; + processors[processor_index].sampleTime = 0; + } + + while (rmt_thread->request_exit == RMT_FALSE) + { + rmtU32 lfsr_seed; + rmtU32 lfsr_value; + + // Query how many threads the gather knows about this time round + rmtU32 nb_thread_profilers = LoadAcquire(&thread_profilers->nbThreadProfilers); + + // Calculate table size log2 required to fit count entries. Normally we would adjust the log2 input by -1 so that + // power-of-2 counts map to their exact bit offset and don't require a twice larger table. You can iterate indices + // 0 to (1<= nb_thread_profilers) + { + continue; + } + + // Ignore our own thread + thread_id = rmtGetCurrentThreadId(); + thread_profiler = thread_profilers->threadProfilers + thread_index; + if (thread_profiler->threadId == thread_id) + { + continue; + } + + // Suspend the thread so we can insert a callback + thread_handle = thread_profiler->threadHandle; + if (rmtSuspendThread(thread_handle) == RMT_FALSE) + { + continue; + } + + // Mark the processor this thread was last recorded as running on. + // Note that a thread might be pre-empted multiple times in-between sampling. Given a sampling rate equal to the + // scheduling quantum, this doesn't happen too often. However in such cases, whoever marks the processor last is + // the one that gets recorded. + sample_time_us = usTimer_Get(thread_profilers->timer); + sample_count = AtomicAdd(&thread_profiler->nbSamplesWithoutCallback, 1); + processor_index = thread_profiler->processorIndex; + if (processor_index != -1) + { + assert(processor_index < nb_processors); + processors[processor_index].threadProfiler = thread_profiler; + processors[processor_index].sampleCount = sample_count; + processors[processor_index].sampleTime = sample_time_us; + } + + // Swap in a new context with our callback if one is not already scheduled on this thread + if (sample_count == 0) + { + if (rmtGetUserModeThreadContext(thread_handle, &context) == RMT_TRUE && + // There is a slight window of opportunity, after which the callback sets nbSamplesWithoutCallback=0, + // for this loop to suspend a thread while it's executing the last instructions of the callback. + ThreadProfilers_ThreadInCallback(thread_profilers, &context) == RMT_FALSE) + { + #ifdef RMT_PLATFORM_WINDOWS + #ifdef RMT_ARCH_64BIT + thread_profiler->registerBackup0 = context.Rbx; + thread_profiler->registerBackup1 = context.Rsi; + thread_profiler->registerBackup2 = context.Rdi; + context.Rbx = context.Rip; + context.Rsi = (rmtU64)GetCurrentProcessorNumber; + context.Rdi = (rmtU64)thread_profiler; + context.Rip = (DWORD64)thread_profilers->compiledSampleFn; + #endif + #ifdef RMT_ARCH_32BIT + thread_profiler->registerBackup0 = context.Ebx; + thread_profiler->registerBackup1 = context.Esi; + thread_profiler->registerBackup2 = context.Edi; + context.Ebx = context.Eip; + context.Esi = (rmtU32)GetCurrentProcessorNumber; + context.Edi = (rmtU32)thread_profiler; + context.Eip = (DWORD)&SampleCallback; + #endif + #endif + + rmtSetThreadContext(thread_handle, &context); + } + else + { + AtomicAdd(&thread_profiler->nbSamplesWithoutCallback, -1); + } + } + + // While the thread is suspended take the chance to check for samples trees that may never complete + // Because SuspendThread on Windows is an async request, this needs to be placed at a point where the request completes + // Calling GetThreadContext will ensure the request is completed so this stall check is placed after that + CheckForStallingSamples(&stalling_sample_tree, thread_profiler, sample_time_us); + + rmtResumeThread(thread_handle); + + if (stalling_sample_tree.root != NULL) + { + // If there is stalling sample tree on this thread then send it to listeners. + // Do the send *outside* of all Suspend/Resume calls as we have no way of knowing who is reading/writing the queue + // Mark this as partial so that the listeners know it will be overwritten. + Sample* sample = stalling_sample_tree.root->first_child; + assert(sample != NULL); + QueueSampleTree(thread_profilers->mqToRmtThread, sample, stalling_sample_tree.allocator, thread_profiler->threadName, thread_profiler, RMT_TRUE); + } + + + } while (lfsr_value != lfsr_seed); + + // Filter all processor samples made in this pass + for (processor_index = 0; processor_index < nb_processors; processor_index++) + { + Processor* processor = processors + processor_index; + ThreadProfiler* thread_profiler = processor->threadProfiler; + + if (thread_profiler != NULL) + { + // If this thread was on another processor on a previous pass and that processor is still tracking that thread, + // remove the thread from it. + rmtU32 last_processor_index = thread_profiler->lastProcessorIndex; + if (last_processor_index != -1 && last_processor_index != processor_index) + { + assert(last_processor_index < nb_processors); + if (processors[last_processor_index].threadProfiler == thread_profiler) + { + processors[last_processor_index].threadProfiler = NULL; + } + } + + // When the thread is still on the same processor, check to see if it hasn't triggered the callback within another + // pass. This suggests the thread has gone to sleep and is no longer assigned to any thread. + else if (processor->sampleCount > 1) + { + processor->threadProfiler = NULL; + } + + thread_profiler->lastProcessorIndex = thread_profiler->processorIndex; + } + } + + // Send current processor state off to remotery + QueueProcessorThreads(thread_profilers->mqToRmtThread, processor_message_index++, nb_processors, processors); + } + +#ifdef RMT_PLATFORM_WINDOWS + timeEndPeriod(1); +#endif + + return RMT_ERROR_NONE; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @REMOTERY: Remotery +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_OPENGL +typedef struct OpenGL_t OpenGL; +static rmtError OpenGL_Create(OpenGL** opengl); +static void OpenGL_Destructor(OpenGL* opengl); +#endif + +#if RMT_USE_METAL +typedef struct Metal_t Metal; +static rmtError Metal_Create(Metal** metal); +static void Metal_Destructor(Metal* metal); +#endif + +struct Remotery +{ + Server* server; + + // Microsecond accuracy timer for CPU timestamps + usTimer timer; + + // Queue between clients and main remotery thread + rmtMessageQueue* mq_to_rmt_thread; + + // The main server thread + rmtThread* thread; + + // String table shared by all threads + StringTable* string_table; + + // Open logfile handle to append events to + FILE* logfile; + + // Set to trigger a map of each message on the remotery thread message queue + void (*map_message_queue_fn)(Remotery* rmt, Message*); + void* map_message_queue_data; + +#if RMT_USE_CUDA + rmtCUDABind cuda; +#endif + +#if RMT_USE_OPENGL + OpenGL* opengl; +#endif + +#if RMT_USE_METAL + Metal* metal; +#endif + + ThreadProfilers* threadProfilers; +}; + +// +// 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 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 = HashCombine(*digest_hash, sample->unique_id); + + { + rmtU8 shift = 4; + + // Get 6 nibbles for lower 3 bytes of the name hash + rmtU8* sample_id = (rmtU8*)&sample->name_hash; + 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) +{ + rmtError error = RMT_ERROR_NONE; + Buffer* bin_buf; + + // Build the buffer as if it's being sent to the server + assert(rmt != NULL); + assert(message != NULL); + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + Buffer_Write(bin_buf, message->payload, message->payload_size); + + // Pass to either the server or the log file + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 20); + } + if (rmt->logfile != NULL) + { + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return error; +} + +static rmtError bin_SampleName(Buffer* buffer, const char* name, rmtU32 name_hash, rmtU32 name_length) +{ + rmtError error; + + BIN_ERROR_CHECK(Buffer_Write(buffer, "SSMP", 4)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, name_hash)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, name_length)); + BIN_ERROR_CHECK(Buffer_Write(buffer, (void*)name, name_length)); + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_AddToStringTable(Remotery* rmt, Message* message) +{ + // Add to the string table + Msg_AddToStringTable* payload = (Msg_AddToStringTable*)message->payload; + const char* name = (const char*)(payload + 1); + StringTable_Insert(rmt->string_table, payload->hash, name); + + // Emit to log file if one is open + if (rmt->logfile != NULL) + { + rmtError error; + + Buffer* bin_buf = rmt->server->bin_buf; + bin_buf->bytes_used = 0; + BIN_ERROR_CHECK(bin_SampleName(bin_buf, name, payload->hash, payload->length)); + + rmtWriteFile(rmt->logfile, bin_buf->data, bin_buf->bytes_used); + } + + return RMT_ERROR_NONE; +} + +static rmtError bin_SampleTree(Buffer* buffer, Msg_SampleTree* msg) +{ + Sample* root_sample; + char thread_name[256]; + rmtU32 digest_hash = 0; + rmtU32 nb_samples = 0; + rmtError error; + + assert(buffer != NULL); + assert(msg != NULL); + + // Get the message root sample + root_sample = msg->rootSample; + assert(root_sample != NULL); + + // Add any sample types as a thread name post-fix to ensure they get their own viewer + thread_name[0] = 0; + strncat_s(thread_name, sizeof(thread_name), msg->threadName, strnlen_s(msg->threadName, 255)); + if (root_sample->type == 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); + } + if (root_sample->type == SampleType_Metal) + { + strncat_s(thread_name, sizeof(thread_name), " (Metal)", 8); + } + + // Get digest hash of samples so that viewer can efficiently rebuild its tables + GetSampleDigest(root_sample, &digest_hash, &nb_samples); + + // Write global message header + BIN_ERROR_CHECK(Buffer_Write(buffer, (void*)"SMPL ", 8)); + + // Write sample message header + BIN_ERROR_CHECK(Buffer_WriteStringWithLength(buffer, thread_name)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, nb_samples)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, digest_hash)); + BIN_ERROR_CHECK(Buffer_WriteU32(buffer, msg->partialTree ? 1 : 0)); + + // Write entire sample tree + BIN_ERROR_CHECK(bin_Sample(buffer, root_sample)); + + // Patch message size + U32ToByteArray(buffer->data + 4, buffer->bytes_used); + + return RMT_ERROR_NONE; +} + +#if RMT_USE_CUDA +static rmtBool AreCUDASamplesReady(Sample* sample); +static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample); +#endif + +static rmtError Remotery_SendSampleTreeMessage(Remotery* rmt, Message* message) +{ + rmtError error = RMT_ERROR_NONE; + + Msg_SampleTree* sample_tree; + Sample* sample; + Buffer* bin_buf; + + assert(rmt != NULL); + assert(message != NULL); + + // Get the message root sample + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample != NULL); + +#if RMT_USE_CUDA + if (sample->type == SampleType_CUDA) + { + // If these CUDA samples aren't ready yet, stick them to the back of the queue and continue + rmtBool are_samples_ready; + rmt_BeginCPUSample(AreCUDASamplesReady, 0); + are_samples_ready = AreCUDASamplesReady(sample); + rmt_EndCPUSample(); + if (!are_samples_ready) + { + QueueSampleTree(rmt->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, + message->threadProfiler, RMT_FALSE); + return RMT_ERROR_NONE; + } + + // Retrieve timing of all CUDA samples + rmt_BeginCPUSample(GetCUDASampleTimes, 0); + GetCUDASampleTimes(sample->parent, sample); + rmt_EndCPUSample(); + } +#endif + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the sample tree + rmt_BeginCPUSample(bin_SampleTree, RMTSF_Aggregate); + error = bin_SampleTree(bin_buf, sample_tree); + rmt_EndCPUSample(); + + // Release sample tree samples back to their allocator + FreeSamples(sample, sample_tree->allocator); + + if (error != RMT_ERROR_NONE) + { + return error; + } + + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + // Send to the viewer with a reasonably long timeout as the size of the sample data may be large + rmt_BeginCPUSample(Server_Send, RMTSF_Aggregate); + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 50000); + rmt_EndCPUSample(); + } + + if (rmt->logfile != NULL) + { + // Write the data after the websocket header + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return error; +} + +static rmtError Remotery_SendProcessorThreads(Remotery* rmt, Message* message) +{ + rmtU32 processor_index; + rmtError error = RMT_ERROR_NONE; + + Msg_ProcessorThreads* processor_threads = (Msg_ProcessorThreads*)message->payload; + + Buffer* bin_buf; + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the message + BIN_ERROR_CHECK(Buffer_Write(bin_buf, (void*)"PRTH", 4)); + BIN_ERROR_CHECK(Buffer_WriteU32(bin_buf, processor_threads->nbProcessors)); + BIN_ERROR_CHECK(Buffer_WriteU64(bin_buf, processor_threads->messageIndex)); + for (processor_index = 0; processor_index < processor_threads->nbProcessors; processor_index++) + { + Processor* processor = processor_threads->processors + processor_index; + if (processor->threadProfiler != NULL) + { + BIN_ERROR_CHECK(Buffer_WriteU32(bin_buf, processor->threadProfiler->threadId)); + BIN_ERROR_CHECK(Buffer_WriteU32(bin_buf, processor->threadProfiler->threadNameHash)); + BIN_ERROR_CHECK(Buffer_WriteU64(bin_buf, processor->sampleTime)); + } + else + { + BIN_ERROR_CHECK(Buffer_WriteU32(bin_buf, (rmtU32)-1)); + BIN_ERROR_CHECK(Buffer_WriteU32(bin_buf, 0)); + BIN_ERROR_CHECK(Buffer_WriteU64(bin_buf, 0)); + } + } + + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + // Send to the viewer + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 50); + } + + if (rmt->logfile != NULL) + { + // Write the data after the websocket header + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return error; +} + +static rmtError Remotery_SendThreadName(Remotery* rmt, Message* message) +{ + rmtU32 name_length; + rmtError error; + + Buffer* bin_buf; + + // Reset the buffer for sending a websocket message + bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + + // Serialise the message + BIN_ERROR_CHECK(Buffer_Write(bin_buf, (void*)"THRN", 4)); + BIN_ERROR_CHECK(Buffer_Write(bin_buf, message->payload, 8)); + name_length = *(rmtU32*)(message->payload + 4); + BIN_ERROR_CHECK(Buffer_Write(bin_buf, message->payload + 8, name_length)); + + if (Server_IsClientConnected(rmt->server) == RMT_TRUE) + { + // Send to the viewer + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 50); + } + + if (rmt->logfile != NULL) + { + // Write the data after the websocket header + rmtWriteFile(rmt->logfile, bin_buf->data + WEBSOCKET_MAX_FRAME_HEADER_SIZE, bin_buf->bytes_used - WEBSOCKET_MAX_FRAME_HEADER_SIZE); + } + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_ConsumeMessageQueue(Remotery* rmt) +{ + rmtU32 nb_messages_sent = 0; + const rmtU32 maxNbMessagesPerUpdate = g_Settings.maxNbMessagesPerUpdate; + + assert(rmt != NULL); + + // Loop reading the max number of messages for this update + // Note some messages don't consume the sent message count as they are small enough to not cause performance issues + while (nb_messages_sent < maxNbMessagesPerUpdate) + { + rmtError error = RMT_ERROR_NONE; + Message* message = rmtMessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread); + if (message == NULL) + break; + + switch (message->id) + { + // This shouldn't be possible + case MsgID_NotReady: + assert(RMT_FALSE); + break; + + // Dispatch to message handler + case MsgID_AddToStringTable: + error = Remotery_AddToStringTable(rmt, message); + break; + case MsgID_LogText: + error = Remotery_SendLogTextMessage(rmt, message); + nb_messages_sent++; + break; + case MsgID_SampleTree: + rmt_BeginCPUSample(SendSampleTreeMessage, RMTSF_Aggregate); + error = Remotery_SendSampleTreeMessage(rmt, message); + nb_messages_sent++; + rmt_EndCPUSample(); + break; + case MsgID_ProcessorThreads: + Remotery_SendProcessorThreads(rmt, message); + nb_messages_sent++; + break; + case MsgID_ThreadName: + Remotery_SendThreadName(rmt, message); + break; + + default: + break; + } + + // Consume the message before reacting to any errors + rmtMessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message); + if (error != RMT_ERROR_NONE) + return error; + } + + return RMT_ERROR_NONE; +} + +static void Remotery_FlushMessageQueue(Remotery* rmt) +{ + assert(rmt != NULL); + + // Loop reading all remaining messages + for (;;) + { + Message* message = rmtMessageQueue_PeekNextMessage(rmt->mq_to_rmt_thread); + if (message == NULL) + break; + + switch (message->id) + { + // These can be safely ignored + case MsgID_NotReady: + case MsgID_AddToStringTable: + case MsgID_LogText: + break; + + // Release all samples back to their allocators + case MsgID_SampleTree: { + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + FreeSamples(sample_tree->rootSample, sample_tree->allocator); + break; + } + + default: + break; + } + + rmtMessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message); + } +} + +static void Remotery_MapMessageQueue(Remotery* rmt) +{ + rmtU32 read_pos, write_pos; + rmtMessageQueue* queue; + + assert(rmt != NULL); + + // Wait until the caller sets the custom data + while (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_data) == NULL) + msSleep(1); + + // Snapshot the current write position so that we're not constantly chasing other threads + // that can have no effect on the thread requesting the map. + queue = rmt->mq_to_rmt_thread; + write_pos = LoadAcquire(&queue->write_pos); + + // Walk every message in the queue and call the map function + read_pos = queue->read_pos; + while (read_pos < write_pos) + { + rmtU32 r = read_pos & (queue->size - 1); + Message* message = (Message*)(queue->data->ptr + r); + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + rmt->map_message_queue_fn(rmt, message); + read_pos += message_size; + } + + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_data, NULL); +} + +static rmtError Remotery_ThreadMain(rmtThread* thread) +{ + Remotery* rmt = (Remotery*)thread->param; + assert(rmt != NULL); + + rmt_SetCurrentThreadName("Remotery"); + + while (thread->request_exit == RMT_FALSE) + { + rmt_BeginCPUSample(Wakeup, 0); + + rmt_BeginCPUSample(ServerUpdate, 0); + Server_Update(rmt->server); + rmt_EndCPUSample(); + + rmt_BeginCPUSample(ConsumeMessageQueue, 0); + Remotery_ConsumeMessageQueue(rmt); + rmt_EndCPUSample(); + + rmt_EndCPUSample(); + + // Process any queue map requests + if (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_fn) != NULL) + { + Remotery_MapMessageQueue(rmt); + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_fn, NULL); + } + + // + // [NOTE-A] + // + // Possible sequence of user events at this point: + // + // 1. Add samples to the queue. + // 2. Shutdown remotery. + // + // This loop will exit with unrelease samples. + // + + msSleep(g_Settings.msSleepBetweenServerUpdates); + } + + // Release all samples to their allocators as a consequence of [NOTE-A] + Remotery_FlushMessageQueue(rmt); + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_ReceiveMessage(void* context, char* message_data, rmtU32 message_length) +{ + Remotery* rmt = (Remotery*)context; + +// Manual dispatch on 4-byte message headers (message ID is little-endian encoded) +#define FOURCC(a, b, c, d) (rmtU32)(((d) << 24) | ((c) << 16) | ((b) << 8) | (a)) + rmtU32 message_id = *(rmtU32*)message_data; + + switch (message_id) + { + case FOURCC('C', 'O', 'N', 'I'): { + rmt_LogText("Console message received..."); + rmt_LogText(message_data + 4); + + // Pass on to any registered handler + if (g_Settings.input_handler != NULL) + g_Settings.input_handler(message_data + 4, g_Settings.input_handler_context); + + break; + } + + case FOURCC('G', 'S', 'M', 'P'): { + rmtPStr name; + + // Convert name hash to integer + rmtU32 name_hash = 0; + const char* cur = message_data + 4; + const char* end = cur + message_length - 4; + while (cur < end) + name_hash = name_hash * 10 + *cur++ - '0'; + + // Search for a matching string hash + name = StringTable_Find(rmt->string_table, name_hash); + if (name != NULL) + { + rmtU32 name_length = (rmtU32)strnlen_s_safe_c(name, 256 - 12); + + // Construct a response message containing the matching name + Buffer* bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + bin_SampleName(bin_buf, name, name_hash, name_length); + + // Send back immediately as we're on the server thread + return Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 10); + } + + break; + } + } + +#undef FOURCC + + return RMT_ERROR_NONE; +} + +static rmtError Remotery_Constructor(Remotery* rmt) +{ + rmtError error; + + assert(rmt != NULL); + + // Set default state + rmt->server = NULL; + rmt->mq_to_rmt_thread = NULL; + rmt->thread = NULL; + rmt->string_table = NULL; + rmt->logfile = NULL; + rmt->map_message_queue_fn = NULL; + rmt->map_message_queue_data = NULL; + rmt->threadProfilers = NULL; + +#if RMT_USE_CUDA + rmt->cuda.CtxSetCurrent = NULL; + rmt->cuda.EventCreate = NULL; + rmt->cuda.EventDestroy = NULL; + rmt->cuda.EventElapsedTime = NULL; + rmt->cuda.EventQuery = NULL; + rmt->cuda.EventRecord = NULL; +#endif + +#if RMT_USE_OPENGL + rmt->opengl = NULL; +#endif + +#if RMT_USE_METAL + rmt->metal = NULL; +#endif + + // Kick-off the timer + usTimer_Init(&rmt->timer); + + // Create the server + New_3(Server, rmt->server, g_Settings.port, g_Settings.reuse_open_port, g_Settings.limit_connections_to_localhost); + if (error != RMT_ERROR_NONE) + return error; + + // Setup incoming message handler + rmt->server->receive_handler = Remotery_ReceiveMessage; + rmt->server->receive_handler_context = rmt; + + // Create the main message thread with only one page + New_1(rmtMessageQueue, rmt->mq_to_rmt_thread, g_Settings.messageQueueSizeInBytes); + if (error != RMT_ERROR_NONE) + return error; + + // Create sample name string table + New_0(StringTable, rmt->string_table); + if (error != RMT_ERROR_NONE) + return error; + + if (g_Settings.logPath != NULL) + { + // Get current date/time + struct tm* now_tm = TimeDateNow(); + + // Start the log path off + char filename[512] = { 0 }; + strncat_s(filename, sizeof(filename), g_Settings.logPath, 512); + strncat_s(filename, sizeof(filename), "/remotery-log-", 14); + + // Append current date and time + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_year + 1900), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_mon + 1), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_mday), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_hour), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_min), 11); + strncat_s(filename, sizeof(filename), "-", 1); + strncat_s(filename, sizeof(filename), itoa_s(now_tm->tm_sec), 11); + + // Just append a custom extension + strncat_s(filename, sizeof(filename), ".rbin", 5); + + // Open and assume any failure simply sets NULL and the file isn't written + rmt->logfile = rmtOpenFile(filename, "w"); + + // Write the header + if (rmt->logfile != NULL) + { + rmtWriteFile(rmt->logfile, "RMTBLOGF", 8); + } + } + +#if RMT_USE_OPENGL + error = OpenGL_Create(&rmt->opengl); + if (error != RMT_ERROR_NONE) + return error; +#endif + +#if RMT_USE_METAL + error = Metal_Create(&rmt->metal); + if (error != RMT_ERROR_NONE) + return error; +#endif + + // Create the thread profilers container + New_2(ThreadProfilers, rmt->threadProfilers, &rmt->timer, rmt->mq_to_rmt_thread); + if (error != RMT_ERROR_NONE) + { + return error; + } + + // Set as the global instance before creating any threads that uses it for sampling itself + assert(g_Remotery == NULL); + g_Remotery = rmt; + g_RemoteryCreated = RMT_TRUE; + + // Ensure global instance writes complete before other threads get a chance to use it + CompilerWriteFence(); + + // Create the main update thread once everything has been defined for the global remotery object + New_2(rmtThread, 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(rmtThread, rmt->thread); + + if (g_RemoteryCreated) + { + g_Remotery = NULL; + g_RemoteryCreated = RMT_FALSE; + } + + Delete(ThreadProfilers, rmt->threadProfilers); + +#if RMT_USE_OPENGL + Delete(OpenGL, rmt->opengl); +#endif + +#if RMT_USE_METAL + Delete(Metal, rmt->metal); +#endif + + rmtCloseFile(rmt->logfile); + + Delete(StringTable, rmt->string_table); + Delete(rmtMessageQueue, rmt->mq_to_rmt_thread); + + Delete(Server, rmt->server); +} + +static void* CRTMalloc(void* mm_context, rmtU32 size) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + return malloc((size_t)size); +} + +static void CRTFree(void* mm_context, void* ptr) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + free(ptr); +} + +static void* CRTRealloc(void* mm_context, void* ptr, rmtU32 size) +{ + RMT_UNREFERENCED_PARAMETER(mm_context); + return realloc(ptr, size); +} + +RMT_API rmtSettings* _rmt_Settings(void) +{ + // Default-initialize on first call + if (g_SettingsInitialized == RMT_FALSE) + { + g_Settings.port = 0x4597; + g_Settings.reuse_open_port = RMT_FALSE; + g_Settings.limit_connections_to_localhost = RMT_FALSE; + g_Settings.enableThreadSampler = RMT_TRUE; + g_Settings.msSleepBetweenServerUpdates = 4; + g_Settings.messageQueueSizeInBytes = 1024 * 1024; + g_Settings.maxNbMessagesPerUpdate = 50; + g_Settings.malloc = CRTMalloc; + g_Settings.free = CRTFree; + g_Settings.realloc = CRTRealloc; + g_Settings.input_handler = NULL; + g_Settings.input_handler_context = NULL; + g_Settings.logPath = NULL; + + g_SettingsInitialized = RMT_TRUE; + } + + return &g_Settings; +} + +RMT_API rmtError _rmt_CreateGlobalInstance(Remotery** remotery) +{ + rmtError error; + + // Ensure load/acquire store/release operations match this enum size + assert(sizeof(MessageID) == sizeof(rmtU32)); + + // Default-initialise if user has not set values + rmt_Settings(); + + // Creating the Remotery instance also records it as the global instance + assert(remotery != NULL); + New_0(Remotery, *remotery); + return error; +} + +RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery) +{ + // Ensure this is the module that created it + assert(g_RemoteryCreated == RMT_TRUE); + assert(g_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 + +wchar_t* MakeWideString(const char* string) +{ + size_t wlen; + wchar_t* wstr; + + // First get the converted length +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + if (mbstowcs_s(&wlen, NULL, 0, string, INT_MAX) != 0) + { + return NULL; + } +#else + wlen = mbstowcs(NULL, string, 256); +#endif + + // Allocate enough words for the converted result + wstr = (wchar_t*)(rmtMalloc((wlen + 1) * sizeof(wchar_t))); + if (wstr == NULL) + { + return NULL; + } + + // Convert +#if defined(RMT_PLATFORM_WINDOWS) && !RMT_USE_TINYCRT + if (mbstowcs_s(&wlen, wstr, wlen + 1, string, wlen) != 0) +#else + if (mbstowcs(wstr, string, wlen + 1) != wlen) +#endif + { + rmtFree(wstr); + return NULL; + } + + return wstr; +} + +static void SetDebuggerThreadName(const char* name) +{ +#ifdef RMT_PLATFORM_WINDOWS + THREADNAME_INFO info; + + // See if SetThreadDescription is available in this version of Windows + // Introduced in Windows 10 build 1607 + HMODULE kernel32 = GetModuleHandleA("Kernel32.dll"); + if (kernel32 != NULL) + { + typedef HRESULT(WINAPI* SETTHREADDESCRIPTION)(HANDLE hThread, PCWSTR lpThreadDescription); + SETTHREADDESCRIPTION SetThreadDescription = (SETTHREADDESCRIPTION)GetProcAddress(kernel32, "SetThreadDescription"); + if (SetThreadDescription != NULL) + { + // Create a wide-string version of the thread name + wchar_t* wstr = MakeWideString(name); + if (wstr != NULL) + { + // Set and return, leaving a fall-through for any failure cases to use the old exception method + SetThreadDescription(GetCurrentThread(), wstr); + rmtFree(wstr); + return; + } + } + } + + info.dwType = 0x1000; + info.szName = name; + info.dwThreadID = (DWORD)-1; + info.dwFlags = 0; + +#ifndef __MINGW32__ + __try + { + RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } + __except (1 /* EXCEPTION_EXECUTE_HANDLER */) + { + } +#endif +#else + RMT_UNREFERENCED_PARAMETER(name); +#endif + +#ifdef RMT_PLATFORM_LINUX + // pthread_setname_np is a non-standard GNU extension. + char name_clamp[16]; + name_clamp[0] = 0; + strncat_s(name_clamp, sizeof(name_clamp), name, 15); +#if defined(__FreeBSD__) || defined(__OpenBSD__) + pthread_set_name_np(pthread_self(), name_clamp); +#else + prctl(PR_SET_NAME, name_clamp, 0, 0, 0); +#endif +#endif +} + +RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + { + return; + } + + // Get data for this thread + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) != RMT_ERROR_NONE) + { + return; + } + + // Copy name and apply to the debugger + strcpy_s(thread_profiler->threadName, sizeof(thread_profiler->threadName), thread_name); + thread_profiler->threadNameHash = MurmurHash3_x86_32(thread_name, strnlen_s(thread_name, 64), 0); + SetDebuggerThreadName(thread_name); + + // Send the thread name for lookup +#ifdef RMT_PLATFORM_WINDOWS + QueueThreadName(g_Remotery->mq_to_rmt_thread, thread_name, thread_profiler); +#endif +} + +static rmtBool QueueLine(rmtMessageQueue* queue, unsigned char* text, rmtU32 size, struct ThreadProfiler* thread_profiler) +{ + Message* message; + rmtU32 text_size; + + assert(queue != NULL); + + // Prefix with text size + text_size = size - 8; + U32ToByteArray(text + 4, text_size); + + // Allocate some space for the line + message = rmtMessageQueue_AllocMessage(queue, size, thread_profiler); + if (message == NULL) + return RMT_FALSE; + + // Copy the text and commit the message + memcpy(message->payload, text, size); + rmtMessageQueue_CommitMessage(message, MsgID_LogText); + + return RMT_TRUE; +} + +RMT_API void _rmt_LogText(rmtPStr text) +{ + int start_offset, offset, i; + unsigned char line_buffer[1024] = {0}; + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) != RMT_ERROR_NONE) + { + return; + } + + // Start the line with the message header + line_buffer[0] = 'L'; + line_buffer[1] = 'O'; + line_buffer[2] = 'G'; + line_buffer[3] = 'M'; + // Fill with spaces to enable viewing line_buffer without offset in a debugger + // (will be overwritten later by QueueLine/rmtMessageQueue_AllocMessage) + line_buffer[4] = ' '; + line_buffer[5] = ' '; + line_buffer[6] = ' '; + line_buffer[7] = ' '; + start_offset = 8; + + // There might be newlines in the buffer, so split them into multiple network calls + offset = start_offset; + for (i = 0; text[i] != 0; i++) + { + char c = text[i]; + + // Line wrap when too long or newline encountered + if (offset == sizeof(line_buffer) - 1 || c == '\n') + { + // Send the line up to now + if (QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, offset, thread_profiler) == RMT_FALSE) + return; + + // Restart line + offset = start_offset; + + // Don't add the newline character (if this was the reason for the flush) + // to the restarted line_buffer, let's skip it + if (c == '\n') + continue; + } + + line_buffer[offset++] = c; + } + + // Send the last line + if (offset > start_offset) + { + assert(offset < (int)sizeof(line_buffer)); + QueueLine(g_Remotery->mq_to_rmt_thread, line_buffer, offset, thread_profiler); + } +} + +RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32 flags, rmtU32* hash_cache) +{ + // 'hash_cache' stores a pointer to a sample name's hash value. Internally this is used to identify unique + // callstacks and it would be ideal that it's not recalculated each time the sample is used. This can be statically + // cached at the point of call or stored elsewhere when dynamic names are required. + // + // If 'hash_cache' is NULL then this call becomes more expensive, as it has to recalculate the hash of the name. + + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + // TODO: Time how long the bits outside here cost and subtract them from the parent + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + if (ThreadProfiler_Push(thread_profiler->sampleTrees[SampleType_CPU], name_hash, flags, &sample) == RMT_ERROR_NONE) + { + // If this is an aggregate sample, store the time in 'end' as we want to preserve 'start' + if (sample->call_count > 1) + sample->us_end = usTimer_Get(&g_Remotery->timer); + else + sample->us_start = usTimer_Get(&g_Remotery->timer); + } + } +} + +RMT_API void _rmt_EndCPUSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample = thread_profiler->sampleTrees[SampleType_CPU]->currentParent; + + if (sample->recurse_depth > 0) + { + sample->recurse_depth--; + } + else + { + rmtU64 us_end = usTimer_Get(&g_Remotery->timer); + Sample_Close(sample, us_end); + ThreadProfiler_Pop(thread_profiler, g_Remotery->mq_to_rmt_thread, sample); + } + } +} + +#if RMT_USE_OPENGL || RMT_USE_D3D11 +static void Remotery_DeleteSampleTree(Remotery* rmt, enum SampleType sample_type) +{ + ThreadProfiler* thread_profiler; + + // Get the attached thread sampler and delete the sample tree + assert(rmt != NULL); + if (ThreadProfilers_GetCurrentThreadProfiler(rmt->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + SampleTree* sample_tree = thread_profiler->sampleTrees[sample_type]; + if (sample_tree != NULL) + { + Delete(SampleTree, sample_tree); + thread_profiler->sampleTrees[sample_type] = NULL; + } + } +} + +static rmtBool rmtMessageQueue_IsEmpty(rmtMessageQueue* queue) +{ + assert(queue != NULL); + return queue->write_pos - queue->read_pos == 0; +} + +typedef struct GatherQueuedSampleData +{ + SampleType sample_type; + Buffer* flush_samples; +} GatherQueuedSampleData; + +static void MapMessageQueueAndWait(Remotery* rmt, void (*map_message_queue_fn)(Remotery* rmt, Message*), void* data) +{ + // Basic spin lock on the map function itself + while (AtomicCompareAndSwapPointer((long* volatile*)&rmt->map_message_queue_fn, NULL, + (long*)map_message_queue_fn) == RMT_FALSE) + msSleep(1); + + StoreReleasePointer((long* volatile*)&rmt->map_message_queue_data, (long*)data); + + // Wait until map completes + while (LoadAcquirePointer((long* volatile*)&rmt->map_message_queue_fn) != NULL) + msSleep(1); +} + +static void GatherQueuedSamples(Remotery* rmt, Message* message) +{ + GatherQueuedSampleData* gather_data = (GatherQueuedSampleData*)rmt->map_message_queue_data; + + // Filter sample trees + if (message->id == MsgID_SampleTree) + { + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + Sample* sample = sample_tree->rootSample; + if (sample->type == gather_data->sample_type) + { + // Make a copy of the entire sample tree as the remotery thread may overwrite it while + // the calling thread tries to delete + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + Buffer_Write(gather_data->flush_samples, message, message_size); + + // Mark the message empty + message->id = MsgID_None; + } + } +} + +static void FreePendingSampleTrees(Remotery* rmt, SampleType sample_type, Buffer* flush_samples) +{ + rmtU8* data; + rmtU8* data_end; + + // Gather all sample trees currently queued for the Remotery thread + GatherQueuedSampleData gather_data; + gather_data.sample_type = sample_type; + gather_data.flush_samples = flush_samples; + MapMessageQueueAndWait(rmt, GatherQueuedSamples, &gather_data); + + // Release all sample trees to their allocators + data = flush_samples->data; + data_end = data + flush_samples->bytes_used; + while (data < data_end) + { + Message* message = (Message*)data; + rmtU32 message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + FreeSamples(sample_tree->rootSample, sample_tree->allocator); + data += message_size; + } +} + +#endif + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @CUDA: CUDA event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_CUDA + +typedef struct CUDASample +{ + // IS-A inheritance relationship + Sample base; + + // Pair of events that wrap the sample + CUevent event_start; + CUevent event_end; + +} CUDASample; + +static rmtError MapCUDAResult(CUresult result) +{ + switch (result) + { + case CUDA_SUCCESS: + return RMT_ERROR_NONE; + case CUDA_ERROR_DEINITIALIZED: + return RMT_ERROR_CUDA_DEINITIALIZED; + case CUDA_ERROR_NOT_INITIALIZED: + return RMT_ERROR_CUDA_NOT_INITIALIZED; + case CUDA_ERROR_INVALID_CONTEXT: + return RMT_ERROR_CUDA_INVALID_CONTEXT; + case CUDA_ERROR_INVALID_VALUE: + return RMT_ERROR_CUDA_INVALID_VALUE; + case CUDA_ERROR_INVALID_HANDLE: + return RMT_ERROR_CUDA_INVALID_HANDLE; + case CUDA_ERROR_OUT_OF_MEMORY: + return RMT_ERROR_CUDA_OUT_OF_MEMORY; + case CUDA_ERROR_NOT_READY: + return RMT_ERROR_ERROR_NOT_READY; + default: + return RMT_ERROR_CUDA_UNKNOWN; + } +} + +#define CUDA_MAKE_FUNCTION(name, params) \ + typedef CUresult(CUDAAPI* name##Ptr) params; \ + name##Ptr name = (name##Ptr)g_Remotery->cuda.name; + +#define CUDA_GUARD(call) \ + { \ + rmtError error = call; \ + if (error != RMT_ERROR_NONE) \ + return error; \ + } + +// Wrappers around CUDA driver functions that manage the active context. +static rmtError CUDASetContext(void* context) +{ + CUDA_MAKE_FUNCTION(CtxSetCurrent, (CUcontext ctx)); + assert(CtxSetCurrent != NULL); + return MapCUDAResult(CtxSetCurrent((CUcontext)context)); +} +static rmtError CUDAGetContext(void** context) +{ + CUDA_MAKE_FUNCTION(CtxGetCurrent, (CUcontext * ctx)); + assert(CtxGetCurrent != NULL); + return MapCUDAResult(CtxGetCurrent((CUcontext*)context)); +} +static rmtError CUDAEnsureContext() +{ + void* current_context; + CUDA_GUARD(CUDAGetContext(¤t_context)); + + assert(g_Remotery != NULL); + if (current_context != g_Remotery->cuda.context) + CUDA_GUARD(CUDASetContext(g_Remotery->cuda.context)); + + return RMT_ERROR_NONE; +} + +// Wrappers around CUDA driver functions that manage events +static rmtError CUDAEventCreate(CUevent* phEvent, unsigned int Flags) +{ + CUDA_MAKE_FUNCTION(EventCreate, (CUevent * phEvent, unsigned int Flags)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventCreate(phEvent, Flags)); +} +static rmtError CUDAEventDestroy(CUevent hEvent) +{ + CUDA_MAKE_FUNCTION(EventDestroy, (CUevent hEvent)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventDestroy(hEvent)); +} +static rmtError CUDAEventRecord(CUevent hEvent, void* hStream) +{ + CUDA_MAKE_FUNCTION(EventRecord, (CUevent hEvent, CUstream hStream)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventRecord(hEvent, (CUstream)hStream)); +} +static rmtError CUDAEventQuery(CUevent hEvent) +{ + CUDA_MAKE_FUNCTION(EventQuery, (CUevent hEvent)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventQuery(hEvent)); +} +static rmtError CUDAEventElapsedTime(float* pMilliseconds, CUevent hStart, CUevent hEnd) +{ + CUDA_MAKE_FUNCTION(EventElapsedTime, (float* pMilliseconds, CUevent hStart, CUevent hEnd)); + CUDA_GUARD(CUDAEnsureContext()); + return MapCUDAResult(EventElapsedTime(pMilliseconds, hStart, hEnd)); +} + +static rmtError CUDASample_Constructor(CUDASample* sample) +{ + rmtError error; + + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = SampleType_CUDA; + sample->event_start = NULL; + sample->event_end = NULL; + + // Create non-blocking events with timing + assert(g_Remotery != NULL); + error = CUDAEventCreate(&sample->event_start, CU_EVENT_DEFAULT); + if (error == RMT_ERROR_NONE) + error = CUDAEventCreate(&sample->event_end, CU_EVENT_DEFAULT); + return error; +} + +static void CUDASample_Destructor(CUDASample* sample) +{ + assert(sample != NULL); + + // Destroy events + if (sample->event_start != NULL) + CUDAEventDestroy(sample->event_start); + if (sample->event_end != NULL) + CUDAEventDestroy(sample->event_end); + + Sample_Destructor((Sample*)sample); +} + +static rmtBool AreCUDASamplesReady(Sample* sample) +{ + rmtError error; + Sample* child; + + CUDASample* cuda_sample = (CUDASample*)sample; + assert(sample->type == SampleType_CUDA); + + // Check to see if both of the CUDA events have been processed + error = CUDAEventQuery(cuda_sample->event_start); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + error = CUDAEventQuery(cuda_sample->event_end); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + + // Check child sample events + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!AreCUDASamplesReady(child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample) +{ + Sample* child; + + CUDASample* cuda_root_sample = (CUDASample*)root_sample; + CUDASample* cuda_sample = (CUDASample*)sample; + + float ms_start, ms_end; + + assert(root_sample != NULL); + assert(sample != NULL); + + // Get millisecond timing of each sample event, relative to initial root sample + if (CUDAEventElapsedTime(&ms_start, cuda_root_sample->event_start, cuda_sample->event_start) != RMT_ERROR_NONE) + return RMT_FALSE; + if (CUDAEventElapsedTime(&ms_end, cuda_root_sample->event_start, cuda_sample->event_end) != RMT_ERROR_NONE) + return RMT_FALSE; + + // Convert to microseconds and add to the sample + sample->us_start = (rmtU64)(ms_start * 1000); + sample->us_end = (rmtU64)(ms_end * 1000); + sample->us_length = sample->us_end - sample->us_start; + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetCUDASampleTimes(root_sample, child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind) +{ + assert(bind != NULL); + if (g_Remotery != NULL) + g_Remotery->cuda = *bind; +} + +RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + rmtError error; + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the CUDA tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a CUDA binding is not yet available. + SampleTree** cuda_tree = &thread_profiler->sampleTrees[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 (ThreadProfiler_Push(*cuda_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + CUDASample* cuda_sample = (CUDASample*)sample; + CUDAEventRecord(cuda_sample->event_start, stream); + } + } +} + +RMT_API void _rmt_EndCUDASample(void* stream) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + CUDASample* sample = (CUDASample*)thread_profiler->sampleTrees[SampleType_CUDA]->currentParent; + if (sample->base.recurse_depth > 0) + { + sample->base.recurse_depth--; + } + else + { + CUDAEventRecord(sample->event_end, stream); + ThreadProfiler_Pop(thread_profiler, g_Remotery->mq_to_rmt_thread, (Sample*)sample); + } + } +} + +#endif // RMT_USE_CUDA + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @D3D11: Direct3D 11 event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_D3D11 + +// As clReflect has no way of disabling C++ compile mode, this forces C interfaces everywhere... +#define CINTERFACE + +// ...unfortunately these C++ helpers aren't wrapped by the same macro but they can be disabled individually +#define D3D11_NO_HELPERS + +// Allow use of the D3D11 helper macros for accessing the C-style vtable +#define COBJMACROS + +#ifdef _MSC_VER +// Disable for d3d11.h +// warning C4201: nonstandard extension used : nameless struct/union +#pragma warning(push) +#pragma warning(disable : 4201) +#endif + +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +typedef struct D3D11 +{ + // Context set by user + ID3D11Device* device; + ID3D11DeviceContext* context; + + HRESULT last_error; + + // Queue to the D3D 11 main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_d3d11_main; + + // Mark the first time so that remaining timestamps are offset from this + rmtU64 first_timestamp; + // Last time in us (CPU time, via usTimer_Get) since we last resync'ed CPU & GPU + rmtU64 last_resync; + + // Sample trees in transit in the message queue for release on shutdown + Buffer* flush_samples; +} D3D11; + +static rmtError D3D11_Create(D3D11** d3d11) +{ + 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)->mq_to_d3d11_main = NULL; + (*d3d11)->first_timestamp = 0; + (*d3d11)->last_resync = 0; + (*d3d11)->flush_samples = NULL; + + New_1(rmtMessageQueue, (*d3d11)->mq_to_d3d11_main, g_Settings.messageQueueSizeInBytes); + if (error != RMT_ERROR_NONE) + { + Delete(D3D11, *d3d11); + return error; + } + + New_1(Buffer, (*d3d11)->flush_samples, 8 * 1024); + if (error != RMT_ERROR_NONE) + { + Delete(D3D11, *d3d11); + return error; + } + + return RMT_ERROR_NONE; +} + +static void D3D11_Destructor(D3D11* d3d11) +{ + assert(d3d11 != NULL); + Delete(Buffer, d3d11->flush_samples); + Delete(rmtMessageQueue, d3d11->mq_to_d3d11_main); +} + +static HRESULT rmtD3D11Finish(ID3D11Device* device, ID3D11DeviceContext* context, rmtU64* out_timestamp, + double* out_frequency) +{ + HRESULT result; + ID3D11Query* full_stall_fence; + ID3D11Query* query_disjoint; + D3D11_QUERY_DESC query_desc; + D3D11_QUERY_DESC disjoint_desc; + UINT64 timestamp; + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint; + + query_desc.Query = D3D11_QUERY_TIMESTAMP; + query_desc.MiscFlags = 0; + result = ID3D11Device_CreateQuery(device, &query_desc, &full_stall_fence); + if (result != S_OK) + return result; + + disjoint_desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + disjoint_desc.MiscFlags = 0; + result = ID3D11Device_CreateQuery(device, &disjoint_desc, &query_disjoint); + if (result != S_OK) + { + ID3D11Query_Release(full_stall_fence); + return result; + } + + ID3D11DeviceContext_Begin(context, (ID3D11Asynchronous*)query_disjoint); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)full_stall_fence); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)query_disjoint); + + result = S_FALSE; + + while (result == S_FALSE) + { + result = + ID3D11DeviceContext_GetData(context, (ID3D11Asynchronous*)query_disjoint, &disjoint, sizeof(disjoint), 0); + if (result != S_OK && result != S_FALSE) + { + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; + } + if (result == S_OK) + { + result = ID3D11DeviceContext_GetData(context, (ID3D11Asynchronous*)full_stall_fence, ×tamp, + sizeof(timestamp), 0); + if (result != S_OK && result != S_FALSE) + { + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; + } + } + // Give HyperThreading threads a breath on this spinlock. + YieldProcessor(); + } + + if (disjoint.Disjoint == FALSE) + { + double frequency = disjoint.Frequency / 1000000.0; + *out_timestamp = timestamp; + *out_frequency = frequency; + } + else + { + result = S_FALSE; + } + + ID3D11Query_Release(full_stall_fence); + ID3D11Query_Release(query_disjoint); + return result; +} + +static HRESULT SyncD3D11CpuGpuTimes(ID3D11Device* device, ID3D11DeviceContext* context, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + rmtU64 cpu_time_start = 0; + rmtU64 cpu_time_stop = 0; + rmtU64 average_half_RTT = 0; // RTT = Rountrip Time. + UINT64 gpu_base = 0; + double frequency = 1; + int i; + + HRESULT result; + result = rmtD3D11Finish(device, context, &gpu_base, &frequency); + if (result != S_OK && result != S_FALSE) + return result; + + for (i = 0; i < RMT_GPU_CPU_SYNC_NUM_ITERATIONS; ++i) + { + rmtU64 half_RTT; + cpu_time_start = usTimer_Get(&g_Remotery->timer); + result = rmtD3D11Finish(device, context, &gpu_base, &frequency); + cpu_time_stop = usTimer_Get(&g_Remotery->timer); + + if (result != S_OK && result != S_FALSE) + return result; + + // Ignore attempts where there was a disjoint, since there would + // be a lot of noise in those readings for measuring the RTT + if (result == S_OK) + { + // Average the time it takes a roundtrip from CPU to GPU + // while doing nothing other than getting timestamps + half_RTT = (cpu_time_stop - cpu_time_start) >> 1ULL; + if (i == 0) + average_half_RTT = half_RTT; + else + average_half_RTT = (average_half_RTT + half_RTT) >> 1ULL; + } + } + + // All GPU times are offset from gpu_base, and then taken to + // the same relative origin CPU timestamps are based on. + // CPU is in us, we must translate it to ns. + *out_first_timestamp = gpu_base - (rmtU64)((cpu_time_start + average_half_RTT) * frequency); + *out_last_resync = cpu_time_stop; + + return result; +} + +typedef struct D3D11Timestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Pair of timestamp queries that wrap the sample + ID3D11Query* query_start; + ID3D11Query* query_end; + + // A disjoint to measure frequency/stability + // TODO: Does *each* sample need one of these? + ID3D11Query* query_disjoint; + + rmtU64 cpu_timestamp; +} D3D11Timestamp; + +static rmtError D3D11Timestamp_Constructor(D3D11Timestamp* stamp) +{ + ThreadProfiler* thread_profiler; + D3D11_QUERY_DESC timestamp_desc; + D3D11_QUERY_DESC disjoint_desc; + ID3D11Device* device; + HRESULT* last_error; + rmtError rmt_error; + + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->query_start = NULL; + stamp->query_end = NULL; + stamp->query_disjoint = NULL; + stamp->cpu_timestamp = 0; + + assert(g_Remotery != NULL); + rmt_error = ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler); + if (rmt_error != RMT_ERROR_NONE) + { + return rmt_error; + } + assert(thread_profiler->d3d11 != NULL); + device = thread_profiler->d3d11->device; + last_error = &thread_profiler->d3d11->last_error; + + // Create start/end timestamp queries + timestamp_desc.Query = D3D11_QUERY_TIMESTAMP; + timestamp_desc.MiscFlags = 0; + *last_error = ID3D11Device_CreateQuery(device, ×tamp_desc, &stamp->query_start); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + *last_error = ID3D11Device_CreateQuery(device, ×tamp_desc, &stamp->query_end); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + + // Create disjoint query + disjoint_desc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + disjoint_desc.MiscFlags = 0; + *last_error = ID3D11Device_CreateQuery(device, &disjoint_desc, &stamp->query_disjoint); + if (*last_error != S_OK) + return RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY; + + return RMT_ERROR_NONE; +} + +static void D3D11Timestamp_Destructor(D3D11Timestamp* stamp) +{ + assert(stamp != NULL); + + // Destroy queries + if (stamp->query_disjoint != NULL) + ID3D11Query_Release(stamp->query_disjoint); + if (stamp->query_end != NULL) + ID3D11Query_Release(stamp->query_end); + if (stamp->query_start != NULL) + ID3D11Query_Release(stamp->query_start); +} + +static void D3D11Timestamp_Begin(D3D11Timestamp* stamp, ID3D11DeviceContext* context) +{ + assert(stamp != NULL); + + // Start of disjoint and first query + stamp->cpu_timestamp = usTimer_Get(&g_Remotery->timer); + ID3D11DeviceContext_Begin(context, (ID3D11Asynchronous*)stamp->query_disjoint); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_start); +} + +static void D3D11Timestamp_End(D3D11Timestamp* stamp, ID3D11DeviceContext* context) +{ + assert(stamp != NULL); + + // End of disjoint and second query + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_end); + ID3D11DeviceContext_End(context, (ID3D11Asynchronous*)stamp->query_disjoint); +} + +static HRESULT D3D11Timestamp_GetData(D3D11Timestamp* stamp, ID3D11Device* device, ID3D11DeviceContext* context, + rmtU64* out_start, rmtU64* out_end, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + ID3D11Asynchronous* query_start; + ID3D11Asynchronous* query_end; + ID3D11Asynchronous* query_disjoint; + HRESULT result; + + UINT64 start; + UINT64 end; + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjoint; + + assert(stamp != NULL); + query_start = (ID3D11Asynchronous*)stamp->query_start; + query_end = (ID3D11Asynchronous*)stamp->query_end; + query_disjoint = (ID3D11Asynchronous*)stamp->query_disjoint; + + // Check to see if all queries are ready + // If any fail to arrive, wait until later + result = ID3D11DeviceContext_GetData(context, query_start, &start, sizeof(start), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + result = ID3D11DeviceContext_GetData(context, query_end, &end, sizeof(end), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + result = ID3D11DeviceContext_GetData(context, query_disjoint, &disjoint, sizeof(disjoint), + D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (result != S_OK) + return result; + + if (disjoint.Disjoint == FALSE) + { + double frequency = disjoint.Frequency / 1000000.0; + + // Mark the first timestamp. We may resync if we detect the GPU timestamp is in the + // past (i.e. happened before the CPU command) since it should be impossible. + assert(out_first_timestamp != NULL); + if (*out_first_timestamp == 0 || ((start - *out_first_timestamp) / frequency) < stamp->cpu_timestamp) + { + result = SyncD3D11CpuGpuTimes(device, context, out_first_timestamp, out_last_resync); + if (result != S_OK) + return result; + } + + // Calculate start and end timestamps from the disjoint info + *out_start = (rmtU64)((start - *out_first_timestamp) / frequency); + *out_end = (rmtU64)((end - *out_first_timestamp) / frequency); + } + else + { +#if RMT_D3D11_RESYNC_ON_DISJOINT + result = SyncD3D11CpuGpuTimes(device, context, out_first_timestamp, out_last_resync); + if (result != S_OK) + return result; +#endif + } + + return S_OK; +} + +typedef struct D3D11Sample +{ + // IS-A inheritance relationship + Sample base; + + D3D11Timestamp* timestamp; + +} D3D11Sample; + +static rmtError D3D11Sample_Constructor(D3D11Sample* sample) +{ + rmtError error; + + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = SampleType_D3D11; + New_0(D3D11Timestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void D3D11Sample_Destructor(D3D11Sample* sample) +{ + Delete(D3D11Timestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +RMT_API void _rmt_BindD3D11(void* device, void* context) +{ + if (g_Remotery != NULL) + { + ThreadProfiler* thread_profiler; + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + assert(thread_profiler->d3d11 != NULL); + + assert(device != NULL); + thread_profiler->d3d11->device = (ID3D11Device*)device; + assert(context != NULL); + thread_profiler->d3d11->context = (ID3D11DeviceContext*)context; + } + } +} + +static void UpdateD3D11Frame(ThreadProfiler* thread_profiler); + +RMT_API void _rmt_UnbindD3D11(void) +{ + if (g_Remotery != NULL) + { + ThreadProfiler* thread_profiler; + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + D3D11* d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + // Stall waiting for the D3D queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(d3d11->mq_to_d3d11_main)) + UpdateD3D11Frame(thread_profiler); + + // There will be a whole bunch of D3D11 sample trees queued up the remotery queue that need releasing + FreePendingSampleTrees(g_Remotery, SampleType_D3D11, d3d11->flush_samples); + + // Inform sampler to not add any more samples + d3d11->device = NULL; + d3d11->context = NULL; + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_DeleteSampleTree(g_Remotery, SampleType_D3D11); + } + } +} + +RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash; + SampleTree** d3d_tree; + + // Has D3D11 been unbound? + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the D3D11 tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a D3D11 binding is not yet available. + d3d_tree = &thread_profiler->sampleTrees[SampleType_D3D11]; + if (*d3d_tree == NULL) + { + rmtError error; + New_3(SampleTree, *d3d_tree, sizeof(D3D11Sample), (ObjConstructor)D3D11Sample_Constructor, + (ObjDestructor)D3D11Sample_Destructor); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*d3d_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + D3D11Sample* d3d_sample = (D3D11Sample*)sample; + D3D11Timestamp_Begin(d3d_sample->timestamp, d3d11->context); + } + } +} + +static rmtBool GetD3D11SampleTimes(Sample* sample, ThreadProfiler* thread_profiler, rmtU64* out_first_timestamp, + rmtU64* out_last_resync) +{ + Sample* child; + + D3D11Sample* d3d_sample = (D3D11Sample*)sample; + + assert(sample != NULL); + if (d3d_sample->timestamp != NULL) + { + HRESULT result; + + D3D11* d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + assert(out_last_resync != NULL); + +#if (RMT_GPU_CPU_SYNC_SECONDS > 0) + if (*out_last_resync < d3d_sample->timestamp->cpu_timestamp) + { + // Convert from us to seconds. + rmtU64 time_diff = (d3d_sample->timestamp->cpu_timestamp - *out_last_resync) / 1000000ULL; + if (time_diff > RMT_GPU_CPU_SYNC_SECONDS) + { + result = SyncD3D11CpuGpuTimes(d3d11->device, d3d11->context, out_first_timestamp, out_last_resync); + if (result != S_OK) + { + d3d11->last_error = result; + return RMT_FALSE; + } + } + } +#endif + + result = D3D11Timestamp_GetData(d3d_sample->timestamp, d3d11->device, d3d11->context, &sample->us_start, + &sample->us_end, out_first_timestamp, out_last_resync); + + if (result != S_OK) + { + d3d11->last_error = result; + return RMT_FALSE; + } + + sample->us_length = sample->us_end - sample->us_start; + } + + // Sum length on the parent to track un-sampled time in the parent + if (sample->parent != NULL) + { + sample->parent->us_sampled_length += sample->us_length; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetD3D11SampleTimes(child, thread_profiler, out_first_timestamp, out_last_resync)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateD3D11Frame(ThreadProfiler* thread_profiler) +{ + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + + rmt_BeginCPUSample(rmt_UpdateD3D11Frame, 0); + + // Process all messages in the D3D queue + for (;;) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(d3d11->mq_to_d3d11_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == SampleType_D3D11); + + // Retrieve timing of all D3D11 samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetD3D11SampleTimes(sample, thread_profiler, &d3d11->first_timestamp, &d3d11->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(d3d11->mq_to_d3d11_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndD3D11Sample(void) +{ + ThreadProfiler* thread_profiler; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + D3D11Sample* d3d_sample; + + // Has D3D11 been unbound? + d3d11 = thread_profiler->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + // Close the timestamp + d3d_sample = (D3D11Sample*)thread_profiler->sampleTrees[SampleType_D3D11]->currentParent; + if (d3d_sample->base.recurse_depth > 0) + { + d3d_sample->base.recurse_depth--; + } + else + { + if (d3d_sample->timestamp != NULL) + D3D11Timestamp_End(d3d_sample->timestamp, d3d11->context); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, d3d11->mq_to_d3d11_main, (Sample*)d3d_sample)) + // Perform ready-polling on popping of the root sample + UpdateD3D11Frame(thread_profiler); + } + } +} + +#endif // RMT_USE_D3D11 + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@OpenGL: OpenGL event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + +#if RMT_USE_OPENGL + +#ifndef APIENTRY +#if defined(__MINGW32__) || defined(__CYGWIN__) +#define APIENTRY __stdcall +#elif (defined(_MSC_VER) && (_MSC_VER >= 800)) || defined(_STDCALL_SUPPORTED) || defined(__BORLANDC__) +#define APIENTRY __stdcall +#else +#define APIENTRY +#endif +#endif + +#ifndef GLAPI +#if defined(__MINGW32__) || defined(__CYGWIN__) +#define GLAPI extern +#elif defined(_WIN32) +#define GLAPI WINGDIAPI +#else +#define GLAPI extern +#endif +#endif + +#ifndef GLAPIENTRY +#define GLAPIENTRY APIENTRY +#endif + +typedef rmtU32 GLenum; +typedef rmtU32 GLuint; +typedef rmtS32 GLint; +typedef rmtS32 GLsizei; +typedef rmtU64 GLuint64; +typedef rmtS64 GLint64; +typedef unsigned char GLubyte; + +typedef GLenum(GLAPIENTRY* PFNGLGETERRORPROC)(void); +typedef void(GLAPIENTRY* PFNGLGENQUERIESPROC)(GLsizei n, GLuint* ids); +typedef void(GLAPIENTRY* PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint* ids); +typedef void(GLAPIENTRY* PFNGLBEGINQUERYPROC)(GLenum target, GLuint id); +typedef void(GLAPIENTRY* PFNGLENDQUERYPROC)(GLenum target); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTUIVPROC)(GLuint id, GLenum pname, GLuint* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64* params); +typedef void(GLAPIENTRY* PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64* params); +typedef void(GLAPIENTRY* PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target); +typedef void(GLAPIENTRY* PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64* data); +typedef void(GLAPIENTRY* PFNGLFINISHPROC)(void); + +#define GL_NO_ERROR 0 +#define GL_QUERY_RESULT 0x8866 +#define GL_QUERY_RESULT_AVAILABLE 0x8867 +#define GL_TIME_ELAPSED 0x88BF +#define GL_TIMESTAMP 0x8E28 + +#define RMT_GL_GET_FUN(x) \ + assert(g_Remotery->opengl->x != NULL); \ + g_Remotery->opengl->x + +#define rmtglGenQueries RMT_GL_GET_FUN(__glGenQueries) +#define rmtglDeleteQueries RMT_GL_GET_FUN(__glDeleteQueries) +#define rmtglBeginQuery RMT_GL_GET_FUN(__glBeginQuery) +#define rmtglEndQuery RMT_GL_GET_FUN(__glEndQuery) +#define rmtglGetQueryObjectiv RMT_GL_GET_FUN(__glGetQueryObjectiv) +#define rmtglGetQueryObjectuiv RMT_GL_GET_FUN(__glGetQueryObjectuiv) +#define rmtglGetQueryObjecti64v RMT_GL_GET_FUN(__glGetQueryObjecti64v) +#define rmtglGetQueryObjectui64v RMT_GL_GET_FUN(__glGetQueryObjectui64v) +#define rmtglQueryCounter RMT_GL_GET_FUN(__glQueryCounter) +#define rmtglGetInteger64v RMT_GL_GET_FUN(__glGetInteger64v) +#define rmtglFinish RMT_GL_GET_FUN(__glFinish) + +struct OpenGL_t +{ + // Handle to the OS OpenGL DLL + void* dll_handle; + + PFNGLGETERRORPROC __glGetError; + PFNGLGENQUERIESPROC __glGenQueries; + PFNGLDELETEQUERIESPROC __glDeleteQueries; + PFNGLBEGINQUERYPROC __glBeginQuery; + PFNGLENDQUERYPROC __glEndQuery; + PFNGLGETQUERYOBJECTIVPROC __glGetQueryObjectiv; + PFNGLGETQUERYOBJECTUIVPROC __glGetQueryObjectuiv; + PFNGLGETQUERYOBJECTI64VPROC __glGetQueryObjecti64v; + PFNGLGETQUERYOBJECTUI64VPROC __glGetQueryObjectui64v; + PFNGLQUERYCOUNTERPROC __glQueryCounter; + PFNGLGETINTEGER64VPROC __glGetInteger64v; + PFNGLFINISHPROC __glFinish; + + // Queue to the OpenGL main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_opengl_main; + + // Mark the first time so that remaining timestamps are offset from this + rmtU64 first_timestamp; + // Last time in us (CPU time, via usTimer_Get) since we last resync'ed CPU & GPU + rmtU64 last_resync; + + // Sample trees in transit in the message queue for release on shutdown + Buffer* flush_samples; +}; + +static GLenum rmtglGetError(void) +{ + if (g_Remotery != NULL) + { + assert(g_Remotery->opengl != NULL); + if (g_Remotery->opengl->__glGetError != NULL) + return g_Remotery->opengl->__glGetError(); + } + + return (GLenum)0; +} + +#ifdef RMT_PLATFORM_LINUX +#ifdef __cplusplus +extern "C" void* glXGetProcAddressARB(const GLubyte*); +#else +extern void* glXGetProcAddressARB(const GLubyte*); +#endif +#endif + +static ProcReturnType rmtglGetProcAddress(OpenGL* opengl, const char* symbol) +{ +#if defined(RMT_PLATFORM_WINDOWS) + { + // Get OpenGL extension-loading function for each call + typedef ProcReturnType(WINAPI * wglGetProcAddressFn)(LPCSTR); + assert(opengl != NULL); + { + wglGetProcAddressFn wglGetProcAddress = + (wglGetProcAddressFn)rmtGetProcAddress(opengl->dll_handle, "wglGetProcAddress"); + if (wglGetProcAddress != NULL) + return wglGetProcAddress(symbol); + } + } + +#elif defined(RMT_PLATFORM_MACOS) && !defined(GLEW_APPLE_GLX) + + return rmtGetProcAddress(opengl->dll_handle, symbol); + +#elif defined(RMT_PLATFORM_LINUX) + + return glXGetProcAddressARB((const GLubyte*)symbol); + +#endif + + return NULL; +} + +static rmtError OpenGL_Create(OpenGL** opengl) +{ + rmtError error; + + assert(opengl != NULL); + + *opengl = (OpenGL*)rmtMalloc(sizeof(OpenGL)); + if (*opengl == NULL) + return RMT_ERROR_MALLOC_FAIL; + + (*opengl)->dll_handle = NULL; + + (*opengl)->__glGetError = NULL; + (*opengl)->__glGenQueries = NULL; + (*opengl)->__glDeleteQueries = NULL; + (*opengl)->__glBeginQuery = NULL; + (*opengl)->__glEndQuery = NULL; + (*opengl)->__glGetQueryObjectiv = NULL; + (*opengl)->__glGetQueryObjectuiv = NULL; + (*opengl)->__glGetQueryObjecti64v = NULL; + (*opengl)->__glGetQueryObjectui64v = NULL; + (*opengl)->__glQueryCounter = NULL; + (*opengl)->__glGetInteger64v = NULL; + (*opengl)->__glFinish = NULL; + + (*opengl)->mq_to_opengl_main = NULL; + (*opengl)->first_timestamp = 0; + (*opengl)->last_resync = 0; + (*opengl)->flush_samples = NULL; + + New_1(Buffer, (*opengl)->flush_samples, 8 * 1024); + if (error != RMT_ERROR_NONE) + { + Delete(OpenGL, *opengl); + return error; + } + + New_1(rmtMessageQueue, (*opengl)->mq_to_opengl_main, g_Settings.messageQueueSizeInBytes); + + return error; +} + +static void OpenGL_Destructor(OpenGL* opengl) +{ + assert(opengl != NULL); + Delete(rmtMessageQueue, opengl->mq_to_opengl_main); + Delete(Buffer, opengl->flush_samples); +} + +static void SyncOpenGLCpuGpuTimes(rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + rmtU64 cpu_time_start = 0; + rmtU64 cpu_time_stop = 0; + rmtU64 average_half_RTT = 0; // RTT = Rountrip Time. + GLint64 gpu_base = 0; + int i; + + rmtglFinish(); + + for (i = 0; i < RMT_GPU_CPU_SYNC_NUM_ITERATIONS; ++i) + { + rmtU64 half_RTT; + + rmtglFinish(); + cpu_time_start = usTimer_Get(&g_Remotery->timer); + rmtglGetInteger64v(GL_TIMESTAMP, &gpu_base); + cpu_time_stop = usTimer_Get(&g_Remotery->timer); + // Average the time it takes a roundtrip from CPU to GPU + // while doing nothing other than getting timestamps + half_RTT = (cpu_time_stop - cpu_time_start) >> 1ULL; + if (i == 0) + average_half_RTT = half_RTT; + else + average_half_RTT = (average_half_RTT + half_RTT) >> 1ULL; + } + + // All GPU times are offset from gpu_base, and then taken to + // the same relative origin CPU timestamps are based on. + // CPU is in us, we must translate it to ns. + *out_first_timestamp = (rmtU64)(gpu_base) - (cpu_time_start + average_half_RTT) * 1000ULL; + *out_last_resync = cpu_time_stop; +} + +typedef struct OpenGLTimestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Pair of timestamp queries that wrap the sample + GLuint queries[2]; + rmtU64 cpu_timestamp; +} OpenGLTimestamp; + +static rmtError OpenGLTimestamp_Constructor(OpenGLTimestamp* stamp) +{ + GLenum error; + + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->queries[0] = stamp->queries[1] = 0; + stamp->cpu_timestamp = 0; + + // Empty the error queue before using it for glGenQueries + while ((error = rmtglGetError()) != GL_NO_ERROR) + ; + + // Create start/end timestamp queries + assert(g_Remotery != NULL); + rmtglGenQueries(2, stamp->queries); + error = rmtglGetError(); + if (error != GL_NO_ERROR) + return RMT_ERROR_OPENGL_ERROR; + + return RMT_ERROR_NONE; +} + +static void OpenGLTimestamp_Destructor(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // Destroy queries + if (stamp->queries[0] != 0) + rmtglDeleteQueries(2, stamp->queries); +} + +static void OpenGLTimestamp_Begin(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // First query + assert(g_Remotery != NULL); + stamp->cpu_timestamp = usTimer_Get(&g_Remotery->timer); + rmtglQueryCounter(stamp->queries[0], GL_TIMESTAMP); +} + +static void OpenGLTimestamp_End(OpenGLTimestamp* stamp) +{ + assert(stamp != NULL); + + // Second query + assert(g_Remotery != NULL); + rmtglQueryCounter(stamp->queries[1], GL_TIMESTAMP); +} + +static rmtBool OpenGLTimestamp_GetData(OpenGLTimestamp* stamp, rmtU64* out_start, rmtU64* out_end, + rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + GLuint64 start = 0, end = 0; + GLint startAvailable = 0, endAvailable = 0; + + assert(g_Remotery != NULL); + + assert(stamp != NULL); + assert(stamp->queries[0] != 0 && stamp->queries[1] != 0); + + // Check to see if all queries are ready + // If any fail to arrive, wait until later + rmtglGetQueryObjectiv(stamp->queries[0], GL_QUERY_RESULT_AVAILABLE, &startAvailable); + if (!startAvailable) + return RMT_FALSE; + rmtglGetQueryObjectiv(stamp->queries[1], GL_QUERY_RESULT_AVAILABLE, &endAvailable); + if (!endAvailable) + return RMT_FALSE; + + rmtglGetQueryObjectui64v(stamp->queries[0], GL_QUERY_RESULT, &start); + rmtglGetQueryObjectui64v(stamp->queries[1], GL_QUERY_RESULT, &end); + + // Mark the first timestamp. We may resync if we detect the GPU timestamp is in the + // past (i.e. happened before the CPU command) since it should be impossible. + assert(out_first_timestamp != NULL); + if (*out_first_timestamp == 0 || ((start - *out_first_timestamp) / 1000ULL) < stamp->cpu_timestamp) + SyncOpenGLCpuGpuTimes(out_first_timestamp, out_last_resync); + + // Calculate start and end timestamps (we want us, the queries give us ns) + *out_start = (rmtU64)(start - *out_first_timestamp) / 1000ULL; + *out_end = (rmtU64)(end - *out_first_timestamp) / 1000ULL; + + return RMT_TRUE; +} + +typedef struct OpenGLSample +{ + // IS-A inheritance relationship + Sample base; + + OpenGLTimestamp* timestamp; + +} OpenGLSample; + +static rmtError OpenGLSample_Constructor(OpenGLSample* sample) +{ + rmtError error; + + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = SampleType_OpenGL; + New_0(OpenGLTimestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void OpenGLSample_Destructor(OpenGLSample* sample) +{ + Delete(OpenGLTimestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +RMT_API void _rmt_BindOpenGL() +{ + if (g_Remotery != NULL) + { + OpenGL* opengl = g_Remotery->opengl; + assert(opengl != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + opengl->dll_handle = rmtLoadLibrary("opengl32.dll"); +#elif defined(RMT_PLATFORM_MACOS) + opengl->dll_handle = rmtLoadLibrary("/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL"); +#elif defined(RMT_PLATFORM_LINUX) + opengl->dll_handle = rmtLoadLibrary("libGL.so"); +#endif + + opengl->__glGetError = (PFNGLGETERRORPROC)rmtGetProcAddress(opengl->dll_handle, "glGetError"); + opengl->__glGenQueries = (PFNGLGENQUERIESPROC)rmtglGetProcAddress(opengl, "glGenQueries"); + opengl->__glDeleteQueries = (PFNGLDELETEQUERIESPROC)rmtglGetProcAddress(opengl, "glDeleteQueries"); + opengl->__glBeginQuery = (PFNGLBEGINQUERYPROC)rmtglGetProcAddress(opengl, "glBeginQuery"); + opengl->__glEndQuery = (PFNGLENDQUERYPROC)rmtglGetProcAddress(opengl, "glEndQuery"); + opengl->__glGetQueryObjectiv = (PFNGLGETQUERYOBJECTIVPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectiv"); + opengl->__glGetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectuiv"); + opengl->__glGetQueryObjecti64v = + (PFNGLGETQUERYOBJECTI64VPROC)rmtglGetProcAddress(opengl, "glGetQueryObjecti64v"); + opengl->__glGetQueryObjectui64v = + (PFNGLGETQUERYOBJECTUI64VPROC)rmtglGetProcAddress(opengl, "glGetQueryObjectui64v"); + opengl->__glQueryCounter = (PFNGLQUERYCOUNTERPROC)rmtglGetProcAddress(opengl, "glQueryCounter"); + opengl->__glGetInteger64v = (PFNGLGETINTEGER64VPROC)rmtglGetProcAddress(opengl, "glGetInteger64v"); + opengl->__glFinish = (PFNGLFINISHPROC)rmtGetProcAddress(opengl->dll_handle, "glFinish"); + } +} + +static void UpdateOpenGLFrame(void); + +RMT_API void _rmt_UnbindOpenGL(void) +{ + if (g_Remotery != NULL) + { + OpenGL* opengl = g_Remotery->opengl; + assert(opengl != NULL); + + // Stall waiting for the OpenGL queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(opengl->mq_to_opengl_main)) + UpdateOpenGLFrame(); + + // There will be a whole bunch of OpenGL sample trees queued up the remotery queue that need releasing + FreePendingSampleTrees(g_Remotery, SampleType_OpenGL, opengl->flush_samples); + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_DeleteSampleTree(g_Remotery, SampleType_OpenGL); + + // Release reference to the OpenGL DLL + if (opengl->dll_handle != NULL) + { + rmtFreeLibrary(opengl->dll_handle); + opengl->dll_handle = NULL; + } + } +} + +RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the OpenGL tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a OpenGL binding is not yet available. + SampleTree** ogl_tree = &thread_profiler->sampleTrees[SampleType_OpenGL]; + if (*ogl_tree == NULL) + { + rmtError error; + New_3(SampleTree, *ogl_tree, sizeof(OpenGLSample), (ObjConstructor)OpenGLSample_Constructor, + (ObjDestructor)OpenGLSample_Destructor); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*ogl_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + OpenGLSample* ogl_sample = (OpenGLSample*)sample; + OpenGLTimestamp_Begin(ogl_sample->timestamp); + } + } +} + +static rmtBool GetOpenGLSampleTimes(Sample* sample, rmtU64* out_first_timestamp, rmtU64* out_last_resync) +{ + Sample* child; + + OpenGLSample* ogl_sample = (OpenGLSample*)sample; + + assert(sample != NULL); + if (ogl_sample->timestamp != NULL) + { + assert(out_last_resync != NULL); +#if (RMT_GPU_CPU_SYNC_SECONDS > 0) + if (*out_last_resync < ogl_sample->timestamp->cpu_timestamp) + { + // Convert from us to seconds. + rmtU64 time_diff = (ogl_sample->timestamp->cpu_timestamp - *out_last_resync) / 1000000ULL; + if (time_diff > RMT_GPU_CPU_SYNC_SECONDS) + SyncOpenGLCpuGpuTimes(out_first_timestamp, out_last_resync); + } +#endif + + if (!OpenGLTimestamp_GetData(ogl_sample->timestamp, &sample->us_start, &sample->us_end, out_first_timestamp, + out_last_resync)) + return RMT_FALSE; + + sample->us_length = sample->us_end - sample->us_start; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetOpenGLSampleTimes(child, out_first_timestamp, out_last_resync)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateOpenGLFrame(void) +{ + OpenGL* opengl; + + if (g_Remotery == NULL) + return; + + opengl = g_Remotery->opengl; + assert(opengl != NULL); + + rmt_BeginCPUSample(rmt_UpdateOpenGLFrame, 0); + + // Process all messages in the OpenGL queue + while (1) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(opengl->mq_to_opengl_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == SampleType_OpenGL); + + // Retrieve timing of all OpenGL samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetOpenGLSampleTimes(sample, &opengl->first_timestamp, &opengl->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(opengl->mq_to_opengl_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndOpenGLSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + // Close the timestamp + OpenGLSample* ogl_sample = (OpenGLSample*)thread_profiler->sampleTrees[SampleType_OpenGL]->currentParent; + if (ogl_sample->base.recurse_depth > 0) + { + ogl_sample->base.recurse_depth--; + } + else + { + if (ogl_sample->timestamp != NULL) + OpenGLTimestamp_End(ogl_sample->timestamp); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, g_Remotery->opengl->mq_to_opengl_main, (Sample*)ogl_sample)) + // Perform ready-polling on popping of the root sample + UpdateOpenGLFrame(); + } + } +} + +#endif // RMT_USE_OPENGL + +/* + ------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------------------------------------------------------ + @Metal: Metal event sampling + ------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------------------------------------------------------ + */ + +#if RMT_USE_METAL + +struct Metal_t +{ + // Queue to the Metal main update thread + // Given that BeginSample/EndSample need to be called from the same thread that does the update, there + // is really no need for this to be a thread-safe queue. I'm using it for its convenience. + rmtMessageQueue* mq_to_metal_main; +}; + +static rmtError Metal_Create(Metal** metal) +{ + rmtError error; + + assert(metal != NULL); + + *metal = (Metal*)rmtMalloc(sizeof(Metal)); + if (*metal == NULL) + return RMT_ERROR_MALLOC_FAIL; + + (*metal)->mq_to_metal_main = NULL; + + New_1(rmtMessageQueue, (*metal)->mq_to_metal_main, g_Settings.messageQueueSizeInBytes); + return error; +} + +static void Metal_Destructor(Metal* metal) +{ + assert(metal != NULL); + Delete(rmtMessageQueue, metal->mq_to_metal_main); +} + +typedef struct MetalTimestamp +{ + // Inherit so that timestamps can be quickly allocated + ObjectLink Link; + + // Output from GPU callbacks + rmtU64 start; + rmtU64 end; + rmtBool ready; +} MetalTimestamp; + +static rmtError MetalTimestamp_Constructor(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->start = 0; + stamp->end = 0; + stamp->ready = RMT_FALSE; + + return RMT_ERROR_NONE; +} + +static void MetalTimestamp_Destructor(MetalTimestamp* stamp) +{ + assert(stamp != NULL); +} + +rmtU64 rmtMetal_usGetTime() +{ + // Share the CPU timer for auto-sync + assert(g_Remotery != NULL); + return usTimer_Get(&g_Remotery->timer); +} + +void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready); + +static void MetalTimestamp_Begin(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + stamp->ready = RMT_FALSE; + + // Metal can currently only issue callbacks at the command buffer level + // So for now measure execution of the entire command buffer + rmtMetal_MeasureCommandBuffer(&stamp->start, &stamp->end, &stamp->ready); +} + +static void MetalTimestamp_End(MetalTimestamp* stamp) +{ + assert(stamp != NULL); + + // As Metal can currently only measure entire command buffers, this function is a no-op + // as the completed handler was already issued in Begin +} + +static rmtBool MetalTimestamp_GetData(MetalTimestamp* stamp, rmtU64* out_start, rmtU64* out_end) +{ + assert(g_Remotery != NULL); + assert(stamp != NULL); + + // GPU writes ready flag when complete handler is called + if (stamp->ready == RMT_FALSE) + return RMT_FALSE; + + *out_start = stamp->start; + *out_end = stamp->end; + + return RMT_TRUE; +} + +typedef struct MetalSample +{ + // IS-A inheritance relationship + Sample base; + + MetalTimestamp* timestamp; + +} MetalSample; + +static rmtError MetalSample_Constructor(MetalSample* sample) +{ + rmtError error; + + assert(sample != NULL); + + // Chain to sample constructor + Sample_Constructor((Sample*)sample); + sample->base.type = SampleType_Metal; + New_0(MetalTimestamp, sample->timestamp); + + return RMT_ERROR_NONE; +} + +static void MetalSample_Destructor(MetalSample* sample) +{ + Delete(MetalTimestamp, sample->timestamp); + Sample_Destructor((Sample*)sample); +} + +static void UpdateOpenGLFrame(void); + +/*RMT_API void _rmt_UnbindMetal(void) +{ + if (g_Remotery != NULL) + { + Metal* metal = g_Remotery->metal; + assert(metal != NULL); + + // Stall waiting for the Metal queue to empty into the Remotery queue + while (!rmtMessageQueue_IsEmpty(metal->mq_to_metal_main)) + UpdateMetalFrame(); + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_BlockingDeleteSampleTree(g_Remotery, SampleType_Metal); + } +}*/ + +RMT_API void _rmt_BeginMetalSample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadProfiler_GetNameHash(thread_profiler, g_Remotery->mq_to_rmt_thread, name, hash_cache); + + // Create the Metal tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a Metal binding is not yet available. + SampleTree** metal_tree = &thread_profiler->sampleTrees[SampleType_Metal]; + if (*metal_tree == NULL) + { + rmtError error; + New_3(SampleTree, *metal_tree, sizeof(MetalSample), (ObjConstructor)MetalSample_Constructor, + (ObjDestructor)MetalSample_Destructor); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the sample and activate the timestamp + if (ThreadProfiler_Push(*metal_tree, name_hash, 0, &sample) == RMT_ERROR_NONE) + { + MetalSample* metal_sample = (MetalSample*)sample; + MetalTimestamp_Begin(metal_sample->timestamp); + } + } +} + +static rmtBool GetMetalSampleTimes(Sample* sample) +{ + Sample* child; + + MetalSample* metal_sample = (MetalSample*)sample; + + assert(sample != NULL); + if (metal_sample->timestamp != NULL) + { + if (!MetalTimestamp_GetData(metal_sample->timestamp, &sample->us_start, &sample->us_end)) + return RMT_FALSE; + + sample->us_length = sample->us_end - sample->us_start; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetMetalSampleTimes(child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + +static void UpdateMetalFrame(void) +{ + Metal* metal; + + if (g_Remotery == NULL) + return; + + metal = g_Remotery->metal; + assert(metal != NULL); + + rmt_BeginCPUSample(rmt_UpdateMetalFrame, 0); + + // Process all messages in the Metal queue + while (1) + { + Msg_SampleTree* sample_tree; + Sample* sample; + + Message* message = rmtMessageQueue_PeekNextMessage(metal->mq_to_metal_main); + if (message == NULL) + break; + + // There's only one valid message type in this queue + assert(message->id == MsgID_SampleTree); + sample_tree = (Msg_SampleTree*)message->payload; + sample = sample_tree->rootSample; + assert(sample->type == SampleType_Metal); + + // Retrieve timing of all Metal samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetMetalSampleTimes(sample)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + QueueSampleTree(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->threadName, + message->threadProfiler, RMT_FALSE); + rmtMessageQueue_ConsumeNextMessage(metal->mq_to_metal_main, message); + } + + rmt_EndCPUSample(); +} + +RMT_API void _rmt_EndMetalSample(void) +{ + ThreadProfiler* thread_profiler; + + if (g_Remotery == NULL) + return; + + if (ThreadProfilers_GetCurrentThreadProfiler(g_Remotery->threadProfilers, &thread_profiler) == RMT_ERROR_NONE) + { + // Close the timestamp + MetalSample* metal_sample = (MetalSample*)thread_profiler->sampleTrees[SampleType_Metal]->currentParent; + if (metal_sample->base.recurse_depth > 0) + { + metal_sample->base.recurse_depth--; + } + else + { + if (metal_sample->timestamp != NULL) + MetalTimestamp_End(metal_sample->timestamp); + + // Send to the update loop for ready-polling + if (ThreadProfiler_Pop(thread_profiler, g_Remotery->metal->mq_to_metal_main, (Sample*)metal_sample)) + // Perform ready-polling on popping of the root sample + UpdateMetalFrame(); + } + } +} + +#endif // RMT_USE_METAL + +#endif // RMT_ENABLED diff --git a/include/Remotery.h b/include/Remotery.h new file mode 100644 index 0000000..9fc52f9 --- /dev/null +++ b/include/Remotery.h @@ -0,0 +1,679 @@ + + +/* +Copyright 2014-2018 Celtoys Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +/* + +Compiling +--------- + +* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include + directories to add Remotery/lib path. The required library ws2_32.lib should be picked + up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c. + +* Mac OS X (XCode) - simply add lib/Remotery.c and lib/Remotery.h to your program. + +* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for + library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c + -I lib -pthread -lm + +You can define some extra macros to modify what features are compiled into Remotery. These are +documented just below this comment. + +*/ + + +#ifndef RMT_INCLUDED_H +#define RMT_INCLUDED_H + + +// Set to 0 to not include any bits of Remotery in your build +#ifndef RMT_ENABLED +#define RMT_ENABLED 1 +#endif + +// Help performance of the server sending data to the client by marking this machine as little-endian +#ifndef RMT_ASSUME_LITTLE_ENDIAN +#define RMT_ASSUME_LITTLE_ENDIAN 0 +#endif + +// Used by the Celtoys TinyCRT library (not released yet) +#ifndef RMT_USE_TINYCRT +#define RMT_USE_TINYCRT 0 +#endif + +// Assuming CUDA headers/libs are setup, allow CUDA profiling +#ifndef RMT_USE_CUDA +#define RMT_USE_CUDA 0 +#endif + +// Assuming Direct3D 11 headers/libs are setup, allow D3D11 profiling +#ifndef RMT_USE_D3D11 +#define RMT_USE_D3D11 0 +#endif + +// Allow OpenGL profiling +#ifndef RMT_USE_OPENGL +#define RMT_USE_OPENGL 0 +#endif + +// Allow Metal profiling +#ifndef RMT_USE_METAL +#define RMT_USE_METAL 0 +#endif + +// Initially use POSIX thread names to name threads instead of Thread0, 1, ... +#ifndef RMT_USE_POSIX_THREADNAMES +#define RMT_USE_POSIX_THREADNAMES 0 +#endif + +// How many times we spin data back and forth between CPU & GPU +// to calculate average RTT (Roundtrip Time). Cannot be 0. +// Affects OpenGL & D3D11 +#ifndef RMT_GPU_CPU_SYNC_NUM_ITERATIONS +#define RMT_GPU_CPU_SYNC_NUM_ITERATIONS 16 +#endif + +// Time in seconds between each resync to compensate for drifting between GPU & CPU timers, +// effects of power saving, etc. Resyncs can cause stutter, lag spikes, stalls. +// Set to 0 for never. +// Affects OpenGL & D3D11 +#ifndef RMT_GPU_CPU_SYNC_SECONDS +#define RMT_GPU_CPU_SYNC_SECONDS 30 +#endif + +// Whether we should automatically resync if we detect a timer disjoint (e.g. +// changed from AC power to battery, GPU is overheating, or throttling up/down +// due to laptop savings events). Set it to 0 to avoid resync in such events. +// Useful if for some odd reason a driver reports a lot of disjoints. +// Affects D3D11 +#ifndef RMT_D3D11_RESYNC_ON_DISJOINT +#define RMT_D3D11_RESYNC_ON_DISJOINT 1 +#endif + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + Compiler/Platform Detection and Preprocessor Utilities +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + +// Platform identification +#if defined(_WINDOWS) || defined(_WIN32) + #define RMT_PLATFORM_WINDOWS +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) + #define RMT_PLATFORM_LINUX + #define RMT_PLATFORM_POSIX +#elif defined(__APPLE__) + #define RMT_PLATFORM_MACOS + #define RMT_PLATFORM_POSIX +#endif + +// Architecture identification +#ifdef RMT_PLATFORM_WINDOWS +#ifdef _M_AMD64 +#define RMT_ARCH_64BIT +#else +#define RMT_ARCH_32BIT +#endif +#endif + +#ifdef RMT_DLL + #if defined (RMT_PLATFORM_WINDOWS) + #if defined (RMT_IMPL) + #define RMT_API __declspec(dllexport) + #else + #define RMT_API __declspec(dllimport) + #endif + #elif defined (RMT_PLATFORM_POSIX) + #if defined (RMT_IMPL) + #define RMT_API __attribute__((visibility("default"))) + #else + #define RMT_API + #endif + #endif +#else + #define RMT_API +#endif + +// Allows macros to be written that can work around the inability to do: #define(x) #ifdef x +// with the C preprocessor. +#if RMT_ENABLED + #define IFDEF_RMT_ENABLED(t, f) t +#else + #define IFDEF_RMT_ENABLED(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_CUDA + #define IFDEF_RMT_USE_CUDA(t, f) t +#else + #define IFDEF_RMT_USE_CUDA(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_D3D11 + #define IFDEF_RMT_USE_D3D11(t, f) t +#else + #define IFDEF_RMT_USE_D3D11(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_OPENGL + #define IFDEF_RMT_USE_OPENGL(t, f) t +#else + #define IFDEF_RMT_USE_OPENGL(t, f) f +#endif +#if RMT_ENABLED && RMT_USE_METAL + #define IFDEF_RMT_USE_METAL(t, f) t +#else + #define IFDEF_RMT_USE_METAL(t, f) f +#endif + + +// Public interface is written in terms of these macros to easily enable/disable itself +#define RMT_OPTIONAL(macro, x) IFDEF_ ## macro(x, ) +#define RMT_OPTIONAL_RET(macro, x, y) IFDEF_ ## macro(x, (y)) + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + Types +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +// Boolean +typedef unsigned int rmtBool; +#define RMT_TRUE ((rmtBool)1) +#define RMT_FALSE ((rmtBool)0) + + +// Unsigned integer types +typedef unsigned char rmtU8; +typedef unsigned short rmtU16; +typedef unsigned int rmtU32; +typedef unsigned long long rmtU64; + + +// Signed integer types +typedef char rmtS8; +typedef short rmtS16; +typedef int rmtS32; +typedef long long rmtS64; + + +// Const, null-terminated string pointer +typedef const char* rmtPStr; + + +// Handle to the main remotery instance +typedef struct Remotery Remotery; + +// All possible error codes +// clang-format off +typedef enum rmtError +{ + RMT_ERROR_NONE, + RMT_ERROR_RECURSIVE_SAMPLE, // Not an error but an internal message to calling code + RMT_ERROR_UNKNOWN, // An error with a message yet to be defined, only for internal error handling + + // System errors + RMT_ERROR_MALLOC_FAIL, // Malloc call within remotery failed + RMT_ERROR_TLS_ALLOC_FAIL, // Attempt to allocate thread local storage failed + RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL, // Failed to create a virtual memory mirror buffer + RMT_ERROR_CREATE_THREAD_FAIL, // Failed to create a thread for the server + RMT_ERROR_OPEN_THREAD_HANDLE_FAIL, // Failed to open a thread handle, given a thread id + + // Network TCP/IP socket errors + RMT_ERROR_SOCKET_INIT_NETWORK_FAIL, // Network initialisation failure (e.g. on Win32, WSAStartup fails) + RMT_ERROR_SOCKET_CREATE_FAIL, // Can't create a socket for connection to the remote viewer + RMT_ERROR_SOCKET_BIND_FAIL, // Can't bind a socket for the server + RMT_ERROR_SOCKET_LISTEN_FAIL, // Created server socket failed to enter a listen state + RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL, // Created server socket failed to switch to a non-blocking state + RMT_ERROR_SOCKET_INVALID_POLL, // Poll attempt on an invalid socket + RMT_ERROR_SOCKET_SELECT_FAIL, // Server failed to call select on socket + RMT_ERROR_SOCKET_POLL_ERRORS, // Poll notified that the socket has errors + RMT_ERROR_SOCKET_ACCEPT_FAIL, // Server failed to accept connection from client + RMT_ERROR_SOCKET_SEND_TIMEOUT, // Timed out trying to send data + RMT_ERROR_SOCKET_SEND_FAIL, // Unrecoverable error occured while client/server tried to send data + RMT_ERROR_SOCKET_RECV_NO_DATA, // No data available when attempting a receive + RMT_ERROR_SOCKET_RECV_TIMEOUT, // Timed out trying to receive data + RMT_ERROR_SOCKET_RECV_FAILED, // Unrecoverable error occured while client/server tried to receive data + + // WebSocket errors + RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET, // WebSocket server handshake failed, not HTTP GET + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION, // WebSocket server handshake failed, can't locate WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION, // WebSocket server handshake failed, unsupported WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST, // WebSocket server handshake failed, can't locate host + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST, // WebSocket server handshake failed, host is not allowed to connect + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY, // WebSocket server handshake failed, can't locate WebSocket key + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY, // WebSocket server handshake failed, WebSocket key is ill-formed + RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL, // WebSocket server handshake failed, internal error, bad string code + RMT_ERROR_WEBSOCKET_DISCONNECTED, // WebSocket server received a disconnect request and closed the socket + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER, // Couldn't parse WebSocket frame header + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE, // Partially received wide frame header size + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_MASK, // Partially received frame header data mask + RMT_ERROR_WEBSOCKET_RECEIVE_TIMEOUT, // Timeout receiving frame header + + RMT_ERROR_REMOTERY_NOT_CREATED, // Remotery object has not been created + RMT_ERROR_SEND_ON_INCOMPLETE_PROFILE, // An attempt was made to send an incomplete profile tree to the client + + // CUDA error messages + RMT_ERROR_CUDA_DEINITIALIZED, // This indicates that the CUDA driver is in the process of shutting down + RMT_ERROR_CUDA_NOT_INITIALIZED, // This indicates that the CUDA driver has not been initialized with cuInit() or that initialization has failed + RMT_ERROR_CUDA_INVALID_CONTEXT, // This most frequently indicates that there is no context bound to the current thread + RMT_ERROR_CUDA_INVALID_VALUE, // This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values + RMT_ERROR_CUDA_INVALID_HANDLE, // This indicates that a resource handle passed to the API call was not valid + RMT_ERROR_CUDA_OUT_OF_MEMORY, // The API call failed because it was unable to allocate enough memory to perform the requested operation + RMT_ERROR_ERROR_NOT_READY, // This indicates that a resource handle passed to the API call was not valid + + // Direct3D 11 error messages + RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY, // Failed to create query for sample + + // OpenGL error messages + RMT_ERROR_OPENGL_ERROR, // Generic OpenGL error, no need to expose detail since app will need an OpenGL error callback registered + + RMT_ERROR_CUDA_UNKNOWN, +} rmtError; +// clang-format on + +typedef enum rmtSampleFlags +{ + // Default behaviour + RMTSF_None = 0, + + // Search parent for same-named samples and merge timing instead of adding a new sample + RMTSF_Aggregate = 1, + + // Merge sample with parent if it's the same sample + RMTSF_Recursive = 2, + + // Set this flag on any of your root samples so that Remotery will assert if it ends up *not* being the root sample. + // This will quickly allow you to detect Begin/End mismatches causing a sample tree imbalance. + RMTSF_Root = 4, +} rmtSampleFlags; + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + Public Interface +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +// Can call remotery functions on a null pointer +// TODO: Can embed extern "C" in these macros? + +#define rmt_Settings() \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_Settings(), NULL ) + +#define rmt_CreateGlobalInstance(rmt) \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_CreateGlobalInstance(rmt), RMT_ERROR_NONE) + +#define rmt_DestroyGlobalInstance(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_DestroyGlobalInstance(rmt)) + +#define rmt_SetGlobalInstance(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_SetGlobalInstance(rmt)) + +#define rmt_GetGlobalInstance() \ + RMT_OPTIONAL_RET(RMT_ENABLED, _rmt_GetGlobalInstance(), NULL) + +#define rmt_SetCurrentThreadName(rmt) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_SetCurrentThreadName(rmt)) + +#define rmt_LogText(text) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_LogText(text)) + +#define rmt_BeginCPUSample(name, flags) \ + RMT_OPTIONAL(RMT_ENABLED, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginCPUSample(#name, flags, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginCPUSampleDynamic(namestr, flags) \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_BeginCPUSample(namestr, flags, NULL)) + +#define rmt_EndCPUSample() \ + RMT_OPTIONAL(RMT_ENABLED, _rmt_EndCPUSample()) + + +// Callback function pointer types +typedef void* (*rmtMallocPtr)(void* mm_context, rmtU32 size); +typedef void* (*rmtReallocPtr)(void* mm_context, void* ptr, rmtU32 size); +typedef void (*rmtFreePtr)(void* mm_context, void* ptr); +typedef void (*rmtInputHandlerPtr)(const char* text, void* context); + + +// Struture to fill in to modify Remotery default settings +typedef struct rmtSettings +{ + // Which port to listen for incoming connections on + rmtU16 port; + + // When this server exits it can leave the port open in TIME_WAIT state for a while. This forces + // subsequent server bind attempts to fail when restarting. If you find restarts fail repeatedly + // with bind attempts, set this to true to forcibly reuse the open port. + rmtBool reuse_open_port; + + // Only allow connections on localhost? + // For dev builds you may want to access your game from other devices but if + // you distribute a game to your players with Remotery active, probably best + // to limit connections to localhost. + rmtBool limit_connections_to_localhost; + + // Whether to enable runtime thread sampling that discovers which processors a thread is running + // on. This will suspend and resume threads from outside repeatdly and inject code into each + // thread that automatically instruments the processor. + // Default: Enabled + rmtBool enableThreadSampler; + + // How long to sleep between server updates, hopefully trying to give + // a little CPU back to other threads. + rmtU32 msSleepBetweenServerUpdates; + + // Size of the internal message queues Remotery uses + // Will be rounded to page granularity of 64k + rmtU32 messageQueueSizeInBytes; + + // If the user continuously pushes to the message queue, the server network + // code won't get a chance to update unless there's an upper-limit on how + // many messages can be consumed per loop. + rmtU32 maxNbMessagesPerUpdate; + + // Callback pointers for memory allocation + rmtMallocPtr malloc; + rmtReallocPtr realloc; + rmtFreePtr free; + void* mm_context; + + // Callback pointer for receiving input from the Remotery console + rmtInputHandlerPtr input_handler; + + // Context pointer that gets sent to Remotery console callback function + void* input_handler_context; + + rmtPStr logPath; +} rmtSettings; + + +// Structure to fill in when binding CUDA to Remotery +typedef struct rmtCUDABind +{ + // The main context that all driver functions apply before each call + void* context; + + // Driver API function pointers that need to be pointed to + // Untyped so that the CUDA headers are not required in this file + // NOTE: These are named differently to the CUDA functions because the CUDA API has a habit of using + // macros to point function calls to different versions, e.g. cuEventDestroy is a macro for + // cuEventDestroy_v2. + void* CtxSetCurrent; + void* CtxGetCurrent; + void* EventCreate; + void* EventDestroy; + void* EventRecord; + void* EventQuery; + void* EventElapsedTime; + +} rmtCUDABind; + + +// Call once after you've initialised CUDA to bind it to Remotery +#define rmt_BindCUDA(bind) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_BindCUDA(bind)) + +// Mark the beginning of a CUDA sample on the specified asynchronous stream +#define rmt_BeginCUDASample(name, stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginCUDASample(#name, &rmt_sample_hash_##name, stream); \ + }) + +// Mark the end of a CUDA sample on the specified asynchronous stream +#define rmt_EndCUDASample(stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_EndCUDASample(stream)) + + +#define rmt_BindD3D11(device, context) \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BindD3D11(device, context)) + +#define rmt_UnbindD3D11() \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_UnbindD3D11()) + +#define rmt_BeginD3D11Sample(name) \ + RMT_OPTIONAL(RMT_USE_D3D11, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginD3D11Sample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginD3D11SampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BeginD3D11Sample(namestr, NULL)) + +#define rmt_EndD3D11Sample() \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_EndD3D11Sample()) + + +#define rmt_BindOpenGL() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BindOpenGL()) + +#define rmt_UnbindOpenGL() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_UnbindOpenGL()) + +#define rmt_BeginOpenGLSample(name) \ + RMT_OPTIONAL(RMT_USE_OPENGL, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginOpenGLSample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginOpenGLSampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_BeginOpenGLSample(namestr, NULL)) + +#define rmt_EndOpenGLSample() \ + RMT_OPTIONAL(RMT_USE_OPENGL, _rmt_EndOpenGLSample()) + + +#define rmt_BindMetal(command_buffer) \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_BindMetal(command_buffer)); + +#define rmt_UnbindMetal() \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_UnbindMetal()); + +#define rmt_BeginMetalSample(name) \ + RMT_OPTIONAL(RMT_USE_METAL, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginMetalSample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_BeginMetalSampleDynamic(namestr) \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_BeginMetalSample(namestr, NULL)) + +#define rmt_EndMetalSample() \ + RMT_OPTIONAL(RMT_USE_METAL, _rmt_EndMetalSample()) + + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + C++ Public Interface Extensions +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#ifdef __cplusplus + + +#if RMT_ENABLED + +// Types that end samples in their destructors +extern "C" RMT_API void _rmt_EndCPUSample(void); +struct rmt_EndCPUSampleOnScopeExit +{ + ~rmt_EndCPUSampleOnScopeExit() + { + _rmt_EndCPUSample(); + } +}; +#if RMT_USE_CUDA +extern "C" RMT_API void _rmt_EndCUDASample(void* stream); +struct rmt_EndCUDASampleOnScopeExit +{ + rmt_EndCUDASampleOnScopeExit(void* stream) : stream(stream) + { + } + ~rmt_EndCUDASampleOnScopeExit() + { + _rmt_EndCUDASample(stream); + } + void* stream; +}; +#endif +#if RMT_USE_D3D11 +extern "C" RMT_API void _rmt_EndD3D11Sample(void); +struct rmt_EndD3D11SampleOnScopeExit +{ + ~rmt_EndD3D11SampleOnScopeExit() + { + _rmt_EndD3D11Sample(); + } +}; +#endif + +#if RMT_USE_OPENGL +extern "C" RMT_API void _rmt_EndOpenGLSample(void); +struct rmt_EndOpenGLSampleOnScopeExit +{ + ~rmt_EndOpenGLSampleOnScopeExit() + { + _rmt_EndOpenGLSample(); + } +}; +#endif + +#if RMT_USE_METAL +extern "C" RMT_API void _rmt_EndMetalSample(void); +struct rmt_EndMetalSampleOnScopeExit +{ + ~rmt_EndMetalSampleOnScopeExit() + { + _rmt_EndMetalSample(); + } +}; +#endif + +#endif + + + +// Pairs a call to rmt_BeginSample with its call to rmt_EndSample when leaving scope +#define rmt_ScopedCPUSample(name, flags) \ + RMT_OPTIONAL(RMT_ENABLED, rmt_BeginCPUSample(name, flags)); \ + RMT_OPTIONAL(RMT_ENABLED, rmt_EndCPUSampleOnScopeExit rmt_ScopedCPUSample##name); +#define rmt_ScopedCUDASample(name, stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, rmt_BeginCUDASample(name, stream)); \ + RMT_OPTIONAL(RMT_USE_CUDA, rmt_EndCUDASampleOnScopeExit rmt_ScopedCUDASample##name(stream)); +#define rmt_ScopedD3D11Sample(name) \ + RMT_OPTIONAL(RMT_USE_D3D11, rmt_BeginD3D11Sample(name)); \ + RMT_OPTIONAL(RMT_USE_D3D11, rmt_EndD3D11SampleOnScopeExit rmt_ScopedD3D11Sample##name); +#define rmt_ScopedOpenGLSample(name) \ + RMT_OPTIONAL(RMT_USE_OPENGL, rmt_BeginOpenGLSample(name)); \ + RMT_OPTIONAL(RMT_USE_OPENGL, rmt_EndOpenGLSampleOnScopeExit rmt_ScopedOpenGLSample##name); +#define rmt_ScopedMetalSample(name) \ + RMT_OPTIONAL(RMT_USE_METAL, rmt_BeginMetalSample(name)); \ + RMT_OPTIONAL(RMT_USE_METAL, rmt_EndMetalSampleOnScopeExit rmt_ScopedMetalSample##name); + +#endif + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + Private Interface - don't directly call these +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#if RMT_ENABLED + +#ifdef __cplusplus +extern "C" { +#endif + +RMT_API rmtSettings* _rmt_Settings( void ); +RMT_API enum rmtError _rmt_CreateGlobalInstance(Remotery** remotery); +RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery); +RMT_API void _rmt_SetGlobalInstance(Remotery* remotery); +RMT_API Remotery* _rmt_GetGlobalInstance(void); +RMT_API void _rmt_SetCurrentThreadName(rmtPStr thread_name); +RMT_API void _rmt_LogText(rmtPStr text); +RMT_API void _rmt_BeginCPUSample(rmtPStr name, rmtU32 flags, rmtU32* hash_cache); +RMT_API void _rmt_EndCPUSample(void); + +#if RMT_USE_CUDA +RMT_API void _rmt_BindCUDA(const rmtCUDABind* bind); +RMT_API void _rmt_BeginCUDASample(rmtPStr name, rmtU32* hash_cache, void* stream); +RMT_API void _rmt_EndCUDASample(void* stream); +#endif + +#if RMT_USE_D3D11 +RMT_API void _rmt_BindD3D11(void* device, void* context); +RMT_API void _rmt_UnbindD3D11(void); +RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndD3D11Sample(void); +#endif + +#if RMT_USE_OPENGL +RMT_API void _rmt_BindOpenGL(); +RMT_API void _rmt_UnbindOpenGL(void); +RMT_API void _rmt_BeginOpenGLSample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndOpenGLSample(void); +#endif + +#if RMT_USE_METAL +RMT_API void _rmt_BeginMetalSample(rmtPStr name, rmtU32* hash_cache); +RMT_API void _rmt_EndMetalSample(void); +#endif + +#ifdef __cplusplus + +} +#endif + +#if RMT_USE_METAL +#ifdef __OBJC__ +RMT_API void _rmt_BindMetal(id command_buffer); +RMT_API void _rmt_UnbindMetal(); +#endif +#endif + +#endif // RMT_ENABLED + + +#endif diff --git a/include/RemoteryMetal.mm b/include/RemoteryMetal.mm new file mode 100644 index 0000000..bb69da9 --- /dev/null +++ b/include/RemoteryMetal.mm @@ -0,0 +1,59 @@ +// +// Copyright 2014-2018 Celtoys Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include +#include +#include + +#import + +// Store command buffer in thread-local so that each thread can point to its own +static void SetCommandBuffer(id command_buffer) +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + thread_data[@"rmtMTLCommandBuffer"] = command_buffer; +} + +static id GetCommandBuffer() +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + return thread_data[@"rmtMTLCommandBuffer"]; +} + +extern "C" void _rmt_BindMetal(id command_buffer) +{ + SetCommandBuffer(command_buffer); +} + +extern "C" void _rmt_UnbindMetal() +{ + SetCommandBuffer(0); +} + +// Needs to be in the same lib for this to work +extern "C" unsigned long long rmtMetal_usGetTime(); + +static void SetTimestamp(void* data) +{ + *((unsigned long long*)data) = rmtMetal_usGetTime(); +} + +extern "C" void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready) +{ + id command_buffer = GetCommandBuffer(); + [command_buffer addScheduledHandler:^(id ){ SetTimestamp(out_start); }]; + [command_buffer addCompletedHandler:^(id ){ SetTimestamp(out_end); *out_ready = 1; }]; +} diff --git a/main.cpp b/main.cpp index f6b7e83..3eae6ce 100644 --- a/main.cpp +++ b/main.cpp @@ -2,8 +2,17 @@ #include #include -#include "rtweekend.hpp" +// Lib includes +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wsign-compare" +#include +#pragma GCC diagnostic pop + +// Internal includes +#include "rtweekend.hpp" #include "color.hpp" #include "hittable_list.hpp" #include "sphere.hpp" @@ -69,6 +78,7 @@ hittable_list random_scene() { color ray_color(const ray& r, const hittable& world, int32_t depth) { + rmt_ScopedCPUSample(Scatter, RMTSF_Aggregate | RMTSF_Recursive); if (depth <= 0) { return color(0,0,0); @@ -79,7 +89,10 @@ color ray_color(const ray& r, const hittable& world, int32_t depth) { ray scattered; color attenuation; - if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) + rmt_BeginCPUSample(Scatter, RMTSF_Aggregate); + bool visible = rec.mat_ptr->scatter(r, rec, attenuation, scattered); + rmt_EndCPUSample(); + if (visible) { return attenuation * ray_color(scattered, world, depth-1); } @@ -94,7 +107,7 @@ color ray_color(const ray& r, const hittable& world, int32_t depth) } double hit_sphere(const point3& center, double radius, const ray& r) -{ +{ vec3 oc = r.origin - center; double a = r.direction.length_squared(); double half_b = dot(oc, r.direction); @@ -107,8 +120,14 @@ double hit_sphere(const point3& center, double radius, const ray& r) return (-half_b - sqrt(discriminant)) / a; } int32_t main() -{ - +{ + /* Profiling library initialization */ + Remotery *rmt; + if (RMT_ERROR_NONE != rmt_CreateGlobalInstance(&rmt)) + { + fprintf(stderr, "Error starting Remotery\n"); + } + // Image const double aspect_ratio = 3.0 / 2.0; @@ -139,14 +158,17 @@ int32_t main() // Render printf("P3\n%d %d\n255\n", image_width, image_height); + for (int32_t j = image_height - 1; j >= 0; --j) { + rmt_ScopedCPUSample(OuterLoop, RMTSF_Aggregate); fprintf(stderr, "\rScanlines remaining: %d ", j); fflush(stderr); for (int32_t i = 0; i < image_width; ++i) - { + { + rmt_ScopedCPUSample(InnerLoop, RMTSF_Aggregate); color pixel_color = color(0,0,0); - + for (int32_t s = 0; s < samples_per_pixel; ++s) { double u = ((i + random_double()) / (image_width-1)); @@ -158,5 +180,7 @@ int32_t main() write_color(stdout, pixel_color, samples_per_pixel); } } + fprintf(stderr, "\nDone\n"); + rmt_DestroyGlobalInstance(rmt); } diff --git a/rtweekend.hpp b/rtweekend.hpp index e996988..8468343 100644 --- a/rtweekend.hpp +++ b/rtweekend.hpp @@ -4,6 +4,15 @@ #include #include +/* Utility macros */ + +#define TIMED_BLOCK_2(c, flags) rmt_ScopedCPUSample(Counter##c, flags) +#define TIMED_BLOCK_1(c, flags) TIMED_BLOCK_2(c, flags) +#define TIMED_BLOCK(flags) TIMED_BLOCK_1(__COUNTER__, flags) + +// #define TIMED_BLOCK_(counter, flags) rmt_ScopedCPUSample(counter, flags) +// #define TIMED_BLOCK(flags) TIMED_BLOCK_(__COUNTER__, flags) + /* Utility functions */ double degrees_to_radians(double d) { diff --git a/sphere.hpp b/sphere.hpp index e0f5c6f..c54965a 100644 --- a/sphere.hpp +++ b/sphere.hpp @@ -25,17 +25,27 @@ struct sphere : hittable { /* Virtual method implementations */ bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { + rmt_ScopedCPUSample(Sphere_Hit, RMTSF_Aggregate); + + // Part 1 + vec3 oc = r.origin - center; double a = r.direction.length_squared(); double half_b = dot(oc, r.direction); double c = oc.length_squared() - radius*radius; + + // Part 2 + double discriminant = half_b*half_b - a*c; if (discriminant < 0) return false; double sqrtd = sqrt(discriminant); + // Find the nearest root that lies in the acceptable range + // Part 3 + double root = (-half_b - sqrtd) / a; if (root < t_min || t_max < root) { @@ -44,6 +54,8 @@ bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) cons return false; } + // Part 4 + rec.t = root; rec.p = r.at(rec.t); vec3 outward_normal = (rec.p - center) / radius; diff --git a/vec3.hpp b/vec3.hpp index 8d51c82..b7340a1 100644 --- a/vec3.hpp +++ b/vec3.hpp @@ -136,12 +136,14 @@ inline vec3 operator/(vec3 v, double t) // Straightforward dot product inline double dot(const vec3 &u, const vec3 &v) { + return u.x*v.x + u.y*v.y + u.z*v.z; } // Cross product between two vectors inline vec3 cross(const vec3 &u, const vec3 &v) { + return vec3(u.y * v.z - u.z * v.y, u.z * v.x - u.x * v.z, u.x * v.y - u.y * v.x); @@ -150,12 +152,13 @@ inline vec3 cross(const vec3 &u, const vec3 &v) // Normalize vector so its length = 1 inline vec3 normalize(const vec3 v) { + return v / v.length(); } // Returns a vec3 of random components between [-1,1) that is inside a unit sphere vec3 random_in_unit_sphere() -{ +{ // Iterate until we find a vector with length < 1 while (true) { @@ -173,7 +176,7 @@ vec3 random_unit_vector() } vec3 random_in_hemisphere(const vec3& normal) -{ +{ vec3 in_unit_sphere = random_in_unit_sphere(); if (dot(in_unit_sphere, normal) > 0.0) diff --git a/vis/Code/Console.js b/vis/Code/Console.js new file mode 100644 index 0000000..1f16cfe --- /dev/null +++ b/vis/Code/Console.js @@ -0,0 +1,218 @@ + +Console = (function() +{ + var BORDER = 10; + var HEIGHT = 200; + + + function Console(wm, server) + { + // Create the window and its controls + this.Window = wm.AddWindow("Console", 10, 10, 100, 100); + this.PageContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160)); + DOM.Node.AddClass(this.PageContainer.Node, "ConsoleText"); + this.AppContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160)); + DOM.Node.AddClass(this.AppContainer.Node, "ConsoleText"); + this.UserInput = this.Window.AddControlNew(new WM.EditBox(10, 5, 400, 30, "Input", "")); + this.UserInput.SetChangeHandler(Bind(ProcessInput, this)); + this.Window.ShowNoAnim(); + + // This accumulates log text as fast as is required + this.PageTextBuffer = ""; + this.PageTextUpdatePending = false; + this.AppTextBuffer = ""; + this.AppTextUpdatePending = false; + + // Setup command history control + this.CommandHistory = LocalStore.Get("App", "Global", "CommandHistory", [ ]); + this.CommandIndex = 0; + this.MaxNbCommands = 10000; + DOM.Event.AddHandler(this.UserInput.EditNode, "keydown", Bind(OnKeyPress, this)); + DOM.Event.AddHandler(this.UserInput.EditNode, "focus", Bind(OnFocus, this)); + + // At a much lower frequency this will update the console window + window.setInterval(Bind(UpdateHTML, this), 500); + + // Setup log requests from the server + this.Server = server; + server.SetConsole(this); + server.AddMessageHandler("LOGM", Bind(OnLog, this)); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + } + + + Console.prototype.Log = function(text) + { + this.PageTextBuffer = LogText(this.PageTextBuffer, text); + this.PageTextUpdatePending = true; + } + + + Console.prototype.WindowResized = function(width, height) + { + // Place window + this.Window.SetPosition(BORDER, height - BORDER - 200); + this.Window.SetSize(width - 2 * BORDER, HEIGHT); + + ResizeInternals(this); + } + + + Console.prototype.TriggerUpdate = function() + { + this.AppTextUpdatePending = true; + } + + + function OnLog(self, socket, data_view_reader) + { + var text = data_view_reader.GetString(); + self.AppTextBuffer = LogText(self.AppTextBuffer, text); + + // Don't register text as updating if disconnected as this implies a trace is being loaded, which we want to speed up + if (self.Server.Connected()) + { + self.AppTextUpdatePending = true; + } + } + + + function LogText(existing_text, new_text) + { + // Filter the text a little to make it safer + if (new_text == null) + new_text = "NULL"; + + // Find and convert any HTML entities, ensuring the browser doesn't parse any embedded HTML code + // This also allows the log to contain arbitrary C++ code (e.g. assert comparison operators) + new_text = Convert.string_to_html_entities(new_text); + + // Prefix date and end with new line + var d = new Date(); + new_text = "[" + d.toLocaleTimeString() + "] " + new_text + "
"; + + // Append to local text buffer and ensure clip the oldest text to ensure a max size + existing_text = existing_text + new_text; + var max_len = 100 * 1024; + var len = existing_text.length; + if (len > max_len) + existing_text = existing_text.substr(len - max_len, max_len); + + return existing_text; + } + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + // Place controls + var parent_size = self.Window.Size; + var mid_w = parent_size[0] / 3; + self.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30); + self.UserInput.SetSize(parent_size[0] - 100, 18); + var output_height = self.UserInput.Position[1] - 2 * BORDER; + self.PageContainer.SetPosition(BORDER, BORDER); + self.PageContainer.SetSize(mid_w - 2 * BORDER, output_height); + self.AppContainer.SetPosition(mid_w, BORDER); + self.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height); + } + + + function UpdateHTML(self) + { + // Reset the current text buffer as html + + if (self.PageTextUpdatePending) + { + var page_node = self.PageContainer.Node; + page_node.innerHTML = self.PageTextBuffer; + page_node.scrollTop = page_node.scrollHeight; + self.PageTextUpdatePending = false; + } + + if (self.AppTextUpdatePending) + { + var app_node = self.AppContainer.Node; + app_node.innerHTML = self.AppTextBuffer; + app_node.scrollTop = app_node.scrollHeight; + self.AppTextUpdatePending = false; + } + } + + + function ProcessInput(self, node) + { + // Send the message exactly + var msg = node.value; + self.Server.Send("CONI" + msg); + + // Emit to console and clear + self.Log("> " + msg); + self.UserInput.SetValue(""); + + // Keep track of recently issued commands, with an upper bound + self.CommandHistory.push(msg); + var extra_commands = self.CommandHistory.length - self.MaxNbCommands; + if (extra_commands > 0) + self.CommandHistory.splice(0, extra_commands); + + // Set command history index to the most recent command + self.CommandIndex = self.CommandHistory.length; + + // Backup to local store + LocalStore.Set("App", "Global", "CommandHistory", self.CommandHistory); + + // Keep focus with the edit box + return true; + } + + + function OnKeyPress(self, evt) + { + evt = DOM.Event.Get(evt); + + if (evt.keyCode == Keyboard.Codes.UP) + { + if (self.CommandHistory.length > 0) + { + // Cycle backwards through the command history + self.CommandIndex--; + if (self.CommandIndex < 0) + self.CommandIndex = self.CommandHistory.length - 1; + var command = self.CommandHistory[self.CommandIndex]; + self.UserInput.SetValue(command); + } + + // Stops default behaviour of moving cursor to the beginning + DOM.Event.StopDefaultAction(evt); + } + + else if (evt.keyCode == Keyboard.Codes.DOWN) + { + if (self.CommandHistory.length > 0) + { + // Cycle fowards through the command history + self.CommandIndex = (self.CommandIndex + 1) % self.CommandHistory.length; + var command = self.CommandHistory[self.CommandIndex]; + self.UserInput.SetValue(command); + } + + // Stops default behaviour of moving cursor to the end + DOM.Event.StopDefaultAction(evt); + } + } + + + function OnFocus(self) + { + // Reset command index on focus + self.CommandIndex = self.CommandHistory.length; + } + + + return Console; +})(); diff --git a/vis/Code/DataViewReader.js b/vis/Code/DataViewReader.js new file mode 100644 index 0000000..e320533 --- /dev/null +++ b/vis/Code/DataViewReader.js @@ -0,0 +1,52 @@ + +// +// Simple wrapper around DataView that auto-advances the read offset and provides +// a few common data type conversions specific to this app +// +DataViewReader = (function () +{ + function DataViewReader(data_view, offset) + { + this.DataView = data_view; + this.Offset = offset; + } + + DataViewReader.prototype.AtEnd = function() + { + return this.Offset >= this.DataView.byteLength; + } + + DataViewReader.prototype.GetUInt32 = function () + { + var v = this.DataView.getUint32(this.Offset, true); + this.Offset += 4; + return v; + } + + DataViewReader.prototype.GetUInt64 = function () + { + var v = this.DataView.getFloat64(this.Offset, true); + this.Offset += 8; + return v; + } + + DataViewReader.prototype.GetStringOfLength = function (string_length) + { + var string = ""; + for (var i = 0; i < string_length; i++) + { + string += String.fromCharCode(this.DataView.getInt8(this.Offset)); + this.Offset++; + } + + return string; + } + + DataViewReader.prototype.GetString = function () + { + var string_length = this.GetUInt32(); + return this.GetStringOfLength(string_length); + } + + return DataViewReader; +})(); diff --git a/vis/Code/NameMap.js b/vis/Code/NameMap.js new file mode 100644 index 0000000..37a966d --- /dev/null +++ b/vis/Code/NameMap.js @@ -0,0 +1,53 @@ +class NameMap +{ + constructor(text_buffer) + { + this.names = { }; + this.textBuffer = text_buffer; + } + + Get(name_hash) + { + // Return immediately if it's in the hash + let name = this.names[name_hash]; + if (name != undefined) + { + return [ true, name ]; + } + + // Create a temporary name that uses the hash + name = { + string: name_hash.toString(), + hash: name_hash + }; + this.names[name_hash] = name; + + // Add to the text buffer the first time this name is encountered + name.textEntry = this.textBuffer.AddText(name.string); + + return [ false, name ]; + } + + Set(name_hash, name_string) + { + // Create the name on-demand if its hash doesn't exist + let name = this.names[name_hash]; + if (name == undefined) + { + name = { + string: name_string, + hash: name_hash + }; + this.names[name_hash] = name; + } + else + { + name.string = name_string; + } + + // Apply the updated text to the buffer + name.textEntry = this.textBuffer.AddText(name_string); + + return name; + } +} \ No newline at end of file diff --git a/vis/Code/PixelTimeRange.js b/vis/Code/PixelTimeRange.js new file mode 100644 index 0000000..a8a726c --- /dev/null +++ b/vis/Code/PixelTimeRange.js @@ -0,0 +1,61 @@ + +class PixelTimeRange +{ + constructor(start_us, span_us, span_px) + { + this.Span_px = span_px; + this.Set(start_us, span_us); + } + + Set(start_us, span_us) + { + this.Start_us = start_us; + this.Span_us = span_us; + this.End_us = this.Start_us + span_us; + this.usPerPixel = this.Span_px / this.Span_us; + } + + SetStart(start_us) + { + this.Start_us = start_us; + this.End_us = start_us + this.Span_us; + } + + SetEnd(end_us) + { + this.End_us = end_us; + this.Start_us = end_us - this.Span_us; + } + + SetPixelSpan(span_px) + { + this.Span_px = span_px; + this.usPerPixel = this.Span_px / this.Span_us; + } + + PixelOffset(time_us) + { + return Math.floor((time_us - this.Start_us) * this.usPerPixel); + } + + PixelSize(time_us) + { + return Math.floor(time_us * this.usPerPixel); + } + + TimeAtPosition(position) + { + return this.Start_us + position / this.usPerPixel; + } + + Clone() + { + return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px); + } + + SetAsUniform(gl, program) + { + glSetUniform(gl, program, "inTimeRange.usStart", this.Start_us); + glSetUniform(gl, program, "inTimeRange.usPerPixel", this.usPerPixel); + } +} diff --git a/vis/Code/Remotery.js b/vis/Code/Remotery.js new file mode 100644 index 0000000..383b017 --- /dev/null +++ b/vis/Code/Remotery.js @@ -0,0 +1,540 @@ + +// +// TODO: Window resizing needs finer-grain control +// TODO: Take into account where user has moved the windows +// TODO: Controls need automatic resizing within their parent windows +// + + +Settings = (function() +{ + function Settings() + { + this.IsPaused = false; + this.SyncTimelines = true; + } + + return Settings; + +})(); + + +Remotery = (function() +{ + // crack the url and get the parameter we want + var getUrlParameter = function getUrlParameter( search_param) + { + var page_url = decodeURIComponent( window.location.search.substring(1) ), + url_vars = page_url.split('&'), + param_name, + i; + + for (i = 0; i < url_vars.length; i++) + { + param_name = url_vars[i].split('='); + + if (param_name[0] === search_param) + { + return param_name[1] === undefined ? true : param_name[1]; + } + } + }; + + function Remotery() + { + this.WindowManager = new WM.WindowManager(); + this.Settings = new Settings(); + + // "addr" param is ip:port and will override the local store version if passed in the URL + var addr = getUrlParameter( "addr" ); + if ( addr != null ) + this.ConnectionAddress = "ws://" + addr + "/rmt"; + else + this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt"); + + this.Server = new WebSocketConnection(); + this.Server.AddConnectHandler(Bind(OnConnect, this)); + this.Server.AddDisconnectHandler(Bind(OnDisconnect, this)); + + // Create the console up front as everything reports to it + this.Console = new Console(this.WindowManager, this.Server); + + // Create required windows + this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress); + this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this)); + this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this)); + this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this)); + this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this)); + this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null); + + this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this)); + this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this)); + + this.TraceDrop = new TraceDrop(this); + + this.NbSampleWindows = 0; + this.SampleWindows = { }; + this.FrameHistory = { }; + this.ProcessorFrameHistory = { }; + this.SelectedFrames = { }; + this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer); + this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer); + + this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this)); + this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this)); + this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this)); + this.Server.AddMessageHandler("THRN", Bind(OnThreadNames, this)); + + // Kick-off the auto-connect loop + AutoConnect(this); + + // Hook up resize event handler + DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this)); + OnResizeWindow(this); + + // Hook up browser-native canvas refresh + this.DisplayFrame = 0; + this.LastKnownPause = this.Settings.IsPaused; + var self = this; + (function display_loop() + { + window.requestAnimationFrame(display_loop); + DrawTimeline(self); + })(); + } + + + Remotery.prototype.Clear = function() + { + // Clear timelines + this.SampleTimelineWindow.Clear(); + this.ProcessorTimelineWindow.Clear(); + + // Close and clear all sample windows + for (var i in this.SampleWindows) + { + var sample_window = this.SampleWindows[i]; + sample_window.Close(); + } + this.NbSampleWindows = 0; + this.SampleWindows = { }; + + // Clear runtime data + this.FrameHistory = { }; + this.ProcessorFrameHistory = { }; + this.SelectedFrames = { }; + this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer); + this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer); + + // Resize everything to fit new layout + OnResizeWindow(this); + } + + + function AutoConnect(self) + { + // Only attempt to connect if there isn't already a connection or an attempt to connect + if (!self.Server.Connected()) + self.Server.Connect(self.ConnectionAddress); + + // Always schedule another check + window.setTimeout(Bind(AutoConnect, self), 2000); + } + + + function OnConnect(self) + { + // Connection address has been validated + LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress); + + self.Clear(); + + // Ensure the viewer is ready for realtime updates + self.TitleWindow.Unpause(); + } + + function OnDisconnect(self) + { + // Pause so the user can inspect the trace + self.TitleWindow.Pause(); + } + + + function OnAddressChanged(self, node) + { + // Update and disconnect, relying on auto-connect to reconnect + self.ConnectionAddress = node.value; + self.Server.Disconnect(); + + // Give input focus away + return false; + } + + + function DrawTimeline(self) + { + // Has pause state changed? + if (self.Settings.IsPaused != self.LastKnownPaused) + { + // When switching TO paused, draw one last frame to ensure the sample text gets drawn + self.LastKnownPaused = self.Settings.IsPaused; + self.SampleTimelineWindow.DrawAllRows(); + self.ProcessorTimelineWindow.DrawAllRows(); + return; + } + + // Don't waste time drawing the timeline when paused + if (self.Settings.IsPaused) + return; + + // requestAnimationFrame can run up to 60hz which is way too much for drawing the timeline + // Assume it's running at 60hz and skip frames to achieve 10hz instead + // Doing this instead of using setTimeout because it's better for browser rendering (or; will be once WebGL is in use) + // TODO: Expose as config variable because high refresh rate is great when using a separate viewiing machine + if ((self.DisplayFrame % 10) == 0) + { + self.SampleTimelineWindow.DrawAllRows(); + self.ProcessorTimelineWindow.DrawAllRows(); + } + + self.DisplayFrame++; + } + + + function DecodeSample(self, data_view_reader) + { + var sample = {}; + + // Get name hash and lookup name it map + sample.name_hash = data_view_reader.GetUInt32(); + let [ name_exists, name ] = self.sampleNames.Get(sample.name_hash); + sample.name = name; + + // If the name doesn't exist in the map yet, request it from the server + if (!name_exists) + { + if (self.Server.Connected()) + { + self.Server.Send("GSMP" + sample.name_hash); + } + } + + // Get the rest of the sample data + sample.id = data_view_reader.GetUInt32(); + sample.colour = data_view_reader.GetStringOfLength(7); + sample.us_start = data_view_reader.GetUInt64(); + sample.us_length = data_view_reader.GetUInt64(); + sample.us_self = data_view_reader.GetUInt64(); + sample.call_count = data_view_reader.GetUInt32(); + sample.recurse_depth = data_view_reader.GetUInt32(); + + // TODO(don): Get the profiler to pass these directly instead of hex colour + const colour = parseInt(sample.colour.slice(1), 16); + const r = (colour >> 16) & 255; + const g = (colour >> 8) & 255; + const b = colour & 255; + sample.rgbColour = [ r, g, b ]; + + // Calculate dependent properties + sample.ms_length = (sample.us_length / 1000.0).toFixed(3); + sample.ms_self = (sample.us_self / 1000.0).toFixed(3); + + // Recurse into children + sample.children = []; + DecodeSampleArray(self, data_view_reader, sample.children); + + return sample; + } + + + function DecodeSampleArray(self, data_view_reader, samples) + { + var nb_samples = data_view_reader.GetUInt32(); + for (var i = 0; i < nb_samples; i++) + { + var sample = DecodeSample(self, data_view_reader); + samples.push(sample) + } + } + + + function DecodeSamples(self, data_view_reader) + { + // Message-specific header + let message = { }; + message.sample_tree_bytes = data_view_reader.GetUInt32(); + message.thread_name = data_view_reader.GetString(); + message.nb_samples = data_view_reader.GetUInt32(); + message.sample_digest = data_view_reader.GetUInt32(); + message.partial_tree = data_view_reader.GetUInt32(); + + // Read samples + message.samples = []; + message.samples.push(DecodeSample(self, data_view_reader)); + + return message; + } + + + function OnSamples(self, socket, data_view_reader) + { + // Discard any new samples while paused and connected + // Otherwise this stops a paused Remotery from loading new samples from disk + if (self.Settings.IsPaused && self.Server.Connected()) + return; + + // Binary decode incoming sample data + var message = DecodeSamples(self, data_view_reader); + var name = message.thread_name; + + // Add to frame history for this thread + var thread_frame = new ThreadFrame(message); + if (!(name in self.FrameHistory)) + { + self.FrameHistory[name] = [ ]; + } + var frame_history = self.FrameHistory[name]; + if (frame_history.length > 0 && frame_history[frame_history.length - 1].PartialTree) + { + // Always overwrite partial trees with new information + frame_history[frame_history.length - 1] = thread_frame; + } + else + { + frame_history.push(thread_frame); + } + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + frame_history.splice(0, extra_frames); + + // Create sample windows on-demand + if (!(name in self.SampleWindows)) + { + self.SampleWindows[name] = new SampleWindow(self.WindowManager, name, self.NbSampleWindows); + self.SampleWindows[name].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window); + self.NbSampleWindows++; + MoveSampleWindows(this); + } + + // Set on the window and timeline if connected as this implies a trace is being loaded, which we want to speed up + if (self.Server.Connected()) + { + self.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples); + self.SampleTimelineWindow.OnSamples(name, frame_history); + } + } + + + function OnSampleName(self, socket, data_view_reader) + { + // Add any names sent by the server to the local map + let name_hash = data_view_reader.GetUInt32(); + let name_string = data_view_reader.GetString(); + self.sampleNames.Set(name_hash, name_string); + } + + + function OnProcessorThreads(self, socket, data_view_reader) + { + let nb_processors = data_view_reader.GetUInt32(); + let message_index = data_view_reader.GetUInt64(); + + // Decode each processor + for (let i = 0; i < nb_processors; i++) + { + let thread_id = data_view_reader.GetUInt32(); + let thread_name_hash = data_view_reader.GetUInt32(); + let sample_time = data_view_reader.GetUInt64(); + + // Add frame history for this processor + let processor_name = "Processor " + i.toString(); + if (!(processor_name in self.ProcessorFrameHistory)) + { + self.ProcessorFrameHistory[processor_name] = [ ]; + } + let frame_history = self.ProcessorFrameHistory[processor_name]; + + if (thread_id == 0xFFFFFFFF) + { + continue; + } + + // Try to merge this frame's samples with the previous frame if the are the same thread + if (frame_history.length > 0) + { + let last_thread_frame = frame_history[frame_history.length - 1]; + if (last_thread_frame.threadId == thread_id && last_thread_frame.messageIndex == message_index - 1) + { + // Update last frame message index so that the next frame can check for continuity + last_thread_frame.messageIndex = message_index; + + // Sum time elapsed on the previous frame + let us_length = sample_time - last_thread_frame.usLastStart; + last_thread_frame.usLastStart = sample_time; + last_thread_frame.EndTime_us += us_length; + last_thread_frame.Samples[0].us_length += us_length; + + continue; + } + } + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + { + frame_history.splice(0, extra_frames); + } + + // Lookup the thread name + let [ _, thread_name ] = self.threadNames.Get(thread_name_hash); + + // Make a pastel-y colour from the thread name hash + let hash = thread_name.hash; + let r = 127 + (hash & 255) / 2; + let g = 127 + ((hash >> 4) & 255) / 2; + let b = 127 + ((hash >> 8) & 255) / 2; + + // We are co-opting the sample rendering functionality of the timeline window to display processor threads as + // thread samples. Fabricate a thread frame message, packing the processor info into one root sample. + // TODO(don): Abstract the timeline window for pure range display as this is quite inefficient. + let thread_message = { + nb_samples: 1, + sample_digest: 0, + samples : [ + { + name_hash: thread_name_hash, + name: thread_name, + id: thread_id, + colour: "#FFFFFF", + us_start: sample_time, + us_length: 250, + rgbColour: [ r, g, b ], + children: [] + } + ] + }; + + // Create a thread frame and annotate with data required to merge processor samples + let thread_frame = new ThreadFrame(thread_message); + thread_frame.threadId = thread_id; + thread_frame.messageIndex = message_index; + thread_frame.usLastStart = sample_time; + frame_history.push(thread_frame); + + if (self.Server.Connected()) + { + self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history); + } + } + } + + + function OnThreadNames(self, socket, data_view_reader) + { + let name_hash = data_view_reader.GetUInt32(); + let name_length = data_view_reader.GetUInt32(); + let name_string = data_view_reader.GetStringOfLength(name_length); + self.threadNames.Set(name_hash, name_string); + } + + + function OnTimelineCheck(self, name, evt) + { + // Show/hide the equivalent sample window and move all the others to occupy any left-over space + var target = DOM.Event.GetNode(evt); + self.SampleWindows[name].SetVisible(target.checked); + MoveSampleWindows(self); + } + + + function MoveSampleWindows(self) + { + // Stack all windows next to each other + var xpos = 0; + for (var i in self.SampleWindows) + { + var sample_window = self.SampleWindows[i]; + if (sample_window.Visible) + { + sample_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window); + } + } + } + + + function OnSampleHover(self, thread_name, hover) + { + if (!self.Settings.IsPaused) + { + return; + } + + for (let window_thread_name in self.SampleWindows) + { + let sample_window = self.SampleWindows[window_thread_name]; + + if (window_thread_name == thread_name && hover != null) + { + // Populate with sample under hover + let frame = hover[0]; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + else + { + // When there's no hover, go back to the selected frame + if (self.SelectedFrames[window_thread_name]) + { + const frame = self.SelectedFrames[window_thread_name]; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + } + } + } + + + function OnSampleSelected(self, thread_name, select) + { + // Lookup sample window set the frame samples on it + if (select && thread_name in self.SampleWindows) + { + var sample_window = self.SampleWindows[thread_name]; + var frame = select[0]; + self.SelectedFrames[thread_name] = frame; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + } + + + function OnResizeWindow(self) + { + // Resize windows + var w = window.innerWidth; + var h = window.innerHeight; + self.Console.WindowResized(w, h); + self.TitleWindow.WindowResized(w, h); + self.SampleTimelineWindow.WindowResized(10, w / 2 - 5, self.TitleWindow.Window); + self.ProcessorTimelineWindow.WindowResized(w / 2 + 5, w / 2 - 5, self.TitleWindow.Window); + for (var i in self.SampleWindows) + { + self.SampleWindows[i].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window); + } + } + + + function OnTimelineMoved(self, timeline) + { + if (self.Settings.SyncTimelines) + { + let other_timeline = timeline == self.ProcessorTimelineWindow ? self.SampleTimelineWindow : self.ProcessorTimelineWindow; + other_timeline.SetTimeRange(timeline.TimeRange.Start_us, timeline.TimeRange.Span_us); + other_timeline.DrawAllRows(); + } + } + + + return Remotery; +})(); \ No newline at end of file diff --git a/vis/Code/SampleWindow.js b/vis/Code/SampleWindow.js new file mode 100644 index 0000000..ea3c163 --- /dev/null +++ b/vis/Code/SampleWindow.js @@ -0,0 +1,221 @@ + +SampleWindow = (function() +{ + function SampleWindow(wm, name, offset) + { + // Sample digest for checking if grid needs to be repopulated + this.NbSamples = 0; + this.SampleDigest = null; + + // Source sample reference to reduce repopulation + this.Samples = null; + + this.XPos = 10 + offset * 410; + this.Window = wm.AddWindow(name, 100, 100, 100, 100); + this.Window.ShowNoAnim(); + this.Visible = true; + + // Create a grid that's indexed by the unique sample ID + this.Grid = this.Window.AddControlNew(new WM.Grid()); + var cell_data = + { + Name: "Samples", + Length: "Time (ms)", + Self: "Self (ms)", + Calls: "Calls", + Recurse: "Recurse", + }; + var cell_classes = + { + Name: "SampleTitleNameCell", + Length: "SampleTitleTimeCell", + Self: "SampleTitleTimeCell", + Calls: "SampleTitleCountCell", + Recurse: "SampleTitleCountCell", + }; + this.RootRow = this.Grid.Rows.Add(cell_data, "GridGroup", cell_classes); + this.RootRow.Rows.AddIndex("_ID"); + } + + + SampleWindow.prototype.Close = function() + { + this.Window.Close(); + } + + + SampleWindow.prototype.SetXPos = function(xpos, top_window, bottom_window) + { + Anim.Animate( + Bind(AnimatedMove, this, top_window, bottom_window), + this.XPos, 10 + xpos * 410, 0.25); + } + + + function AnimatedMove(self, top_window, bottom_window, val) + { + self.XPos = val; + self.WindowResized(top_window, bottom_window); + } + + + SampleWindow.prototype.SetVisible = function(visible) + { + if (visible != this.Visible) + { + if (visible == true) + this.Window.ShowNoAnim(); + else + this.Window.HideNoAnim(); + + this.Visible = visible; + } + } + + + SampleWindow.prototype.WindowResized = function(top_window, bottom_window) + { + var top = top_window.Position[1] + top_window.Size[1] + 10; + this.Window.SetPosition(this.XPos, top_window.Position[1] + top_window.Size[1] + 10); + this.Window.SetSize(400, bottom_window.Position[1] - 10 - top); + } + + + SampleWindow.prototype.OnSamples = function(nb_samples, sample_digest, samples) + { + if (!this.Visible) + return; + + // If the source hasn't changed, don't repopulate + if (this.Samples == samples) + return; + this.Samples = samples; + + // Recreate all the HTML if the number of samples gets bigger + if (nb_samples > this.NbSamples) + { + GrowGrid(this.RootRow, nb_samples); + this.NbSamples = nb_samples; + } + + // If the content of the samples changes from previous update, update them all + if (this.SampleDigest != sample_digest) + { + this.RootRow.Rows.ClearIndex("_ID"); + var index = UpdateAllSampleFields(this.RootRow, samples, 0, ""); + this.SampleDigest = sample_digest; + + // Clear out any left-over rows + for (var i = index; i < this.RootRow.Rows.Rows.length; i++) + { + var row = this.RootRow.Rows.Rows[i]; + DOM.Node.Hide(row.Node); + } + } + + else if (this.Visible) + { + // Otherwise just update the existing sample fields + UpdateChangedSampleFields(this.RootRow, samples, ""); + } + } + + + function GrowGrid(parent_row, nb_samples) + { + parent_row.Rows.Clear(); + + for (var i = 0; i < nb_samples; i++) + { + var cell_data = + { + _ID: i, + Name: "", + Length: "", + Self: "", + Calls: "", + Recurse: "", + }; + + var cell_classes = + { + Name: "SampleNameCell", + Length: "SampleTimeCell", + Self: "SampleTimeCell", + Calls: "SampleCountCell", + Recurse: "SampleCountCell", + }; + + parent_row.Rows.Add(cell_data, null, cell_classes); + } + } + + + function UpdateAllSampleFields(parent_row, samples, index, indent) + { + for (var i in samples) + { + var sample = samples[i]; + + // Match row allocation in GrowGrid + var row = parent_row.Rows.Rows[index++]; + + // Sample row may have been hidden previously + DOM.Node.Show(row.Node); + + // Assign unique ID so that the common fast path of updating sample times only + // can lookup target samples in the grid + row.CellData._ID = sample.id; + parent_row.Rows.AddRowToIndex("_ID", sample.id, row); + + // Record sample name for later comparison + row.CellData.Name = sample.name.string; + + // Set sample name and colour + var name_node = row.CellNodes["Name"]; + name_node.innerHTML = indent + sample.name.string; + DOM.Node.SetColour(name_node, sample.colour); + + row.CellNodes["Length"].innerHTML = sample.ms_length; + row.CellNodes["Self"].innerHTML = sample.ms_self; + row.CellNodes["Calls"].innerHTML = sample.call_count; + row.CellNodes["Recurse"].innerHTML = sample.recurse_depth; + + index = UpdateAllSampleFields(parent_row, sample.children, index, indent + "     "); + } + + return index; + } + + + function UpdateChangedSampleFields(parent_row, samples, indent) + { + for (var i in samples) + { + var sample = samples[i]; + + var row = parent_row.Rows.GetBy("_ID", sample.id); + if (row) + { + row.CellNodes["Length"].innerHTML = sample.ms_length; + row.CellNodes["Self"].innerHTML = sample.ms_self; + row.CellNodes["Calls"].innerHTML = sample.call_count; + row.CellNodes["Recurse"].innerHTML = sample.recurse_depth; + + // Sample name will change when it switches from hash ID to network-retrieved + // name. Quickly check that before re-applying the HTML for the name. + if (row.CellData.Name != sample.name.string) + { + var name_node = row.CellNodes["Name"]; + row.CellData.Name = sample.name.string; + name_node.innerHTML = indent + sample.name.string; + } + } + + UpdateChangedSampleFields(parent_row, sample.children, indent + "     "); + } + } + + + return SampleWindow; +})(); diff --git a/vis/Code/Shaders.js b/vis/Code/Shaders.js new file mode 100644 index 0000000..0e795e8 --- /dev/null +++ b/vis/Code/Shaders.js @@ -0,0 +1,275 @@ +const TimelineVShader =`#version 300 es + +#define CANVAS_BORDER 1.0 +#define SAMPLE_HEIGHT 16.0 +#define SAMPLE_BORDER 1.0 +#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0) +#define SAMPLE_Y_OFFSET (CANVAS_BORDER + 1.0) + +struct Viewport +{ + float width; + float height; +}; + +struct TimeRange +{ + float usStart; + float usPerPixel; +}; + +struct Row +{ + float yOffset; +}; + +uniform Viewport inViewport; +uniform TimeRange inTimeRange; +uniform Row inRow; + +in vec4 inSample_TextOffset; +in vec4 inColour_TextLength; + +out vec4 varColour_TimeMs; +out vec4 varPosInBoxPx_TextEntry; +out float varTimeChars; + +//#define PIXEL_ROUNDED_OFFSETS + +float PixelOffset(float time_us) +{ + float offset = (time_us - inTimeRange.usStart) * inTimeRange.usPerPixel; + #ifdef PIXEL_ROUNDED_OFFSETS + return floor(offset); + #else + return offset; + #endif +} + +float PixelSize(float time_us) +{ + float size = time_us * inTimeRange.usPerPixel; + #ifdef PIXEL_ROUNDED_OFFSETS + return floor(size); + #else + return size; + #endif +} + +void main() +{ + // Unpack input data + float us_start = inSample_TextOffset.x; + float us_length = inSample_TextOffset.y; + float depth = inSample_TextOffset.z; + float text_buffer_offset = inSample_TextOffset.w; + vec3 box_colour = inColour_TextLength.rgb; + float text_length_chars = inColour_TextLength.w; + + // Determine pixel range of the sample + float x0 = PixelOffset(us_start); + float x1 = x0 + PixelSize(us_length); + + // Calculate box to render + float offset_x = x0; + float offset_y = inRow.yOffset + SAMPLE_Y_OFFSET + (depth - 1.0) * SAMPLE_Y_SPACING; + float size_x = max(x1 - x0, 1.0); + float size_y = SAMPLE_HEIGHT; + + // Box range + float min_x = offset_x; + float min_y = offset_y; + float max_x = offset_x + size_x; + float max_y = offset_y + size_y; + + // Quad indices are: + // + // 2 3 + // +----+ + // | | + // +----+ + // 0 1 + // + vec2 position; + position.x = (gl_VertexID & 1) == 0 ? min_x : max_x; + position.y = (gl_VertexID & 2) == 0 ? min_y : max_y; + + // + // NDC is: + // -1 to 1, left to right + // -1 to 1, bottom to top + // + vec4 ndc_pos; + ndc_pos.x = (position.x / inViewport.width) * 2.0 - 1.0; + ndc_pos.y = 1.0 - (position.y / inViewport.height) * 2.0; + ndc_pos.z = 0.0; + ndc_pos.w = 1.0; + + // Calculate number of characters required to display the millisecond time + float time_ms = us_length / 1000.0; + float time_ms_int = floor(time_ms); + float time_chars = time_ms_int == 0.0 ? 1.0 : floor(log(time_ms_int) / 2.302585092994046) + 1.0; + + gl_Position = ndc_pos; + + varColour_TimeMs = vec4(box_colour / 255.0, time_ms); + varPosInBoxPx_TextEntry = vec4(position.x - offset_x, position.y - offset_y, text_buffer_offset, text_length_chars); + varTimeChars = time_chars; +} +`; + +const TimelineFShader = `#version 300 es + +precision mediump float; + +#define SAMPLE_HEIGHT 16.0 + +struct TextBufferDesc +{ + float fontWidth; + float fontHeight; + float textBufferLength; +}; + +uniform sampler2D inFontAtlasTextre; +uniform sampler2D inTextBuffer; +uniform TextBufferDesc inTextBufferDesc; + +in vec4 varColour_TimeMs; +in vec4 varPosInBoxPx_TextEntry; +in float varTimeChars; + +out vec4 outColour; + +vec4 LookupCharacter(float char_ascii, float pos_x, float pos_y, float font_width_px, float font_height_px) +{ + // 2D index of the ASCII character in the font atlas + float char_index_y = floor(char_ascii / 16.0); + float char_index_x = char_ascii - char_index_y * 16.0; + + // Start UV of the character in the font atlas + float char_base_uv_x = char_index_x / 16.0; + float char_base_uv_y = char_index_y / 16.0; + + // UV within the character itself, scaled to the font atlas + float char_uv_x = pos_x / (font_width_px * 16.0); + float char_uv_y = pos_y / (font_height_px * 16.0); + + vec2 uv; + uv.x = char_base_uv_x + char_uv_x; + uv.y = char_base_uv_y + char_uv_y; + + // Apply colour to the text in premultiplied alpha space + vec4 t = texture(inFontAtlasTextre, uv); + vec3 colour = vec3(1.0, 1.0, 1.0) * 0.25; + return vec4(colour * t.a, t.a); +} + +void main() +{ + // Font description + float font_width_px = inTextBufferDesc.fontWidth; + float font_height_px = inTextBufferDesc.fontHeight; + float text_buffer_length = inTextBufferDesc.textBufferLength; + + // Text range in the text buffer + vec2 pos_in_box_px = varPosInBoxPx_TextEntry.xy; + float text_buffer_offset = varPosInBoxPx_TextEntry.z; + float text_length_chars = varPosInBoxPx_TextEntry.w; + float text_length_px = text_length_chars * font_width_px; + + // Text placement offset within the box + const vec2 text_offset_px = vec2(4.0, 3.0); + + vec4 box_colour = vec4(varColour_TimeMs.rgb, 1.0); + + // Add a subtle border to the box so that you can visually separate samples when they are next to each other + vec2 top_left = min(pos_in_box_px.xy, 2.0); + float both = min(top_left.x, top_left.y); + box_colour.rgb *= (0.8 + both * 0.1); + + vec4 text_colour = vec4(0.0); + + float text_end_px = text_length_px + text_offset_px.x + font_width_px; + float time_length_px = (varTimeChars + 4.0) * font_width_px; + if (pos_in_box_px.x > text_end_px && pos_in_box_px.x < text_end_px + time_length_px) + { + float time_ms = varColour_TimeMs.w; + + vec2 time_pixel_pos; + time_pixel_pos.x = max(min(pos_in_box_px.x - text_end_px, time_length_px), 0.0); + time_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0); + + float time_index = floor(time_pixel_pos.x / font_width_px); + if (time_index < varTimeChars) + { + // Use base-10 integer digit counting to calculate the divisor needed to bring this digit below 10 + float time_divisor = 1.0; + for (int i = 0; i < int(varTimeChars - time_index - 1.0); i++) + { + time_divisor *= 10.0; + } + + // Calculate digit + float time_shifted_int = floor(time_ms / time_divisor); + float time_digit = floor(mod(time_shifted_int, 10.0)); + + text_colour = LookupCharacter(48.0 + time_digit, + time_pixel_pos.x - time_index * font_width_px, + time_pixel_pos.y, + font_width_px, font_height_px); + } + else if (time_index == varTimeChars) + { + text_colour = LookupCharacter(46.0, + time_pixel_pos.x - time_index * font_width_px, + time_pixel_pos.y, + font_width_px, font_height_px); + } + else if (time_index == varTimeChars + 1.0) + { + float time_digit = floor(mod(time_ms * 10.0, 10.0)); + text_colour = LookupCharacter(48.0 + time_digit, + time_pixel_pos.x - time_index * font_width_px, + time_pixel_pos.y, + font_width_px, font_height_px); + } + else if (time_index == varTimeChars + 2.0) + { + float time_digit = floor(mod(time_ms * 10.0, 10.0)); + text_colour = LookupCharacter(109.0, + time_pixel_pos.x - time_index * font_width_px, + time_pixel_pos.y, + font_width_px, font_height_px); + } + else if (time_index == varTimeChars + 3.0) + { + float time_digit = floor(mod(time_ms * 10.0, 10.0)); + text_colour = LookupCharacter(115.0, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px); + } + } + else + { + // Text pixel position clamped to the bounds of the full word, allowing leakage to neighbouring NULL characters to pad zeroes + vec2 text_pixel_pos; + text_pixel_pos.x = max(min(pos_in_box_px.x - text_offset_px.x, text_length_px), -1.0); + text_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0); + + // Index of the current character in the text buffer + float text_index = text_buffer_offset + floor(text_pixel_pos.x / font_width_px); + + // Sample the 1D text buffer to get the ASCII character index + vec2 char_uv = vec2((text_index + 0.5) / text_buffer_length, 0.5); + float char_ascii = texture(inTextBuffer, char_uv).a * 255.0; + + text_colour = LookupCharacter(char_ascii, + text_pixel_pos.x - (text_index - text_buffer_offset) * font_width_px, + text_pixel_pos.y, + font_width_px, font_height_px); + } + + // Bring out of premultiplied alpha space and lerp with the box colour + float inv_alpha = text_colour.a == 0.0 ? 1.0 : 1.0 / text_colour.a; + outColour = mix(box_colour, vec4(text_colour.rgb * inv_alpha, 1.0), text_colour.a); +} +`; diff --git a/vis/Code/ThreadFrame.js b/vis/Code/ThreadFrame.js new file mode 100644 index 0000000..675f469 --- /dev/null +++ b/vis/Code/ThreadFrame.js @@ -0,0 +1,29 @@ + + +ThreadFrame = (function() +{ + function ThreadFrame(message) + { + // Persist the required message data + this.NbSamples = message.nb_samples; + this.SampleDigest = message.sample_digest; + this.Samples = message.samples; + this.PartialTree = message.partial_tree > 0 ? true : false; + + // Calculate the frame start/end times + this.StartTime_us = 0; + this.EndTime_us = 0; + var nb_root_samples = this.Samples.length; + if (nb_root_samples > 0) + { + var last_sample = this.Samples[nb_root_samples - 1]; + this.StartTime_us = this.Samples[0].us_start; + this.EndTime_us = last_sample.us_start + last_sample.us_length; + } + + this.Length_us = this.EndTime_us - this.StartTime_us; + } + + + return ThreadFrame; +})(); diff --git a/vis/Code/TimelineMarkers.js b/vis/Code/TimelineMarkers.js new file mode 100644 index 0000000..9054916 --- /dev/null +++ b/vis/Code/TimelineMarkers.js @@ -0,0 +1,186 @@ + +function GetTimeText(seconds) +{ + if (seconds < 0) + { + return ""; + } + + var text = ""; + + // Add any contributing hours + var h = Math.floor(seconds / 3600); + seconds -= h * 3600; + if (h) + { + text += h + "h "; + } + + // Add any contributing minutes + var m = Math.floor(seconds / 60); + seconds -= m * 60; + if (m) + { + text += m + "m "; + } + + // Add any contributing seconds or always add seconds when hours or minutes have no contribution + // This ensures the 0s marker displays + var s = Math.floor(seconds); + seconds -= s; + if (s || text == "") + { + text += s + "s "; + } + + // Add remaining milliseconds + var ms = Math.floor(seconds * 1000); + if (ms) + { + text += ms + "ms"; + } + + return text; +} + + +class TimelineMarkers +{ + constructor(timeline) + { + this.timeline = timeline; + + // Need a 2D drawing context + this.markerContainer = timeline.Window.AddControlNew(new WM.Container(10, 10, 10, 10)); + this.markerCanvas = document.createElement("canvas"); + this.markerContainer.Node.appendChild(this.markerCanvas); + this.markerContext = this.markerCanvas.getContext("2d"); + } + + Draw(time_range) + { + let ctx = this.markerContext; + + ctx.clearRect(0, 0, this.markerCanvas.width, this.markerCanvas.height); + + // Setup render state for the time line markers + ctx.strokeStyle = "#BBB"; + ctx.fillStyle = "#BBB"; + ctx.lineWidth = 1; + ctx.font = "9px LocalFiraCode"; + + // A list of all supported units of time (measured in seconds) that require markers + let units = [ 0.001, 0.01, 0.1, 1, 10, 60, 60 * 5, 60 * 60, 60 * 60 * 24 ]; + + // Given the current pixel size of a second, calculate the spacing for each unit marker + let second_pixel_size = time_range.PixelSize(1000 * 1000); + let sizeof_units = [ ]; + for (let unit of units) + { + sizeof_units.push(unit * second_pixel_size); + } + + // Calculate whether each unit marker is visible at the current zoom level + var show_unit = [ ]; + for (let sizeof_unit of sizeof_units) + { + show_unit.push(Math.max(Math.min((sizeof_unit - 4) * 0.25, 1), 0)); + } + + // Find the first visible unit + for (let i = 0; i < units.length; i++) + { + if (show_unit[i] > 0) + { + // Cut out unit information for the first set of units not visible + units = units.slice(i); + sizeof_units = sizeof_units.slice(i); + show_unit = show_unit.slice(i); + break; + } + } + + let timeline_end = this.markerCanvas.width; + for (let i = 0; i < 3; i++) + { + // Round the start time up to the next visible unit + let time_start = time_range.Start_us / (1000 * 1000); + let unit_time_start = Math.ceil(time_start / units[i]) * units[i]; + + // Calculate the canvas offset required to step to the first visible unit + let pre_step_x = time_range.PixelOffset(unit_time_start * (1000 * 1000)); + + // Draw lines for every unit at this level, keeping tracking of the seconds + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + // For the first two units, don't draw the units above it to prevent + // overdraw and the visual errors that causes + // The last unit always draws + if (i > 1 || (seconds % units[i + 1])) + { + // Only the first two units scale with unit visibility + // The last unit maintains its size + let height = Math.min(i * 4 + 4 * show_unit[i], 16); + + // Draw the line on an integer boundary, shifted by 0.5 to get an un-anti-aliased 1px line + let ix = Math.floor(x); + ctx.beginPath(); + ctx.moveTo(ix + 0.5, 1); + ctx.lineTo(ix + 0.5, 1 + height); + ctx.stroke(); + } + + seconds += units[i]; + } + + if (i == 1) + { + // Draw text labels for the second unit, fading them out as they slowly + // become the first unit + ctx.globalAlpha = show_unit[0]; + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + if (seconds % units[2]) + { + this.DrawTimeText(seconds, x, 16); + } + seconds += units[i]; + } + + // Restore alpha + ctx.globalAlpha = 1; + } + + else if (i == 2) + { + // Draw text labels for the third unit with no fade + var seconds = unit_time_start; + for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i]) + { + this.DrawTimeText(seconds, x, 16); + seconds += units[i]; + } + } + } + } + + DrawTimeText(seconds, x, y) + { + // Use text measuring to centre the text horizontally on the input x + var text = GetTimeText(seconds); + var width = this.markerContext.measureText(text).width; + this.markerContext.fillText(text, Math.floor(x) - width / 2, y); + } + + Resize(x, y, w, h) + { + this.markerContainer.SetPosition(x, y); + this.markerContainer.SetSize(w, h); + + // Match canvas size to container + this.markerCanvas.width = this.markerContainer.Node.clientWidth; + this.markerCanvas.height = this.markerContainer.Node.clientHeight; + } +} \ No newline at end of file diff --git a/vis/Code/TimelineRow.js b/vis/Code/TimelineRow.js new file mode 100644 index 0000000..562298b --- /dev/null +++ b/vis/Code/TimelineRow.js @@ -0,0 +1,389 @@ + + +TimelineRow = (function() +{ + const RowLabelTemplate = ` +
+
+ +
+
+
+
+
+
+
-
+
+
+
+
` + + + var CANVAS_BORDER = 1; + var SAMPLE_HEIGHT = 16; + var SAMPLE_BORDER = 1; + var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2; + + + function TimelineRow(gl, name, timeline, frame_history, check_handler) + { + this.Name = name; + this.timeline = timeline; + + // Create the row HTML and add to the parent + this.LabelContainerNode = DOM.Node.CreateHTML(RowLabelTemplate); + const label_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowLabel"); + label_node.innerHTML = name; + timeline.TimelineLabels.Node.appendChild(this.LabelContainerNode); + + // All sample view windows visible by default + const checkbox_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowCheckbox"); + checkbox_node.checked = true; + checkbox_node.addEventListener("change", (e) => check_handler(name, e)); + + // Manually hook-up events to simulate div:active + // I can't get the equivalent CSS to work in Firefox, so... + const expand_node_0 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 0); + const expand_node_1 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 1); + const inc_node = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton"); + const dec_node = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton"); + inc_node.addEventListener("mousedown", ExpandButtonDown); + inc_node.addEventListener("mouseup", ExpandButtonUp); + inc_node.addEventListener("mouseleave", ExpandButtonUp); + dec_node.addEventListener("mousedown", ExpandButtonDown); + dec_node.addEventListener("mouseup", ExpandButtonUp); + dec_node.addEventListener("mouseleave", ExpandButtonUp); + + // Pressing +/i increases/decreases depth + inc_node.addEventListener("click", () => this.IncDepth()); + dec_node.addEventListener("click", () => this.DecDepth()); + + // Frame index to start at when looking for first visible sample + this.StartFrameIndex = 0; + + this.FrameHistory = frame_history; + this.VisibleFrames = [ ]; + this.VisibleTimeRange = null; + this.Depth = 1; + + // Currently selected sample + this.SelectSampleInfo = null; + + // Create WebGL sample buffers + this.sampleBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8); + this.colourBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8); + + // Create a vertex array for these buffers + this.vertexArrayObject = gl.createVertexArray(); + gl.bindVertexArray(this.vertexArrayObject); + this.sampleBuffer.BindAsInstanceAttribute(timeline.Program, "inSample_TextOffset"); + this.colourBuffer.BindAsInstanceAttribute(timeline.Program, "inColour_TextLength"); + + // An initial SetSize call to restore containers to their original size after traces were loaded prior to this + this.SetSize(); + } + + + TimelineRow.prototype.SetSize = function() + { + this.LabelContainerNode.style.height = CANVAS_BORDER + SAMPLE_BORDER + SAMPLE_Y_SPACING * this.Depth; + } + + + TimelineRow.prototype.SetVisibleFrames = function(time_range) + { + // Clear previous visible list + this.VisibleFrames = [ ]; + if (this.FrameHistory.length == 0) + return; + + // Store a copy of the visible time range rather than referencing it + // This prevents external modifications to the time range from affecting rendering/selection + time_range = time_range.Clone(); + this.VisibleTimeRange = time_range; + + // The frame history can be reset outside this class + // This also catches the overflow to the end of the frame list below when a thread stops sending samples + var max_frame = Math.max(this.FrameHistory.length - 1, 0); + var start_frame_index = Math.min(this.StartFrameIndex, max_frame); + + // First do a back-track in case the time range moves negatively + while (start_frame_index > 0) + { + var frame = this.FrameHistory[start_frame_index]; + if (time_range.Start_us > frame.StartTime_us) + break; + start_frame_index--; + } + + // Then search from this point for the first visible frame + while (start_frame_index < this.FrameHistory.length) + { + var frame = this.FrameHistory[start_frame_index]; + if (frame.EndTime_us > time_range.Start_us) + break; + start_frame_index++; + } + + // Gather all frames up to the end point + this.StartFrameIndex = start_frame_index; + for (var i = start_frame_index; i < this.FrameHistory.length; i++) + { + var frame = this.FrameHistory[i]; + if (frame.StartTime_us > time_range.End_us) + break; + this.VisibleFrames.push(frame); + } + } + + + TimelineRow.prototype.DrawSampleHighlight = function(sample, depth, colour, y_scroll_offset) + { + if (depth <= this.Depth) + { + // Determine pixel range of the sample + var x0 = this.VisibleTimeRange.PixelOffset(sample.us_start); + var x1 = x0 + this.VisibleTimeRange.PixelSize(sample.us_length); + + var offset_x = x0; + var offset_y = this.LabelContainerNode.offsetTop + 2 + (depth - 1) * SAMPLE_Y_SPACING + y_scroll_offset; + var size_x = x1 - x0; + var size_y = SAMPLE_HEIGHT; + + // Normal rendering + var ctx = this.timeline.drawContext; + ctx.lineWidth = 2; + ctx.strokeStyle = colour; + ctx.strokeRect(offset_x + 2.5, offset_y - 0.5, size_x - 3, size_y + 1); + } + } + + + TimelineRow.prototype.DisplayHeight = function() + { + return this.LabelContainerNode.clientHeight; + } + + + TimelineRow.prototype.YOffset = function() + { + return this.LabelContainerNode.offsetTop; + } + + + TimelineRow.prototype.DrawBackground = function(hover_sample_info, y_scroll_offset) + { + // Fill box that shows the boundary between thread rows + this.timeline.drawContext.fillStyle = "#444" + var b = CANVAS_BORDER; + this.timeline.drawContext.fillRect(b, this.YOffset() + y_scroll_offset + b, this.timeline.drawCanvas.width - b * 2, this.DisplayHeight() - b * 2); + + // Draw the selected sample for this row + if (this.SelectSampleInfo != null) + { + const sample = this.SelectSampleInfo[1]; + const depth = this.SelectSampleInfo[2]; + this.DrawSampleHighlight(sample, depth, "#FF0000", y_scroll_offset); + } + + // Draw the current hover sample if it's over this row + if (hover_sample_info != null && hover_sample_info[3] == this) + { + const sample = hover_sample_info[1]; + const depth = hover_sample_info[2]; + const thread_row = hover_sample_info[3]; + this.DrawSampleHighlight(sample, depth, "#FFFFFF", y_scroll_offset); + } + } + + + TimelineRow.prototype.Draw = function(gl, draw_text, y_scroll_offset) + { + let samples_per_depth = []; + + // Gather all root samples in the visible frame set + for (var i in this.VisibleFrames) + { + var frame = this.VisibleFrames[i]; + GatherSamples(this, frame.Samples, 1, draw_text, samples_per_depth); + } + + // Count number of samples required + let nb_samples = 0; + for (const samples_this_depth of samples_per_depth) + { + nb_samples += samples_this_depth.length; + } + + // Resize buffers to match any new count of samples + if (nb_samples > this.sampleBuffer.nbEntries) + { + this.sampleBuffer.ResizeToFitNextPow2(nb_samples); + this.colourBuffer.ResizeToFitNextPow2(nb_samples); + + // Have to create a new VAO for these buffers + this.vertexArrayObject = gl.createVertexArray(); + gl.bindVertexArray(this.vertexArrayObject); + this.sampleBuffer.BindAsInstanceAttribute(this.timeline.Program, "inSample_TextOffset"); + this.colourBuffer.BindAsInstanceAttribute(this.timeline.Program, "inColour_TextLength"); + } + + // CPU write destination for samples + let cpu_samples = this.sampleBuffer.cpuArray; + let cpu_colours = this.colourBuffer.cpuArray; + let sample_pos = 0; + + const empty_text_entry = { + offset: 0, + length: 1, + }; + + // Copy samples to the CPU buffer + // TODO(don): Use a ring buffer instead and take advantage of timeline scrolling adding new samples at the beginning/end + for (let depth = 0; depth < samples_per_depth.length; depth++) + { + let samples_this_depth = samples_per_depth[depth]; + for (const sample of samples_this_depth) + { + const text_entry = sample.name.textEntry != null ? sample.name.textEntry : empty_text_entry; + + cpu_samples[sample_pos + 0] = sample.us_start; + cpu_samples[sample_pos + 1] = sample.us_length; + cpu_samples[sample_pos + 2] = depth; + cpu_samples[sample_pos + 3] = text_entry.offset; + + cpu_colours[sample_pos + 0] = sample.rgbColour[0]; + cpu_colours[sample_pos + 1] = sample.rgbColour[1]; + cpu_colours[sample_pos + 2] = sample.rgbColour[2]; + cpu_colours[sample_pos + 3] = text_entry.length; + + sample_pos += 4; + } + } + + // Upload to GPU + this.sampleBuffer.UploadData(); + this.colourBuffer.UploadData(); + this.timeline.textBuffer.UploadData(); + + // Set row parameters + glSetUniform(gl, this.timeline.Program, "inRow.yOffset", this.YOffset() + y_scroll_offset); + + gl.bindVertexArray(this.vertexArrayObject); + gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, nb_samples); + } + + + function GatherSamples(self, samples, depth, draw_text, samples_per_depth) + { + // Ensure there's enough entries for each depth + while (depth >= samples_per_depth.length) + { + samples_per_depth.push([]); + } + let samples_this_depth = samples_per_depth[depth]; + + for (var i in samples) + { + var sample = samples[i]; + samples_this_depth.push(sample); + + if (depth < self.Depth && sample.children != null) + GatherSamples(self, sample.children, depth + 1, draw_text, samples_per_depth); + } + } + + + TimelineRow.prototype.SetSelectSample = function(sample_info) + { + this.SelectSampleInfo = sample_info; + } + + + function ExpandButtonDown(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.AddClass(node, "TimelineRowExpandButtonActive"); + } + + + function ExpandButtonUp(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.RemoveClass(node, "TimelineRowExpandButtonActive"); + } + + + TimelineRow.prototype.IncDepth = function() + { + this.Depth++; + this.SetSize(); + this.timeline.DrawAllRows(); + } + + + TimelineRow.prototype.DecDepth = function() + { + if (this.Depth > 1) + { + this.Depth--; + this.SetSize(); + + // Trigger scroll handling to ensure reducing the depth reduces the display height + this.timeline.ScrollVertically(0); + + this.timeline.DrawAllRows(); + } + } + + + TimelineRow.prototype.GetSampleAtPosition = function(time_us, mouse_y) + { + // Calculate depth of the mouse cursor + var depth = Math.min(Math.floor(mouse_y / SAMPLE_Y_SPACING) + 1, this.Depth); + + // Search for the first frame to intersect this time + for (var i in this.VisibleFrames) + { + // Use the sample's closed interval to detect hits. + // Rendering of samples ensures a sample is never smaller than one pixel so that all samples always draw, irrespective + // of zoom level. If a half-open interval is used then some visible samples will be unselectable due to them being + // smaller than a pixel. This feels pretty odd and the closed interval fixes this feeling well. + // TODO(don): There are still inconsistencies, need to shift to pixel range checking to match exactly. + var frame = this.VisibleFrames[i]; + if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us) + { + var found_sample = FindSample(this, frame.Samples, time_us, depth, 1); + if (found_sample != null) + return [ frame, found_sample[0], found_sample[1], this ]; + } + } + + return null; + } + + + function FindSample(self, samples, time_us, target_depth, depth) + { + for (var i in samples) + { + var sample = samples[i]; + if (depth == target_depth) + { + if (time_us >= sample.us_start && time_us < sample.us_start + sample.us_length) + return [ sample, depth ]; + } + + else if (depth < target_depth && sample.children != null) + { + var found_sample = FindSample(self, sample.children, time_us, target_depth, depth + 1); + if (found_sample != null) + return found_sample; + } + } + + return null; + } + + + return TimelineRow; +})(); diff --git a/vis/Code/TimelineWindow.js b/vis/Code/TimelineWindow.js new file mode 100644 index 0000000..87a723e --- /dev/null +++ b/vis/Code/TimelineWindow.js @@ -0,0 +1,494 @@ + +// TODO(don): Separate all knowledge of threads from this timeline + +TimelineWindow = (function() +{ + var BORDER = 10; + + function TimelineWindow(wm, name, settings, check_handler) + { + this.Settings = settings; + + // Create timeline window + this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100); + this.Window.SetTitle(name); + this.Window.ShowNoAnim(); + + this.timelineMarkers = new TimelineMarkers(this); + + // DO THESE need to be containers... can they just be divs? + // divs need a retrieval function + this.TimelineLabelScrollClipper = this.Window.AddControlNew(new WM.Container(10, 10, 10, 10)); + DOM.Node.AddClass(this.TimelineLabelScrollClipper.Node, "TimelineLabelScrollClipper"); + this.TimelineLabels = this.TimelineLabelScrollClipper.AddControlNew(new WM.Container(0, 0, 10, 10)); + DOM.Node.AddClass(this.TimelineLabels.Node, "TimelineLabels"); + + // Ordered list of thread rows on the timeline + this.ThreadRows = [ ]; + + // Create timeline container + this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160)); + DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer"); + + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + + // Setup timeline manipulation + this.MouseDown = false; + this.LastMouseState = null; + this.TimelineMoved = false; + DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this)); + DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this)); + DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this)); + DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseleave", Bind(OnMouseLeave, this)); + + // Create a canvas for timeline 2D rendering + // TODO(don): Port this to shaders + this.drawCanvas = document.createElement("canvas"); + this.drawCanvas.width = this.TimelineContainer.Node.clientWidth; + this.drawCanvas.height = this.TimelineContainer.Node.clientHeight; + this.TimelineContainer.Node.appendChild(this.drawCanvas); + this.drawContext = this.drawCanvas.getContext("2d"); + + // Create a canvas for timeline 3D accelerated rendering + this.glCanvas = document.createElement("canvas"); + this.glCanvas.width = this.TimelineContainer.Node.clientWidth; + this.glCanvas.height = this.TimelineContainer.Node.clientHeight; + this.TimelineContainer.Node.appendChild(this.glCanvas); + + // OVERLAY - add to CSS + this.glCanvas.style.position = "absolute"; + this.glCanvas.style.top = 0; + this.glCanvas.style.left = 0; + + // For now a gl context per timeline + let gl = this.glCanvas.getContext("webgl2"); + this.gl = gl; + + const vshader = glCompileShader(gl, gl.VERTEX_SHADER, "TimelineVShader", TimelineVShader); + const fshader = glCompileShader(gl, gl.FRAGMENT_SHADER, "TimelineFShader", TimelineFShader); + this.Program = glCreateProgram(gl, vshader, fshader); + + this.font = new glFont(gl); + this.textBuffer = new glTextBuffer(gl, this.font); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + + this.Clear(); + + this.OnHoverHandler = null; + this.OnSelectedHandler = null; + this.OnMovedHandler = null; + this.CheckHandler = check_handler; + + this.yScrollOffset = 0; + + this.HoverSampleInfo = null; + } + + + TimelineWindow.prototype.Clear = function() + { + // Clear out labels + this.TimelineLabels.ClearControls(); + + this.ThreadRows = [ ]; + this.TimeRange = new PixelTimeRange(0, 200 * 1000, this.TimelineContainer.Node.clientWidth); + } + + + TimelineWindow.prototype.SetOnHover = function(handler) + { + this.OnHoverHandler = handler; + } + + + TimelineWindow.prototype.SetOnSelected = function(handler) + { + this.OnSelectedHandler = handler; + } + + + TimelineWindow.prototype.SetOnMoved = function(handler) + { + this.OnMovedHandler = handler; + } + + + TimelineWindow.prototype.WindowResized = function(x, width, top_window) + { + // Resize window + var top = top_window.Position[1] + top_window.Size[1] + 10; + this.Window.SetPosition(x, top); + this.Window.SetSize(width - 2 * 10, 260); + + ResizeInternals(this); + } + + + TimelineWindow.prototype.OnSamples = function(thread_name, frame_history) + { + // Shift the timeline to the last entry on this thread + // As multiple threads come through here with different end frames, only do this for the latest + var last_frame = frame_history[frame_history.length - 1]; + if (last_frame.EndTime_us > this.TimeRange.End_us) + this.TimeRange.SetEnd(last_frame.EndTime_us); + + // Search for the index of this thread + var thread_index = -1; + for (var i in this.ThreadRows) + { + if (this.ThreadRows[i].Name == thread_name) + { + thread_index = i; + break; + } + } + + // If this thread has not been seen before, add a new row to the list and re-sort + if (thread_index == -1) + { + var row = new TimelineRow(this.gl, thread_name, this, frame_history, this.CheckHandler); + this.ThreadRows.push(row); + } + } + + + TimelineWindow.prototype.DrawBackground = function() + { + // TODO(don): Port all this lot to shader, maybe... it's not performance sensitive + + this.drawContext.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); + + // Draw thread row backgrounds + for (let thread_row of this.ThreadRows) + { + thread_row.DrawBackground(this.HoverSampleInfo, this.yScrollOffset); + } + + this.timelineMarkers.Draw(this.TimeRange); + } + + + TimelineWindow.prototype.DrawAllRows = function() + { + let gl = this.gl; + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.useProgram(this.Program); + + // Set viewport parameters + glSetUniform(gl, this.Program, "inViewport.width", gl.canvas.width); + glSetUniform(gl, this.Program, "inViewport.height", gl.canvas.height); + + // Set time range parameters + const time_range = this.TimeRange; + time_range.SetAsUniform(gl, this.Program); + + // Set text rendering resources + // Note it might not be loaded yet so we need the null check + if (this.font.atlasTexture != null) + { + glSetUniform(gl, this.Program, "inFontAtlasTextre", this.font.atlasTexture, 0); + this.textBuffer.SetAsUniform(gl, this.Program, "inTextBuffer", 1); + } + + const draw_text = this.Settings.IsPaused; + for (let i in this.ThreadRows) + { + var thread_row = this.ThreadRows[i]; + thread_row.SetVisibleFrames(time_range); + thread_row.Draw(gl, draw_text, this.yScrollOffset); + } + + // Render last so that each thread row uses any new time ranges + this.DrawBackground(); + } + + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + // .TimelineRowLabel + // .TimelineRowExpand + // .TimelineRowExpand + // .TimelineRowCheck + // Window padding + let offset_x = 145+19+19+19+10; + + let MarkersHeight = 18; + + var parent_size = self.Window.Size; + + self.timelineMarkers.Resize(BORDER + offset_x, 10, parent_size[0] - 2* BORDER - offset_x, MarkersHeight); + + // Resize controls + self.TimelineContainer.SetPosition(BORDER + offset_x, 10 + MarkersHeight); + self.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER - offset_x, parent_size[1] - MarkersHeight - 40); + + self.TimelineLabelScrollClipper.SetPosition(10, 10 + MarkersHeight); + self.TimelineLabelScrollClipper.SetSize(offset_x, parent_size[1] - MarkersHeight - 40); + self.TimelineLabels.SetSize(offset_x, parent_size[1] - MarkersHeight - 40); + + // Match canvas size to container + const width = self.TimelineContainer.Node.clientWidth; + const height = self.TimelineContainer.Node.clientHeight; + self.drawCanvas.width = width; + self.drawCanvas.height = height; + self.glCanvas.width = width; + self.glCanvas.height = height; + + // Adjust time range to new width + self.TimeRange.SetPixelSpan(width); + self.DrawAllRows(); + } + + + function OnMouseScroll(self, evt) + { + let mouse_state = new Mouse.State(evt); + let scale = 1.11; + if (mouse_state.WheelDelta > 0) + scale = 1 / scale; + + // What time is the mouse hovering over? + let mouse_pos = self.TimelineMousePosition(mouse_state); + let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + + // Calculate start time relative to the mouse hover position + var time_start_us = self.TimeRange.Start_us - time_us; + + // Scale and offset back to the hover time + self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale); + self.DrawAllRows(); + + if (self.OnMovedHandler) + { + self.OnMovedHandler(self); + } + + // Prevent vertical scrolling on mouse-wheel + DOM.Event.StopDefaultAction(evt); + } + + + TimelineWindow.prototype.SetTimeRange = function(start_us, span_us) + { + this.TimeRange.Set(start_us, span_us); + } + + + TimelineWindow.prototype.DisplayHeight = function() + { + // Sum height of each thread row + let height = 0; + for (thread_row of this.ThreadRows) + { + height += thread_row.DisplayHeight(); + } + + return height; + } + + + TimelineWindow.prototype.ScrollVertically = function(y_scroll) + { + // Calculate the minimum negative value the position of the labels can be to account for scrolling to the bottom + // of the label/depth list + let display_height = this.DisplayHeight(); + let container_height = this.TimelineLabelScrollClipper.Node.clientHeight; + let minimum_y = Math.min(container_height - display_height, 0.0); + + // Resize the label container to match the display height + this.TimelineLabels.Node.style.height = Math.max(display_height, container_height); + + // Increment the y-scroll using just-calculated limits + let old_y_scroll_offset = this.yScrollOffset; + this.yScrollOffset = Math.min(Math.max(this.yScrollOffset + y_scroll, minimum_y), 0); + + // Calculate how much the labels should actually scroll after limiting and apply + let y_scroll_px = this.yScrollOffset - old_y_scroll_offset; + this.TimelineLabels.Node.style.top = this.TimelineLabels.Node.offsetTop + y_scroll_px; + } + + + TimelineWindow.prototype.TimelineMousePosition = function(mouse_state) + { + // Position of the mouse relative to the timeline container + let node_offset = DOM.Node.GetPosition(this.TimelineContainer.Node); + let mouse_x = mouse_state.Position[0] - node_offset[0]; + let mouse_y = mouse_state.Position[1] - node_offset[1]; + + // Offset by the amount of scroll + mouse_y -= this.yScrollOffset; + + return [ mouse_x, mouse_y ]; + } + + + TimelineWindow.prototype.GetHoverThreadRow = function(mouse_pos) + { + // Search for the thread row the mouse intersects + let height = 0; + for (let thread_row of this.ThreadRows) + { + let row_height = thread_row.DisplayHeight(); + if (mouse_pos[1] >= height && mouse_pos[1] < height + row_height) + { + // Mouse y relative to row start + mouse_pos[1] -= height; + return thread_row; + } + height += row_height; + } + + return null; + } + + + function OnMouseDown(self, evt) + { + // Only manipulate the timeline when paused + if (!self.Settings.IsPaused) + return; + + self.MouseDown = true; + self.LastMouseState = new Mouse.State(evt); + self.TimelineMoved = false; + DOM.Event.StopDefaultAction(evt); + } + + + function OnMouseUp(self, evt) + { + // Only manipulate the timeline when paused + if (!self.Settings.IsPaused) + return; + + var mouse_state = new Mouse.State(evt); + + self.MouseDown = false; + + if (!self.TimelineMoved) + { + // Are we hovering over a thread row? + let mouse_pos = self.TimelineMousePosition(mouse_state); + let hover_thread_row = self.GetHoverThreadRow(mouse_pos); + if (hover_thread_row != null) + { + // Are we hovering over a sample? + let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + let sample_info = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]); + if (sample_info != null) + { + // Redraw with new select sample + hover_thread_row.SetSelectSample(sample_info); + self.DrawBackground(); + + // Call any selection handlers + if (self.OnSelectedHandler) + { + self.OnSelectedHandler(hover_thread_row.Name, sample_info); + } + } + } + } + } + + + function OnMouseMove(self, evt) + { + // Only manipulate the timeline when paused + if (!self.Settings.IsPaused) + return; + + var mouse_state = new Mouse.State(evt); + + if (self.MouseDown) + { + let movement = false; + + // Shift the visible time range with mouse movement + let time_offset_us = (mouse_state.Position[0] - self.LastMouseState.Position[0]) / self.TimeRange.usPerPixel; + if (time_offset_us != 0) + { + self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us); + movement = true; + } + + // Control vertical movement + let y_offset_px = mouse_state.Position[1] - self.LastMouseState.Position[1]; + if (y_offset_px != 0) + { + self.ScrollVertically(y_offset_px); + movement = true; + } + + // Redraw everything if there is movement + if (movement) + { + self.DrawAllRows(); + self.TimelineMoved = true; + + if (self.OnMovedHandler) + { + self.OnMovedHandler(self); + } + } + } + + else + { + // Are we hovering over a thread row? + let mouse_pos = self.TimelineMousePosition(mouse_state); + let hover_thread_row = self.GetHoverThreadRow(mouse_pos); + if (hover_thread_row != null) + { + // Are we hovering over a sample? + let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]); + self.HoverSampleInfo = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]); + + // Tell listeners which sample we're hovering over + if (self.OnHoverHandler != null) + { + self.OnHoverHandler(hover_thread_row.Name, self.HoverSampleInfo); + } + } + else + { + self.HoverSampleInfo = null; + } + + // Redraw to update highlights + self.DrawBackground(); + } + + self.LastMouseState = mouse_state; + } + + + function OnMouseLeave(self, evt) + { + // Only manipulate the timeline when paused + if (!self.Settings.IsPaused) + return; + + // Cancel scrolling + self.MouseDown = false; + + // Cancel hovering + if (self.OnHoverHandler != null) + { + self.OnHoverHandler(null, null); + } + } + + + return TimelineWindow; +})(); + diff --git a/vis/Code/TitleWindow.js b/vis/Code/TitleWindow.js new file mode 100644 index 0000000..bae31dd --- /dev/null +++ b/vis/Code/TitleWindow.js @@ -0,0 +1,105 @@ + +TitleWindow = (function() +{ + function TitleWindow(wm, settings, server, connection_address) + { + this.Settings = settings; + + this.Window = wm.AddWindow("     Remotery", 10, 10, 100, 100); + this.Window.ShowNoAnim(); + + this.PingContainer = this.Window.AddControlNew(new WM.Container(4, -13, 10, 10)); + DOM.Node.AddClass(this.PingContainer.Node, "PingContainer"); + + this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address", connection_address)); + + // Setup pause button + this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true })); + this.PauseButton.SetOnClick(Bind(OnPausePressed, this)); + + this.SyncButton = this.Window.AddControlNew(new WM.Button("Sync Timelines", 5, 5, { toggle: true})); + this.SyncButton.SetOnClick(Bind(OnSyncPressed, this)); + this.SyncButton.SetState(this.Settings.SyncTimelines); + + server.AddMessageHandler("PING", Bind(OnPing, this)); + + this.Window.SetOnResize(Bind(OnUserResize, this)); + } + + + TitleWindow.prototype.SetConnectionAddressChanged = function(handler) + { + this.EditBox.SetChangeHandler(handler); + } + + + TitleWindow.prototype.WindowResized = function(width, height) + { + this.Window.SetSize(width - 2 * 10, 50); + ResizeInternals(this); + } + + TitleWindow.prototype.Pause = function() + { + if (!this.Settings.IsPaused) + { + this.PauseButton.SetText("Paused"); + this.PauseButton.SetState(true); + this.Settings.IsPaused = true; + } + } + + TitleWindow.prototype.Unpause = function() + { + if (this.Settings.IsPaused) + { + this.PauseButton.SetText("Pause"); + this.PauseButton.SetState(false); + this.Settings.IsPaused = false; + } + } + + function OnUserResize(self, evt) + { + ResizeInternals(self); + } + + function ResizeInternals(self) + { + self.PauseButton.SetPosition(self.Window.Size[0] - 60, 5); + self.SyncButton.SetPosition(self.Window.Size[0] - 155, 5); + } + + + function OnPausePressed(self) + { + if (self.PauseButton.IsPressed()) + { + self.Pause(); + } + else + { + self.Unpause(); + } + } + + + function OnSyncPressed(self) + { + self.Settings.SyncTimelines = self.SyncButton.IsPressed(); + } + + + function OnPing(self, server) + { + // Set the ping container as active and take it off half a second later + DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive"); + window.setTimeout(Bind(function(self) + { + DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive"); + }, self), 500); + } + + + return TitleWindow; +})(); \ No newline at end of file diff --git a/vis/Code/TraceDrop.js b/vis/Code/TraceDrop.js new file mode 100644 index 0000000..c6eda8a --- /dev/null +++ b/vis/Code/TraceDrop.js @@ -0,0 +1,134 @@ + +class TraceDrop +{ + constructor(remotery) + { + this.Remotery = remotery; + + // Create a full-page overlay div for dropping files onto + this.DropNode = DOM.Node.CreateHTML("
Load Remotery Trace
"); + document.body.appendChild(this.DropNode); + + // Attach drop handlers + window.addEventListener("dragenter", () => this.ShowDropZone()); + this.DropNode.addEventListener("dragenter", (e) => this.AllowDrag(e)); + this.DropNode.addEventListener("dragover", (e) => this.AllowDrag(e)); + this.DropNode.addEventListener("dragleave", () => this.HideDropZone()); + this.DropNode.addEventListener("drop", (e) => this.OnDrop(e)); + } + + ShowDropZone() + { + this.DropNode.style.display = "flex"; + } + + HideDropZone() + { + this.DropNode.style.display = "none"; + } + + AllowDrag(evt) + { + // Prevent the default drag handler kicking in + evt.preventDefault(); + + evt.dataTransfer.dropEffect = "copy"; + } + + OnDrop(evt) + { + // Prevent the default drop handler kicking in + evt.preventDefault(); + + this.HideDropZone(evt); + + // Get the file that was dropped + let files = DOM.Event.GetDropFiles(evt); + if (files.length == 0) + { + alert("No files dropped"); + return; + } + if (files.length > 1) + { + alert("Too many files dropped"); + return; + } + + // Check file type + let file = files[0]; + if (!file.name.endsWith(".rbin")) + { + alert("Not the correct .rbin file type"); + return; + } + + // Background-load the file + var remotery = this.Remotery; + let file_reader = new FileReader(); + file_reader.onload = function() + { + // Create the data reader and verify the header + let data_view = new DataView(this.result); + let data_view_reader = new DataViewReader(data_view, 0); + let header = data_view_reader.GetStringOfLength(8); + if (header != "RMTBLOGF") + { + alert("Not a valid Remotery Log File"); + return; + } + + remotery.Clear(); + + try + { + // Forward all recorded events to message handlers + while (!data_view_reader.AtEnd()) + { + remotery.Server.CallMessageHandlers(data_view_reader); + } + } + catch (e) + { + // The last message may be partially written due to process exit + // Catch this safely as it's a valid state for the file to be in + if (e instanceof RangeError) + { + console.log("Aborted reading last message"); + } + } + + // After loading completes, populate the UI which wasn't updated during loading + + remotery.Console.TriggerUpdate(); + + // Set frame history for each timeline thread + for (let name in remotery.FrameHistory) + { + let frame_history = remotery.FrameHistory[name]; + remotery.SampleTimelineWindow.OnSamples(name, frame_history); + } + remotery.SampleTimelineWindow.DrawAllRows(); + + for (let name in remotery.ProcessorFrameHistory) + { + let frame_history = remotery.ProcessorFrameHistory[name]; + remotery.ProcessorTimelineWindow.OnSamples(name, frame_history); + } + remotery.ProcessorTimelineWindow.DrawAllRows(); + + // Set the last frame of each thread sample history on the sample windows + for (let name in remotery.SampleWindows) + { + let sample_window = remotery.SampleWindows[name]; + let frame_history = remotery.FrameHistory[name]; + let frame = frame_history[frame_history.length - 1]; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + + // Pause for viewing + remotery.TitleWindow.Pause(); + }; + file_reader.readAsArrayBuffer(file); + } +} \ No newline at end of file diff --git a/vis/Code/WebGL.js b/vis/Code/WebGL.js new file mode 100644 index 0000000..43fc9cc --- /dev/null +++ b/vis/Code/WebGL.js @@ -0,0 +1,238 @@ + +function assert(condition, message) +{ + if (!condition) + { + throw new Error(message || "Assertion failed"); + } +} + +function glCompileShader(gl, type, name, source) +{ + console.log("Compiling " + name); + + // Compile the shader + let shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + // Report any errors + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) + { + console.log("Error compiling " + name); + console.log(gl.getShaderInfoLog(shader)); + console.trace(); + } + + return shader; +} + +function glCreateProgram(gl, vshader, fshader) +{ + // Attach shaders and link + let program = gl.createProgram(); + gl.attachShader(program, vshader); + gl.attachShader(program, fshader); + gl.linkProgram(program); + + // Report any errors + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) + { + console.log("Failed to link program"); + console.trace(); + } + + return program; +} + +function glSetUniform(gl, program, name, value, index) +{ + // Get location + const location = gl.getUniformLocation(program, name); + assert(location != null, "Can't find uniform " + name); + + // Dispatch to uniform function by type + assert(value != null, "Value is null"); + const type = Object.prototype.toString.call(value).slice(8, -1); + switch (type) + { + case "Number": + gl.uniform1f(location, value); + break; + + case "WebGLTexture": + gl.activeTexture(gl.TEXTURE0 + index); + gl.bindTexture(gl.TEXTURE_2D, value); + gl.uniform1i(location, index); + break; + + default: + assert(false, "Unhandled type " + type); + break; + } +} + +function glCreateTexture(gl, width, height, data) +{ + const texture = gl.createTexture(); + + // Set filtering/wrapping to nearest/clamp + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); + + return texture; +} + +const glDynamicBufferType = Object.freeze({ + Buffer: 1, + Texture: 2 +}); + +class glDynamicBuffer +{ + constructor(gl, element_type, nb_elements, nb_entries, buffer_type) + { + this.gl = gl; + this.elementType = element_type; + this.nbElements = nb_elements; + this.bufferType = buffer_type == undefined ? glDynamicBufferType.Buffer : buffer_type; + this.dirty = false; + + this.Resize(nb_entries); + } + + BindAsInstanceAttribute(program, attrib_name) + { + assert(this.bufferType == glDynamicBufferType.Buffer, "Can only call BindAsInstanceAttribute with Buffer types"); + + let gl = this.gl; + + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + + // The attribute referenced in the program + const attrib_location = gl.getAttribLocation(program, attrib_name); + + gl.enableVertexAttribArray(attrib_location); + gl.vertexAttribPointer(attrib_location, this.nbElements, this.elementType, false, 0, 0); + + // One per instance + gl.vertexAttribDivisor(attrib_location, 1); + } + + UploadData() + { + let gl = this.gl; + + switch (this.bufferType) + { + case glDynamicBufferType.Buffer: + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.cpuArray); + break; + + case glDynamicBufferType.Texture: + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray); + break; + } + } + + UploadDirtyData() + { + if (this.dirty) + { + this.UploadData(); + this.dirty = false; + } + } + + ResizeToFitNextPow2(target_count) + { + let nb_entries = this.nbEntries; + while (target_count > nb_entries) + { + nb_entries <<= 1; + } + + if (nb_entries > this.nbEntries) + { + this.Resize(nb_entries); + } + } + + Resize(nb_entries) + { + let gl = this.gl; + + this.nbEntries = nb_entries; + + // Create the CPU array + const old_array = this.cpuArray; + switch (this.elementType) + { + case gl.FLOAT: + this.nbElementBytes = 4; + this.cpuArray = new Float32Array(this.nbElements * this.nbEntries); + break; + + case gl.BYTE: + this.nbElementBytes = 1; + this.cpuArray = new Uint8Array(this.nbElements * this.nbEntries); + break; + + default: + assert(false, "Unsupported dynamic buffer element type"); + } + + // Calculate byte size of the buffer + this.nbBytes = this.nbElementBytes * this.nbElements * this.nbEntries; + + if (old_array != undefined) + { + // Copy the values of the previous array over + this.cpuArray.set(old_array); + + console.log(`glDynamicBuffer: Resizing to ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`); + } + else + { + console.log(`glDynamicBuffer: Creating ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`); + } + + // Create the GPU buffer + switch (this.bufferType) + { + case glDynamicBufferType.Buffer: + this.buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, this.nbBytes, gl.DYNAMIC_DRAW); + break; + + case glDynamicBufferType.Texture: + this.texture = gl.createTexture(); + + // Point sampling with clamp for indexing + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + // Fixed-format for now + assert(this.elementType == gl.BYTE); + assert(this.nbElements == 1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray); + + break; + + default: + assert(false, "Unsupported dynamic buffer type"); + } + } +}; + diff --git a/vis/Code/WebGLFont.js b/vis/Code/WebGLFont.js new file mode 100644 index 0000000..263e039 --- /dev/null +++ b/vis/Code/WebGLFont.js @@ -0,0 +1,119 @@ + +class glFont +{ + constructor(gl) + { + // Offscreen canvas for rendering individual characters + this.charCanvas = document.createElement("canvas"); + this.charContext = this.charCanvas.getContext("2d"); + + // Describe the font + const font_size = 9; + this.fontWidth = 5; + this.fontHeight = 12; + const font_face = "LocalFiraCode"; + const font_desc = font_size + "px " + font_face; + + // Ensure the CSS font is loaded before we do any work with it + const self = this; + document.fonts.load(font_desc).then(function (){ + + // Create a canvas atlas for all characters in the font + const atlas_canvas = document.createElement("canvas"); + const atlas_context = atlas_canvas.getContext("2d"); + atlas_canvas.width = 16 * self.fontWidth; + atlas_canvas.height = 16 * self.fontHeight; + + // Add each character to the atlas + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-+=[]{};\'~#,./<>?!\"£$%%^&*()"; + for (let char of chars) + { + // Render this character to the canvas on its own + self.RenderTextToCanvas(char, font_desc, self.fontWidth, self.fontHeight); + + // Calculate a location for it in the atlas using its ASCII code + const ascii_code = char.charCodeAt(0); + assert(ascii_code < 256); + const y_index = Math.floor(ascii_code / 16); + const x_index = ascii_code - y_index * 16 + assert(x_index < 16); + assert(y_index < 16); + + // Copy into the atlas + atlas_context.drawImage(self.charCanvas, x_index * self.fontWidth, y_index * self.fontHeight); + } + + // Create the atlas texture and store it in the destination object + self.atlasTexture = glCreateTexture(gl, atlas_canvas.width, atlas_canvas.height, atlas_canvas); + }); + } + + RenderTextToCanvas(text, font, width, height) + { + // Resize canvas to match + this.charCanvas.width = width; + this.charCanvas.height = height; + + // Clear the background + this.charContext.fillStyle = "black"; + this.charContext.clearRect(0, 0, width, height); + + // Render the text + this.charContext.font = font; + this.charContext.textAlign = "left"; + this.charContext.textBaseline = "top"; + this.charContext.fillText(text, 0, 1.5); + } +} + +class glTextBuffer +{ + constructor(gl, font) + { + this.font = font; + this.textMap = {}; + this.textBuffer = new glDynamicBuffer(gl, gl.BYTE, 1, 8, glDynamicBufferType.Texture); + this.textBufferPos = 0; + this.textEncoder = new TextEncoder(); + } + + AddText(text) + { + // Return if it already exists + const existing_entry = this.textMap[text]; + if (existing_entry != undefined) + { + return existing_entry; + } + + // Add to the map + // Note we're leaving an extra NULL character before every piece of text so that the shader can sample into it on text + // boundaries and sample a zero colour for clamp. + let entry = { + offset: this.textBufferPos + 1, + length: text.length, + }; + this.textMap[text] = entry; + + // Ensure there's always enough space in the text buffer before adding + this.textBuffer.ResizeToFitNextPow2(entry.offset + entry.length + 1); + this.textBuffer.cpuArray.set(this.textEncoder.encode(text), entry.offset, entry.length); + this.textBuffer.dirty = true; + this.textBufferPos = entry.offset + entry.length; + + return entry; + } + + UploadData() + { + this.textBuffer.UploadDirtyData(); + } + + SetAsUniform(gl, program, name, index) + { + glSetUniform(gl, program, name, this.textBuffer.texture, index); + glSetUniform(gl, program, "inTextBufferDesc.fontWidth", this.font.fontWidth); + glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight); + glSetUniform(gl, program, "inTextBufferDesc.textBufferLength", this.textBuffer.nbEntries); + } +} \ No newline at end of file diff --git a/vis/Code/WebSocketConnection.js b/vis/Code/WebSocketConnection.js new file mode 100644 index 0000000..92a6153 --- /dev/null +++ b/vis/Code/WebSocketConnection.js @@ -0,0 +1,137 @@ + +WebSocketConnection = (function() +{ + function WebSocketConnection() + { + this.MessageHandlers = { }; + this.Socket = null; + this.Console = null; + } + + + WebSocketConnection.prototype.SetConsole = function(console) + { + this.Console = console; + } + + + WebSocketConnection.prototype.Connected = function() + { + // Will return true if the socket is also in the process of connecting + return this.Socket != null; + } + + + WebSocketConnection.prototype.AddConnectHandler = function(handler) + { + this.AddMessageHandler("__OnConnect__", handler); + } + + + WebSocketConnection.prototype.AddDisconnectHandler = function(handler) + { + this.AddMessageHandler("__OnDisconnect__", handler); + } + + + WebSocketConnection.prototype.AddMessageHandler = function(message_name, handler) + { + // Create the message handler array on-demand + if (!(message_name in this.MessageHandlers)) + this.MessageHandlers[message_name] = [ ]; + this.MessageHandlers[message_name].push(handler); + } + + + WebSocketConnection.prototype.Connect = function(address) + { + // Disconnect if already connected + if (this.Connected()) + this.Disconnect(); + + Log(this, "Connecting to " + address); + + this.Socket = new WebSocket(address); + this.Socket.binaryType = "arraybuffer"; + this.Socket.onopen = Bind(OnOpen, this); + this.Socket.onmessage = Bind(OnMessage, this); + this.Socket.onclose = Bind(OnClose, this); + this.Socket.onerror = Bind(OnError, this); + } + + + WebSocketConnection.prototype.Disconnect = function() + { + Log(this, "Disconnecting"); + if (this.Connected()) + this.Socket.close(); + } + + + WebSocketConnection.prototype.Send = function(msg) + { + if (this.Connected()) + this.Socket.send(msg); + } + + + function Log(self, message) + { + self.Console.Log(message); + } + + + function CallMessageHandlers(self, message_name, data_view) + { + if (message_name in self.MessageHandlers) + { + var handlers = self.MessageHandlers[message_name]; + for (var i in handlers) + handlers[i](self, data_view); + } + } + + + function OnOpen(self, event) + { + Log(self, "Connected"); + CallMessageHandlers(self, "__OnConnect__"); + } + + + function OnClose(self, event) + { + // Clear all references + self.Socket.onopen = null; + self.Socket.onmessage = null; + self.Socket.onclose = null; + self.Socket.onerror = null; + self.Socket = null; + + Log(self, "Disconnected"); + CallMessageHandlers(self, "__OnDisconnect__"); + } + + + function OnError(self, event) + { + Log(self, "Connection Error "); + } + + + function OnMessage(self, event) + { + let data_view = new DataView(event.data); + let data_view_reader = new DataViewReader(data_view, 0); + self.CallMessageHandlers(data_view_reader); + } + + WebSocketConnection.prototype.CallMessageHandlers = function(data_view_reader) + { + let id = data_view_reader.GetStringOfLength(4); + CallMessageHandlers(this, id, data_view_reader); + } + + + return WebSocketConnection; +})(); diff --git a/vis/Styles/Fonts/FiraCode/FiraCode-Regular.ttf b/vis/Styles/Fonts/FiraCode/FiraCode-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0d570685b44c04723bf4ca6c954839034346723d GIT binary patch literal 299152 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 literal 0 HcmV?d00001 diff --git a/vis/Styles/Fonts/FiraCode/LICENSE b/vis/Styles/Fonts/FiraCode/LICENSE new file mode 100644 index 0000000..805e0b3 --- /dev/null +++ b/vis/Styles/Fonts/FiraCode/LICENSE @@ -0,0 +1,93 @@ +Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/vis/Styles/Remotery.css b/vis/Styles/Remotery.css new file mode 100644 index 0000000..24d69ce --- /dev/null +++ b/vis/Styles/Remotery.css @@ -0,0 +1,274 @@ + +body +{ + /* Take up the full page */ + width: 100%; + height: 100%; + margin: 0px; + + background-color: #999; + + touch-action: none; +} + + +/* Override default container style to remove 3D effect */ +.Container +{ + border: none; + box-shadow: none; +} + + +/* Override default edit box style to remove 3D effect */ +.EditBox +{ + border: none; + box-shadow: none; + width:200; +} + + +@font-face +{ + font-family: "LocalFiraCode"; + src:url("Fonts/FiraCode/FiraCode-Regular.ttf"); +} + +.ConsoleText +{ + overflow:auto; + color: #BBB; + font: 10px LocalFiraCode; + margin: 3px; + white-space: pre; + line-height:14px; +} + + +.PingContainer +{ + background-color: #F55; + border-radius: 2px; + + /* Transition from green is gradual */ + transition: background-color 0.25s ease-in; +} + + +.PingContainerActive +{ + background-color: #5F5; + + /* Transition to green is instant */ + transition: none; +} + + +.SampleNameCell +{ + width:243px; +} +.SampleTimeCell +{ + width:52px; +} +.SampleCountCell +{ + width:43px; +} +.SampleTitleNameCell +{ + width:238px; + + padding: 1px 1px 1px 2px; + border: 1px solid; + border-radius: 2px; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; +} +.SampleTitleTimeCell +{ + width:47px; + + padding: 1px 1px 1px 2px; + border: 1px solid; + border-radius: 2px; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; +} +.SampleTitleCountCell +{ + width:38px; + + padding: 1px 1px 1px 2px; + border: 1px solid; + border-radius: 2px; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; +} + + +.TimelineBox +{ + /* Following style generally copies GridRowCell.GridGroup from BrowserLib */ + + padding: 1px 1px 1px 2px; + margin: 1px; + + border: 1px solid; + border-radius: 2px; + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; + + font: 9px Verdana; + color: #BBB; +} +.TimelineRow +{ + width: 100%; +} +.TimelineRowCheckbox +{ + width: 12px; + height: 12px; + margin: 0px; +} +.TimelineRowCheck +{ + /* Pull .TimelineRowExpand to the right of the checkbox */ + float:left; + + width: 14px; + height: 14px; +} +.TimelineRowExpand +{ + /* Pull .TimelineRowLabel to the right of +/- buttons */ + float:left; + + width: 14px; + height: 14px; +} +.TimelineRowExpandButton +{ + width: 11px; + height: 12px; + + color: #333; + + border: 1px solid; + + border-top-color:#F4F4F4; + border-left-color:#F4F4F4; + border-bottom-color:#8E8F8F; + border-right-color:#8E8F8F; + + /* Top-right to bottom-left grey background gradient */ + background: #f6f6f6; /* Old browsers */ + background: -moz-linear-gradient(-45deg, #f6f6f6 0%, #abaeb2 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,#f6f6f6), color-stop(100%,#abaeb2)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(-45deg, #f6f6f6 0%,#abaeb2 100%); /* IE10+ */ + background: linear-gradient(135deg, #f6f6f6 0%,#abaeb2 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f6f6f6', endColorstr='#abaeb2',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ + + text-align: center; + vertical-align: center; +} +.TimelineRowExpandButton:hover +{ + border-top-color:#79C6F9; + border-left-color:#79C6F9; + border-bottom-color:#385D72; + border-right-color:#385D72; + + /* Top-right to bottom-left blue background gradient, matching border */ + background: #f3f3f3; /* Old browsers */ + background: -moz-linear-gradient(-45deg, #f3f3f3 0%, #79c6f9 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,#f3f3f3), color-stop(100%,#79c6f9)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(-45deg, #f3f3f3 0%,#79c6f9 100%); /* IE10+ */ + background: linear-gradient(135deg, #f3f3f3 0%,#79c6f9 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f3f3f3', endColorstr='#79c6f9',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ +} +.TimelineRowExpandButtonActive +{ + /* Simple means of shifting text within a div to the bottom-right */ + padding-left:1px; + padding-top:1px; + width:10px; + height:11px; +} +.TimelineRowLabel +{ + float:left; + + width: 140px; + height: 14px; +} + +.TimelineContainer +{ +} +.TimelineLabels +{ + padding: 0; + margin: 0; + border: 0; + overflow-y: hidden; +} +.TimelineLabelScrollClipper +{ + padding: 0; + margin: 0; + border: 0; + overflow-y: hidden; +} + +.DropZone +{ + /* Covers the whole page, initially hidden */ + box-sizing: border-box; + display: none; + position: fixed; + width: 100%; + height: 100%; + left: 0; + top: 0; + + /* On top of everything possible */ + z-index: 99999; + + /* Styling for when visible */ + background: rgba(32, 4, 136, 0.25); + border: 3px dashed white; + + /* Styling for text when visible */ + color: white; + font-family: Arial, Helvetica, sans-serif; + font-size: xx-large; + align-items: center; + justify-content: center; +} diff --git a/vis/extern/BrowserLib/Core/Code/Animation.js b/vis/extern/BrowserLib/Core/Code/Animation.js new file mode 100644 index 0000000..516aa9c --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Animation.js @@ -0,0 +1,65 @@ + +// +// Very basic linear value animation system, for now. +// + + +namespace("Anim"); + + +Anim.Animation = (function() +{ + var anim_hz = 60; + + + function Animation(anim_func, start_value, end_value, time, end_callback) + { + // Setup initial parameters + this.StartValue = start_value; + this.EndValue = end_value; + this.ValueInc = (end_value - start_value) / (time * anim_hz); + this.Value = start_value; + this.Complete = false; + this.EndCallback = end_callback; + + // Cache the update function to prevent recreating the closure + var self = this; + this.AnimFunc = anim_func; + this.AnimUpdate = function() { Update(self); } + + // Call for the start value + this.AnimUpdate(); + } + + + function Update(self) + { + // Queue up the next frame immediately + var id = window.setTimeout(self.AnimUpdate, 1000 / anim_hz); + + // Linear step the value and check for completion + self.Value += self.ValueInc; + if (Math.abs(self.Value - self.EndValue) < 0.01) + { + self.Value = self.EndValue; + self.Complete = true; + + if (self.EndCallback) + self.EndCallback(); + + window.clearTimeout(id); + } + + // Pass to the animation function + self.AnimFunc(self.Value); + } + + + return Animation; +})(); + + +Anim.Animate = function(anim_func, start_value, end_value, time, end_callback) +{ + return new Anim.Animation(anim_func, start_value, end_value, time, end_callback); +} diff --git a/vis/extern/BrowserLib/Core/Code/Bind.js b/vis/extern/BrowserLib/Core/Code/Bind.js new file mode 100644 index 0000000..102ee26 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Bind.js @@ -0,0 +1,92 @@ +// +// This will generate a closure for the given function and optionally bind an arbitrary number of +// its initial arguments to specific values. +// +// Parameters: +// +// 0: Either the function scope or the function. +// 1: If 0 is the function scope, this is the function. +// Otherwise it's the start of the optional bound argument list. +// 2: Start of the optional bound argument list if 1 is the function. +// +// Examples: +// +// function GlobalFunction(p0, p1, p2) { } +// function ThisFunction(p0, p1, p2) { } +// +// var a = Bind("GlobalFunction"); +// var b = Bind(this, "ThisFunction"); +// var c = Bind("GlobalFunction", BoundParam0, BoundParam1); +// var d = Bind(this, "ThisFunction", BoundParam0, BoundParam1); +// var e = Bind(GlobalFunction); +// var f = Bind(this, ThisFunction); +// var g = Bind(GlobalFunction, BoundParam0, BoundParam1); +// var h = Bind(this, ThisFunction, BoundParam0, BoundParam1); +// +// a(0, 1, 2); +// b(0, 1, 2); +// c(2); +// d(2); +// e(0, 1, 2); +// f(0, 1, 2); +// g(2); +// h(2); +// +function Bind() +{ + // No closure to define? + if (arguments.length == 0) + return null; + + // Figure out which of the 4 call types is being used to bind + // Locate scope, function and bound parameter start index + + if (typeof(arguments[0]) == "string") + { + var scope = window; + var func = window[arguments[0]]; + var start = 1; + } + + else if (typeof(arguments[0]) == "function") + { + var scope = window; + var func = arguments[0]; + var start = 1; + } + + else if (typeof(arguments[1]) == "string") + { + var scope = arguments[0]; + var func = scope[arguments[1]]; + var start = 2; + } + + else if (typeof(arguments[1]) == "function") + { + var scope = arguments[0]; + var func = arguments[1]; + var start = 2; + } + + else + { + // unknown + console.log("Bind() ERROR: Unknown bind parameter configuration"); + return; + } + + // Convert the arguments list to an array + var arg_array = Array.prototype.slice.call(arguments, start); + start = arg_array.length; + + return function() + { + // Concatenate incoming arguments + for (var i = 0; i < arguments.length; i++) + arg_array[start + i] = arguments[i]; + + // Call the function in the given scope with the new arguments + return func.apply(scope, arg_array); + } +} diff --git a/vis/extern/BrowserLib/Core/Code/Convert.js b/vis/extern/BrowserLib/Core/Code/Convert.js new file mode 100644 index 0000000..b1f5461 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Convert.js @@ -0,0 +1,218 @@ + +namespace("Convert"); + + +// +// Convert between utf8 and b64 without raising character out of range exceptions with unicode strings +// Technique described here: http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html +// +Convert.utf8string_to_b64string = function(str) +{ + return btoa(unescape(encodeURIComponent(str))); +} +Convert.b64string_to_utf8string = function(str) +{ + return decodeURIComponent(escape(atob(str))); +} + + +// +// More general approach, converting between byte arrays and b64 +// Info here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding +// +Convert.b64string_to_Uint8Array = function(sBase64, nBlocksSize) +{ + function b64ToUint6 (nChr) + { + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + } + + var + sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), + nInLen = sB64Enc.length, + nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, + taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) + { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) + { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + nUint24 = 0; + } + } + + return taBytes; +} +Convert.Uint8Array_to_b64string = function(aBytes) +{ + function uint6ToB64 (nUint6) + { + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; + } + + var nMod3, sB64Enc = ""; + + for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) + { + nMod3 = nIdx % 3; + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) + sB64Enc += "\r\n"; + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) + { + sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + + return sB64Enc.replace(/A(?=A$|$)/g, "="); +} + + +// +// Unicode and arbitrary value safe conversion between strings and Uint8Arrays +// +Convert.Uint8Array_to_string = function(aBytes) +{ + var sView = ""; + + for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) + { + nPart = aBytes[nIdx]; + sView += String.fromCharCode( + nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */ + /* (nPart - 252 << 32) is not possible in ECMAScript! So...: */ + (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */ + (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */ + (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */ + (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128 + : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */ + (nPart - 192 << 6) + aBytes[++nIdx] - 128 + : /* nPart < 127 ? */ /* one byte */ + nPart + ); + } + + return sView; +} +Convert.string_to_Uint8Array = function(sDOMStr) +{ + var aBytes, nChr, nStrLen = sDOMStr.length, nArrLen = 0; + + /* mapping... */ + + for (var nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) + { + nChr = sDOMStr.charCodeAt(nMapIdx); + nArrLen += nChr < 0x80 ? 1 : nChr < 0x800 ? 2 : nChr < 0x10000 ? 3 : nChr < 0x200000 ? 4 : nChr < 0x4000000 ? 5 : 6; + } + + aBytes = new Uint8Array(nArrLen); + + /* transcription... */ + + for (var nIdx = 0, nChrIdx = 0; nIdx < nArrLen; nChrIdx++) + { + nChr = sDOMStr.charCodeAt(nChrIdx); + if (nChr < 128) + { + /* one byte */ + aBytes[nIdx++] = nChr; + } + else if (nChr < 0x800) + { + /* two bytes */ + aBytes[nIdx++] = 192 + (nChr >>> 6); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x10000) + { + /* three bytes */ + aBytes[nIdx++] = 224 + (nChr >>> 12); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x200000) + { + /* four bytes */ + aBytes[nIdx++] = 240 + (nChr >>> 18); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else if (nChr < 0x4000000) + { + /* five bytes */ + aBytes[nIdx++] = 248 + (nChr >>> 24); + aBytes[nIdx++] = 128 + (nChr >>> 18 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + else /* if (nChr <= 0x7fffffff) */ + { + /* six bytes */ + aBytes[nIdx++] = 252 + /* (nChr >>> 32) is not possible in ECMAScript! So...: */ (nChr / 1073741824); + aBytes[nIdx++] = 128 + (nChr >>> 24 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 18 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 12 & 63); + aBytes[nIdx++] = 128 + (nChr >>> 6 & 63); + aBytes[nIdx++] = 128 + (nChr & 63); + } + } + + return aBytes; +} + + +// +// Converts all characters in a string that have equivalent entities to their ampersand/entity names. +// Based on https://gist.github.com/jonathantneal/6093551 +// +Convert.string_to_html_entities = (function() +{ + 'use strict'; + + var data = '34quot38amp39apos60lt62gt160nbsp161iexcl162cent163pound164curren165yen166brvbar167sect168uml169copy170ordf171laquo172not173shy174reg175macr176deg177plusmn178sup2179sup3180acute181micro182para183middot184cedil185sup1186ordm187raquo188frac14189frac12190frac34191iquest192Agrave193Aacute194Acirc195Atilde196Auml197Aring198AElig199Ccedil200Egrave201Eacute202Ecirc203Euml204Igrave205Iacute206Icirc207Iuml208ETH209Ntilde210Ograve211Oacute212Ocirc213Otilde214Ouml215times216Oslash217Ugrave218Uacute219Ucirc220Uuml221Yacute222THORN223szlig224agrave225aacute226acirc227atilde228auml229aring230aelig231ccedil232egrave233eacute234ecirc235euml236igrave237iacute238icirc239iuml240eth241ntilde242ograve243oacute244ocirc245otilde246ouml247divide248oslash249ugrave250uacute251ucirc252uuml253yacute254thorn255yuml402fnof913Alpha914Beta915Gamma916Delta917Epsilon918Zeta919Eta920Theta921Iota922Kappa923Lambda924Mu925Nu926Xi927Omicron928Pi929Rho931Sigma932Tau933Upsilon934Phi935Chi936Psi937Omega945alpha946beta947gamma948delta949epsilon950zeta951eta952theta953iota954kappa955lambda956mu957nu958xi959omicron960pi961rho962sigmaf963sigma964tau965upsilon966phi967chi968psi969omega977thetasym978upsih982piv8226bull8230hellip8242prime8243Prime8254oline8260frasl8472weierp8465image8476real8482trade8501alefsym8592larr8593uarr8594rarr8595darr8596harr8629crarr8656lArr8657uArr8658rArr8659dArr8660hArr8704forall8706part8707exist8709empty8711nabla8712isin8713notin8715ni8719prod8721sum8722minus8727lowast8730radic8733prop8734infin8736ang8743and8744or8745cap8746cup8747int8756there48764sim8773cong8776asymp8800ne8801equiv8804le8805ge8834sub8835sup8836nsub8838sube8839supe8853oplus8855otimes8869perp8901sdot8968lceil8969rceil8970lfloor8971rfloor9001lang9002rang9674loz9824spades9827clubs9829hearts9830diams338OElig339oelig352Scaron353scaron376Yuml710circ732tilde8194ensp8195emsp8201thinsp8204zwnj8205zwj8206lrm8207rlm8211ndash8212mdash8216lsquo8217rsquo8218sbquo8220ldquo8221rdquo8222bdquo8224dagger8225Dagger8240permil8249lsaquo8250rsaquo8364euro'; + var charCodes = data.split(/[A-z]+/); + var entities = data.split(/\d+/).slice(1); + + return function encodeHTMLEntities(text) + { + return text.replace(/[\u00A0-\u2666<>"'&]/g, function (match) + { + var charCode = String(match.charCodeAt(0)); + var index = charCodes.indexOf(charCode); + return '&' + (entities[index] ? entities[index] : '#' + charCode) + ';'; + }); + }; +})(); diff --git a/vis/extern/BrowserLib/Core/Code/Core.js b/vis/extern/BrowserLib/Core/Code/Core.js new file mode 100644 index 0000000..542f54f --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Core.js @@ -0,0 +1,26 @@ + +// TODO: requires function for checking existence of dependencies + + +function namespace(name) +{ + // Ensure all nested namespaces are created only once + + var ns_list = name.split("."); + var parent_ns = window; + + for (var i in ns_list) + { + var ns_name = ns_list[i]; + if (!(ns_name in parent_ns)) + parent_ns[ns_name] = { }; + + parent_ns = parent_ns[ns_name]; + } +} + + +function multiline(fn) +{ + return fn.toString().split(/\n/).slice(1, -1).join("\n"); +} diff --git a/vis/extern/BrowserLib/Core/Code/DOM.js b/vis/extern/BrowserLib/Core/Code/DOM.js new file mode 100644 index 0000000..ccb1276 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/DOM.js @@ -0,0 +1,526 @@ + +namespace("DOM.Node"); +namespace("DOM.Event"); +namespace("DOM.Applet"); + + + +// +// ===================================================================================================================== +// ----- DOCUMENT NODE/ELEMENT EXTENSIONS ------------------------------------------------------------------------------ +// ===================================================================================================================== +// + + + +DOM.Node.Get = function(id) +{ + return document.getElementById(id); +} + + +// +// Set node position +// +DOM.Node.SetPosition = function(node, position) +{ + node.style.left = position[0]; + node.style.top = position[1]; +} +DOM.Node.SetX = function(node, x) +{ + node.style.left = x; +} +DOM.Node.SetY = function(node, y) +{ + node.style.top = y; +} + + +// +// Get the absolute position of a HTML element on the page +// +DOM.Node.GetPosition = function(element, account_for_scroll) +{ + // Recurse up through parents, summing offsets from their parent + var x = 0, y = 0; + for (var node = element; node != null; node = node.offsetParent) + { + x += node.offsetLeft; + y += node.offsetTop; + } + + if (account_for_scroll) + { + // Walk up the hierarchy subtracting away any scrolling + for (var node = element; node != document.body; node = node.parentNode) + { + x -= node.scrollLeft; + y -= node.scrollTop; + } + } + + return [x, y]; +} + + +// +// Set node size +// +DOM.Node.SetSize = function(node, size) +{ + node.style.width = size[0]; + node.style.height = size[1]; +} +DOM.Node.SetWidth = function(node, width) +{ + node.style.width = width; +} +DOM.Node.SetHeight = function(node, height) +{ + node.style.height = height; +} + + +// +// Get node OFFSET size: +// clientX includes padding +// offsetX includes padding and borders +// scrollX includes padding, borders and size of contained node +// +DOM.Node.GetSize = function(node) +{ + return [ node.offsetWidth, node.offsetHeight ]; +} +DOM.Node.GetWidth = function(node) +{ + return node.offsetWidth; +} +DOM.Node.GetHeight = function(node) +{ + return node.offsetHeight; +} + + +// +// Set node opacity +// +DOM.Node.SetOpacity = function(node, value) +{ + node.style.opacity = value; +} + + +DOM.Node.SetColour = function(node, colour) +{ + node.style.color = colour; +} + + +// +// Hide a node by completely disabling its rendering (it no longer contributes to document layout) +// +DOM.Node.Hide = function(node) +{ + node.style.display = "none"; +} + + +// +// Show a node by restoring its influcen in document layout +// +DOM.Node.Show = function(node) +{ + node.style.display = "block"; +} + + +// +// Add a CSS class to a HTML element, specified last +// +DOM.Node.AddClass = function(node, class_name) +{ + // Ensure the class hasn't already been added + DOM.Node.RemoveClass(node, class_name); + node.className += " " + class_name; +} + + +// +// Remove a CSS class from a HTML element +// +DOM.Node.RemoveClass = function(node, class_name) +{ + // Remove all variations of where the class name can be in the string list + var regexp = new RegExp("\\b" + class_name + "\\b"); + node.className = node.className.replace(regexp, ""); +} + + + +// +// Check to see if a HTML element contains a class +// +DOM.Node.HasClass = function(node, class_name) +{ + var regexp = new RegExp("\\b" + class_name + "\\b"); + return regexp.test(node.className); +} + + +// +// Recursively search for a node with the given class name +// +DOM.Node.FindWithClass = function(parent_node, class_name, index) +{ + // Search the children looking for a node with the given class name + for (var i in parent_node.childNodes) + { + var node = parent_node.childNodes[i]; + if (DOM.Node.HasClass(node, class_name)) + { + if (index === undefined || index-- == 0) + return node; + } + + // Recurse into children + node = DOM.Node.FindWithClass(node, class_name); + if (node != null) + return node; + } + + return null; +} + + +// +// Check to see if one node logically contains another +// +DOM.Node.Contains = function(node, container_node) +{ + while (node != null && node != container_node) + node = node.parentNode; + return node != null; +} + + +// +// Create the HTML nodes specified in the text passed in +// Assumes there is only one root node in the text +// +DOM.Node.CreateHTML = function(html) +{ + var div = document.createElement("div"); + div.innerHTML = html; + + // First child may be a text node, followed by the created HTML + var child = div.firstChild; + if (child != null && child.nodeType == 3) + child = child.nextSibling; + return child; +} + + +// +// Make a copy of a HTML element, making it visible and clearing its ID to ensure it's not a duplicate +// +DOM.Node.Clone = function(name) +{ + // Get the template element and clone it, making sure it's renderable + var node = DOM.Node.Get(name); + node = node.cloneNode(true); + node.id = null; + node.style.display = "block"; + return node; +} + + +// +// Append an arbitrary block of HTML to an existing node +// +DOM.Node.AppendHTML = function(node, html) +{ + var child = DOM.Node.CreateHTML(html); + node.appendChild(child); + return child; +} + + +// +// Append a div that clears the float style +// +DOM.Node.AppendClearFloat = function(node) +{ + var div = document.createElement("div"); + div.style.clear = "both"; + node.appendChild(div); +} + + +// +// Check to see that the object passed in is an instance of a DOM node +// +DOM.Node.IsNode = function(object) +{ + return object instanceof Element; +} + + +// +// Create an "iframe shim" so that elements within it render over a Java Applet +// http://web.archive.org/web/20110707212850/http://www.oratransplant.nl/2007/10/26/using-iframe-shim-to-partly-cover-a-java-applet/ +// +DOM.Node.CreateShim = function(parent) +{ + var shimmer = document.createElement("iframe"); + + // Position the shimmer so that it's the same location/size as its parent + shimmer.style.position = "fixed"; + shimmer.style.left = parent.style.left; + shimmer.style.top = parent.style.top; + shimmer.style.width = parent.offsetWidth; + shimmer.style.height = parent.offsetHeight; + + // We want the shimmer to be one level below its contents + shimmer.style.zIndex = parent.style.zIndex - 1; + + // Ensure its empty + shimmer.setAttribute("frameborder", "0"); + shimmer.setAttribute("src", ""); + + // Add to the document and the parent + document.body.appendChild(shimmer); + parent.Shimmer = shimmer; + return shimmer; +} + + + +// +// ===================================================================================================================== +// ----- EVENT HANDLING EXTENSIONS ------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Retrieves the event from the first parameter passed into an HTML event +// +DOM.Event.Get = function(evt) +{ + // Internet explorer doesn't pass the event + return window.event || evt; +} + + +// +// Retrieves the element that triggered an event from the event object +// +DOM.Event.GetNode = function(evt) +{ + evt = DOM.Event.Get(evt); + + // Get the target element + var element; + if (evt.target) + element = evt.target; + else if (e.srcElement) + element = evt.srcElement; + + // Default Safari bug + if (element.nodeType == 3) + element = element.parentNode; + + return element; +} + + +// +// Stop default action for an event +// +DOM.Event.StopDefaultAction = function(evt) +{ + if (evt && evt.preventDefault) + evt.preventDefault(); + else if (window.event && window.event.returnValue) + window.event.returnValue = false; +} + + +// +// Stops events bubbling up to parent event handlers +// +DOM.Event.StopPropagation = function(evt) +{ + evt = DOM.Event.Get(evt); + if (evt) + { + evt.cancelBubble = true; + if (evt.stopPropagation) + evt.stopPropagation(); + } +} + + +// +// Stop both event default action and propagation +// +DOM.Event.StopAll = function(evt) +{ + DOM.Event.StopDefaultAction(evt); + DOM.Event.StopPropagation(evt); +} + + +// +// Adds an event handler to an event +// +DOM.Event.AddHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.addEventListener) + obj.addEventListener(evt, func, false); + else if (obj.attachEvent) + obj.attachEvent("on" + evt, func); + } +} + + +// +// Removes an event handler from an event +// +DOM.Event.RemoveHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.removeEventListener) + obj.removeEventListener(evt, func, false); + else if (obj.detachEvent) + obj.detachEvent("on" + evt, func); + } +} + + +// +// Get the position of the mouse cursor, page relative +// +DOM.Event.GetMousePosition = function(evt) +{ + evt = DOM.Event.Get(evt); + + var px = 0; + var py = 0; + if (evt.pageX || evt.pageY) + { + px = evt.pageX; + py = evt.pageY; + } + else if (evt.clientX || evt.clientY) + { + px = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + py = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return [px, py]; +} + + +// +// Get the list of files attached to a drop event +// +DOM.Event.GetDropFiles = function(evt) +{ + let files = []; + if (evt.dataTransfer.items) + { + for (let i = 0; i < evt.dataTransfer.items.length; i++) + { + if (evt.dataTransfer.items[i].kind === 'file') + { + files.push(evt.dataTransfer.items[i].getAsFile()); + } + } + } + else + { + for (let i = 0; i < evt.dataTransfer.files.length; i++) + { + files.push(evt.dataTransfer.files[i]); + } + } + return files; +} + + + +// +// ===================================================================================================================== +// ----- JAVA APPLET EXTENSIONS ---------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Create an applet element for loading a Java applet, attaching it to the specified node +// +DOM.Applet.Load = function(dest_id, id, code, archive) +{ + // Lookup the applet destination + var dest = DOM.Node.Get(dest_id); + if (!dest) + return; + + // Construct the applet element and add it to the destination + Debug.Log("Injecting applet DOM code"); + var applet = ""; + applet += ""; + dest.innerHTML = applet; +} + + +// +// Moves and resizes a named applet so that it fits in the destination div element. +// The applet must be contained by a div element itself. This container div is moved along +// with the applet. +// +DOM.Applet.Move = function(dest_div, applet, z_index, hide) +{ + if (!applet || !dest_div) + return; + + // Before modifying any location information, hide the applet so that it doesn't render over + // any newly visible elements that appear while the location information is being modified. + if (hide) + applet.style.visibility = "hidden"; + + // Get its view rect + var pos = DOM.Node.GetPosition(dest_div); + var w = dest_div.offsetWidth; + var h = dest_div.offsetHeight; + + // It needs to be embedded in a

for correct scale/position adjustment + var container = applet.parentNode; + if (!container || container.localName != "div") + { + Debug.Log("ERROR: Couldn't find source applet's div container"); + return; + } + + // Reposition and resize the containing div element + container.style.left = pos[0]; + container.style.top = pos[1]; + container.style.width = w; + container.style.height = h; + container.style.zIndex = z_index; + + // Resize the applet itself + applet.style.width = w; + applet.style.height = h; + + // Everything modified, safe to show + applet.style.visibility = "visible"; +} diff --git a/vis/extern/BrowserLib/Core/Code/Keyboard.js b/vis/extern/BrowserLib/Core/Code/Keyboard.js new file mode 100644 index 0000000..f70f4ea --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Keyboard.js @@ -0,0 +1,149 @@ + +namespace("Keyboard") + + +// ===================================================================================================================== +// Key codes copied from closure-library +// https://code.google.com/p/closure-library/source/browse/closure/goog/events/keycodes.js +// --------------------------------------------------------------------------------------------------------------------- +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +Keyboard.Codes = { + WIN_KEY_FF_LINUX : 0, + MAC_ENTER : 3, + BACKSPACE : 8, + TAB : 9, + NUM_CENTER : 12, // NUMLOCK on FF/Safari Mac + ENTER : 13, + SHIFT : 16, + CTRL : 17, + ALT : 18, + PAUSE : 19, + CAPS_LOCK : 20, + ESC : 27, + SPACE : 32, + PAGE_UP : 33, // also NUM_NORTH_EAST + PAGE_DOWN : 34, // also NUM_SOUTH_EAST + END : 35, // also NUM_SOUTH_WEST + HOME : 36, // also NUM_NORTH_WEST + LEFT : 37, // also NUM_WEST + UP : 38, // also NUM_NORTH + RIGHT : 39, // also NUM_EAST + DOWN : 40, // also NUM_SOUTH + PRINT_SCREEN : 44, + INSERT : 45, // also NUM_INSERT + DELETE : 46, // also NUM_DELETE + ZERO : 48, + ONE : 49, + TWO : 50, + THREE : 51, + FOUR : 52, + FIVE : 53, + SIX : 54, + SEVEN : 55, + EIGHT : 56, + NINE : 57, + FF_SEMICOLON : 59, // Firefox (Gecko) fires this for semicolon instead of 186 + FF_EQUALS : 61, // Firefox (Gecko) fires this for equals instead of 187 + FF_DASH : 173, // Firefox (Gecko) fires this for dash instead of 189 + QUESTION_MARK : 63, // needs localization + A : 65, + B : 66, + C : 67, + D : 68, + E : 69, + F : 70, + G : 71, + H : 72, + I : 73, + J : 74, + K : 75, + L : 76, + M : 77, + N : 78, + O : 79, + P : 80, + Q : 81, + R : 82, + S : 83, + T : 84, + U : 85, + V : 86, + W : 87, + X : 88, + Y : 89, + Z : 90, + META : 91, // WIN_KEY_LEFT + WIN_KEY_RIGHT : 92, + CONTEXT_MENU : 93, + NUM_ZERO : 96, + NUM_ONE : 97, + NUM_TWO : 98, + NUM_THREE : 99, + NUM_FOUR : 100, + NUM_FIVE : 101, + NUM_SIX : 102, + NUM_SEVEN : 103, + NUM_EIGHT : 104, + NUM_NINE : 105, + NUM_MULTIPLY : 106, + NUM_PLUS : 107, + NUM_MINUS : 109, + NUM_PERIOD : 110, + NUM_DIVISION : 111, + F1 : 112, + F2 : 113, + F3 : 114, + F4 : 115, + F5 : 116, + F6 : 117, + F7 : 118, + F8 : 119, + F9 : 120, + F10 : 121, + F11 : 122, + F12 : 123, + NUMLOCK : 144, + SCROLL_LOCK : 145, + + // OS-specific media keys like volume controls and browser controls. + FIRST_MEDIA_KEY : 166, + LAST_MEDIA_KEY : 183, + + SEMICOLON : 186, // needs localization + DASH : 189, // needs localization + EQUALS : 187, // needs localization + COMMA : 188, // needs localization + PERIOD : 190, // needs localization + SLASH : 191, // needs localization + APOSTROPHE : 192, // needs localization + TILDE : 192, // needs localization + SINGLE_QUOTE : 222, // needs localization + OPEN_SQUARE_BRACKET : 219, // needs localization + BACKSLASH : 220, // needs localization + CLOSE_SQUARE_BRACKET: 221, // needs localization + WIN_KEY : 224, + MAC_FF_META : 224, // Firefox (Gecko) fires this for the meta key instead of 91 + MAC_WK_CMD_LEFT : 91, // WebKit Left Command key fired, same as META + MAC_WK_CMD_RIGHT : 93, // WebKit Right Command key fired, different from META + WIN_IME : 229, + + // We've seen users whose machines fire this keycode at regular one + // second intervals. The common thread among these users is that + // they're all using Dell Inspiron laptops, so we suspect that this + // indicates a hardware/bios problem. + // http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx + PHANTOM : 255 +}; +// ===================================================================================================================== diff --git a/vis/extern/BrowserLib/Core/Code/LocalStore.js b/vis/extern/BrowserLib/Core/Code/LocalStore.js new file mode 100644 index 0000000..7bb8481 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/LocalStore.js @@ -0,0 +1,40 @@ + +namespace("LocalStore"); + + +LocalStore.Set = function(class_name, class_id, variable_id, data) +{ + try + { + if (typeof(Storage) != "undefined") + { + var name = class_name + "_" + class_id + "_" + variable_id; + localStorage[name] = JSON.stringify(data); + } + } + catch (e) + { + console.log("Local Storage Set Error: " + e.message); + } +} + + +LocalStore.Get = function(class_name, class_id, variable_id, default_data) +{ + try + { + if (typeof(Storage) != "undefined") + { + var name = class_name + "_" + class_id + "_" + variable_id; + var data = localStorage[name] + if (data) + return JSON.parse(data); + } + } + catch (e) + { + console.log("Local Storage Get Error: " + e.message); + } + + return default_data; +} \ No newline at end of file diff --git a/vis/extern/BrowserLib/Core/Code/Mouse.js b/vis/extern/BrowserLib/Core/Code/Mouse.js new file mode 100644 index 0000000..a694b80 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/Mouse.js @@ -0,0 +1,83 @@ + +namespace("Mouse"); + + +Mouse.State =(function() +{ + function State(event) + { + // Get button press states + if (typeof event.buttons != "undefined") + { + // Firefox + this.Left = (event.buttons & 1) != 0; + this.Right = (event.buttons & 2) != 0; + this.Middle = (event.buttons & 4) != 0; + } + else + { + // Chrome + this.Left = (event.button == 0); + this.Middle = (event.button == 1); + this.Right = (event.button == 2); + } + + // Get page-relative mouse position + this.Position = DOM.Event.GetMousePosition(event); + + // Get wheel delta + var delta = 0; + if (event.wheelDelta) + delta = event.wheelDelta / 120; // IE/Opera + else if (event.detail) + delta = -event.detail / 3; // Mozilla + this.WheelDelta = delta; + + // Get the mouse position delta + // Requires Pointer Lock API support + this.PositionDelta = [ + event.movementX || event.mozMovementX || event.webkitMovementX || 0, + event.movementY || event.mozMovementY || event.webkitMovementY || 0 + ]; + } + + return State; +})(); + + +// +// Basic Pointer Lock API support +// https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock +// http://www.chromium.org/developers/design-documents/mouse-lock +// +// Note that API has not been standardised yet so browsers can implement functions with prefixes +// + + +Mouse.PointerLockSupported = function() +{ + return 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document; +} + + +Mouse.RequestPointerLock = function(element) +{ + element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; + if (element.requestPointerLock) + element.requestPointerLock(); +} + + +Mouse.ExitPointerLock = function() +{ + document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; + if (document.exitPointerLock) + document.exitPointerLock(); +} + + +// Can use this element to detect whether pointer lock is enabled (returns non-null) +Mouse.PointerLockElement = function() +{ + return document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement; +} diff --git a/vis/extern/BrowserLib/Core/Code/MurmurHash3.js b/vis/extern/BrowserLib/Core/Code/MurmurHash3.js new file mode 100644 index 0000000..c423d49 --- /dev/null +++ b/vis/extern/BrowserLib/Core/Code/MurmurHash3.js @@ -0,0 +1,68 @@ + +namespace("Hash"); + +/** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} key ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + +Hash.Murmur3 = function(key, seed) +{ + var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; + + remainder = key.length & 3; // key.length % 4 + bytes = key.length - remainder; + h1 = seed; + c1 = 0xcc9e2d51; + c2 = 0x1b873593; + i = 0; + + while (i < bytes) { + k1 = + ((key.charCodeAt(i) & 0xff)) | + ((key.charCodeAt(++i) & 0xff) << 8) | + ((key.charCodeAt(++i) & 0xff) << 16) | + ((key.charCodeAt(++i) & 0xff) << 24); + ++i; + + k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); + h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; + h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); + } + + k1 = 0; + + switch (remainder) { + case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + case 1: k1 ^= (key.charCodeAt(i) & 0xff); + + k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + h1 ^= k1; + } + + h1 ^= key.length; + + h1 ^= h1 >>> 16; + h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 13; + h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; + h1 ^= h1 >>> 16; + + return h1 >>> 0; +} \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Code/Button.js b/vis/extern/BrowserLib/WindowManager/Code/Button.js new file mode 100644 index 0000000..12e0981 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Button.js @@ -0,0 +1,131 @@ + +namespace("WM"); + + +WM.Button = (function() +{ + var template_html = "
"; + + + function Button(text, x, y, opts) + { + this.OnClick = null; + this.Toggle = opts && opts.toggle; + + this.Node = DOM.Node.CreateHTML(template_html); + + // Set node dimensions + this.SetPosition(x, y); + if (opts && opts.w && opts.h) + this.SetSize(opts.w, opts.h); + + // Override the default class name + if (opts && opts.class) + this.Node.className = opts.class; + + this.SetText(text); + + // Create the mouse press event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + this.OnMouseOutDelegate = Bind(OnMouseUp, this, false); + this.OnMouseUpDelegate = Bind(OnMouseUp, this, true); + } + + + Button.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Button.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Button.prototype.SetText = function(text) + { + this.Node.innerHTML = text; + } + + + Button.prototype.SetOnClick = function(on_click) + { + this.OnClick = on_click; + } + + + Button.prototype.SetState = function(pressed) + { + if (pressed) + DOM.Node.AddClass(this.Node, "ButtonHeld"); + else + DOM.Node.RemoveClass(this.Node, "ButtonHeld"); + } + + + Button.prototype.ToggleState = function() + { + if (DOM.Node.HasClass(this.Node, "ButtonHeld")) + this.SetState(false); + else + this.SetState(true); + } + + + Button.prototype.IsPressed = function() + { + return DOM.Node.HasClass(this.Node, "ButtonHeld"); + } + + + function OnMouseDown(self, evt) + { + // Decide how to set the button state + if (self.Toggle) + self.ToggleState(); + else + self.SetState(true); + + // Activate release handlers + DOM.Event.AddHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.AddHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + DOM.Event.StopAll(evt); + } + + + function OnMouseUp(self, confirm, evt) + { + if (confirm) + { + // Only release for non-toggles + if (!self.Toggle) + self.SetState(false); + } + else + { + // Decide how to set the button state + if (self.Toggle) + self.ToggleState(); + else + self.SetState(false); + } + + // Remove release handlers + DOM.Event.RemoveHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.RemoveHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + // Call the click handler if this is a button press + if (confirm && self.OnClick) + self.OnClick(self); + + DOM.Event.StopAll(evt); + } + + + return Button; +})(); \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js b/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js new file mode 100644 index 0000000..d199b3a --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js @@ -0,0 +1,237 @@ + +namespace("WM"); + + +WM.ComboBoxPopup = (function() +{ + var body_template_html = "
"; + + var item_template_html = " \ +
\ +
\ +
\ +
\ +
"; + + + function ComboBoxPopup(combo_box) + { + this.ComboBox = combo_box; + this.ParentNode = combo_box.Node; + this.ValueNodes = [ ]; + + // Create the template node + this.Node = DOM.Node.CreateHTML(body_template_html); + + DOM.Event.AddHandler(this.Node, "mousedown", Bind(SelectItem, this)); + this.CancelDelegate = Bind(this, "Cancel"); + } + + + ComboBoxPopup.prototype.SetValues = function(values) + { + // Clear existing values + this.Node.innerHTML = ""; + + // Generate HTML nodes for each value + this.ValueNodes = [ ]; + for (var i in values) + { + var item_node = DOM.Node.CreateHTML(item_template_html); + var text_node = DOM.Node.FindWithClass(item_node, "ComboBoxPopupItemText"); + + item_node.Value = values[i]; + text_node.innerHTML = values[i]; + + this.Node.appendChild(item_node); + this.ValueNodes.push(item_node); + } + } + + + ComboBoxPopup.prototype.Show = function(selection_index) + { + // Initially match the position of the parent node + var pos = DOM.Node.GetPosition(this.ParentNode); + DOM.Node.SetPosition(this.Node, pos); + + // Take the width/z-index from the parent node + this.Node.style.width = this.ParentNode.offsetWidth; + this.Node.style.zIndex = this.ParentNode.style.zIndex + 1; + + // Setup event handlers + DOM.Event.AddHandler(document.body, "mousedown", this.CancelDelegate); + + // Show the popup so that the HTML layout engine kicks in before + // the layout info is used below + this.ParentNode.appendChild(this.Node); + + // Show/hide the tick image based on which node is selected + for (var i in this.ValueNodes) + { + var node = this.ValueNodes[i]; + var icon_node = DOM.Node.FindWithClass(node, "ComboBoxPopupItemIcon"); + + if (i == selection_index) + { + icon_node.style.display = "block"; + + // Also, shift the popup up so that the mouse is over the selected item and is highlighted + var item_pos = DOM.Node.GetPosition(this.ValueNodes[selection_index]); + var diff_pos = [ item_pos[0] - pos[0], item_pos[1] - pos[1] ]; + pos = [ pos[0] - diff_pos[0], pos[1] - diff_pos[1] ]; + } + else + { + icon_node.style.display = "none"; + } + } + + DOM.Node.SetPosition(this.Node, pos); + } + + + ComboBoxPopup.prototype.Hide = function() + { + DOM.Event.RemoveHandler(document.body, "mousedown", this.CancelDelegate); + this.ParentNode.removeChild(this.Node); + } + + + function SelectItem(self, evt) + { + // Search for which item node is being clicked on + var node = DOM.Event.GetNode(evt); + for (var i in self.ValueNodes) + { + var value_node = self.ValueNodes[i]; + if (DOM.Node.Contains(node, value_node)) + { + // Set the value on the combo box + self.ComboBox.SetValue(value_node.Value); + self.Hide(); + break; + } + } + } + + + function Cancel(self, evt) + { + // Don't cancel if the mouse up is anywhere on the popup or combo box + var node = DOM.Event.GetNode(evt); + if (!DOM.Node.Contains(node, self.Node) && + !DOM.Node.Contains(node, self.ParentNode)) + { + self.Hide(); + } + + + DOM.Event.StopAll(evt); + } + + + return ComboBoxPopup; +})(); + + +WM.ComboBox = (function() +{ + var template_html = " \ +
\ +
\ +
\ +
\ +
"; + + + function ComboBox() + { + this.OnChange = null; + + // Create the template node and locate key nodes + this.Node = DOM.Node.CreateHTML(template_html); + this.TextNode = DOM.Node.FindWithClass(this.Node, "ComboBoxText"); + + // Create a reusable popup + this.Popup = new WM.ComboBoxPopup(this); + + // Set an empty set of values + this.SetValues([]); + this.SetValue("<empty>"); + + // Create the mouse press event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + this.OnMouseOutDelegate = Bind(OnMouseUp, this, false); + this.OnMouseUpDelegate = Bind(OnMouseUp, this, true); + } + + + ComboBox.prototype.SetOnChange = function(on_change) + { + this.OnChange = on_change; + } + + + ComboBox.prototype.SetValues = function(values) + { + this.Values = values; + this.Popup.SetValues(values); + } + + + ComboBox.prototype.SetValue = function(value) + { + // Set the value and its HTML rep + var old_value = this.Value; + this.Value = value; + this.TextNode.innerHTML = value; + + // Call change handler + if (this.OnChange) + this.OnChange(value, old_value); + } + + + ComboBox.prototype.GetValue = function() + { + return this.Value; + } + + + function OnMouseDown(self, evt) + { + // If this check isn't made, the click will trigger from the popup, too + var node = DOM.Event.GetNode(evt); + if (DOM.Node.Contains(node, self.Node)) + { + // Add the depression class and activate release handlers + DOM.Node.AddClass(self.Node, "ComboBoxPressed"); + DOM.Event.AddHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.AddHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + DOM.Event.StopAll(evt); + } + } + + + function OnMouseUp(self, confirm, evt) + { + // Remove depression class and remove release handlers + DOM.Node.RemoveClass(self.Node, "ComboBoxPressed"); + DOM.Event.RemoveHandler(self.Node, "mouseout", self.OnMouseOutDelegate); + DOM.Event.RemoveHandler(self.Node, "mouseup", self.OnMouseUpDelegate); + + // If this is a confirmed press and there are some values in the list, show the popup + if (confirm && self.Values.length > 0) + { + var selection_index = self.Values.indexOf(self.Value); + self.Popup.Show(selection_index); + } + + DOM.Event.StopAll(evt); + } + + + return ComboBox; +})(); diff --git a/vis/extern/BrowserLib/WindowManager/Code/Container.js b/vis/extern/BrowserLib/WindowManager/Code/Container.js new file mode 100644 index 0000000..6a6bdf1 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Container.js @@ -0,0 +1,48 @@ + +namespace("WM"); + + +WM.Container = (function() +{ + var template_html = "
"; + + + function Container(x, y, w, h) + { + // Create a simple container node + this.Node = DOM.Node.CreateHTML(template_html); + this.SetPosition(x, y); + this.SetSize(w, h); + } + + + Container.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Container.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Container.prototype.AddControlNew = function(control) + { + control.ParentNode = this.Node; + this.Node.appendChild(control.Node); + return control; + } + + + Container.prototype.ClearControls = function() + { + this.Node.innerHTML = ""; + } + + + return Container; +})(); \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Code/EditBox.js b/vis/extern/BrowserLib/WindowManager/Code/EditBox.js new file mode 100644 index 0000000..fd0a039 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/EditBox.js @@ -0,0 +1,119 @@ + +namespace("WM"); + + +WM.EditBox = (function() +{ + var template_html = " \ +
\ +
Label
\ + \ +
"; + + + function EditBox(x, y, w, h, label, text) + { + this.ChangeHandler = null; + + // Create node and locate its internal nodes + this.Node = DOM.Node.CreateHTML(template_html); + this.LabelNode = DOM.Node.FindWithClass(this.Node, "EditBoxLabel"); + this.EditNode = DOM.Node.FindWithClass(this.Node, "EditBox"); + + // Set label and value + this.LabelNode.innerHTML = label; + this.SetValue(text); + + this.SetPosition(x, y); + this.SetSize(w, h); + + this.PreviousValue = ""; + + // Hook up the event handlers + DOM.Event.AddHandler(this.EditNode, "focus", Bind(OnFocus, this)); + DOM.Event.AddHandler(this.EditNode, "keypress", Bind(OnKeyPress, this)); + DOM.Event.AddHandler(this.EditNode, "keydown", Bind(OnKeyDown, this)); + } + + + EditBox.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + EditBox.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.EditNode, this.Size); + } + + + EditBox.prototype.SetChangeHandler = function(handler) + { + this.ChangeHandler = handler; + } + + + EditBox.prototype.SetValue = function(value) + { + if (this.EditNode) + this.EditNode.value = value; + } + + + EditBox.prototype.GetValue = function() + { + if (this.EditNode) + return this.EditNode.value; + + return null; + } + + + EditBox.prototype.LoseFocus = function() + { + if (this.EditNode) + this.EditNode.blur(); + } + + + function OnFocus(self, evt) + { + // Backup on focus + self.PreviousValue = self.EditNode.value; + } + + + function OnKeyPress(self, evt) + { + // Allow enter to confirm the text only when there's data + if (evt.keyCode == 13 && self.EditNode.value != "" && self.ChangeHandler) + { + var focus = self.ChangeHandler(self.EditNode); + if (!focus) + self.EditNode.blur(); + self.PreviousValue = ""; + } + } + + + function OnKeyDown(self, evt) + { + // Allow escape to cancel any text changes + if (evt.keyCode == 27) + { + // On initial edit of the input, escape should NOT replace with the empty string + if (self.PreviousValue != "") + { + self.EditNode.value = self.PreviousValue; + } + + self.EditNode.blur(); + } + } + + + return EditBox; +})(); diff --git a/vis/extern/BrowserLib/WindowManager/Code/Grid.js b/vis/extern/BrowserLib/WindowManager/Code/Grid.js new file mode 100644 index 0000000..bb72858 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Grid.js @@ -0,0 +1,248 @@ + +namespace("WM"); + + +WM.GridRows = (function() +{ + function GridRows(parent_object) + { + this.ParentObject = parent_object; + + // Array of rows in the order they were added + this.Rows = [ ]; + + // Collection of custom row indexes for fast lookup + this.Indexes = { }; + } + + + GridRows.prototype.AddIndex = function(cell_field_name) + { + var index = { }; + + // Go through existing rows and add to the index + for (var i in this.Rows) + { + var row = this.Rows[i]; + if (cell_field_name in row.CellData) + { + var cell_field = row.CellData[cell_field_name]; + index[cell_field] = row; + } + } + + this.Indexes[cell_field_name] = index; + } + + + GridRows.prototype.ClearIndex = function(index_name) + { + this.Indexes[index_name] = { }; + } + + GridRows.prototype.AddRowToIndex = function(index_name, cell_data, row) + { + this.Indexes[index_name][cell_data] = row; + } + + + GridRows.prototype.Add = function(cell_data, row_classes, cell_classes) + { + var row = new WM.GridRow(this.ParentObject, cell_data, row_classes, cell_classes); + this.Rows.push(row); + return row; + } + + + GridRows.prototype.GetBy = function(cell_field_name, cell_data) + { + var index = this.Indexes[cell_field_name]; + return index[cell_data]; + } + + + GridRows.prototype.Clear = function() + { + // Remove all node references from the parent + for (var i in this.Rows) + { + var row = this.Rows[i]; + row.Parent.BodyNode.removeChild(row.Node); + } + + // Clear all indexes + for (var i in this.Indexes) + this.Indexes[i] = { }; + + this.Rows = [ ]; + } + + + return GridRows; +})(); + + +WM.GridRow = (function() +{ + var template_html = "
"; + + + // + // 'cell_data' is an object with a variable number of fields. + // Any fields prefixed with an underscore are hidden. + // + function GridRow(parent, cell_data, row_classes, cell_classes) + { + // Setup data + this.Parent = parent; + this.IsOpen = true; + this.AnimHandle = null; + this.Rows = new WM.GridRows(this); + this.CellData = cell_data; + this.CellNodes = { } + + // Create the main row node + this.Node = DOM.Node.CreateHTML(template_html); + if (row_classes) + DOM.Node.AddClass(this.Node, row_classes); + + // Embed a pointer to the row in the root node so that it can be clicked + this.Node.GridRow = this; + + // Create nodes for each required cell + for (var attr in this.CellData) + { + if (this.CellData.hasOwnProperty(attr)) + { + var data = this.CellData[attr]; + + // Update any grid row index references + if (attr in parent.Rows.Indexes) + parent.Rows.AddRowToIndex(attr, data, this); + + // Hide any cells with underscore prefixes + if (attr[0] == "_") + continue; + + // Create a node for the cell and add any custom classes + var node = DOM.Node.AppendHTML(this.Node, "
"); + if (cell_classes && attr in cell_classes) + DOM.Node.AddClass(node, cell_classes[attr]); + this.CellNodes[attr] = node; + + // If this is a Window Control, add its node to the cell + if (data instanceof Object && "Node" in data && DOM.Node.IsNode(data.Node)) + { + data.ParentNode = node; + node.appendChild(data.Node); + } + + else + { + // Otherwise just assign the data as text + node.innerHTML = data; + } + } + } + + // Add the body node for any children + if (!this.Parent.BodyNode) + this.Parent.BodyNode = DOM.Node.AppendHTML(this.Parent.Node, "
"); + + // Add the row to the parent + this.Parent.BodyNode.appendChild(this.Node); + } + + + GridRow.prototype.Open = function() + { + // Don't allow open while animating + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + this.IsOpen = true; + + // Kick off open animation + var node = this.BodyNode; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(node, val) }, + 0, this.Height, 0.2); + } + } + + + GridRow.prototype.Close = function() + { + // Don't allow close while animating + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + this.IsOpen = false; + + // Record height for the next open request + this.Height = this.BodyNode.offsetHeight; + + // Kick off close animation + var node = this.BodyNode; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(node, val) }, + this.Height, 0, 0.2); + } + } + + + GridRow.prototype.Toggle = function() + { + if (this.IsOpen) + this.Close(); + else + this.Open(); + } + + + return GridRow; +})(); + + +WM.Grid = (function() +{ + var template_html = " \ +
\ +
\ +
"; + + + function Grid() + { + this.Rows = new WM.GridRows(this); + + this.Node = DOM.Node.CreateHTML(template_html); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "GridBody"); + + DOM.Event.AddHandler(this.Node, "dblclick", OnDblClick); + + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + } + + function OnDblClick(evt) + { + // Clicked on a header? + var node = DOM.Event.GetNode(evt); + if (DOM.Node.HasClass(node, "GridRowName")) + { + // Toggle rows open/close + var row = node.parentNode.GridRow; + if (row) + row.Toggle(); + } + } + + + function OnMouseScroll(self, evt) + { + var mouse_state = new Mouse.State(evt); + self.Node.scrollTop -= mouse_state.WheelDelta * 20; + } + + + return Grid; +})(); diff --git a/vis/extern/BrowserLib/WindowManager/Code/Label.js b/vis/extern/BrowserLib/WindowManager/Code/Label.js new file mode 100644 index 0000000..9b1d852 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Label.js @@ -0,0 +1,31 @@ + +namespace("WM"); + + +WM.Label = (function() +{ + var template_html = "
"; + + + function Label(x, y, text) + { + // Create the node + this.Node = DOM.Node.CreateHTML(template_html); + + // Allow position to be optional + if (x != null && y != null) + DOM.Node.SetPosition(this.Node, [x, y]); + + this.SetText(text); + } + + + Label.prototype.SetText = function(text) + { + if (text != null) + this.Node.innerHTML = text; + } + + + return Label; +})(); \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Code/Treeview.js b/vis/extern/BrowserLib/WindowManager/Code/Treeview.js new file mode 100644 index 0000000..66ef80e --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Treeview.js @@ -0,0 +1,352 @@ + +namespace("WM"); + + +WM.Treeview = (function() +{ + var Margin = 10; + + + var tree_template_html = " \ +
\ +
\ +
\ +
\ +
\ +
\ +
"; + + + var item_template_html = " \ +
\ + \ +
\ +
\ +
\ +
\ +
"; + + + // TODO: Remove parent_node (required for stuff that doesn't use the WM yet) + function Treeview(x, y, width, height, parent_node) + { + // Cache initialisation options + this.ParentNode = parent_node; + this.Position = [ x, y ]; + this.Size = [ width, height ]; + + this.Node = null; + this.ScrollbarNode = null; + this.SelectedItem = null; + this.ContentsNode = null; + + // Setup options + this.HighlightOnHover = false; + this.EnableScrollbar = true; + this.HorizontalLayoutDepth = 1; + + // Generate an empty tree + this.Clear(); + } + + + Treeview.prototype.SetHighlightOnHover = function(highlight) + { + this.HighlightOnHover = highlight; + } + + + Treeview.prototype.SetEnableScrollbar = function(enable) + { + this.EnableScrollbar = enable; + } + + + Treeview.prototype.SetHorizontalLayoutDepth = function(depth) + { + this.HorizontalLayoutDepth = depth; + } + + + Treeview.prototype.SetNodeSelectedHandler = function(handler) + { + this.NodeSelectedHandler = handler; + } + + + Treeview.prototype.Clear = function() + { + this.RootItem = new WM.TreeviewItem(this, null, null, null, null); + this.GenerateHTML(); + } + + + Treeview.prototype.Root = function() + { + return this.RootItem; + } + + + Treeview.prototype.ClearSelection = function() + { + if (this.SelectedItem != null) + { + DOM.Node.RemoveClass(this.SelectedItem.Node, "TreeviewItemSelected"); + this.SelectedItem = null; + } + } + + + Treeview.prototype.SelectItem = function(item, mouse_pos) + { + // Notify the select handler + if (this.NodeSelectedHandler) + this.NodeSelectedHandler(item.Node, this.SelectedItem, item, mouse_pos); + + // Remove highlight from the old selection + this.ClearSelection(); + + // Swap in new selection and apply highlight + this.SelectedItem = item; + DOM.Node.AddClass(this.SelectedItem.Node, "TreeviewItemSelected"); + } + + + Treeview.prototype.GenerateHTML = function() + { + // Clone the template and locate important nodes + var old_node = this.Node; + this.Node = DOM.Node.CreateHTML(tree_template_html); + this.ChildrenNode = DOM.Node.FindWithClass(this.Node, "TreeviewItemChildren"); + this.ScrollbarNode = DOM.Node.FindWithClass(this.Node, "TreeviewScrollbar"); + + DOM.Node.SetPosition(this.Node, this.Position); + DOM.Node.SetSize(this.Node, this.Size); + + // Generate the contents of the treeview + GenerateTree(this, this.ChildrenNode, this.RootItem.Children, 0); + + // Cross-browser (?) means of adding a mouse wheel handler + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + + DOM.Event.AddHandler(this.Node, "dblclick", Bind(OnMouseDoubleClick, this)); + DOM.Event.AddHandler(this.Node, "mousedown", Bind(OnMouseDown, this)); + DOM.Event.AddHandler(this.Node, "mouseup", OnMouseUp); + + // Swap in the newly generated control node if it's already been attached to a parent + if (old_node && old_node.parentNode) + { + old_node.parentNode.removeChild(old_node); + this.ParentNode.appendChild(this.Node); + } + + if (this.EnableScrollbar) + { + this.UpdateScrollbar(); + DOM.Event.AddHandler(this.ScrollbarNode, "mousedown", Bind(OnMouseDown_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mouseup", Bind(OnMouseUp_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mouseout", Bind(OnMouseUp_Scrollbar, this)); + DOM.Event.AddHandler(this.ScrollbarNode, "mousemove", Bind(OnMouseMove_Scrollbar, this)); + } + + else + { + DOM.Node.Hide(DOM.Node.FindWithClass(this.Node, "TreeviewScrollbarInset")); + } + } + + + Treeview.prototype.UpdateScrollbar = function() + { + if (!this.EnableScrollbar) + return; + + var scrollbar_scale = Math.min((this.Node.offsetHeight - Margin * 2) / this.ChildrenNode.offsetHeight, 1); + this.ScrollbarNode.style.height = parseInt(scrollbar_scale * 100) + "%"; + + // Shift the scrollbar container along with the parent window + this.ScrollbarNode.parentNode.style.top = this.Node.scrollTop; + + var scroll_fraction = this.Node.scrollTop / (this.Node.scrollHeight - this.Node.offsetHeight); + var max_height = this.Node.offsetHeight - Margin; + var max_scrollbar_offset = max_height - this.ScrollbarNode.offsetHeight; + var scrollbar_offset = scroll_fraction * max_scrollbar_offset; + this.ScrollbarNode.style.top = scrollbar_offset; + } + + + function GenerateTree(self, parent_node, items, depth) + { + if (items.length == 0) + return null; + + for (var i in items) + { + var item = items[i]; + + // Create the node for this item and locate important nodes + var node = DOM.Node.CreateHTML(item_template_html); + var img = DOM.Node.FindWithClass(node, "TreeviewItemImage"); + var text = DOM.Node.FindWithClass(node, "TreeviewItemText"); + var children = DOM.Node.FindWithClass(node, "TreeviewItemChildren"); + + // Attach the item to the node + node.TreeviewItem = item; + item.Node = node; + + // Add the class which highlights selection on hover + if (self.HighlightOnHover) + DOM.Node.AddClass(node, "TreeviewItemHover"); + + // Instruct the children to wrap around + if (depth >= self.HorizontalLayoutDepth) + node.style.cssFloat = "left"; + + if (item.OpenImage == null || item.CloseImage == null) + { + // If there no images, remove the image node + node.removeChild(img); + } + else + { + // Set the image source to open + img.src = item.OpenImage.src; + img.style.width = item.OpenImage.width; + img.style.height = item.OpenImage.height; + item.ImageNode = img; + } + + // Setup the text to display + text.innerHTML = item.Label; + + // Add the div to the parent and recurse into children + parent_node.appendChild(node); + GenerateTree(self, children, item.Children, depth + 1); + item.ChildrenNode = children; + } + + // Clear the wrap-around + if (depth >= self.HorizontalLayoutDepth) + DOM.Node.AppendClearFloat(parent_node.parentNode); + } + + + function OnMouseScroll(self, evt) + { + // Get mouse wheel movement + var delta = evt.detail ? evt.detail * -1 : evt.wheelDelta; + delta *= 8; + + // Scroll the main window with wheel movement and clamp + self.Node.scrollTop -= delta; + self.Node.scrollTop = Math.min(self.Node.scrollTop, (self.ChildrenNode.offsetHeight - self.Node.offsetHeight) + Margin * 2); + + self.UpdateScrollbar(); + } + + + function OnMouseDoubleClick(self, evt) + { + DOM.Event.StopDefaultAction(evt); + + // Get the tree view item being clicked, if any + var node = DOM.Event.GetNode(evt); + var tvitem = GetTreeviewItemFromNode(self, node); + if (tvitem == null) + return; + + if (tvitem.Children.length) + tvitem.Toggle(); + } + + + function OnMouseDown(self, evt) + { + DOM.Event.StopDefaultAction(evt); + + // Get the tree view item being clicked, if any + var node = DOM.Event.GetNode(evt); + var tvitem = GetTreeviewItemFromNode(self, node); + if (tvitem == null) + return; + + // If clicking on the image, expand any children + if (node.tagName == "IMG" && tvitem.Children.length) + { + tvitem.Toggle(); + } + + else + { + var mouse_pos = DOM.Event.GetMousePosition(evt); + self.SelectItem(tvitem, mouse_pos); + } + } + + + function OnMouseUp(evt) + { + // Event handler used merely to stop events bubbling up to containers + DOM.Event.StopPropagation(evt); + } + + + function OnMouseDown_Scrollbar(self, evt) + { + self.ScrollbarHeld = true; + + // Cache the mouse height relative to the scrollbar + self.LastY = evt.clientY; + self.ScrollY = self.Node.scrollTop; + + DOM.Node.AddClass(self.ScrollbarNode, "TreeviewScrollbarHeld"); + DOM.Event.StopDefaultAction(evt); + } + + + function OnMouseUp_Scrollbar(self, evt) + { + self.ScrollbarHeld = false; + DOM.Node.RemoveClass(self.ScrollbarNode, "TreeviewScrollbarHeld"); + } + + + function OnMouseMove_Scrollbar(self, evt) + { + if (self.ScrollbarHeld) + { + var delta_y = evt.clientY - self.LastY; + self.LastY = evt.clientY; + + var max_height = self.Node.offsetHeight - Margin; + var max_scrollbar_offset = max_height - self.ScrollbarNode.offsetHeight; + var max_contents_scroll = self.Node.scrollHeight - self.Node.offsetHeight; + var scale = max_contents_scroll / max_scrollbar_offset; + + // Increment the local float variable and assign, as scrollTop is of type int + self.ScrollY += delta_y * scale; + self.Node.scrollTop = self.ScrollY; + self.Node.scrollTop = Math.min(self.Node.scrollTop, (self.ChildrenNode.offsetHeight - self.Node.offsetHeight) + Margin * 2); + + self.UpdateScrollbar(); + } + } + + + function GetTreeviewItemFromNode(self, node) + { + // Walk up toward the tree view node looking for this first item + while (node && node != self.Node) + { + if ("TreeviewItem" in node) + return node.TreeviewItem; + + node = node.parentNode; + } + + return null; + } + + return Treeview; +})(); diff --git a/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js b/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js new file mode 100644 index 0000000..ac6133e --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js @@ -0,0 +1,109 @@ + +namespace("WM"); + + +WM.TreeviewItem = (function() +{ + function TreeviewItem(treeview, name, data, open_image, close_image) + { + // Assign members + this.Treeview = treeview; + this.Label = name; + this.Data = data; + this.OpenImage = open_image; + this.CloseImage = close_image; + + this.Children = [ ]; + + // The HTML node wrapping the item and its children + this.Node = null; + + // The HTML node storing the image for the open/close state feedback + this.ImageNode = null; + + // The HTML node storing just the children + this.ChildrenNode = null; + + // Animation handle for opening and closing the child nodes, only used + // if the tree view item as children + this.AnimHandle = null; + + // Open state of the item + this.IsOpen = true; + } + + + TreeviewItem.prototype.AddItem = function(name, data, open_image, close_image) + { + var item = new WM.TreeviewItem(this.Treeview, name, data, open_image, close_image); + this.Children.push(item); + return item; + } + + + TreeviewItem.prototype.Open = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the open state + this.IsOpen = true; + if (this.ImageNode != null && this.OpenImage != null) + this.ImageNode.src = this.OpenImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var end_height = this.StartHeight; + var treeview = this.Treeview; + + // Reveal the children and animate their height to max + this.ChildrenNode.style.display = "block"; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + 0, end_height, 0.2, + function() { treeview.UpdateScrollbar(); }); + + // Fade the children in + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 0, 1, 0.2); + } + } + + + TreeviewItem.prototype.Close = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the close state + this.IsOpen = false; + if (this.ImageNode != null && this.CloseImage != null) + this.ImageNode.src = this.CloseImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var treeview = this.Treeview; + + // Mark the height of the item for reload later + this.StartHeight = child_node.offsetHeight; + + // Shrink the height of the children and hide them upon completion + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + this.ChildrenNode.offsetHeight, 0, 0.2, + function() { child_node.style.display = "none"; treeview.UpdateScrollbar(); }); + + // Fade the children out + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 1, 0, 0.2); + } + } + + + TreeviewItem.prototype.Toggle = function() + { + if (this.IsOpen) + this.Close(); + else + this.Open(); + } + + + return TreeviewItem; +})(); diff --git a/vis/extern/BrowserLib/WindowManager/Code/Window.js b/vis/extern/BrowserLib/WindowManager/Code/Window.js new file mode 100644 index 0000000..b91db6b --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/Window.js @@ -0,0 +1,314 @@ + +namespace("WM"); + + +WM.Window = (function() +{ + var template_html = multiline(function(){/* \ +
+
+
Window Title Bar
+
+
+
+
+
+
+ */}); + + + function Window(manager, title, x, y, width, height, parent_node) + { + this.Manager = manager; + this.ParentNode = parent_node || document.body; + this.OnMove = null; + this.OnResize = null; + this.Visible = false; + this.AnimatedShow = false; + + // Clone the window template and locate key nodes within it + this.Node = DOM.Node.CreateHTML(template_html); + this.TitleBarNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBar"); + this.TitleBarTextNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarText"); + this.TitleBarCloseNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarClose"); + this.ResizeHandleNode = DOM.Node.FindWithClass(this.Node, "WindowResizeHandle"); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "WindowBody"); + + // Setup the position and dimensions of the window + this.SetPosition(x, y); + this.SetSize(width, height); + + // Set the title text + this.TitleBarTextNode.innerHTML = title; + + // Hook up event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(this, "SetTop")); + DOM.Event.AddHandler(this.TitleBarNode, "mousedown", Bind(this, "BeginMove")); + DOM.Event.AddHandler(this.ResizeHandleNode, "mousedown", Bind(this, "BeginResize")); + DOM.Event.AddHandler(this.TitleBarCloseNode, "mouseup", Bind(this, "Hide")); + + // Create delegates for removable handlers + this.MoveDelegate = Bind(this, "Move"); + this.EndMoveDelegate = Bind(this, "EndMove") + this.ResizeDelegate = Bind(this, "Resize"); + this.EndResizeDelegate = Bind(this, "EndResize"); + } + + Window.prototype.SetOnMove = function(on_move) + { + this.OnMove = on_move; + } + + Window.prototype.SetOnResize = function(on_resize) + { + this.OnResize = on_resize; + } + + + Window.prototype.Show = function() + { + if (this.Node.parentNode != this.ParentNode) + { + this.ShowNoAnim(); + Anim.Animate(Bind(this, "OpenAnimation"), 0, 1, 1); + } + } + + + Window.prototype.ShowNoAnim = function() + { + // Add to the document + this.ParentNode.appendChild(this.Node); + this.AnimatedShow = false; + this.Visible = true; + } + + + Window.prototype.Hide = function(evt) + { + if (this.Node.parentNode == this.ParentNode && evt.button == 0) + { + if (this.AnimatedShow) + { + // Trigger animation that ends with removing the window from the document + Anim.Animate( + Bind(this, "CloseAnimation"), + 0, 1, 0.25, + Bind(this, "HideNoAnim")); + } + else + { + this.HideNoAnim(); + } + } + } + + + Window.prototype.HideNoAnim = function() + { + if (this.Node.parentNode == this.ParentNode) + { + // Remove node + this.ParentNode.removeChild(this.Node); + this.Visible = false; + } + } + + + Window.prototype.Close = function() + { + this.HideNoAnim(); + this.Manager.RemoveWindow(this); + } + + + Window.prototype.SetTop = function() + { + this.Manager.SetTopWindow(this); + } + + + + Window.prototype.SetTitle = function(title) + { + this.TitleBarTextNode.innerHTML = title; + } + + + // TODO: Update this + Window.prototype.AddControl = function(control) + { + // Get all arguments to this function and replace the first with this window node + var args = [].slice.call(arguments); + args[0] = this.BodyNode; + + // Create the control and call its Init method with the modified arguments + var instance = new control(); + instance.Init.apply(instance, args); + + return instance; + } + + + Window.prototype.AddControlNew = function(control) + { + control.ParentNode = this.BodyNode; + this.BodyNode.appendChild(control.Node); + return control; + } + + + Window.prototype.RemoveControl = function(control) + { + if (control.ParentNode == this.BodyNode) + { + control.ParentNode.removeChild(control.Node); + } + } + + + Window.prototype.Scale = function(t) + { + // Calculate window bounds centre/extents + var ext_x = this.Size[0] / 2; + var ext_y = this.Size[1] / 2; + var mid_x = this.Position[0] + ext_x; + var mid_y = this.Position[1] + ext_y; + + // Scale from the mid-point + DOM.Node.SetPosition(this.Node, [ mid_x - ext_x * t, mid_y - ext_y * t ]); + DOM.Node.SetSize(this.Node, [ this.Size[0] * t, this.Size[1] * t ]); + } + + + Window.prototype.OpenAnimation = function(val) + { + // Power ease in + var t = 1 - Math.pow(1 - val, 8); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, 1 - Math.pow(1 - val, 8)); + this.AnimatedShow = true; + } + + + Window.prototype.CloseAnimation = function(val) + { + // Power ease out + var t = 1 - Math.pow(val, 4); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, t); + } + + + Window.prototype.NotifyChange = function() + { + if (this.OnMove) + { + var pos = DOM.Node.GetPosition(this.Node); + this.OnMove(this, pos); + } + } + + + Window.prototype.BeginMove = function(evt) + { + // Calculate offset of the window from the mouse down position + var mouse_pos = DOM.Event.GetMousePosition(evt); + this.Offset = [ mouse_pos[0] - this.Position[0], mouse_pos[1] - this.Position[1] ]; + + // Dynamically add handlers for movement and release + DOM.Event.AddHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.AddHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.Move = function(evt) + { + // Use the offset at the beginning of movement to drag the window around + var mouse_pos = DOM.Event.GetMousePosition(evt); + var offset = this.Offset; + var pos = [ mouse_pos[0] - offset[0], mouse_pos[1] - offset[1] ]; + this.SetPosition(pos[0], pos[1]); + + if (this.OnMove) + this.OnMove(this, pos); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.EndMove = function(evt) + { + // Remove handlers added during mouse down + DOM.Event.RemoveHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.RemoveHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.BeginResize = function(evt) + { + // Calculate offset of the window from the mouse down position + var mouse_pos = DOM.Event.GetMousePosition(evt); + this.MousePosBeforeResize = [ mouse_pos[0], mouse_pos[1] ]; + this.SizeBeforeResize = this.Size; + + // Dynamically add handlers for movement and release + DOM.Event.AddHandler(document, "mousemove", this.ResizeDelegate); + DOM.Event.AddHandler(document, "mouseup", this.EndResizeDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.Resize = function(evt) + { + // Use the offset at the beginning of movement to drag the window around + var mouse_pos = DOM.Event.GetMousePosition(evt); + var offset = [ mouse_pos[0] - this.MousePosBeforeResize[0], mouse_pos[1] - this.MousePosBeforeResize[1] ]; + this.SetSize(this.SizeBeforeResize[0] + offset[0], this.SizeBeforeResize[1] + offset[1]); + + if (this.OnResize) + this.OnResize(this, this.Size); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.EndResize = function(evt) + { + // Remove handlers added during mouse down + DOM.Event.RemoveHandler(document, "mousemove", this.ResizeDelegate); + DOM.Event.RemoveHandler(document, "mouseup", this.EndResizeDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Window.prototype.SetSize = function(w, h) + { + w = Math.max(80, w); + h = Math.max(15, h); + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Window.prototype.GetZIndex = function() + { + return parseInt(this.Node.style.zIndex); + } + + + return Window; +})(); \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js b/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js new file mode 100644 index 0000000..a4b80aa --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js @@ -0,0 +1,65 @@ + +namespace("WM"); + + +WM.WindowManager = (function() +{ + function WindowManager() + { + // An empty list of windows under window manager control + this.Windows = [ ]; + } + + + WindowManager.prototype.AddWindow = function(title, x, y, width, height, parent_node) + { + // Create the window and add it to the list of windows + var wnd = new WM.Window(this, title, x, y, width, height, parent_node); + this.Windows.push(wnd); + + // Always bring to the top on creation + wnd.SetTop(); + + return wnd; + } + + + WindowManager.prototype.RemoveWindow = function(window) + { + // Remove from managed window list + var index = this.Windows.indexOf(window); + if (index != -1) + { + this.Windows.splice(index, 1); + } + } + + + WindowManager.prototype.SetTopWindow = function(top_wnd) + { + // Bring the window to the top of the window list + var top_wnd_index = this.Windows.indexOf(top_wnd); + if (top_wnd_index != -1) + this.Windows.splice(top_wnd_index, 1); + this.Windows.push(top_wnd); + + // Set a CSS z-index for each visible window from the bottom up + for (var i in this.Windows) + { + var wnd = this.Windows[i]; + if (!wnd.Visible) + continue; + + // Ensure there's space between each window for the elements inside to be sorted + var z = (parseInt(i) + 1) * 10; + wnd.Node.style.zIndex = z; + + // Notify window that its z-order has changed + wnd.NotifyChange(); + } + } + + + return WindowManager; + +})(); \ No newline at end of file diff --git a/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css b/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css new file mode 100644 index 0000000..e367bc0 --- /dev/null +++ b/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css @@ -0,0 +1,652 @@ + + +.notextsel +{ + /* Disable text selection so that it doesn't interfere with button-clicking */ + user-select: none; + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer */ + -khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */ + -webkit-user-select: none; /* Chrome, Safari, and Opera */ + -webkit-touch-callout: none; /* Disable Android and iOS callouts*/ + + /* Stops the text cursor over the label */ + cursor:default; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Window Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + +body +{ + /* Clip contents to browser window without adding scrollbars */ + overflow: hidden; +} + +.Window +{ + position:absolute; + + /* Clip all contents to the window border */ + overflow: hidden; + + background: #555; + + /*padding: 0px !important;*/ + + border-radius: 3px; + -moz-border-radius: 5px; + + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +/*:root +{ + --SideBarSize: 5px; +} + +.WindowBodyDebug +{ + color: #BBB; + font: 9px Verdana; + white-space: nowrap; +} + +.WindowSizeLeft +{ + position: absolute; + left: 0px; + top: 0px; + width: var(--SideBarSize); + height: 100%; +} +.WindowSizeRight +{ + position: absolute; + left: calc(100% - var(--SideBarSize)); + top:0px; + width: var(--SideBarSize); + height:100%; +} +.WindowSizeTop +{ + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: var(--SideBarSize); +} +.WindowSizeBottom +{ + position: absolute; + left: 0px; + top: calc(100% - var(--SideBarSize)); + width: 100%; + height: var(--SideBarSize); +}*/ + + +.Window_Transparent +{ + /* Set transparency changes to fade in/out */ + opacity: 0.5; + transition: opacity 0.5s ease-out; + -moz-transition: opacity 0.5s ease-out; + -webkit-transition: opacity 0.5s ease-out; +} + +.Window_Transparent:hover +{ + opacity: 1; +} + +.WindowTitleBar +{ + height: 17px; + cursor: move; + /*overflow: hidden;*/ + + border-bottom: 1px solid #303030; + border-radius: 5px; +} + +.WindowTitleBarText +{ + color: #BBB; + font: 9px Verdana; + /*white-space: nowrap;*/ + + padding: 3px; + cursor: move; +} + +.WindowTitleBarClose +{ + color: #999999; + font: 9px Verdana; + + padding: 3px; + cursor: default; +} + +.WindowTitleBarClose:hover { + color: #bbb; +} + +.WindowResizeHandle +{ + color: #999999; + font: 17px Verdana; + padding: 3px; + cursor: se-resize; + position: absolute; + bottom: -7px; + right: -3px; +} + +.WindowBody { + position: absolute; + /* overflow: hidden; */ + display: block; + padding: 10px; + border-top: 1px solid #606060; + top: 18px; + left: 0; + right: 0; + bottom: 0; + height: auto; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Container Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Container +{ + /* Position relative to the parent window */ + position: absolute; + + /* Clip contents */ + /*overflow: hidden;*/ + + background:#2C2C2C; + + border: 1px black solid; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +/*.Panel +{*/ + /* Position relative to the parent window */ + /*position: absolute;*/ + + /* Clip contents */ + /*overflow: hidden; + + background:#2C2C2C; + + border: 1px black solid;*/ + + /* Two inset box shadows to simulate depressing */ + /*-webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset;*/ +/*}*/ + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Ruler Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +/*.Ruler +{ + position: absolute; + + border: dashed 1px; + + opacity: 0.35; +}*/ + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Treeview Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Treeview +{ + position: absolute; + + background:#2C2C2C; + border: 1px solid black; + overflow:hidden; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewItem +{ + margin:1px; + padding:2px; + border:solid 1px #2C2C2C; + background-color:#2C2C2C; +} + +.TreeviewItemImage +{ + float: left; +} + +.TreeviewItemText +{ + float: left; + margin-left:4px; +} + +.TreeviewItemChildren +{ + overflow: hidden; +} + +.TreeviewItemSelected +{ + background-color:#444; + border-color:#FFF; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +/* Used to populate treeviews that want highlight on hover behaviour */ +.TreeviewItemHover +{ +} + +.TreeviewItemHover:hover +{ + background-color:#111; + border-color:#444; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +.TreeviewScrollbarInset +{ + float: right; + + position:relative; + + height: 100%; + + /* CRAZINESS PART A: Trying to get the inset and scrollbar to have 100% height match its container */ + margin: -8px -8px 0 0; + padding: 0 1px 14px 1px; + + width:20px; + background:#2C2C2C; + border: 1px solid black; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewScrollbar +{ + position:relative; + + background:#2C2C2C; + border: 1px solid black; + + /* CRAZINESS PART B: Trying to get the inset and scrollbar to have 100% height match its container */ + padding: 0 0 10px 0; + margin: 1px 0 0 0; + + width: 18px; + height: 100%; + + border-radius:6px; + border-color:#000; + border-width:1px; + border-style:solid; + + /* The gradient for the button background */ + background-color:#666; + background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#383838)); + background: -moz-linear-gradient(top, #666, #383838); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#666666', endColorstr='#383838'); + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.TreeviewScrollbarHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Edit Box Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.EditBoxContainer +{ + position: absolute; + padding:2px 10px 2px 10px; +} + +.EditBoxLabel +{ + float:left; + padding: 3px 4px 4px 4px; + font: 9px Verdana; +} + +.EditBox +{ + float:left; + + background:#666; + border: 1px solid; + border-radius: 6px; + padding: 3px 4px 3px 4px; + height: 20px; + + box-shadow: 1px 1px 1px #222 inset; + + transition: all 0.3s ease-in-out; +} + +.EditBox:focus +{ + background:#FFF; + outline:0; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Label Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Label +{ + /* Position relative to the parent window */ + position:absolute; + + color: #BBB; + font: 9px Verdana; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Combo Box Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.ComboBox +{ + position:absolute; + + /* TEMP! */ + width:90px; + + /* Height is fixed to match the font */ + height:14px; + + /* Align the text within the combo box */ + padding: 1px 0 0 5px; + + /* Solid, rounded border */ + border: 1px solid #111; + border-radius: 5px; + + /* http://www.colorzilla.com/gradient-editor/#e3e3e3+0,c6c6c6+22,b7b7b7+33,afafaf+50,a7a7a7+67,797979+82,414141+100;Custom */ + background: #e3e3e3; + background: -moz-linear-gradient(top, #e3e3e3 0%, #c6c6c6 22%, #b7b7b7 33%, #afafaf 50%, #a7a7a7 67%, #797979 82%, #414141 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e3e3e3), color-stop(22%,#c6c6c6), color-stop(33%,#b7b7b7), color-stop(50%,#afafaf), color-stop(67%,#a7a7a7), color-stop(82%,#797979), color-stop(100%,#414141)); + background: -webkit-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -o-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -ms-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e3e3e3', endColorstr='#414141',GradientType=0 ); +} + +.ComboBoxPressed +{ + /* The reverse of the default background, simulating depression */ + background: #414141; + background: -moz-linear-gradient(top, #414141 0%, #797979 18%, #a7a7a7 33%, #afafaf 50%, #b7b7b7 67%, #c6c6c6 78%, #e3e3e3 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#414141), color-stop(18%,#797979), color-stop(33%,#a7a7a7), color-stop(50%,#afafaf), color-stop(67%,#b7b7b7), color-stop(78%,#c6c6c6), color-stop(100%,#e3e3e3)); + background: -webkit-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -o-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -ms-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#414141', endColorstr='#e3e3e3',GradientType=0 ); +} + +.ComboBoxText +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + float:left; +} + +.ComboBoxIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopup +{ + position: fixed; + + background: #CCC; + + border-radius: 5px; + + padding: 1px 0 1px 0; +} + +.ComboBoxPopupItem +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + padding: 1px 1px 1px 5px; + + border-bottom: 1px solid #AAA; + border-top: 1px solid #FFF; +} + +.ComboBoxPopupItemText +{ + float:left; +} + +.ComboBoxPopupItemIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopupItem:first-child +{ + border-top: 0px; +} + +.ComboBoxPopupItem:last-child +{ + border-bottom: 0px; +} + +.ComboBoxPopupItem:hover +{ + color:#FFF; + background: #2036E1; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Grid Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + +.Grid { + overflow: auto; + background: #333; + height: 100%; + border-radius: 2px; +} + +.GridBody +{ + overflow-x: auto; + overflow-y: auto; + height: inherit; +} + +.GridRow +{ + display: inline-block; + white-space: nowrap; + + background:#303030; + + color: #BBB; + font: 9px Verdana; + + padding: 2px; +} + +.GridRow.GridGroup +{ + padding: 0px; +} + +.GridRow:nth-child(odd) +{ + background:#333; +} + +.GridRowCell +{ + display: inline-block; +} +.GridRowCell.GridGroup +{ + color: #BBB; + + /* Override default from name */ + width: 100%; + + padding: 1px 1px 1px 2px; + border: 1px solid; + border-radius: 2px; + + border-top-color:#555; + border-left-color:#555; + border-bottom-color:#111; + border-right-color:#111; + + background: #222; +} + +.GridRowBody +{ + /* Clip all contents for show/hide group*/ + overflow: hidden; + + /* Crazy CSS rules: controls for properties don't clip if this isn't set on this parent */ + position: relative; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Button Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Button +{ + /* Position relative to the parent window */ + position:absolute; + + border-radius:4px; + + /* Padding at the top includes 2px for the text drop-shadow */ + padding: 2px 5px 3px 5px; + + color: #BBB; + font: 9px Verdana; + text-shadow: 1px 1px 1px black; + text-align: center; + + background-color:#555; + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.Button:hover { + background-color: #616161; +} + +.Button.ButtonHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} diff --git a/vis/index.html b/vis/index.html new file mode 100644 index 0000000..f8d5177 --- /dev/null +++ b/vis/index.html @@ -0,0 +1,61 @@ + + + + + + Remotery Viewer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file