diff --git a/CMakeLists.txt b/CMakeLists.txt index 626562dd88..81eac6a0a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,14 @@ if(NOT DEFINED BUILD_TESTING) set(BUILD_TESTING OFF) endif() +option(ENABLE_PROFILER "Enable Gazebo Profiler" FALSE) + +if(ENABLE_PROFILER) + add_definitions("-DGZ_PROFILER_ENABLE=1") +else() + add_definitions("-DGZ_PROFILER_ENABLE=0") +endif() + #============================================================================ # We turn off extensions because (1) we do not ever want to use non-standard # compiler extensions, and (2) this variable is on by default, causing cmake @@ -254,7 +262,11 @@ filter_valid_compiler_flags(${WARN_LEVEL} # Check and add visibility hidden by default. Only in UNIX # Windows and MacosX does not handled properly the hidden compilation if (UNIX AND NOT APPLE) - filter_valid_compiler_flags(-fvisibility=hidden -fvisibility-inlines-hidden) + if (ENABLE_PROFILER) + filter_valid_compiler_flags(-fvisibility-inlines-hidden) + else() + filter_valid_compiler_flags(-fvisibility=hidden -fvisibility-inlines-hidden) + endif() endif() if (MSVC) diff --git a/gazebo/Server.cc b/gazebo/Server.cc index 5e3160d307..7ae2516534 100644 --- a/gazebo/Server.cc +++ b/gazebo/Server.cc @@ -33,6 +33,7 @@ #include #include +#include "gazebo/common/Profiler.hh" #include "gazebo/gazebo.hh" #include "gazebo/transport/transport.hh" @@ -596,19 +597,31 @@ void Server::Run() this->dataPtr->initialized = true; + GZ_PROFILE_THREAD_NAME("gzserver"); // Stay on this loop until Gazebo needs to be shut down // The server and sensor manager outlive worlds while (!this->dataPtr->stop) { + GZ_PROFILE("Server::Run"); + GZ_PROFILE_BEGIN("ProcessControlMsgs"); if (this->dataPtr->lockstep) rendering::wait_for_render_request("", 0.100); this->ProcessControlMsgs(); + GZ_PROFILE_END(); if (physics::worlds_running()) + { + GZ_PROFILE_BEGIN("run_once"); sensors::run_once(); + GZ_PROFILE_END(); + } else if (sensors::running()) + { + GZ_PROFILE_BEGIN("stop"); sensors::stop(); + GZ_PROFILE_END(); + } if (!this->dataPtr->lockstep) common::Time::MSleep(1); diff --git a/gazebo/common/CMakeLists.txt b/gazebo/common/CMakeLists.txt index 2a2c601423..bc447dec47 100644 --- a/gazebo/common/CMakeLists.txt +++ b/gazebo/common/CMakeLists.txt @@ -215,6 +215,62 @@ if (HAVE_IGNITION_FUEL_TOOLS) ) endif() +# Start profiler +set (sources ${sources} + Profiler.cc +) + +set (remotery_sources + ./Remotery/lib/Remotery.c + ./Remotery/lib/Remotery.h + RemoteryProfilerImpl.cc +) + +if (APPLE) + set (remotery_sources ${remotery_sources} + ./Remotery/lib/RemoteryMetal.mm + ) + find_library(FOUNDATION Foundation + HINTS /System/Library/Frameworks + ) +endif() + +list(APPEND sources ${remotery_sources}) + +set(RMT_ENABLED 1) +set(RMT_USE_TINYCRT 0) +set(RMT_USE_CUDA 0) +set(RMT_USE_D3D11 0) +set(RMT_USE_OPENGL 0) +set(RMT_USE_METAL 0) + +if(UNIX OR WIN32) + set(RMT_USE_OPENGL 1) +endif() + +if(WIN32) + set(RMT_USE_D3D11 1) +endif() + +if(APPLE) + set (RMT_USE_METAL 1) +endif() + +configure_file(RemoteryConfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/RemoteryConfig.h) + +set(GZ_PROFILER_VIS_PATH share/gazebo-${GAZEBO_MAJOR_VERSION}/profiler_vis) + +configure_file(Remotery/gz_remotery_vis.in + ${CMAKE_CURRENT_BINARY_DIR}/gz_remotery_vis) + +install(PROGRAMS + ${CMAKE_CURRENT_BINARY_DIR}/gz_remotery_vis + DESTINATION bin) + +install(DIRECTORY Remotery/vis/ + DESTINATION ${CMAKE_INSTALL_PREFIX}/${GZ_PROFILER_VIS_PATH}) +# End profiler + gz_build_tests(${gtest_sources} EXTRA_LIBS gazebo_common ${tinyxml_LIBRARIES}) set (common_headers "") @@ -262,8 +318,15 @@ target_link_libraries(gazebo_common ${TBB_LIBRARIES} ${SDFormat_LIBRARIES} ${IGNITION-FUEL_TOOLS_LIBRARIES} + ${PROFILER_LIBRARIES} ) +if(ENABLE_PROFILER) + target_compile_definitions(gazebo_common + PRIVATE BUILDING_DLL_GZ_PROFILER + ) +endif() + if (UNIX) target_link_libraries(gazebo_common pthread) endif() diff --git a/gazebo/common/Profiler.cc b/gazebo/common/Profiler.cc new file mode 100644 index 0000000000..fb17f315d9 --- /dev/null +++ b/gazebo/common/Profiler.cc @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 Open Source Robotics Foundation + * + * 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. + * + */ + +/* + This is a backport of Ignition Common's profiler +*/ + +#include "Profiler.hh" +#include "ProfilerImpl.hh" +#include "RemoteryProfilerImpl.hh" + +using namespace gazebo::common; + +////////////////////////////////////////////////// +Profiler::Profiler(): + impl(nullptr) +{ + impl = new RemoteryProfilerImpl(); +} + +////////////////////////////////////////////////// +Profiler::~Profiler() +{ + if (this->impl) + delete this->impl; + this->impl = nullptr; +} + +////////////////////////////////////////////////// +void Profiler::SetThreadName(const char * _name) +{ + if (this->impl) + this->impl->SetThreadName(_name); +} + +////////////////////////////////////////////////// +void Profiler::LogText(const char * _text) +{ + if (this->impl) + this->impl->LogText(_text); +} + +////////////////////////////////////////////////// +void Profiler::BeginSample(const char * _name, uint32_t* _hash) +{ + if (this->impl) + this->impl->BeginSample(_name, _hash); +} + +////////////////////////////////////////////////// +void Profiler::EndSample() +{ + if (this->impl) + this->impl->EndSample(); +} + +////////////////////////////////////////////////// +std::string Profiler::ImplementationName() const +{ + if (this->impl) + return this->impl->Name(); + else + return "disabled"; +} + + +////////////////////////////////////////////////// +bool Profiler::Valid() const +{ + return this->impl != nullptr; +} diff --git a/gazebo/common/Profiler.hh b/gazebo/common/Profiler.hh new file mode 100644 index 0000000000..e1aa7a61a6 --- /dev/null +++ b/gazebo/common/Profiler.hh @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018 Open Source Robotics Foundation + * + * 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. + * + */ + +/* + This is a backport of ignition/common/Profiler.hh +*/ + + +#ifndef GAZEBO_COMMON_PROFILER_HH_ +#define GAZEBO_COMMON_PROFILER_HH_ + +#include +#include + +#include "gazebo/common/SingletonT.hh" +#include "gazebo/common/Remotery/lib/Remotery.h" +#include "gazebo/util/system.hh" + +/// \brief Explicit instantiation for typed SingletonT. +GZ_SINGLETON_DECLARE(GZ_COMMON_VISIBLE, gazebo, common, Profiler) + +namespace gazebo +{ + namespace common + { + class ProfilerImpl; + + /// \brief Used to perform application-wide performance profiling + /// + /// This class provides the necessary infrastructure for recording profiling + /// information of an application while it is running. The actual profiler + /// implementation can be choosen at runtime, and is invoked through a + /// series of macros found below. + /// + /// In general, users shouldn't directly interface with this class, + /// but instead use the profiling macros, which can be enabled/disabled + /// at compile time, which eliminates any performance impact of profiling. + /// + /// Profiler is enabled by setting IGN_ENABLE_PROFILER at compile time. + /// + /// The profiler header also exports several convenience macros to make + /// adding inspection points easier. + /// + /// * GZ_PROFILE_THREAD_NAME - Set the name of the current profiled thread. + /// * GZ_PROFILE_LOG_TEXT - Log text to the profiler console (if supported) + /// * GZ_PROFILE_BEGIN - Begin a named profile sample + /// * GZ_PROFILE_END - End a named profile sample + /// * GZ_PROFILE - RAII-style profile sample. The sample will end at the + /// end of the current scope. + class GZ_COMMON_VISIBLE Profiler + : public virtual SingletonT + { + /// \brief Constructor + protected: Profiler(); + + /// \brief Destructor + protected: ~Profiler(); + + /// \brief Set the name of the current thread + /// \param[in] _name Name to set + public: void SetThreadName(const char *_name); + + /// \brief Log text to profiler output (if supported) + /// If the underlying profiler implementation supports additional + /// log messages, this can be used to send. + /// + /// Currently, the Remotery implentation supports this functionality. + /// \param[in] _text Text to log. + public: void LogText(const char *_text); + + /// \brief Begin a named profiling sample. + /// Begins a CPU profiler sample with a given name. Can optionally take + /// a hash parameter to be cached between executions of `BeginSample`, so + /// that hashes don't need to be recomputed. + /// \param[in] _name Name of the sample + /// \param[in,out] _hash An optional hash value that can be cached + /// between executions. + public: void BeginSample(const char *_name, uint32_t *_hash = nullptr); + + /// \brief End a profiling sample. + public: void EndSample(); + + /// \brief Get the underlying profiler implentation name + public: std::string ImplementationName() const; + + /// \brief Detect if profiler is enabled and has an implementation + public: bool Valid() const; + + /// \brief Pointer to the profiler implementation + private: ProfilerImpl *impl; + + /// \brief Needed for SingletonT. + private: friend class SingletonT; + }; + + /// \brief Used to provide C++ RAII-style profiling sample. + /// The sample will start on the construction of the `ScopedProfile` object + /// and stop when the object leaves scope. + class GZ_COMMON_VISIBLE ScopedProfile + { + /// \brief Constructor. Starts profile sample. + /// \param[in] _name Name of the sample + /// \param[in,out] _hash An optional hash value that can be cached + /// between executions. + public: ScopedProfile(const char *_name, uint32_t *_hash) + { + Profiler::Instance()->BeginSample(_name, _hash); + } + + /// \brief Destructor. Stops profile sample. + public: ~ScopedProfile() + { + Profiler::Instance()->EndSample(); + } + }; + } +} + +#ifndef GZ_PROFILER_ENABLE +/// Always set this variable to some value +#define GZ_PROFILER_ENABLE 0 +#endif + +#if GZ_PROFILER_ENABLE +/// \brief Set name of profiled thread +#define GZ_PROFILE_THREAD_NAME(name) \ + gazebo::common::Profiler::Instance()->SetThreadName(name); +/// \brief Log profiling text, if supported by implementation +#define GZ_PROFILE_LOG_TEXT(name) \ + gazebo::common::Profiler::Instance()->LogText(name); +/// \brief Being profiling sample +#define GZ_PROFILE_BEGIN(name) \ + gazebo::common::Profiler::Instance()->BeginSample(name) +/// \brief End profiling sample +#define GZ_PROFILE_END() \ + gazebo::common::Profiler::Instance()->EndSample() + +/// \brief Convenience wrapper for scoped profiling sample. Use GZ_PROFILE +#define GZ_PROFILE_L(name, line) \ +static uint32_t __hash##line = 0; \ +gazebo::common::ScopedProfile __profile##line(name, &__hash##line); +/// \brief Scoped profiling sample. Sample will stop at end of scope. +#define GZ_PROFILE(name) GZ_PROFILE_L(name, __LINE__); + +#else + +#define GZ_PROFILE_THREAD_NAME(name) ((void) name) +#define GZ_PROFILE_LOG_TEXT(name) ((void) name) +#define GZ_PROFILE_BEGIN(name) ((void) name) +#define GZ_PROFILE_END() ((void) 0) +#define GZ_PROFILE_L(name, line) ((void) name) +#define GZ_PROFILE(name) ((void) name) +#endif // GZ_PROFILER_ENABLE + +/// \brief Macro to determine if profiler is enabled and has an implementation. +#define GZ_PROFILER_VALID \ + GZ_PROFILER_ENABLE && gazebo::common::Profiler::Instance()->Valid() + +#endif // GAZEBO_COMMON_PROFILER_HH_ diff --git a/gazebo/common/ProfilerImpl.hh b/gazebo/common/ProfilerImpl.hh new file mode 100644 index 0000000000..f3bfc59326 --- /dev/null +++ b/gazebo/common/ProfilerImpl.hh @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 Open Source Robotics Foundation + * + * 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. + * + */ + +/* + This is a backport of Ignition Common's profiler +*/ + +#ifndef GAZEBO_COMMON_PROFILERIMPL_HH_ +#define GAZEBO_COMMON_PROFILERIMPL_HH_ + +#include + +namespace gazebo +{ + namespace common + { + /// \brief Interface to be implemented by profiler implementations. + class ProfilerImpl + { + /// \brief Constructor. + public: ProfilerImpl() = default; + + /// \brief Destructor. + public: virtual ~ProfilerImpl() = default; + + /// \brief Retrieve profiler name. + public: virtual std::string Name() const = 0; + + /// \brief Set the name of the current thread + /// \param[in] _name Name to set + public: virtual void SetThreadName(const char *_name) = 0; + + /// \brief Log text to profiler output (if supported) + /// If the underlying profiler implementation supports additional + /// log messages, this can be used to send. + /// \param[in] _text Text to log. + public: virtual void LogText(const char *_text) = 0; + + /// \brief Begin a named profiling sample. + /// Begins a CPU profiler sample with a given name. Can optionally take + /// a hash parameter to be cached between executions of `BeginSample`, so + /// that hashes don't need to be recomputed. + /// \param[in] _name Name of the sample + /// \param[in,out] _hash An optional hash value that can be cached + /// between executions. + public: virtual void BeginSample(const char *_name, uint32_t *_hash) = 0; + + /// \brief End a profiling sample. + public: virtual void EndSample() = 0; + }; + } +} + +#endif // GAZEBO_COMMON_PROFILERIMPL_HH_ diff --git a/gazebo/common/Remotery/LICENSE b/gazebo/common/Remotery/LICENSE new file mode 100644 index 0000000000..67db858821 --- /dev/null +++ b/gazebo/common/Remotery/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/gazebo/common/Remotery/gz_remotery_vis.in b/gazebo/common/Remotery/gz_remotery_vis.in new file mode 100644 index 0000000000..436303ad56 --- /dev/null +++ b/gazebo/common/Remotery/gz_remotery_vis.in @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +VIS_PATH=@CMAKE_INSTALL_PREFIX@/@GZ_PROFILER_VIS_PATH@/index.html + +if [ -x "$(command -v xdg-open)" ]; then + xdg-open $VIS_PATH +elif [ -x "$(command -v open)" ]; then + open $VIS_PATH +fi diff --git a/gazebo/common/Remotery/lib/Remotery.c b/gazebo/common/Remotery/lib/Remotery.c new file mode 100644 index 0000000000..8d81e4ce19 --- /dev/null +++ b/gazebo/common/Remotery/lib/Remotery.c @@ -0,0 +1,7150 @@ +// +// 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 + @VMBUFFER: Mirror Buffer using Virtual Memory for auto-wrap + @NEW: New/Delete operators with error values for simplifying object create/destroy + @THREADS: Threads + @SAFEC: Safe C Library excerpts + @OBJALLOC: Reusable Object Allocator + @DYNBUF: Dynamic Buffer + @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 + @TSAMPLER: Per-Thread Sampler + @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") +#endif + + +#if RMT_ENABLED + + +// Global settings +static rmtSettings g_Settings; +static rmtBool g_SettingsInitialized = RMT_FALSE; + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @DEPS: External Dependencies +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +// +// Required CRT dependencies +// +#if RMT_USE_TINYCRT + + #include + #include + #include + + #define CreateFileMapping CreateFileMappingA + +#else + + #ifdef RMT_PLATFORM_MACOS + #include + #include + #include + #include + #else + #ifndef __FreeBSD__ + #include + #endif + #endif + + #include + + #ifdef RMT_PLATFORM_WINDOWS + #include + #ifndef __MINGW32__ + #include + #endif + #undef min + #undef max + #ifdef _XBOX_ONE + #include "xmem.h" + #endif + #endif + + #ifdef RMT_PLATFORM_LINUX + #include + #ifdef __FreeBSD__ + #include + #else + #include + #endif + #endif + + #if defined(RMT_PLATFORM_POSIX) + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #endif + + #ifdef __MINGW32__ + #include + #endif + +#endif + +#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 + + +#if RMT_USE_CUDA + #include +#endif + +#ifndef _WIN32 + #pragma GCC diagnostic ignored "-Wswitch-default" +#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 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 ); +} + +#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 + #ifdef __FreeBSD__ + 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 +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TLS: Thread-Local Storage +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#define TLS_INVALID_HANDLE 0xFFFFFFFF + +#if defined(RMT_PLATFORM_WINDOWS) + typedef rmtU32 rmtTLS; +#else + typedef pthread_key_t rmtTLS; +#endif + +static rmtError tlsAlloc(rmtTLS* handle) +{ + assert(handle != NULL); + +#if defined(RMT_PLATFORM_WINDOWS) + + *handle = (rmtTLS)TlsAlloc(); + if (*handle == TLS_OUT_OF_INDEXES) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } + +#elif defined(RMT_PLATFORM_POSIX) + + if (pthread_key_create(handle, NULL) != 0) + { + *handle = TLS_INVALID_HANDLE; + return RMT_ERROR_TLS_ALLOC_FAIL; + } + +#endif + + return RMT_ERROR_NONE; +} + + +static void tlsFree(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); + +#if defined(RMT_PLATFORM_WINDOWS) + + TlsFree(handle); + +#elif defined(RMT_PLATFORM_POSIX) + + pthread_key_delete((pthread_key_t)handle); + +#endif +} + + +static void tlsSet(rmtTLS handle, void* value) +{ + assert(handle != TLS_INVALID_HANDLE); + +#if defined(RMT_PLATFORM_WINDOWS) + + TlsSetValue(handle, value); + +#elif defined(RMT_PLATFORM_POSIX) + + pthread_setspecific((pthread_key_t)handle, value); + +#endif +} + + +static void* tlsGet(rmtTLS handle) +{ + assert(handle != TLS_INVALID_HANDLE); + +#if defined(RMT_PLATFORM_WINDOWS) + + return TlsGetValue(handle); + +#elif defined(RMT_PLATFORM_POSIX) + + return pthread_getspecific((pthread_key_t)handle); + +#endif +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @ATOMIC: Atomic Operations +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + +static rmtBool AtomicCompareAndSwap(rmtU32 volatile* val, long old_val, long new_val) +{ + #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return _InterlockedCompareExchange((long volatile*)val, new_val, old_val) == old_val ? RMT_TRUE : RMT_FALSE; + #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_bool_compare_and_swap(val, old_val, new_val) ? RMT_TRUE : RMT_FALSE; + #endif +} + + +static rmtBool AtomicCompareAndSwapPointer(long* volatile* ptr, long* old_ptr, long* new_ptr) +{ + #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + #ifdef _WIN64 + return _InterlockedCompareExchange64((__int64 volatile*)ptr, (__int64)new_ptr, (__int64)old_ptr) == (__int64)old_ptr ? RMT_TRUE : RMT_FALSE; + #else + return _InterlockedCompareExchange((long volatile*)ptr, (long)new_ptr, (long)old_ptr) == (long)old_ptr ? RMT_TRUE : RMT_FALSE; + #endif + #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_bool_compare_and_swap(ptr, old_ptr, new_ptr) ? RMT_TRUE : RMT_FALSE; + #endif +} + + +// +// NOTE: Does not guarantee a memory barrier +// TODO: Make sure all platforms don't insert a memory barrier as this is only for stats +// Alternatively, add strong/weak memory order equivalents +// +static rmtS32 AtomicAdd(rmtS32 volatile* value, rmtS32 add) +{ + #if defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + return _InterlockedExchangeAdd((long volatile*)value, (long)add); + #elif defined(RMT_PLATFORM_POSIX) || defined(__MINGW32__) + return __sync_fetch_and_add(value, add); + #endif +} + + +static void AtomicSub(rmtS32 volatile* value, rmtS32 sub) +{ + // Not all platforms have an implementation so just negate and add + AtomicAdd(value, -sub); +} + + +// Compiler write fences +static void WriteFence() +{ +#if defined (__clang__) + __asm__ volatile("" : : : "memory"); +#elif defined(RMT_PLATFORM_WINDOWS) && !defined(__MINGW32__) + _WriteBarrier(); +#else + asm volatile ("" : : : "memory"); +#endif +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @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 + #ifdef __FreeBSD__ + 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 = 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; + + // Reserve two contiguous pages of virtual memory + desired_addr = (rmtU8*)VirtualAlloc(0, size * 2, MEM_RESERVE, PAGE_NOACCESS); + if (desired_addr == NULL) + break; + + // Release the range immediately but retain the address for the next sequence of code to + // try and map to it. In the mean-time some other OS thread may come along and allocate this + // address range from underneath us so multiple attempts need to be made. + VirtualFree(desired_addr, 0, MEM_RELEASE); + + // Immediately try to point both pages at the file mapping + if (MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr) == desired_addr && + MapViewOfFileEx(buffer->file_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, size, desired_addr + size) == desired_addr + size) + { + buffer->ptr = desired_addr; + break; + } + + // Failed to map the virtual pages; cleanup and try again + CloseHandle(buffer->file_map_handle); + buffer->file_map_handle = NULL; + } + +#endif // _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) + { + 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; +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @THREADS: Threads +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + +typedef struct Thread_t rmtThread; +typedef rmtError(*ThreadProc)(rmtThread* thread); + + +struct Thread_t +{ + // OS-specific data + #if defined(RMT_PLATFORM_WINDOWS) + HANDLE handle; + #else + pthread_t handle; + #endif + + // 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 + } +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @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 + +#if (!defined(_WIN64) && !defined(__APPLE__)) || (defined(__MINGW32__) && !(defined(RSIZE_T_DEFINED) || defined(_RSIZE_T_DEFINED))) +typedef unsigned int rsize_t; +#endif + +#if defined(RMT_PLATFORM_MACOS) && !defined(_RSIZE_T) +typedef __darwin_size_t rsize_t; +#endif + +static rsize_t +strnlen_s (const char *dest, rsize_t dmax) +{ + rsize_t count; + + if (dest == NULL) { + return RCNEGATE(0); + } + + if (dmax == 0) { + return RCNEGATE(0); + } + + if (dmax > RSIZE_MAX_STR) { + return RCNEGATE(0); + } + + count = 0; + while (*dest && dmax) { + count++; + dmax--; + dest++; + } + + return RCNEGATE(count); +} + + +static errno_t +strstr_s (char *dest, rsize_t dmax, + const char *src, rsize_t slen, char **substring) +{ + rsize_t len; + rsize_t dlen; + int i; + + if (substring == NULL) { + return RCNEGATE(ESNULLP); + } + *substring = NULL; + + if (dest == NULL) { + return RCNEGATE(ESNULLP); + } + + if (dmax == 0) { + return RCNEGATE(ESZEROL); + } + + if (dmax > RSIZE_MAX_STR) { + return RCNEGATE(ESLEMAX); + } + + if (src == NULL) { + return RCNEGATE(ESNULLP); + } + + if (slen == 0) { + return RCNEGATE(ESZEROL); + } + + if (slen > RSIZE_MAX_STR) { + return RCNEGATE(ESLEMAX); + } + + /* + * src points to a string with zero length, or + * src equals dest, return dest + */ + if (*src == '\0' || dest == src) { + *substring = dest; + return RCNEGATE(EOK); + } + + while (*dest && dmax) { + i = 0; + len = slen; + dlen = dmax; + + while (src[i] && dlen) { + + /* not a match, not a substring */ + if (dest[i] != src[i]) { + break; + } + + /* move to the next char */ + i++; + len--; + dlen--; + + if (src[i] == '\0' || !len) { + *substring = dest; + return RCNEGATE(EOK); + } + } + dest++; + dmax--; + } + + /* + * substring was not found, return NULL + */ + *substring = NULL; + return RCNEGATE(ESNOTFND); +} + + +static errno_t +strncat_s (char *dest, rsize_t dmax, const char *src, rsize_t slen) +{ + 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, rsize_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, rsize_t dmax, rmtS32 value ) +{ + rsize_t len; + rmtS32 halfbytepos; + + halfbytepos = 8; + + /* strip leading 0's */ + while (halfbytepos > 1) + { + --halfbytepos; + if (value >> (4 * halfbytepos) & 0xF) + { + ++halfbytepos; + break; + } + } + + len = 0; + while(len + 1 < dmax && halfbytepos > 0) + { + --halfbytepos; + dest[len] = hex_encoding_table[value >> (4 * halfbytepos) & 0xF]; + ++len; + } + + if (len < dmax) + { + dest[len] = 0; + } +} + +#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); + assert(allocator->first_free != NULL); + + // CAS pop from the front of the list + for (;;) + { + ObjectLink* old_link = (ObjectLink*)allocator->first_free; + ObjectLink* next_link = old_link->next; + if (AtomicCompareAndSwapPointer((long* volatile*)&allocator->first_free, (long*)old_link, (long*)next_link) == RMT_TRUE) + { + link = old_link; + break; + } + } + + link->next = NULL; + + return link; +} + + +static rmtError ObjectAllocator_Alloc(ObjectAllocator* allocator, void** object) +{ + // This function only calls the object constructor on initial malloc of an object + + assert(allocator != NULL); + assert(object != NULL); + + // Has the free list run out? + if (allocator->first_free == NULL) + { + rmtError error; + + // Allocate/construct a new object + void* free_object = rmtMalloc( allocator->object_size ); + if (free_object == NULL) + return RMT_ERROR_MALLOC_FAIL; + assert(allocator->constructor != NULL); + error = allocator->constructor(free_object); + if (error != RMT_ERROR_NONE) + { + // Auto-teardown on failure + assert(allocator->destructor != NULL); + allocator->destructor(free_object); + rmtFree(free_object); + return error; + } + + // Add to the free list + ObjectAllocator_Push(allocator, (ObjectLink*)free_object, (ObjectLink*)free_object); + AtomicAdd(&allocator->nb_allocated, 1); + AtomicAdd(&allocator->nb_free, 1); + } + + // Pull available objects from the free list + *object = ObjectAllocator_Pop(allocator); + AtomicSub(&allocator->nb_free, 1); + AtomicAdd(&allocator->nb_inuse, 1); + + return RMT_ERROR_NONE; +} + + +static void ObjectAllocator_Free(ObjectAllocator* allocator, void* object) +{ + // Add back to the free-list + assert(allocator != NULL); + ObjectAllocator_Push(allocator, (ObjectLink*)object, (ObjectLink*)object); + AtomicSub(&allocator->nb_inuse, 1); + AtomicAdd(&allocator->nb_free, 1); +} + + +static void ObjectAllocator_FreeRange(ObjectAllocator* allocator, void* start, void* end, rmtU32 count) +{ + assert(allocator != NULL); + ObjectAllocator_Push(allocator, (ObjectLink*)start, (ObjectLink*)end); + AtomicSub(&allocator->nb_inuse, count); + AtomicAdd(&allocator->nb_free, count); +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @DYNBUF: Dynamic Buffer +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +typedef struct +{ + rmtU32 alloc_granularity; + + rmtU32 bytes_allocated; + rmtU32 bytes_used; + + rmtU8* data; +} Buffer; + + +static rmtError Buffer_Constructor(Buffer* buffer, rmtU32 alloc_granularity) +{ + assert(buffer != NULL); + buffer->alloc_granularity = alloc_granularity; + buffer->bytes_allocated = 0; + buffer->bytes_used = 0; + buffer->data = NULL; + return RMT_ERROR_NONE; +} + + +static void Buffer_Destructor(Buffer* buffer) +{ + assert(buffer != NULL); + + if (buffer->data != NULL) + { + rmtFree(buffer->data); + buffer->data = NULL; + } +} + + +static rmtError Buffer_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 0xFFFFFFFF + + +typedef struct +{ + // Non-zero, pre-hashed key + rmtU32 key; + + // Value that's not equal to RMT_NOT_FOUND + rmtU32 value; +} HashSlot; + + +typedef struct +{ + // Stats + rmtU32 max_nb_slots; + rmtU32 nb_slots; + + // Data + HashSlot* slots; +} rmtHashTable; + + +static rmtError rmtHashTable_Constructor(rmtHashTable* table, rmtU32 max_nb_slots) +{ + // Default initialise + assert(table != NULL); + table->max_nb_slots = max_nb_slots; + table->nb_slots = 0; + + // Allocate and clear the hash slots + table->slots = (HashSlot*)rmtMalloc(table->max_nb_slots * sizeof(HashSlot)); + if (table->slots == NULL) + return RMT_ERROR_MALLOC_FAIL; + memset(table->slots, 0, table->max_nb_slots * 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, rmtU32 value) +{ + HashSlot* slot = NULL; + rmtError error = RMT_ERROR_NONE; + + // Calculate initial slot location for this key + rmtU32 index_mask = table->max_nb_slots - 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->nb_slots--; + break; + } + + index = (index + 1) & index_mask; + } + + // Just verify that I've got no errors in the code above + assert(index < table->max_nb_slots); + + // Add to the table + slot = table->slots + index; + slot->key = key; + slot->value = value; + table->nb_slots++; + + // Resize when load factor is greater than 2/3 + if (table->nb_slots > (table->max_nb_slots * 2) / 3) + error = rmtHashTable_Resize(table); + + return error; +} + + +static rmtError rmtHashTable_Resize(rmtHashTable* table) +{ + rmtU32 old_max_nb_slots = table->max_nb_slots; + HashSlot* new_slots = NULL; + HashSlot* old_slots = table->slots; + rmtU32 i; + + // Increase the table size + rmtU32 new_max_nb_slots = table->max_nb_slots; + 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->max_nb_slots = new_max_nb_slots; + table->nb_slots = 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 rmtU32 rmtHashTable_Find(rmtHashTable* table, rmtU32 key) +{ + // Calculate initial slot location for this key + rmtU32 index_mask = table->max_nb_slots - 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) +{ + rmtU32 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 + rmtU32 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 TCPSocketWouldBlock() +{ +#ifdef RMT_PLATFORM_WINDOWS + DWORD error = WSAGetLastError(); + return (error == WSAEWOULDBLOCK); + #else + int error = errno; + return (error == EAGAIN || error == EWOULDBLOCK); +#endif + +} + + +static rmtError TCPSocket_Send(TCPSocket* tcp_socket, const void* data, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data = NULL; + char* end_data = NULL; + rmtU32 start_ms = 0; + rmtU32 cur_ms = 0; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Loop until timeout checking whether data can be written + status.can_write = RMT_FALSE; + while (!status.can_write) + { + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + + cur_ms = msTimer_Get(); + if (cur_ms - start_ms > timeout_ms) + return RMT_ERROR_SOCKET_SEND_TIMEOUT; + } + + cur_data = (char*)data; + end_data = cur_data + length; + + while (cur_data < end_data) + { + // Attempt to send the remaining chunk of data + int bytes_sent; + int send_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 && !TCPSocketWouldBlock()) + return RMT_ERROR_SOCKET_SEND_FAIL; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) endpoint is no longer there + // 2) endpoint can't consume quick enough + // 3) local buffers overflow + // + // As none of these are actually errors, we have to pass this timeout back to the caller. + // + // TODO: This strategy breaks down if a send partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return RMT_ERROR_SOCKET_SEND_TIMEOUT; + } + } + else + { + // Jump over the data sent + cur_data += bytes_sent; + } + } + + return RMT_ERROR_NONE; +} + + +static rmtError TCPSocket_Receive(TCPSocket* tcp_socket, void* data, rmtU32 length, rmtU32 timeout_ms) +{ + SocketStatus status; + char* cur_data = NULL; + char* end_data = NULL; + rmtU32 start_ms = 0; + rmtU32 cur_ms = 0; + + assert(tcp_socket != NULL); + + // Ensure there is data to receive + status = TCPSocket_PollStatus(tcp_socket); + if (status.error_state != RMT_ERROR_NONE) + return status.error_state; + if (!status.can_read) + return RMT_ERROR_SOCKET_RECV_NO_DATA; + + cur_data = (char*)data; + end_data = cur_data + length; + + // Loop until all data has been received + start_ms = msTimer_Get(); + while (cur_data < end_data) + { + int bytes_received = (int)recv(tcp_socket->socket, cur_data, (int)(end_data - cur_data), 0); + + if (bytes_received == SOCKET_ERROR || bytes_received == 0) + { + // Close the connection if receiving fails for any other reason other than blocking + if (bytes_received != 0 && !TCPSocketWouldBlock()) + return RMT_ERROR_SOCKET_RECV_FAILED; + + // First check for tick-count overflow and reset, giving a slight hitch every 49.7 days + cur_ms = msTimer_Get(); + if (cur_ms < start_ms) + { + start_ms = cur_ms; + continue; + } + + // + // Timeout can happen when: + // + // 1) data is delayed by sender + // 2) sender fails to send a complete set of packets + // + // As not all of these scenarios are errors, we need to pass this information back to the caller. + // + // TODO: This strategy breaks down if a receive partially completes and then times out! + // + if (cur_ms - start_ms > timeout_ms) + { + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + } + } + else + { + // Jump over the data received + cur_data += bytes_received; + } + } + + return RMT_ERROR_NONE; +} + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @SHA1: SHA-1 Cryptographic Hash Function +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + +// +// Typed to allow enforced data size specification +// +typedef struct +{ + rmtU8 data[20]; +} SHA1; + + +/* + Copyright (c) 2011, Micael Hildenborg + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Micael Hildenborg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY Micael Hildenborg ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL Micael Hildenborg BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + Contributors: + Gustav + Several members in the gamedev.se forum. + Gregory Petrosyan + */ + + +// Rotate an integer value to left. +static unsigned int rol(const unsigned int value, const unsigned int steps) +{ + return ((value << steps) | (value >> (32 - steps))); +} + + +// Sets the first 16 integers in the buffert to zero. +// Used for clearing the W buffert. +static void clearWBuffert(unsigned int* buffert) +{ + int pos; + for (pos = 16; --pos >= 0;) + { + buffert[pos] = 0; + } +} + +static void innerHash(unsigned int* result, unsigned int* w) +{ + unsigned int a = result[0]; + unsigned int b = result[1]; + unsigned int c = result[2]; + unsigned int d = result[3]; + unsigned int e = result[4]; + + int round = 0; + + #define sha1macro(func,val) \ + { \ + const unsigned int t = rol(a, 5) + (func) + e + val + w[round]; \ + e = d; \ + d = c; \ + c = rol(b, 30); \ + b = a; \ + a = t; \ + } + + while (round < 16) + { + sha1macro((b & c) | (~b & d), 0x5a827999) + ++round; + } + while (round < 20) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (~b & d), 0x5a827999) + ++round; + } + while (round < 40) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0x6ed9eba1) + ++round; + } + while (round < 60) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro((b & c) | (b & d) | (c & d), 0x8f1bbcdc) + ++round; + } + while (round < 80) + { + w[round] = rol((w[round - 3] ^ w[round - 8] ^ w[round - 14] ^ w[round - 16]), 1); + sha1macro(b ^ c ^ d, 0xca62c1d6) + ++round; + } + + #undef sha1macro + + result[0] += a; + result[1] += b; + result[2] += c; + result[3] += d; + result[4] += e; +} + + +static void calc(const void* src, const int bytelength, unsigned char* hash) +{ + int roundPos; + int lastBlockBytes; + int hashByte; + + // Init the result array. + unsigned int result[5] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 }; + + // Cast the void src pointer to be the byte array we can work with. + const unsigned char* sarray = (const unsigned char*) src; + + // The reusable round buffer + unsigned int w[80]; + + // Loop through all complete 64byte blocks. + const int endOfFullBlocks = bytelength - 64; + int endCurrentBlock; + int currentBlock = 0; + + while (currentBlock <= endOfFullBlocks) + { + endCurrentBlock = currentBlock + 64; + + // Init the round buffer with the 64 byte block data. + for (roundPos = 0; currentBlock < endCurrentBlock; currentBlock += 4) + { + // This line will swap endian on big endian and keep endian on little endian. + w[roundPos++] = (unsigned int) sarray[currentBlock + 3] + | (((unsigned int) sarray[currentBlock + 2]) << 8) + | (((unsigned int) sarray[currentBlock + 1]) << 16) + | (((unsigned int) sarray[currentBlock]) << 24); + } + innerHash(result, w); + } + + // Handle the last and not full 64 byte block if existing. + endCurrentBlock = bytelength - currentBlock; + clearWBuffert(w); + lastBlockBytes = 0; + for (;lastBlockBytes < endCurrentBlock; ++lastBlockBytes) + { + w[lastBlockBytes >> 2] |= (unsigned int) sarray[lastBlockBytes + currentBlock] << ((3 - (lastBlockBytes & 3)) << 3); + } + w[lastBlockBytes >> 2] |= 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 or can only +// handle aligned reads, do the conversion here +static rmtU32 getblock32(const rmtU32* p, int i) +{ + return p[i]; +} + + +// Finalization mix - force all bits of a hash block to avalanche +static rmtU32 fmix32(rmtU32 h) +{ + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + return h; +} + + +static rmtU32 MurmurHash3_x86_32(const void* key, int len, rmtU32 seed) +{ + const rmtU8* data = (const rmtU8*)key; + const int nblocks = len / 4; + + rmtU32 h1 = seed; + + const rmtU32 c1 = 0xcc9e2d51; + const rmtU32 c2 = 0x1b873593; + + int i; + + const rmtU32 * blocks = (const rmtU32 *)(data + nblocks*4); + const rmtU8 * tail = (const rmtU8*)(data + nblocks*4); + + rmtU32 k1 = 0; + + //---------- + // body + + for (i = -nblocks; i; i++) + { + rmtU32 k2 = getblock32(blocks,i); + + k2 *= c1; + k2 = rotl32(k2,15); + k2 *= c2; + + h1 ^= k2; + h1 = rotl32(h1,13); + h1 = h1*5+0xe6546b64; + } + + //---------- + // tail + + switch(len & 3) + { + case 3: k1 ^= tail[2] << 16; // 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, rsize_t buffer_length, rmtPStr field_name) +{ + char* field = NULL; + char* buffer_end = buffer + buffer_length - 1; + + rsize_t field_length = strnlen_s(field_name, buffer_length); + if (field_length == 0) + return NULL; + + // Search for the start of the field + if (strstr_s(buffer, buffer_length, field_name, field_length, &field) != EOK) + return NULL; + + // Field name is now guaranteed to be in the buffer so its safe to jump over it without hitting the bounds + field += strlen(field_name); + + // Skip any trailing whitespace + while (*field == ' ') + { + if (field >= buffer_end) + return NULL; + field++; + } + + return field; +} + + +static const char websocket_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +static const char websocket_response[] = + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: "; + + +static rmtError WebSocketHandshake(TCPSocket* tcp_socket, rmtPStr limit_host) +{ + rmtU32 start_ms, now_ms; + + // Parsing scratchpad + char buffer[1024]; + char* buffer_ptr = buffer; + int buffer_len = sizeof(buffer) - 1; + char* buffer_end = buffer + buffer_len; + + char response_buffer[256]; + int response_buffer_len = sizeof(response_buffer) - 1; + + char* version; + char* host; + char* key; + char* key_end; + SHA1 hash; + + assert(tcp_socket != NULL); + + start_ms = msTimer_Get(); + + // Really inefficient way of receiving the handshake data from the browser + // Not really sure how to do this any better, as the termination requirement is \r\n\r\n + while (buffer_ptr - buffer < buffer_len) + { + rmtError error = TCPSocket_Receive(tcp_socket, buffer_ptr, 1, 20); + if (error == RMT_ERROR_SOCKET_RECV_FAILED) + return error; + + // If there's a stall receiving the data, check for a handshake timeout + if (error == RMT_ERROR_SOCKET_RECV_NO_DATA || error == RMT_ERROR_SOCKET_RECV_TIMEOUT) + { + now_ms = msTimer_Get(); + if (now_ms - start_ms > 1000) + return RMT_ERROR_SOCKET_RECV_TIMEOUT; + + continue; + } + + // Just in case new enums are added... + assert(error == RMT_ERROR_NONE); + + if (buffer_ptr - buffer >= 4) + { + if (*(buffer_ptr - 3) == '\r' && + *(buffer_ptr - 2) == '\n' && + *(buffer_ptr - 1) == '\r' && + *(buffer_ptr - 0) == '\n') + break; + } + + buffer_ptr++; + } + *buffer_ptr = 0; + + // HTTP GET instruction + if (memcmp(buffer, "GET", 3) != 0) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET; + + // Look for the version number and verify that it's supported + version = GetField(buffer, buffer_len, "Sec-WebSocket-Version:"); + if (version == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION; + if (buffer_end - version < 2 || (version[0] != '8' && (version[0] != '1' || version[1] != '3'))) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION; + + // Make sure this connection comes from a known host + host = GetField(buffer, buffer_len, "Host:"); + if (host == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST; + if (limit_host != NULL) + { + rsize_t limit_host_len = strnlen_s(limit_host, 128); + char* found = NULL; + if (strstr_s(host, buffer_end - host, limit_host, limit_host_len, &found) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST; + } + + // Look for the key start and null-terminate it within the receive buffer + key = GetField(buffer, buffer_len, "Sec-WebSocket-Key:"); + if (key == NULL) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY; + if (strstr_s(key, buffer_end - key, "\r\n", 2, &key_end) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY; + *key_end = 0; + + // Concatenate the browser's key with the WebSocket Protocol GUID and base64 encode + // the hash, to prove to the browser that this is a bonafide WebSocket server + buffer[0] = 0; + if (strncat_s(buffer, buffer_len, key, key_end - key) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(buffer, buffer_len, websocket_guid, sizeof(websocket_guid)) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + hash = SHA1_Calculate(buffer, (rmtU32)strnlen_s(buffer, buffer_len)); + Base64_Encode(hash.data, sizeof(hash.data), (rmtU8*)buffer); + + // Send the response back to the server with a longer timeout than usual + response_buffer[0] = 0; + if (strncat_s(response_buffer, response_buffer_len, websocket_response, sizeof(websocket_response)) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(response_buffer, response_buffer_len, buffer, buffer_len) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + if (strncat_s(response_buffer, response_buffer_len, "\r\n\r\n", 4) != EOK) + return RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL; + + return TCPSocket_Send(tcp_socket, response_buffer, (rmtU32)strnlen_s(response_buffer, response_buffer_len), 1000); +} + + +static rmtError WebSocket_Constructor(WebSocket* web_socket, TCPSocket* tcp_socket) +{ + rmtError error = RMT_ERROR_NONE; + + assert(web_socket != NULL); + web_socket->tcp_socket = tcp_socket; + web_socket->mode = WEBSOCKET_NONE; + web_socket->frame_bytes_remaining = 0; + web_socket->mask_offset = 0; + web_socket->data.mask[0] = 0; + web_socket->data.mask[1] = 0; + web_socket->data.mask[2] = 0; + web_socket->data.mask[3] = 0; + + // Caller can optionally specify which TCP socket to use + if (web_socket->tcp_socket == NULL) + New_0(TCPSocket, web_socket->tcp_socket); + + return error; +} + + +static void WebSocket_Destructor(WebSocket* web_socket) +{ + WebSocket_Close(web_socket); +} + + +static rmtError WebSocket_RunServer(WebSocket* web_socket, 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_LogText, + MsgID_SampleTree, +} MessageID; + + +typedef struct Message +{ + MessageID id; + + rmtU32 payload_size; + + // For telling which thread the message came from in the debugger + struct ThreadSampler* thread_sampler; + + rmtU8 payload[1]; +} Message; + + +// Multiple producer, single consumer message queue that uses its own data buffer +// to store the message data. +typedef struct 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 ThreadSampler* thread_sampler) +{ + Message* msg; + + rmtU32 write_size = rmtMessageQueue_SizeForPayload(payload_size); + + assert(queue != NULL); + + for (;;) + { + // Check for potential overflow + rmtU32 s = queue->size; + rmtU32 r = queue->read_pos; + rmtU32 w = queue->write_pos; + if ((int)(w - r) > ((int)(s - write_size))) + return NULL; + + // Point to the newly allocated space + msg = (Message*)(queue->data->ptr + (w & (s - 1))); + + // Increment the write position, leaving the loop if this is the thread that succeeded + if (AtomicCompareAndSwap(&queue->write_pos, w, w + write_size) == RMT_TRUE) + { + // Safe to set payload size after thread claims ownership of this allocated range + msg->payload_size = payload_size; + msg->thread_sampler = thread_sampler; + break; + } + } + + return msg; +} + + +static void rmtMessageQueue_CommitMessage(Message* message, MessageID id) +{ + assert(message != NULL); + + // Ensure message writes complete before commit + WriteFence(); + + // Setting the message ID signals to the consumer that the message is ready + assert(message->id == MsgID_NotReady); + message->id = id; +} + + +Message* rmtMessageQueue_PeekNextMessage(rmtMessageQueue* queue) +{ + Message* ptr; + rmtU32 r; + + assert(queue != NULL); + + // First check that there are bytes queued + if (queue->write_pos - queue->read_pos == 0) + return NULL; + + // Messages are in the queue but may not have been commit yet + // Messages behind this one may have been commit but it's not reachable until + // the next one in the queue is ready. + r = queue->read_pos & (queue->size - 1); + ptr = (Message*)(queue->data->ptr + r); + if (ptr->id != MsgID_NotReady) + return ptr; + + return NULL; +} + + +static void rmtMessageQueue_ConsumeNextMessage(rmtMessageQueue* queue, Message* message) +{ + rmtU32 message_size; + + assert(queue != NULL); + assert(message != NULL); + + // Setting the message ID to "not ready" serves as a marker to the consumer that even though + // space has been allocated for a message, the message isn't ready to be consumed + // yet. + // + // We can't do that when allocating the message because multiple threads will be fighting for + // the same location. Instead, clear out any messages just read by the consumer before advancing + // the read position so that a winning thread's allocation will inherit the "not ready" state. + // + // This costs some write bandwidth and has the potential to flush cache to other cores. + message_size = rmtMessageQueue_SizeForPayload(message->payload_size); + memset(message, MsgID_NotReady, message_size); + + // Ensure clear completes before advancing the read position + WriteFence(); + queue->read_pos += message_size; +} + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @NETWORK: Network Server +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + +typedef 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; + WriteFence(); + Delete(WebSocket, client_socket); +} + + +static rmtError Server_Send(Server* server, const void* data, rmtU32 length, rmtU32 timeout) +{ + assert(server != NULL); + if (Server_IsClientConnected(server)) + { + rmtError error = WebSocket_Send(server->client_socket, data, length, timeout); + if (error == RMT_ERROR_SOCKET_SEND_FAIL) + Server_DisconnectClient(server); + + return error; + } + + return RMT_ERROR_NONE; +} + + +static rmtError Server_ReceiveMessage(Server* server, char message_first_byte, rmtU32 message_length) +{ + char message_data[1024]; + rmtError error; + + // Check for potential message data overflow + if (message_length >= sizeof(message_data) - 1) + { + rmt_LogText("Ignoring console input bigger than internal receive buffer (1024 bytes)"); + return RMT_ERROR_NONE; + } + + // Receive the rest of the message + message_data[0] = message_first_byte; + error = WebSocket_Receive(server->client_socket, message_data + 1, NULL, message_length - 1, 100); + if (error != RMT_ERROR_NONE) + return error; + message_data[message_length] = 0; + + // Each message must have a descriptive 4 byte header + if (message_length < 4) + return RMT_ERROR_NONE; + + // 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 + + +enum SampleType +{ + SampleType_CPU, + SampleType_CUDA, + SampleType_D3D11, + SampleType_OpenGL, + SampleType_Metal, + SampleType_Count, +}; + + +typedef struct Sample +{ + // Inherit so that samples can be quickly allocated + ObjectLink Link; + + enum SampleType type; + + // Used to anonymously copy sample data without knowning its type + rmtU32 size_bytes; + + // 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->size_bytes = sizeof(Sample); + 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; +} + + +#define BIN_ERROR_CHECK(stmt) { error = stmt; if (error != RMT_ERROR_NONE) return error; } + + +static rmtError bin_SampleArray(Buffer* buffer, Sample* first_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* current_parent; + +} SampleTree; + + +static rmtError SampleTree_Constructor(SampleTree* tree, rmtU32 sample_size, ObjConstructor constructor, ObjDestructor destructor) +{ + rmtError error; + + assert(tree != NULL); + + tree->allocator = NULL; + tree->root = NULL; + tree->current_parent = NULL; + + // Create the sample allocator + New_3(ObjectAllocator, tree->allocator, sample_size, constructor, destructor); + if (error != RMT_ERROR_NONE) + return error; + + // Create a root sample that's around for the lifetime of the thread + error = ObjectAllocator_Alloc(tree->allocator, (void**)&tree->root); + if (error != RMT_ERROR_NONE) + return error; + Sample_Prepare(tree->root, 0, NULL); + tree->current_parent = tree->root; + + return RMT_ERROR_NONE; +} + + +static void SampleTree_Destructor(SampleTree* tree) +{ + assert(tree != NULL); + + if (tree->root != NULL) + { + ObjectAllocator_Free(tree->allocator, tree->root); + tree->root = NULL; + } + + Delete(ObjectAllocator, tree->allocator); +} + + +static rmtU32 HashCombine(rmtU32 hash_a, rmtU32 hash_b) +{ + // A sequence of 32 uniformly random bits so that each bit of the combined hash is changed on application + // Derived from the golden ratio: UINT_MAX / ((1 + sqrt(5)) / 2) + // In reality it's just an arbitrary value which happens to work well, avoiding mapping all zeros to zeros. + // http://burtleburtle.net/bob/hash/doobs.html + static rmtU32 random_bits = 0x9E3779B9; + hash_a ^= hash_b + random_bits + (hash_a << 6) + (hash_a >> 2); + return hash_a; +} + + +static rmtError SampleTree_Push(SampleTree* tree, 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->current_parent != NULL); + parent = tree->current_parent; + + 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->current_parent = 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->current_parent = *sample; + + return RMT_ERROR_NONE; +} + + +static void SampleTree_Pop(SampleTree* tree, Sample* sample) +{ + assert(tree != NULL); + assert(sample != NULL); + assert(sample != tree->root); + tree->current_parent = sample->parent; +} + + +static ObjectLink* FlattenSampleTree(Sample* sample, rmtU32* nb_samples) +{ + Sample* child; + ObjectLink* cur_link = &sample->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 = FlattenSampleTree(child, nb_samples); + last_link->next = (ObjectLink*)child->next_sibling; + cur_link = last_link; + } + + // Clear child info + sample->first_child = NULL; + sample->last_child = NULL; + sample->nb_children = 0; + + return cur_link; +} + + +static void FreeSampleTree(Sample* sample, ObjectAllocator* allocator) +{ + // Chain all samples together in a flat list + rmtU32 nb_cleared_samples = 0; + ObjectLink* last_link = FlattenSampleTree(sample, &nb_cleared_samples); + + // Release the complete sample memory range + if (sample->Link.next != NULL) + ObjectAllocator_FreeRange(allocator, sample, last_link, nb_cleared_samples); + else + ObjectAllocator_Free(allocator, sample); +} + + +typedef struct Msg_SampleTree +{ + Sample* root_sample; + + ObjectAllocator* allocator; + + rmtPStr thread_name; +} Msg_SampleTree; + + +static void AddSampleTreeMessage(rmtMessageQueue* queue, Sample* sample, ObjectAllocator* allocator, rmtPStr thread_name, struct ThreadSampler* thread_sampler) +{ + Msg_SampleTree* payload; + + // Attempt to allocate a message for sending the tree to the viewer + Message* message = rmtMessageQueue_AllocMessage(queue, sizeof(Msg_SampleTree), thread_sampler); + if (message == NULL) + { + // Discard the tree on failure + FreeSampleTree(sample, allocator); + return; + } + + // Populate and commit + payload = (Msg_SampleTree*)message->payload; + payload->root_sample = sample; + payload->allocator = allocator; + payload->thread_name = thread_name; + rmtMessageQueue_CommitMessage(message, MsgID_SampleTree); +} + + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @TSAMPLER: Per-Thread Sampler +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +typedef struct ThreadSampler +{ + // Name to assign to the thread in the viewer + rmtS8 name[256]; + + // Store a unique sample tree for each type + SampleTree* sample_trees[SampleType_Count]; + + // Table of all sample names encountered on this thread + StringTable* names; + + // Next in the global list of active thread samplers + struct ThreadSampler* volatile next; + +} ThreadSampler; + +static rmtError ThreadSampler_Constructor(ThreadSampler* thread_sampler) +{ + rmtError error; + int i; + + assert(thread_sampler != NULL); + + // Set defaults + for (i = 0; i < SampleType_Count; i++) + thread_sampler->sample_trees[i] = NULL; + thread_sampler->names = NULL; + thread_sampler->next = NULL; + + // Set the initial name to Thread0 etc. or use the existing Linux name. + thread_sampler->name[0] = 0; + #if defined(RMT_PLATFORM_LINUX) && RMT_USE_POSIX_THREADNAMES && !defined(__FreeBSD__) + prctl(PR_GET_NAME,thread_sampler->name,0,0,0); + #else + { + static rmtS32 countThreads = 0; + strncat_s(thread_sampler->name, sizeof(thread_sampler->name), "Thread", 6); + itoahex_s(thread_sampler->name + 6, sizeof(thread_sampler->name) - 6, AtomicAdd(&countThreads, 1)); + } + #endif + + // Create the CPU sample tree only - the rest are created on-demand as they need + // extra context information to function correctly. + New_3(SampleTree, thread_sampler->sample_trees[SampleType_CPU], sizeof(Sample), (ObjConstructor)Sample_Constructor, (ObjDestructor)Sample_Destructor); + if (error != RMT_ERROR_NONE) + return error; + + // Create sample name string table + New_0(StringTable, thread_sampler->names); + if (error != RMT_ERROR_NONE) + return error; + + return RMT_ERROR_NONE; +} + + +static void ThreadSampler_Destructor(ThreadSampler* ts) +{ + int i; + + assert(ts != NULL); + + Delete(StringTable, ts->names); + + for (i = 0; i < SampleType_Count; i++) + Delete(SampleTree, ts->sample_trees[i]); +} + + +static rmtError ThreadSampler_Push(SampleTree* tree, rmtU32 name_hash, rmtU32 flags, Sample** sample) +{ + return SampleTree_Push(tree, name_hash, flags, sample); +} + + +static rmtBool ThreadSampler_Pop(ThreadSampler* ts, rmtMessageQueue* queue, Sample* sample) +{ + SampleTree* tree = ts->sample_trees[sample->type]; + SampleTree_Pop(tree, sample); + + // Are we back at the root? + if (tree->current_parent == tree->root) + { + // Disconnect all samples from the root and pack in the chosen message queue + Sample* root = tree->root; + root->first_child = NULL; + root->last_child = NULL; + root->nb_children = 0; + AddSampleTreeMessage(queue, sample, tree->allocator, ts->name, ts); + + return RMT_TRUE; + } + + return RMT_FALSE; +} + + +static rmtU32 ThreadSampler_GetNameHash(ThreadSampler* ts, rmtPStr name, rmtU32* hash_cache) +{ + rmtU32 name_hash = 0; + + // Hash cache provided? + if (hash_cache != NULL) + { + // Calculate the hash first time round only + if (*hash_cache == 0) + { + assert(name != NULL); + *hash_cache = MurmurHash3_x86_32(name, (int)strnlen_s(name, 256), 0); + + // Also add to the string table on its first encounter + StringTable_Insert(ts->names, *hash_cache, name); + } + + return *hash_cache; + } + + // Have to recalculate and speculatively insert the name every time when no cache storage exists + name_hash = MurmurHash3_x86_32(name, (int)strnlen_s(name, 256), 0); + StringTable_Insert(ts->names, name_hash, name); + return name_hash; +} + + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @REMOTERY: Remotery +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#if RMT_USE_D3D11 +typedef struct D3D11 D3D11; +static rmtError D3D11_Create(D3D11** d3d11); +static void D3D11_Destructor(D3D11* d3d11); +#endif + + +#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; + + rmtTLS thread_sampler_tls_handle; + + // Linked list of all known threads being sampled + ThreadSampler* volatile first_thread_sampler; + + // Queue between clients and main remotery thread + rmtMessageQueue* mq_to_rmt_thread; + + // The main server thread + rmtThread* thread; + +#if RMT_USE_CUDA + rmtCUDABind cuda; +#endif + +#if RMT_USE_D3D11 + D3D11* d3d11; +#endif + +#if RMT_USE_OPENGL + OpenGL* opengl; +#endif + +#if RMT_USE_METAL + Metal* metal; +#endif +}; + + +// +// Global remotery context +// +static Remotery* g_Remotery = NULL; + + +// +// This flag marks the EXE/DLL that created the global remotery instance. We want to allow +// only the creating EXE/DLL to destroy the remotery instance. +// +static rmtBool g_RemoteryCreated = RMT_FALSE; + + +static void Remotery_DestroyThreadSamplers(Remotery* rmt); + + +static const rmtU8 g_DecimalToHex[17] = "0123456789abcdef"; + + +static void GetSampleDigest(Sample* sample, rmtU32* digest_hash, rmtU32* nb_samples) +{ + Sample* child; + + assert(sample != NULL); + assert(digest_hash != NULL); + assert(nb_samples != NULL); + + // Concatenate this sample + (*nb_samples)++; + *digest_hash = 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) +{ + Buffer* bin_buf; + + 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); + + return Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 20); +} + + +static rmtError bin_SampleTree(Buffer* buffer, Msg_SampleTree* msg) +{ + Sample* root_sample; + char thread_name[256]; + rmtU32 digest_hash = 0, nb_samples = 0; + rmtError error; + + assert(buffer != NULL); + assert(msg != NULL); + + // Get the message root sample + root_sample = msg->root_sample; + assert(root_sample != NULL); + + // Add any sample types as a thread name post-fix to ensure they get their own viewer + thread_name[0] = 0; + strncat_s(thread_name, sizeof(thread_name), msg->thread_name, strnlen_s(msg->thread_name, 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)); + + // 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) +{ + Msg_SampleTree* sample_tree; + rmtError error = RMT_ERROR_NONE; + 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->root_sample; + 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) + { + AddSampleTreeMessage(rmt->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler); + return RMT_ERROR_NONE; + } + + // Retrieve timing of all CUDA samples + rmt_BeginCPUSample(GetCUDASampleTimes, 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 and send to the viewer with a reasonably long timeout as the size + // of the sample data may be large + rmt_BeginCPUSample(bin_SampleTree, RMTSF_Aggregate); + error = bin_SampleTree(bin_buf, sample_tree); + rmt_EndCPUSample(); + if (error == RMT_ERROR_NONE) + { + rmt_BeginCPUSample(Server_Send, RMTSF_Aggregate); + error = Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 50000); + rmt_EndCPUSample(); + } + + // Release the sample tree back to its allocator + FreeSampleTree(sample, sample_tree->allocator); + + return error; +} + + +static rmtError Remotery_ConsumeMessageQueue(Remotery* rmt) +{ + rmtU32 nb_messages_sent = 0; + const rmtU32 maxNbMessagesPerUpdate = g_Settings.maxNbMessagesPerUpdate; + + assert(rmt != NULL); + + // Absorb as many messages in the queue while disconnected + if (Server_IsClientConnected(rmt->server) == RMT_FALSE) + return RMT_ERROR_NONE; + + // Loop reading the max number of messages for this update + while( nb_messages_sent++ < maxNbMessagesPerUpdate ) + { + rmtError error = RMT_ERROR_NONE; + Message* message = 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_LogText: + error = Remotery_SendLogTextMessage(rmt, message); + break; + case MsgID_SampleTree: + rmt_BeginCPUSample(SendSampleTreeMessage, RMTSF_Aggregate); + error = Remotery_SendSampleTreeMessage(rmt, message); + rmt_EndCPUSample(); + 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_LogText: + break; + + // Release all samples back to their allocators + case MsgID_SampleTree: + { + Msg_SampleTree* sample_tree = (Msg_SampleTree*)message->payload; + FreeSampleTree(sample_tree->root_sample, sample_tree->allocator); + break; + } + } + + rmtMessageQueue_ConsumeNextMessage(rmt->mq_to_rmt_thread, message); + } +} + + +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(); + + // + // [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'): + { + ThreadSampler* ts; + + // 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 all threads for a matching string hash + for (ts = rmt->first_thread_sampler; ts != NULL; ts = ts->next) + { + rmtPStr name = StringTable_Find(ts->names, name_hash); + if (name != NULL) + { + rmtU32 name_length; + + // Construct a response message containing the matching name + Buffer* bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + Buffer_Write(bin_buf, "SSMP", 4); + Buffer_WriteU32(bin_buf, name_hash); + name_length = (rmtU32)strnlen_s(name, 256 - 12); + Buffer_WriteU32(bin_buf, name_length); + Buffer_Write(bin_buf, (void*)name, 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->thread_sampler_tls_handle = TLS_INVALID_HANDLE; + rmt->first_thread_sampler = NULL; + rmt->mq_to_rmt_thread = NULL; + rmt->thread = 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_D3D11 + rmt->d3d11 = 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); + + // Allocate a TLS handle for the thread sampler + error = tlsAlloc(&rmt->thread_sampler_tls_handle); + if (error != RMT_ERROR_NONE) + return error; + + // Create the server + New_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; + + #if RMT_USE_D3D11 + error = D3D11_Create(&rmt->d3d11); + if (error != RMT_ERROR_NONE) + return error; + #endif + + #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 + + + // Set as the global instance before creating any threads that uses it for sampling itself + assert(g_Remotery == NULL); + g_Remotery = rmt; + g_RemoteryCreated = RMT_TRUE; + + // Ensure global instance writes complete before other threads get a chance to use it + WriteFence(); + + // Create the main update thread once everything has been defined for the global remotery object + New_2(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; + } + + #if RMT_USE_D3D11 + Delete(D3D11, rmt->d3d11); + #endif + + #if RMT_USE_OPENGL + Delete(OpenGL, rmt->opengl); + #endif + + #if RMT_USE_METAL + Delete(Metal, rmt->metal); + #endif + + Delete(rmtMessageQueue, rmt->mq_to_rmt_thread); + + Remotery_DestroyThreadSamplers(rmt); + + Delete(Server, rmt->server); + + if (rmt->thread_sampler_tls_handle != TLS_INVALID_HANDLE) + { + tlsFree(rmt->thread_sampler_tls_handle); + rmt->thread_sampler_tls_handle = 0; + } +} + + +static rmtError Remotery_GetThreadSampler(Remotery* rmt, ThreadSampler** thread_sampler) +{ + ThreadSampler* ts; + + // Is there a thread sampler associated with this thread yet? + assert(rmt != NULL); + ts = (ThreadSampler*)tlsGet(rmt->thread_sampler_tls_handle); + if (ts == NULL) + { + // Allocate on-demand + rmtError error; + New_0(ThreadSampler, *thread_sampler); + if (error != RMT_ERROR_NONE) + return error; + ts = *thread_sampler; + + // Add to the beginning of the global linked list of thread samplers + for (;;) + { + ThreadSampler* old_ts = rmt->first_thread_sampler; + ts->next = old_ts; + + // If the old value is what we expect it to be then no other thread has + // changed it since this thread sampler was used as a candidate first list item + if (AtomicCompareAndSwapPointer((long* volatile*)&rmt->first_thread_sampler, (long*)old_ts, (long*)ts) == RMT_TRUE) + break; + } + + tlsSet(rmt->thread_sampler_tls_handle, ts); + } + + assert(thread_sampler != NULL); + *thread_sampler = ts; + return RMT_ERROR_NONE; +} + +static void Remotery_DestroyThreadSamplers(Remotery* rmt) +{ + // If the handle failed to create in the first place then it shouldn't be possible to create thread samplers + assert(rmt != NULL); + if (rmt->thread_sampler_tls_handle == TLS_INVALID_HANDLE) + { + assert(rmt->first_thread_sampler == NULL); + return; + } + + // Keep popping thread samplers off the linked list until they're all gone + // This does not make any assumptions, making it possible for thread samplers to be created while they're all + // deleted. While this is erroneous calling code, this will prevent a confusing crash. + while (rmt->first_thread_sampler != NULL) + { + ThreadSampler* ts; + + for (;;) + { + ThreadSampler* old_ts = rmt->first_thread_sampler; + ThreadSampler* next_ts = old_ts->next; + + if (AtomicCompareAndSwapPointer((long* volatile*)&rmt->first_thread_sampler, (long*)old_ts, (long*)next_ts) == RMT_TRUE) + { + ts = old_ts; + break; + } + } + + Delete(ThreadSampler, ts); + } +} + + +static void* CRTMalloc(void* mm_context, rmtU32 size) +{ + 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.msSleepBetweenServerUpdates = 10; + g_Settings.messageQueueSizeInBytes = 128 * 1024; + g_Settings.maxNbMessagesPerUpdate = 10; + g_Settings.malloc = CRTMalloc; + g_Settings.free = CRTFree; + g_Settings.realloc = CRTRealloc; + g_Settings.input_handler = NULL; + g_Settings.input_handler_context = NULL; + g_Settings.logFilename = "rmtLog.txt"; + + g_SettingsInitialized = RMT_TRUE; + } + + return &g_Settings; +} + + +RMT_API rmtError _rmt_CreateGlobalInstance(Remotery** remotery) +{ + rmtError error; + + // Default-initialise if user has not set values + rmt_Settings(); + + // Creating the Remotery instance also records it as the global instance + assert(remotery != NULL); + New_0(Remotery, *remotery); + return error; +} + + +RMT_API void _rmt_DestroyGlobalInstance(Remotery* remotery) +{ + // 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 + +static void SetDebuggerThreadName(const char* name) +{ + #ifdef RMT_PLATFORM_WINDOWS + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = name; + info.dwThreadID = (DWORD)-1; + info.dwFlags = 0; + + #ifndef __MINGW32__ + __try + { + RaiseException(0x406D1388, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } + __except(1 /* EXCEPTION_EXECUTE_HANDLER */) + { + } + #endif + #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); + #ifdef __FreeBSD__ + 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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + // Get data for this thread + if (Remotery_GetThreadSampler(g_Remotery, &ts) != RMT_ERROR_NONE) + return; + + // Copy name and apply to the debugger + strcpy_s(ts->name, sizeof(ts->name), thread_name); + SetDebuggerThreadName(thread_name); +} + + +static rmtBool QueueLine(rmtMessageQueue* queue, unsigned char* text, rmtU32 size, struct ThreadSampler* thread_sampler) +{ + 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_sampler); + 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 }; + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + Remotery_GetThreadSampler(g_Remotery, &ts); + + // 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, ts) == 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, ts); + } +} + + +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. + + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + // TODO: Time how long the bits outside here cost and subtract them from the parent + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadSampler_GetNameHash(ts, name, hash_cache); + if (ThreadSampler_Push(ts->sample_trees[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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + Sample* sample = ts->sample_trees[SampleType_CPU]->current_parent; + + if (sample->recurse_depth > 0) + { + sample->recurse_depth--; + } + else + { + rmtU64 us_end = usTimer_Get(&g_Remotery->timer); + + // 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; + + ThreadSampler_Pop(ts, g_Remotery->mq_to_rmt_thread, sample); + } + } +} + +#if RMT_USE_OPENGL || RMT_USE_D3D11 +static void Remotery_BlockingDeleteSampleTree(Remotery* rmt, enum SampleType sample_type) +{ + ThreadSampler* ts; + + // Get the attached thread sampler + assert(rmt != NULL); + if (Remotery_GetThreadSampler(rmt, &ts) == RMT_ERROR_NONE) + { + SampleTree* sample_tree = ts->sample_trees[sample_type]; + if (sample_tree != NULL) + { + // Wait around until the Remotery server thread has sent all sample trees + // of this type to the client + while (sample_tree->allocator->nb_inuse > 1) + msSleep(1); + + // Now free to delete + Delete(SampleTree, sample_tree); + ts->sample_trees[sample_type] = NULL; + } + } +} + +static rmtBool rmtMessageQueue_IsEmpty(rmtMessageQueue* queue) +{ + assert(queue != NULL); + return queue->write_pos - queue->read_pos == 0; +} + +#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->base.size_bytes = sizeof(CUDASample); + sample->event_start = NULL; + sample->event_end = NULL; + + // Create non-blocking events with timing + assert(g_Remotery != NULL); + error = CUDAEventCreate(&sample->event_start, CU_EVENT_DEFAULT); + if (error == RMT_ERROR_NONE) + error = CUDAEventCreate(&sample->event_end, CU_EVENT_DEFAULT); + return error; +} + + +static void CUDASample_Destructor(CUDASample* sample) +{ + assert(sample != NULL); + + // Destroy events + if (sample->event_start != NULL) + CUDAEventDestroy(sample->event_start); + if (sample->event_end != NULL) + CUDAEventDestroy(sample->event_end); + + Sample_Destructor((Sample*)sample); +} + + +static rmtBool AreCUDASamplesReady(Sample* sample) +{ + rmtError error; + Sample* child; + + CUDASample* cuda_sample = (CUDASample*)sample; + assert(sample->type == SampleType_CUDA); + + // Check to see if both of the CUDA events have been processed + error = CUDAEventQuery(cuda_sample->event_start); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + error = CUDAEventQuery(cuda_sample->event_end); + if (error != RMT_ERROR_NONE) + return RMT_FALSE; + + // Check child sample events + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!AreCUDASamplesReady(child)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + + +static rmtBool GetCUDASampleTimes(Sample* root_sample, Sample* sample) +{ + Sample* child; + + CUDASample* cuda_root_sample = (CUDASample*)root_sample; + CUDASample* cuda_sample = (CUDASample*)sample; + + float ms_start, ms_end; + + assert(root_sample != NULL); + assert(sample != NULL); + + // Get millisecond timing of each sample event, relative to initial root sample + if (CUDAEventElapsedTime(&ms_start, cuda_root_sample->event_start, cuda_sample->event_start) != RMT_ERROR_NONE) + return RMT_FALSE; + if (CUDAEventElapsedTime(&ms_end, cuda_root_sample->event_start, cuda_sample->event_end) != RMT_ERROR_NONE) + return RMT_FALSE; + + // Convert to microseconds and add to the sample + sample->us_start = (rmtU64)(ms_start * 1000); + sample->us_end = (rmtU64)(ms_end * 1000); + 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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + rmtError error; + Sample* sample; + rmtU32 name_hash = ThreadSampler_GetNameHash(ts, name, hash_cache); + + // Create the CUDA tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a CUDA binding is not yet available. + SampleTree** cuda_tree = &ts->sample_trees[SampleType_CUDA]; + if (*cuda_tree == NULL) + { + CUDASample* root_sample; + + New_3(SampleTree, *cuda_tree, sizeof(CUDASample), (ObjConstructor)CUDASample_Constructor, (ObjDestructor)CUDASample_Destructor); + if (error != RMT_ERROR_NONE) + return; + + // Record an event once on the root sample, used to measure absolute sample + // times since this point + root_sample = (CUDASample*)(*cuda_tree)->root; + error = CUDAEventRecord(root_sample->event_start, stream); + if (error != RMT_ERROR_NONE) + return; + } + + // Push the same and record its event + if (ThreadSampler_Push(*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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + CUDASample* sample = (CUDASample*)ts->sample_trees[SampleType_CUDA]->current_parent; + if (sample->base.recurse_depth > 0) + { + sample->base.recurse_depth--; + } + else + { + CUDAEventRecord(sample->event_end, stream); + ThreadSampler_Pop(ts, g_Remotery->mq_to_rmt_thread, (Sample*)sample); + } + } +} + + +#endif // RMT_USE_CUDA + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ + @D3D11: Direct3D 11 event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#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; +} 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; + + New_1(rmtMessageQueue, (*d3d11)->mq_to_d3d11_main, g_Settings.messageQueueSizeInBytes); + if (error != RMT_ERROR_NONE) + { + Delete(D3D11, *d3d11); + return error; + } + + return RMT_ERROR_NONE; +} + + +static void D3D11_Destructor(D3D11* d3d11) +{ + assert(d3d11 != NULL); + Delete(rmtMessageQueue, d3d11->mq_to_d3d11_main); +} + +static HRESULT rmtD3D11Finish(rmtU64 *out_timestamp, double *out_frequency) +{ + HRESULT result; + ID3D11Device* device = g_Remotery->d3d11->device; + ID3D11DeviceContext* context = g_Remotery->d3d11->context; + 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(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(&gpu_base, &frequency); + if (result != S_OK && result != S_FALSE) + return result; + + for (i=0; itimer); + result = rmtD3D11Finish(&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) +{ + D3D11_QUERY_DESC timestamp_desc; + D3D11_QUERY_DESC disjoint_desc; + ID3D11Device* device; + HRESULT* last_error; + + assert(stamp != NULL); + + ObjectLink_Constructor((ObjectLink*)stamp); + + // Set defaults + stamp->query_start = NULL; + stamp->query_end = NULL; + stamp->query_disjoint = NULL; + stamp->cpu_timestamp = 0; + + assert(g_Remotery != NULL); + assert(g_Remotery->d3d11 != NULL); + device = g_Remotery->d3d11->device; + last_error = &g_Remotery->d3d11->last_error; + + // Create start/end timestamp queries + timestamp_desc.Query = D3D11_QUERY_TIMESTAMP; + timestamp_desc.MiscFlags = 0; + *last_error = ID3D11Device_CreateQuery(device, ×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, 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(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(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; + sample->base.size_bytes = sizeof(D3D11Sample); + 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) + { + assert(g_Remotery->d3d11 != NULL); + + assert(device != NULL); + g_Remotery->d3d11->device = (ID3D11Device*)device; + assert(context != NULL); + g_Remotery->d3d11->context = (ID3D11DeviceContext*)context; + } +} + + +static void UpdateD3D11Frame(void); + + +RMT_API void _rmt_UnbindD3D11(void) +{ + if (g_Remotery != NULL) + { + D3D11* d3d11 = g_Remotery->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(); + + // 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_BlockingDeleteSampleTree(g_Remotery, SampleType_D3D11); + } +} + + +RMT_API void _rmt_BeginD3D11Sample(rmtPStr name, rmtU32* hash_cache) +{ + ThreadSampler* ts; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + // Has D3D11 been unbound? + d3d11 = g_Remotery->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadSampler_GetNameHash(ts, name, hash_cache); + + // Create the D3D11 tree on-demand as the tree needs an up-front-created root. + // This is not possible to create on initialisation as a D3D11 binding is not yet available. + SampleTree** d3d_tree = &ts->sample_trees[SampleType_D3D11]; + if (*d3d_tree == NULL) + { + 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 (ThreadSampler_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, 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 = g_Remotery->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(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->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; + } + + // Get child sample times + for (child = sample->first_child; child != NULL; child = child->next_sibling) + { + if (!GetD3D11SampleTimes(child, out_first_timestamp, out_last_resync)) + return RMT_FALSE; + } + + return RMT_TRUE; +} + + +static void UpdateD3D11Frame(void) +{ + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + d3d11 = g_Remotery->d3d11; + assert(d3d11 != NULL); + + rmt_BeginCPUSample(rmt_UpdateD3D11Frame, 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->root_sample; + assert(sample->type == SampleType_D3D11); + + // Retrieve timing of all D3D11 samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetD3D11SampleTimes(sample, &d3d11->first_timestamp, &d3d11->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + AddSampleTreeMessage(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler); + rmtMessageQueue_ConsumeNextMessage(d3d11->mq_to_d3d11_main, message); + } + + rmt_EndCPUSample(); +} + + +RMT_API void _rmt_EndD3D11Sample(void) +{ + ThreadSampler* ts; + D3D11* d3d11; + + if (g_Remotery == NULL) + return; + + // Has D3D11 been unbound? + d3d11 = g_Remotery->d3d11; + assert(d3d11 != NULL); + if (d3d11->device == NULL || d3d11->context == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + // Close the timestamp + D3D11Sample* d3d_sample = (D3D11Sample*)ts->sample_trees[SampleType_D3D11]->current_parent; + if (d3d_sample->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 (ThreadSampler_Pop(ts, d3d11->mq_to_d3d11_main, (Sample*)d3d_sample)) + // Perform ready-polling on popping of the root sample + UpdateD3D11Frame(); + } + } +} + + +#endif // RMT_USE_D3D11 + + + +/* +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +@OpenGL: OpenGL event sampling +------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------ +*/ + + + +#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; +}; + + +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; + + 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); +} + +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; itimer); + 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; + sample->base.size_bytes = sizeof(OpenGLSample); + 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(); + + // Forcefully delete sample tree on this thread to release time stamps from + // the same thread that created them + Remotery_BlockingDeleteSampleTree(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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadSampler_GetNameHash(ts, 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 = &ts->sample_trees[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 (ThreadSampler_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->root_sample; + assert(sample->type == SampleType_OpenGL); + + // Retrieve timing of all OpenGL samples + // If they aren't ready leave the message unconsumed, holding up later frames and maintaining order + if (!GetOpenGLSampleTimes(sample, &opengl->first_timestamp,&opengl->last_resync)) + break; + + // Pass samples onto the remotery thread for sending to the viewer + AddSampleTreeMessage(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler); + rmtMessageQueue_ConsumeNextMessage(opengl->mq_to_opengl_main, message); + } + + rmt_EndCPUSample(); +} + + +RMT_API void _rmt_EndOpenGLSample(void) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + // Close the timestamp + OpenGLSample* ogl_sample = (OpenGLSample*)ts->sample_trees[SampleType_OpenGL]->current_parent; + if (ogl_sample->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 (ThreadSampler_Pop(ts, g_Remotery->opengl->mq_to_opengl_main, (Sample*)ogl_sample)) + // Perform ready-polling on popping of the root sample + UpdateOpenGLFrame(); + } + } +} + + + +#endif // RMT_USE_OPENGL + + + +/* + ------------------------------------------------------------------------------------------------------------------------ + ------------------------------------------------------------------------------------------------------------------------ + @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; + sample->base.size_bytes = sizeof(MetalSample); + 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) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + Sample* sample; + rmtU32 name_hash = ThreadSampler_GetNameHash(ts, 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 = &ts->sample_trees[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 (ThreadSampler_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->root_sample; + 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 + AddSampleTreeMessage(g_Remotery->mq_to_rmt_thread, sample, sample_tree->allocator, sample_tree->thread_name, message->thread_sampler); + rmtMessageQueue_ConsumeNextMessage(metal->mq_to_metal_main, message); + } + + rmt_EndCPUSample(); +} + + +RMT_API void _rmt_EndMetalSample(void) +{ + ThreadSampler* ts; + + if (g_Remotery == NULL) + return; + + if (Remotery_GetThreadSampler(g_Remotery, &ts) == RMT_ERROR_NONE) + { + // Close the timestamp + MetalSample* metal_sample = (MetalSample*)ts->sample_trees[SampleType_Metal]->current_parent; + 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 (ThreadSampler_Pop(ts, 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/gazebo/common/Remotery/lib/Remotery.h b/gazebo/common/Remotery/lib/Remotery.h new file mode 100644 index 0000000000..f79ebfdf63 --- /dev/null +++ b/gazebo/common/Remotery/lib/Remotery.h @@ -0,0 +1,659 @@ + + +/* +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__) + #define RMT_PLATFORM_LINUX + #define RMT_PLATFORM_POSIX +#elif defined(__APPLE__) + #define RMT_PLATFORM_MACOS + #define RMT_PLATFORM_POSIX +#endif + +#ifdef RMT_DLL + #if defined (RMT_PLATFORM_WINDOWS) + #if defined (RMT_IMPL) + #define RMT_API __declspec(dllexport) + #else + #define RMT_API __declspec(dllimport) + #endif + #elif defined (RMT_PLATFORM_POSIX) + #if defined (RMT_IMPL) + #define RMT_API __attribute__((visibility("default"))) + #else + #define RMT_API + #endif + #endif +#else + #define RMT_API +#endif + +// Allows macros to be written that can work around the inability to do: #define(x) #ifdef x +// with the C preprocessor. +#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 +typedef enum rmtError +{ + RMT_ERROR_NONE, + RMT_ERROR_RECURSIVE_SAMPLE, // Not an error but an internal message to calling code + + // System errors + RMT_ERROR_MALLOC_FAIL, // Malloc call within remotery failed + RMT_ERROR_TLS_ALLOC_FAIL, // Attempt to allocate thread local storage failed + RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL, // Failed to create a virtual memory mirror buffer + RMT_ERROR_CREATE_THREAD_FAIL, // Failed to create a thread for the server + + // Network TCP/IP socket errors + RMT_ERROR_SOCKET_INIT_NETWORK_FAIL, // Network initialisation failure (e.g. on Win32, WSAStartup fails) + RMT_ERROR_SOCKET_CREATE_FAIL, // Can't create a socket for connection to the remote viewer + RMT_ERROR_SOCKET_BIND_FAIL, // Can't bind a socket for the server + RMT_ERROR_SOCKET_LISTEN_FAIL, // Created server socket failed to enter a listen state + RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL, // Created server socket failed to switch to a non-blocking state + RMT_ERROR_SOCKET_INVALID_POLL, // Poll attempt on an invalid socket + RMT_ERROR_SOCKET_SELECT_FAIL, // Server failed to call select on socket + RMT_ERROR_SOCKET_POLL_ERRORS, // Poll notified that the socket has errors + RMT_ERROR_SOCKET_ACCEPT_FAIL, // Server failed to accept connection from client + RMT_ERROR_SOCKET_SEND_TIMEOUT, // Timed out trying to send data + RMT_ERROR_SOCKET_SEND_FAIL, // Unrecoverable error occured while client/server tried to send data + RMT_ERROR_SOCKET_RECV_NO_DATA, // No data available when attempting a receive + RMT_ERROR_SOCKET_RECV_TIMEOUT, // Timed out trying to receive data + RMT_ERROR_SOCKET_RECV_FAILED, // Unrecoverable error occured while client/server tried to receive data + + // WebSocket errors + RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET, // WebSocket server handshake failed, not HTTP GET + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION, // WebSocket server handshake failed, can't locate WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION, // WebSocket server handshake failed, unsupported WebSocket version + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST, // WebSocket server handshake failed, can't locate host + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST, // WebSocket server handshake failed, host is not allowed to connect + RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY, // WebSocket server handshake failed, can't locate WebSocket key + RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY, // WebSocket server handshake failed, WebSocket key is ill-formed + RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL, // WebSocket server handshake failed, internal error, bad string code + RMT_ERROR_WEBSOCKET_DISCONNECTED, // WebSocket server received a disconnect request and closed the socket + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER, // Couldn't parse WebSocket frame header + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE, // Partially received wide frame header size + RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_MASK, // Partially received frame header data mask + RMT_ERROR_WEBSOCKET_RECEIVE_TIMEOUT, // Timeout receiving frame header + + RMT_ERROR_REMOTERY_NOT_CREATED, // Remotery object has not been created + RMT_ERROR_SEND_ON_INCOMPLETE_PROFILE, // An attempt was made to send an incomplete profile tree to the client + + // CUDA error messages + RMT_ERROR_CUDA_DEINITIALIZED, // This indicates that the CUDA driver is in the process of shutting down + RMT_ERROR_CUDA_NOT_INITIALIZED, // This indicates that the CUDA driver has not been initialized with cuInit() or that initialization has failed + RMT_ERROR_CUDA_INVALID_CONTEXT, // This most frequently indicates that there is no context bound to the current thread + RMT_ERROR_CUDA_INVALID_VALUE, // This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values + RMT_ERROR_CUDA_INVALID_HANDLE, // This indicates that a resource handle passed to the API call was not valid + RMT_ERROR_CUDA_OUT_OF_MEMORY, // The API call failed because it was unable to allocate enough memory to perform the requested operation + RMT_ERROR_ERROR_NOT_READY, // This indicates that a resource handle passed to the API call was not valid + + // Direct3D 11 error messages + RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY, // Failed to create query for sample + + // OpenGL error messages + RMT_ERROR_OPENGL_ERROR, // Generic OpenGL error, no need to expose detail since app will need an OpenGL error callback registered + + RMT_ERROR_CUDA_UNKNOWN, +} rmtError; + + +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, +} 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; + + // How long to sleep between server updates, hopefully trying to give + // a little CPU back to other threads. + rmtU32 msSleepBetweenServerUpdates; + + // Size of the internal message queues Remotery uses + // Will be rounded to page granularity of 64k + rmtU32 messageQueueSizeInBytes; + + // If the user continuously pushes to the message queue, the server network + // code won't get a chance to update unless there's an upper-limit on how + // many messages can be consumed per loop. + rmtU32 maxNbMessagesPerUpdate; + + // Callback pointers for memory allocation + rmtMallocPtr malloc; + rmtReallocPtr realloc; + rmtFreePtr free; + void* mm_context; + + // Callback pointer for receiving input from the Remotery console + rmtInputHandlerPtr input_handler; + + // Context pointer that gets sent to Remotery console callback function + void* input_handler_context; + + rmtPStr logFilename; +} rmtSettings; + + +// Structure to fill in when binding CUDA to Remotery +typedef struct rmtCUDABind +{ + // The main context that all driver functions apply before each call + void* context; + + // Driver API function pointers that need to be pointed to + // Untyped so that the CUDA headers are not required in this file + // NOTE: These are named differently to the CUDA functions because the CUDA API has a habit of using + // macros to point function calls to different versions, e.g. cuEventDestroy is a macro for + // cuEventDestroy_v2. + void* CtxSetCurrent; + void* CtxGetCurrent; + void* EventCreate; + void* EventDestroy; + void* EventRecord; + void* EventQuery; + void* EventElapsedTime; + +} rmtCUDABind; + + +// Call once after you've initialised CUDA to bind it to Remotery +#define rmt_BindCUDA(bind) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_BindCUDA(bind)) + +// Mark the beginning of a CUDA sample on the specified asynchronous stream +#define rmt_BeginCUDASample(name, stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginCUDASample(#name, &rmt_sample_hash_##name, stream); \ + }) + +// Mark the end of a CUDA sample on the specified asynchronous stream +#define rmt_EndCUDASample(stream) \ + RMT_OPTIONAL(RMT_USE_CUDA, _rmt_EndCUDASample(stream)) + + +#define rmt_BindD3D11(device, context) \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_BindD3D11(device, context)) + +#define rmt_UnbindD3D11() \ + RMT_OPTIONAL(RMT_USE_D3D11, _rmt_UnbindD3D11()) + +#define rmt_BeginD3D11Sample(name) \ + RMT_OPTIONAL(RMT_USE_D3D11, { \ + static rmtU32 rmt_sample_hash_##name = 0; \ + _rmt_BeginD3D11Sample(#name, &rmt_sample_hash_##name); \ + }) + +#define rmt_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/gazebo/common/Remotery/lib/RemoteryMetal.mm b/gazebo/common/Remotery/lib/RemoteryMetal.mm new file mode 100644 index 0000000000..bb69da9ae1 --- /dev/null +++ b/gazebo/common/Remotery/lib/RemoteryMetal.mm @@ -0,0 +1,59 @@ +// +// Copyright 2014-2018 Celtoys Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include +#include +#include + +#import + +// Store command buffer in thread-local so that each thread can point to its own +static void SetCommandBuffer(id command_buffer) +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + thread_data[@"rmtMTLCommandBuffer"] = command_buffer; +} + +static id GetCommandBuffer() +{ + NSMutableDictionary* thread_data = [[NSThread currentThread] threadDictionary]; + return thread_data[@"rmtMTLCommandBuffer"]; +} + +extern "C" void _rmt_BindMetal(id command_buffer) +{ + SetCommandBuffer(command_buffer); +} + +extern "C" void _rmt_UnbindMetal() +{ + SetCommandBuffer(0); +} + +// Needs to be in the same lib for this to work +extern "C" unsigned long long rmtMetal_usGetTime(); + +static void SetTimestamp(void* data) +{ + *((unsigned long long*)data) = rmtMetal_usGetTime(); +} + +extern "C" void rmtMetal_MeasureCommandBuffer(unsigned long long* out_start, unsigned long long* out_end, unsigned int* out_ready) +{ + id command_buffer = GetCommandBuffer(); + [command_buffer addScheduledHandler:^(id ){ SetTimestamp(out_start); }]; + [command_buffer addCompletedHandler:^(id ){ SetTimestamp(out_end); *out_ready = 1; }]; +} diff --git a/gazebo/common/Remotery/readme.md b/gazebo/common/Remotery/readme.md new file mode 100644 index 0000000000..405c4851b7 --- /dev/null +++ b/gazebo/common/Remotery/readme.md @@ -0,0 +1,237 @@ +Remotery +-------- + +[![Build Status](https://travis-ci.org/Celtoys/Remotery.svg?branch=master)](https://travis-ci.org/Celtoys/Remotery) +[![Build status](https://ci.appveyor.com/api/projects/status/d1o8620mws9ihbsd?svg=true)](https://ci.appveyor.com/project/Celtoys/remotery) + +A realtime CPU/GPU profiler hosted in a single C file with a viewer that runs in a web browser. + +![screenshot](screenshot.png?raw=true) + +Supported Platforms: + +* Windows +* Linux +* OSX +* iOS +* Android +* XBox One +* FreeBSD + +Supported GPU Profiling APIS: + +* D3D 11 +* OpenGL +* CUDA +* Metal + +Features: + +* Lightweight instrumentation of multiple threads running on the CPU. +* Web viewer that runs in Chrome, Firefox and Safari. Custom WebSockets server + transmits sample data to the browser on a latent thread. +* Profiles itself and shows how it's performing in the viewer. +* Console output for logging text. +* Console input for sending commands to your game. + + +Compiling +--------- + +* Windows (MSVC) - add lib/Remotery.c and lib/Remotery.h to your program. Set include + directories to add Remotery/lib path. The required library ws2_32.lib should be picked + up through the use of the #pragma comment(lib, "ws2_32.lib") directive in Remotery.c. + +* Mac OS X (XCode) - simply add lib/Remotery.c, lib/Remotery.h and lib/Remotery.mm to your program. + +* Linux (GCC) - add the source in lib folder. Compilation of the code requires -pthreads for + library linkage. For example to compile the same run: cc lib/Remotery.c sample/sample.c + -I lib -pthread -lm + +You can define some extra macros to modify what features are compiled into Remotery: + + Macro Default Description + + RMT_ENABLED 1 Disable this to not include any bits of Remotery in your build + RMT_USE_TINYCRT 0 Used by the Celtoys TinyCRT library (not released yet) + RMT_USE_CUDA 0 Assuming CUDA headers/libs are setup, allow CUDA profiling + RMT_USE_D3D11 0 Assuming Direct3D 11 headers/libs are setup, allow D3D11 GPU profiling + RMT_USE_OPENGL 0 Allow OpenGL GPU profiling (dynamically links OpenGL libraries on available platforms) + RMT_USE_METAL 0 Allow Metal profiling of command buffers + + +Basic Use +--------- + +See the sample directory for further examples. A quick example: + + int main() + { + // Create the main instance of Remotery. + // You need only do this once per program. + Remotery* rmt; + rmt_CreateGlobalInstance(&rmt); + + // Explicit begin/end for C + { + rmt_BeginCPUSample(LogText, 0); + rmt_LogText("Time me, please!"); + rmt_EndCPUSample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedCPUSample(LogText, 0); + rmt_LogText("Time me, too!"); + } + + // Destroy the main instance of Remotery. + rmt_DestroyGlobalInstance(rmt); + } + + +Running the Viewer +------------------ + +Double-click or launch `vis/index.html` from the browser. + + +Sampling CUDA GPU activity +-------------------------- + +Remotery allows for profiling multiple threads of CUDA execution using different asynchronous streams +that must all share the same context. After initialising both Remotery and CUDA you need to bind the +two together using the call: + + rmtCUDABind bind; + bind.context = m_Context; + bind.CtxSetCurrent = &cuCtxSetCurrent; + bind.CtxGetCurrent = &cuCtxGetCurrent; + bind.EventCreate = &cuEventCreate; + bind.EventDestroy = &cuEventDestroy; + bind.EventRecord = &cuEventRecord; + bind.EventQuery = &cuEventQuery; + bind.EventElapsedTime = &cuEventElapsedTime; + rmt_BindCUDA(&bind); + +Explicitly pointing to the CUDA interface allows Remotery to be included anywhere in your project without +need for you to link with the required CUDA libraries. After the bind completes you can safely sample any +CUDA activity: + + CUstream stream; + + // Explicit begin/end for C + { + rmt_BeginCUDASample(UnscopedSample, stream); + // ... CUDA code ... + rmt_EndCUDASample(stream); + } + + // Scoped begin/end for C++ + { + rmt_ScopedCUDASample(ScopedSample, stream); + // ... CUDA code ... + } + +Remotery supports only one context for all threads and will use cuCtxGetCurrent and cuCtxSetCurrent to +ensure the current thread has the context you specify in rmtCUDABind.context. + + +Sampling Direct3D 11 GPU activity +--------------------------------- + +Remotery allows sampling of GPU activity on your main D3D11 context. After initialising Remotery, you need +to bind it to D3D11 with a single call from the thread that owns the device context: + + // Parameters are ID3D11Device* and ID3D11DeviceContext* + rmt_BindD3D11(d3d11_device, d3d11_context); + +Sampling is then a simple case of: + + // Explicit begin/end for C + { + rmt_BeginD3D11Sample(UnscopedSample); + // ... D3D code ... + rmt_EndD3D11Sample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedD3D11Sample(ScopedSample); + // ... D3D code ... + } + +Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown +your D3D11 device and context, ensure you notify Remotery before shutting down Remotery itself: + + rmt_UnbindD3D11(); + + +Sampling OpenGL GPU activity +---------------------------- + +Remotery allows sampling of GPU activity on your main OpenGL context. After initialising Remotery, you need +to bind it to OpenGL with the single call: + + rmt_BindOpenGL(); + +Sampling is then a simple case of: + + // Explicit begin/end for C + { + rmt_BeginOpenGLSample(UnscopedSample); + // ... OpenGL code ... + rmt_EndOpenGLSample(); + } + + // Scoped begin/end for C++ + { + rmt_ScopedOpenGLSample(ScopedSample); + // ... OpenGL code ... + } + +Support for multiple contexts can be added pretty easily if there is demand for the feature. When you shutdown +your OpenGL device and context, ensure you notify Remotery before shutting down Remotery itself: + + rmt_UnbindOpenGL(); + + +Sampling Metal GPU activity +--------------------------- + +Remotery can sample Metal command buffers issued to the GPU from multiple threads. As the Metal API does not +support finer grained profiling, samples will return only the timing of the bound command buffer, irrespective +of how many you issue. As such, make sure you bind and sample the command buffer for each call site: + + rmt_BindMetal(mtl_command_buffer); + rmt_ScopedMetalSample(command_buffer_name); + +The C API supports begin/end also: + + rmt_BindMetal(mtl_command_buffer); + rmt_BeginMetalSample(command_buffer_name); + ... + rmt_EndMetalSample(); + + +Applying Configuration Settings +------------------------------- + +Before creating your Remotery instance, you can configure its behaviour by retrieving its settings object: + + rmtSettings* settings = rmt_Settings(); + +Some important settings are: + + // Redirect any Remotery allocations to your own malloc/free, with an additional context pointer + // that gets passed to your callbacks. + settings->malloc; + settings->free; + settings->mm_context; + + // Specify an input handler that receives text input from the Remotery console, with an additional + // context pointer that gets passed to your callback. + // The handler will be called from the Remotery thread so synchronization with a mutex or atomics + // might be needed to avoid race conditions with your threads. + settings->input_handler; + settings->input_handler_context; diff --git a/gazebo/common/Remotery/upstream-rev b/gazebo/common/Remotery/upstream-rev new file mode 100644 index 0000000000..4f9d35eb24 --- /dev/null +++ b/gazebo/common/Remotery/upstream-rev @@ -0,0 +1 @@ +e3281eebee43f6b41dda30c118bab06d9ce69d09 diff --git a/gazebo/common/Remotery/vis/Code/Console.js b/gazebo/common/Remotery/vis/Code/Console.js new file mode 100644 index 0000000000..e2b0dbd326 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/Console.js @@ -0,0 +1,196 @@ + +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 = 200; + 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)); + } + + + 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); + + // Place controls + var parent_size = this.Window.Size; + var mid_w = parent_size[0] / 3; + this.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30); + this.UserInput.SetSize(parent_size[0] - 100, 18); + var output_height = this.UserInput.Position[1] - 2 * BORDER; + this.PageContainer.SetPosition(BORDER, BORDER); + this.PageContainer.SetSize(mid_w - 2 * BORDER, output_height); + this.AppContainer.SetPosition(mid_w, BORDER); + this.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height); + } + + + function OnLog(self, socket, data_view) + { + var data_view_reader = new DataViewReader(data_view, 4); + var text = data_view_reader.GetString(); + self.AppTextBuffer = LogText(self.AppTextBuffer, text); + 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 = 10 * 1024; + var len = existing_text.length; + if (len > max_len) + existing_text = existing_text.substr(len - max_len, max_len); + + return existing_text; + } + + + function UpdateHTML(self) + { + // Reset the current text buffer as html + + 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/gazebo/common/Remotery/vis/Code/DataViewReader.js b/gazebo/common/Remotery/vis/Code/DataViewReader.js new file mode 100644 index 0000000000..ba3fc8e0da --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/DataViewReader.js @@ -0,0 +1,47 @@ + +// +// 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.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/gazebo/common/Remotery/vis/Code/PixelTimeRange.js b/gazebo/common/Remotery/vis/Code/PixelTimeRange.js new file mode 100644 index 0000000000..14a06683b3 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/PixelTimeRange.js @@ -0,0 +1,61 @@ + + +PixelTimeRange = (function() +{ + function PixelTimeRange(start_us, span_us, span_px) + { + this.Span_px = span_px; + this.Set(start_us, span_us); + } + + + PixelTimeRange.prototype.Set = function(start_us, span_us) + { + this.Start_us = start_us; + this.Span_us = span_us; + this.End_us = this.Start_us + span_us; + this.usPerPixel = this.Span_px / this.Span_us; + } + + + PixelTimeRange.prototype.SetStart = function(start_us) + { + this.Start_us = start_us; + this.End_us = start_us + this.Span_us; + } + + + PixelTimeRange.prototype.SetEnd = function(end_us) + { + this.End_us = end_us; + this.Start_us = end_us - this.Span_us; + } + + + PixelTimeRange.prototype.SetPixelSpan = function(span_px) + { + this.Span_px = span_px; + this.usPerPixel = this.Span_px / this.Span_us; + } + + + PixelTimeRange.prototype.PixelOffset = function(time_us) + { + return Math.floor((time_us - this.Start_us) * this.usPerPixel); + } + + + PixelTimeRange.prototype.PixelSize = function(time_us) + { + return Math.floor(time_us * this.usPerPixel); + } + + + PixelTimeRange.prototype.Clone = function() + { + return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px); + } + + + return PixelTimeRange; +})(); diff --git a/gazebo/common/Remotery/vis/Code/Remotery.js b/gazebo/common/Remotery/vis/Code/Remotery.js new file mode 100644 index 0000000000..90a7d807ec --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/Remotery.js @@ -0,0 +1,338 @@ + +// +// TODO: Window resizing needs finer-grain control +// TODO: Take into account where user has moved the windows +// TODO: Controls need automatic resizing within their parent windows +// + + +Settings = (function() +{ + function Settings() + { + this.IsPaused = false; + } + + return Settings; + +})(); + + +Remotery = (function() +{ + // 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)); + + // Create the console up front as everything reports to it + this.Console = new Console(this.WindowManager, this.Server); + + // Create required windows + this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress); + this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this)); + this.TimelineWindow = new TimelineWindow(this.WindowManager, this.Settings, this.Server, Bind(OnTimelineCheck, this)); + this.TimelineWindow.SetOnHover(Bind(OnSampleHover, this)); + this.TimelineWindow.SetOnSelected(Bind(OnSampleSelected, this)); + + this.NbSampleWindows = 0; + this.SampleWindows = { }; + this.FrameHistory = { }; + this.SelectedFrames = { }; + this.NameMap = { }; + + this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this)); + this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this)); + + // Kick-off the auto-connect loop + AutoConnect(this); + + // Hook up resize event handler + DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this)); + OnResizeWindow(this); + + // Hook up browser-native canvas refresh + this.DisplayFrame = 0; + this.LastKnownPause = this.Settings.IsPaused; + var self = this; + (function display_loop() + { + window.requestAnimationFrame(display_loop); + DrawTimeline(self); + })(); + } + + + function AutoConnect(self) + { + // Only attempt to connect if there isn't already a connection or an attempt to connect + if (!self.Server.Connected()) + self.Server.Connect(self.ConnectionAddress); + + // Always schedule another check + window.setTimeout(Bind(AutoConnect, self), 2000); + } + + + function OnConnect(self) + { + // Connection address has been validated + LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress); + } + + + 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.TimelineWindow.DrawAllRows(); + return; + } + + // Don't waste time drawing the timeline when paused + if (self.Settings.IsPaused) + return; + + // requestAnimationFrame can run up to 60hz which is way too much for drawing the timeline + // Assume it's running at 60hz and skip frames to achieve 10hz instead + // Doing this instead of using setTimeout because it's better for browser rendering (or; will be once WebGL is in use) + // TODO: Expose as config variable because high refresh rate is great when using a separate viewiing machine + if ((self.DisplayFrame % 10) == 0) + self.TimelineWindow.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(); + sample.name = self.NameMap[sample.name_hash]; + + // If the name doesn't exist in the map yet, request it from the server + if (sample.name == undefined) + { + // Meanwhile, store the hash as the name + sample.name = { "string": sample.name_hash }; + self.NameMap[sample.name_hash] = sample.name; + 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(); + + // 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 + var message = { }; + message.thread_name = data_view_reader.GetString(); + message.nb_samples = data_view_reader.GetUInt32(); + message.sample_digest = data_view_reader.GetUInt32(); + + // Read samples + message.samples = []; + message.samples.push(DecodeSample(self, data_view_reader)); + + return message; + } + + + function OnSamples(self, socket, data_view) + { + // Discard any new samples while paused + if (self.Settings.IsPaused) + return; + + // Binary decode incoming sample data + var message = DecodeSamples(self, new DataViewReader(data_view, 8)); + 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]; + frame_history.push(thread_frame); + + // Discard old frames to keep memory-use constant + var max_nb_frames = 10000; + var extra_frames = frame_history.length - max_nb_frames; + if (extra_frames > 0) + frame_history.splice(0, extra_frames); + + // Create sample windows on-demand + if (!(name in self.SampleWindows)) + { + self.SampleWindows[name] = new SampleWindow(self.WindowManager, name, self.NbSampleWindows); + self.SampleWindows[name].WindowResized(self.TimelineWindow.Window, self.Console.Window); + self.NbSampleWindows++; + MoveSampleWindows(this); + } + + // Set on the window and timeline + self.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples); + self.TimelineWindow.OnSamples(name, frame_history); + } + + + function OnSampleName(self, socket, data_view) + { + // Add any names sent by the server to the local map + var data_view_reader = new DataViewReader(data_view, 4); + var name_hash = data_view_reader.GetUInt32(); + var name = data_view_reader.GetString(); + self.NameMap[name_hash].string = name; + } + + + function OnTimelineCheck(self, name, evt) + { + // Show/hide the equivalent sample window and move all the others to occupy any left-over space + var target = DOM.Event.GetNode(evt); + self.SampleWindows[name].SetVisible(target.checked); + MoveSampleWindows(self); + } + + + function MoveSampleWindows(self) + { + // Stack all windows next to each other + var xpos = 0; + for (var i in self.SampleWindows) + { + var sample_window = self.SampleWindows[i]; + if (sample_window.Visible) + sample_window.SetXPos(xpos++, self.TimelineWindow.Window, self.Console.Window); + } + } + + + function OnSampleHover(self, thread_name, hover) + { + // Hover only changes sample window contents when paused + var sample_window = self.SampleWindows[thread_name]; + if (sample_window && self.Settings.IsPaused) + { + if (hover == null) + { + // When there's no hover, go back to the selected frame + if (self.SelectedFrames[thread_name]) + { + var frame = self.SelectedFrames[thread_name]; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + } + + else + { + // Populate with sample under hover + var frame = hover[0]; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + } + } + + + function OnSampleSelected(self, thread_name, select) + { + // Lookup sample window set the frame samples on it + if (select && thread_name in self.SampleWindows) + { + var sample_window = self.SampleWindows[thread_name]; + var frame = select[0]; + self.SelectedFrames[thread_name] = frame; + sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples); + } + } + + + function OnResizeWindow(self) + { + // Resize windows + var w = window.innerWidth; + var h = window.innerHeight; + self.Console.WindowResized(w, h); + self.TitleWindow.WindowResized(w, h); + self.TimelineWindow.WindowResized(w, 600, self.TitleWindow.Window); + for (var i in self.SampleWindows) + self.SampleWindows[i].WindowResized(self.TimelineWindow.Window, self.Console.Window); + } + + + return Remotery; +})(); \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/Code/SampleWindow.js b/gazebo/common/Remotery/vis/Code/SampleWindow.js new file mode 100644 index 0000000000..4a2f44f187 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/SampleWindow.js @@ -0,0 +1,215 @@ + +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.Show(); + this.Visible = true; + + // Create a grid that's indexed by the unique sample ID + this.Grid = this.Window.AddControlNew(new WM.Grid(0, 0, 380, "calc( 100% - 17px )")); + 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.SetXPos = function(xpos, top_window, bottom_window) + { + Anim.Animate( + Bind(AnimatedMove, this, top_window, bottom_window), + this.XPos, 10 + xpos * 410, 0.25); + } + + + function AnimatedMove(self, top_window, bottom_window, val) + { + self.XPos = val; + self.WindowResized(top_window, bottom_window); + } + + + SampleWindow.prototype.SetVisible = function(visible) + { + if (visible != this.Visible) + { + if (visible == true) + this.Window.Show(); + else + this.Window.Hide(); + + this.Visible = visible; + } + } + + + SampleWindow.prototype.WindowResized = function(top_window, bottom_window) + { + var top = top_window.Position[1] + top_window.Size[1] + 10; + this.Window.SetPosition(this.XPos, top_window.Position[1] + top_window.Size[1] + 10); + this.Window.SetSize(400, bottom_window.Position[1] - 10 - top); + } + + + SampleWindow.prototype.OnSamples = function(nb_samples, sample_digest, samples) + { + if (!this.Visible) + return; + + // 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/gazebo/common/Remotery/vis/Code/ThreadFrame.js b/gazebo/common/Remotery/vis/Code/ThreadFrame.js new file mode 100644 index 0000000000..a631590c66 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/ThreadFrame.js @@ -0,0 +1,28 @@ + + +ThreadFrame = (function() +{ + function ThreadFrame(message) + { + // Persist the required message data + this.NbSamples = message.nb_samples; + this.SampleDigest = message.sample_digest; + this.Samples = message.samples; + + // Calculate the frame start/end times + this.StartTime_us = 0; + this.EndTime_us = 0; + var nb_root_samples = this.Samples.length; + if (nb_root_samples > 0) + { + var last_sample = this.Samples[nb_root_samples - 1]; + this.StartTime_us = this.Samples[0].us_start; + this.EndTime_us = last_sample.us_start + last_sample.us_length; + } + + this.Length_us = this.EndTime_us - this.StartTime_us; + } + + + return ThreadFrame; +})(); diff --git a/gazebo/common/Remotery/vis/Code/TimelineRow.js b/gazebo/common/Remotery/vis/Code/TimelineRow.js new file mode 100644 index 0000000000..8fcc5ed409 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/TimelineRow.js @@ -0,0 +1,379 @@ + + +TimelineRow = (function() +{ + var row_template = function(){/* +
+
+ +
+
+
+
+
+
+
-
+
+
+ +
+
+*/}.toString().split(/\n/).slice(1, -1).join("\n"); + + + var CANVAS_Y_OFFSET = 0; + var CANVAS_BORDER = 1; + var SAMPLE_HEIGHT = 16; + var SAMPLE_BORDER = 1; + var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2; + var SAMPLE_Y_OFFSET = CANVAS_Y_OFFSET + CANVAS_BORDER + 1; + + + function TimelineRow(name, width, parent_node, frame_history, check_handler) + { + this.Name = name; + + // Create the row HTML and add to the parent + this.ContainerNode = DOM.Node.CreateHTML(row_template); + this.Node = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowData"); + this.LabelNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowLabel"); + this.LabelNode.innerHTML = name; + this.CheckboxNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCheckbox"); + var expand_node_0 = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowExpand", 0); + var expand_node_1 = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowExpand", 1); + this.IncNode = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton"); + this.DecNode = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton"); + this.CanvasNode = DOM.Node.FindWithClass(this.ContainerNode, "TimelineRowCanvas"); + parent_node.appendChild(this.ContainerNode); + + // All sample view windows visible by default + this.CheckboxNode.checked = true; + DOM.Event.AddHandler(this.CheckboxNode, "change", function(evt) { check_handler(name, evt); }); + + // Manually hook-up events to simulate div:active + // I can't get the equivalent CSS to work in Firefox, so... + DOM.Event.AddHandler(this.IncNode, "mousedown", ExpandButtonDown); + DOM.Event.AddHandler(this.IncNode, "mouseup", ExpandButtonUp); + DOM.Event.AddHandler(this.IncNode, "mouseleave", ExpandButtonUp); + DOM.Event.AddHandler(this.DecNode, "mousedown", ExpandButtonDown); + DOM.Event.AddHandler(this.DecNode, "mouseup", ExpandButtonUp); + DOM.Event.AddHandler(this.DecNode, "mouseleave", ExpandButtonUp); + + // Pressing +/i increases/decreases depth + DOM.Event.AddHandler(this.IncNode, "click", Bind(IncDepth, this)); + DOM.Event.AddHandler(this.DecNode, "click", Bind(DecDepth, this)); + + // Setup the canvas + this.Depth = 1; + this.Ctx = this.CanvasNode.getContext("2d"); + this.SetSize(width); + this.Clear(); + + // Frame index to start at when looking for first visible sample + this.StartFrameIndex = 0; + + this.FrameHistory = frame_history; + this.VisibleFrames = [ ]; + this.VisibleTimeRange = null; + + // Sample the mouse is currently hovering over + this.HoverSample = null; + this.HoverSampleDepth = 0; + + // Currently selected sample + this.SelectedSample = null; + this.SelectedSampleDepth = 0; + } + + + TimelineRow.prototype.SetSize = function(width) + { + // Must ALWAYS set the width/height properties together. Setting one on its own has weird side-effects. + this.CanvasNode.width = width; + this.CanvasNode.height = CANVAS_BORDER + SAMPLE_BORDER + SAMPLE_Y_SPACING * this.Depth; + this.Draw(true); + } + + + TimelineRow.prototype.Clear = function() + { + // Fill box that shows the boundary between thread rows + this.Ctx.fillStyle = "#666" + var b = CANVAS_BORDER; + this.Ctx.fillRect(b, b, this.CanvasNode.width - b * 2, this.CanvasNode.height - b * 2); + } + + + TimelineRow.prototype.SetVisibleFrames = function(time_range) + { + // Clear previous visible list + this.VisibleFrames = [ ]; + if (this.FrameHistory.length == 0) + return; + + // Store a copy of the visible time range rather than referencing it + // This prevents external modifications to the time range from affecting rendering/selection + time_range = time_range.Clone(); + this.VisibleTimeRange = time_range; + + // The frame history can be reset outside this class + // This also catches the overflow to the end of the frame list below when a thread stops sending samples + var max_frame = Math.max(this.FrameHistory.length - 1, 0); + var start_frame_index = Math.min(this.StartFrameIndex, max_frame); + + // First do a back-track in case the time range moves negatively + while (start_frame_index > 0) + { + var frame = this.FrameHistory[start_frame_index]; + if (time_range.Start_us > frame.StartTime_us) + break; + start_frame_index--; + } + + // Then search from this point for the first visible frame + while (start_frame_index < this.FrameHistory.length) + { + var frame = this.FrameHistory[start_frame_index]; + if (frame.EndTime_us > time_range.Start_us) + break; + start_frame_index++; + } + + // Gather all frames up to the end point + this.StartFrameIndex = start_frame_index; + for (var i = start_frame_index; i < this.FrameHistory.length; i++) + { + var frame = this.FrameHistory[i]; + if (frame.StartTime_us > time_range.End_us) + break; + this.VisibleFrames.push(frame); + } + } + + + TimelineRow.prototype.Draw = function(draw_text) + { + this.Clear(); + + // Draw all root samples in the visible frame set + for (var i in this.VisibleFrames) + { + var frame = this.VisibleFrames[i]; + DrawSamples(this, frame.Samples, 1, draw_text); + } + } + + + function DrawSamples(self, samples, depth, draw_text) + { + for (var i in samples) + { + var sample = samples[i]; + DrawSample(self, sample, depth, draw_text); + + if (depth < self.Depth && sample.children != null) + DrawSamples(self, sample.children, depth + 1, draw_text); + } + } + + + TimelineRow.prototype.UpdateHoverSample = function(mouse_state, x_offset) + { + var hover = GetSampleAtPosition(this, mouse_state, x_offset); + if (hover) + this.SetHoverSample(hover[1], hover[2]); + return hover; + } + + + TimelineRow.prototype.UpdateSelectedSample = function(mouse_state, x_offset) + { + var select = GetSampleAtPosition(this, mouse_state, x_offset); + if (select) + this.SetSelectedSample(select[1], select[2]); + return select; + } + + + TimelineRow.prototype.SetHoverSample = function(sample, sample_depth) + { + if (sample != this.HoverSample) + { + // Discard old highlight + // TODO: When zoomed right out, tiny samples are anti-aliased and this becomes inaccurate + var old_sample = this.HoverSample; + var old_sample_depth = this.HoverSampleDepth; + this.HoverSample = null; + this.HoverSampleDepth = 0; + DrawSample(this, old_sample, old_sample_depth, true); + + // Add new highlight + this.HoverSample = sample; + this.HoverSampleDepth = sample_depth; + DrawSample(this, sample, sample_depth, true); + } + } + + + TimelineRow.prototype.SetSelectedSample = function(sample, sample_depth) + { + if (sample != this.SelectedSample) + { + // Discard old highlight + // TODO: When zoomed right out, tiny samples are anti-aliased and this becomes inaccurate + var old_sample = this.SelectedSample; + var old_sample_depth = this.SelectedSampleDepth; + this.SelectedSample = null; + this.SelectedSampleDepth = 0; + DrawSample(this, old_sample, old_sample_depth, true); + + // Add new highlight + this.SelectedSample = sample; + this.SelectedSampleDepth = sample_depth; + DrawSample(this, sample, sample_depth, true); + } + } + + + function ExpandButtonDown(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.AddClass(node, "TimelineRowExpandButtonActive"); + } + + + function ExpandButtonUp(evt) + { + var node = DOM.Event.GetNode(evt); + DOM.Node.RemoveClass(node, "TimelineRowExpandButtonActive"); + } + + + function IncDepth(self) + { + self.Depth++; + self.SetSize(self.CanvasNode.width); + } + + + function DecDepth(self) + { + if (self.Depth > 1) + { + self.Depth--; + self.SetSize(self.CanvasNode.width); + } + } + + + function GetSampleAtPosition(self, mouse_state, x_offset) + { + // Mouse movement can occur before any data is sent to a timeline row + var time_range = self.VisibleTimeRange; + if (time_range == null) + return; + + // Get the time the mouse is over + var x = mouse_state.Position[0] - x_offset; + var time_us = time_range.Start_us + x / time_range.usPerPixel; + + var canvas_y_offset = DOM.Node.GetPosition(self.CanvasNode)[1]; + var mouse_y_offset = mouse_state.Position[1] - canvas_y_offset; + mouse_y_offset = Math.min(Math.max(mouse_y_offset, 0), self.CanvasNode.height); + var depth = Math.floor(mouse_y_offset / SAMPLE_Y_SPACING) + 1; + + // Search for the first frame to intersect this time + for (var i in self.VisibleFrames) + { + var frame = self.VisibleFrames[i]; + if (time_us >= frame.StartTime_us && time_us < frame.EndTime_us) + { + var found_sample = FindSample(self, frame.Samples, time_us, depth, 1); + if (found_sample != null) + return [ frame, found_sample[0], found_sample[1] ]; + } + } + + return null; + } + + + function FindSample(self, samples, time_us, target_depth, depth) + { + for (var i in samples) + { + var sample = samples[i]; + if (depth == target_depth) + { + if (time_us >= sample.us_start && time_us < sample.us_start + sample.us_length) + return [ sample, depth ]; + } + + else if (depth < target_depth && sample.children != null) + { + var found_sample = FindSample(self, sample.children, time_us, target_depth, depth + 1); + if (found_sample != null) + return found_sample; + } + } + + return null; + } + + + function DrawSample(self, sample, depth, draw_text) + { + if (sample == null) + return; + + // Determine pixel range of the sample + var time_range = self.VisibleTimeRange; + var x0 = time_range.PixelOffset(sample.us_start); + var x1 = x0 + time_range.PixelSize(sample.us_length); + + // Clip to padded timeline row + var min_x = 3; + var max_x = self.CanvasNode.width - 5; + x0 = Math.min(Math.max(x0, min_x), max_x); + x1 = Math.min(Math.max(x1, min_x), max_x); + + var offset_x = x0; + var offset_y = SAMPLE_Y_OFFSET + (depth - 1) * SAMPLE_Y_SPACING; + var size_x = x1 - x0; + var size_y = SAMPLE_HEIGHT; + + // Normal rendering + var ctx = self.Ctx; + ctx.fillStyle = sample.colour; + ctx.fillRect(offset_x, offset_y, size_x, size_y); + + // Highlight rendering + var b = (sample == self.HoverSample) ? 255 : 0; + var r = (sample == self.SelectedSample) ? 255 : 0; + if (b + r > 0) + { + ctx.lineWidth = 1; + ctx.strokeStyle = "rgb(" + r + ", 0, " + b + ")"; + ctx.strokeRect(offset_x + 0.5, offset_y + 0.5, size_x - 1, size_y - 1); + } + + // Draw sample names clipped to the bounds of the sample + // Also reject tiny samples with no space to render text + if (draw_text && size_x > 8) + { + ctx.save(); + ctx.beginPath(); + ctx.rect(offset_x + 2.5, offset_y + 1.5, size_x - 5, size_y - 3); + ctx.clip(); + ctx.font = "9px verdana"; + ctx.fillStyle = "black"; + var text = sample.name.string + text += " (" + sample.ms_length + "ms"; + text += ", " + sample.call_count + "c)"; + ctx.fillText(text, offset_x + 5.5, offset_y + 1.5 + 9); + ctx.restore(); + } + } + + + return TimelineRow; +})(); diff --git a/gazebo/common/Remotery/vis/Code/TimelineWindow.js b/gazebo/common/Remotery/vis/Code/TimelineWindow.js new file mode 100644 index 0000000000..9321766fd4 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/TimelineWindow.js @@ -0,0 +1,274 @@ + +// +// TODO: Use WebGL and instancing for quicker renders +// + + +TimelineWindow = (function() +{ + var BORDER = 10; + + var ROW_START_SIZE = 210; + + var ROW_END_SIZE = 20; // make room for scrollbar + + var box_template = "
"; + + + function TimelineWindow(wm, settings, server, check_handler) + { + this.Settings = settings; + + // Ordered list of thread rows on the timeline + this.ThreadRows = [ ]; + + // Create window and containers + this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100); + this.Window.ShowNoAnim(); + this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160)); + DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer"); + + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + + // Setup timeline manipulation + this.MouseDown = false; + this.LastMouseState = null; + this.TimelineMoved = false; + this.OnHoverHandler = null; + this.OnSelectedHandler = null; + DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this)); + DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this)); + DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this)); + + // Set time range AFTER the window has been created, as it uses the window to determine pixel coverage + this.TimeRange = new PixelTimeRange(0, 200 * 1000, RowWidth(this)); + + this.CheckHandler = check_handler; + } + + + TimelineWindow.prototype.SetOnHover = function(handler) + { + this.OnHoverHandler = handler; + } + + + TimelineWindow.prototype.SetOnSelected = function(handler) + { + this.OnSelectedHandler = handler; + } + + + TimelineWindow.prototype.WindowResized = function(width, height, top_window) + { + // Resize window + var top = top_window.Position[1] + top_window.Size[1] + 10; + this.Window.SetPosition(10, top); + this.Window.SetSize(width - 2 * 10, height); + + // Resize controls + var parent_size = this.Window.Size; + this.TimelineContainer.SetPosition(BORDER, 10); + this.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER, height); + + // Resize rows + var row_width = RowWidth(this); + for (var i in this.ThreadRows) + { + var row = this.ThreadRows[i]; + row.SetSize(row_width); + } + + // Adjust time range to new width + this.TimeRange.SetPixelSpan(row_width); + this.DrawAllRows(); + } + + + TimelineWindow.prototype.ResetTimeRange = function() + { + this.TimeRange.SetStart(0); + } + + + TimelineWindow.prototype.OnSamples = function(thread_name, frame_history) + { + // Shift the timeline to the last entry on this thread + // As multiple threads come through here with different end frames, only do this for the latest + var last_frame = frame_history[frame_history.length - 1]; + if (last_frame.EndTime_us > this.TimeRange.End_us) + this.TimeRange.SetEnd(last_frame.EndTime_us); + + // Search for the index of this thread + var thread_index = -1; + for (var i in this.ThreadRows) + { + if (this.ThreadRows[i].Name == thread_name) + { + thread_index = i; + break; + } + } + + // If this thread has not been seen before, add a new row to the list and re-sort + if (thread_index == -1) + { + var row = new TimelineRow(thread_name, RowWidth(this), this.TimelineContainer.Node, frame_history, this.CheckHandler); + this.ThreadRows.push(row); + this.ThreadRows.sort(function(a, b) { return b.Name.localeCompare(a.Name); }); + } + } + + + TimelineWindow.prototype.DrawAllRows = function() + { + var time_range = this.TimeRange; + var draw_text = this.Settings.IsPaused; + for (var i in this.ThreadRows) + { + var thread_row = this.ThreadRows[i]; + thread_row.SetVisibleFrames(time_range); + thread_row.Draw(draw_text); + } + } + + + function RowXOffset(self) + { + // Add sizing of the label + // TODO: Use computed size + return DOM.Node.GetPosition(self.TimelineContainer.Node)[0] + ROW_START_SIZE; + } + + + function RowWidth(self) + { + // Subtract sizing of the label + // TODO: Use computed size + return self.TimelineContainer.Size[0] - (ROW_START_SIZE + ROW_END_SIZE); + } + + + function OnMouseScroll(self, evt) + { + var mouse_state = new Mouse.State(evt); + var scale = 1.11; + if (mouse_state.WheelDelta > 0) + scale = 1 / scale; + + // What time is the mouse hovering over? + var x = mouse_state.Position[0] - RowXOffset(self); + var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel; + + // Calculate start time relative to the mouse hover position + var time_start_us = self.TimeRange.Start_us - time_us; + + // Scale and offset back to the hover time + self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale); + self.DrawAllRows(); + + // Prevent vertical scrolling on mouse-wheel + DOM.Event.StopDefaultAction(evt); + } + + + function OnMouseDown(self, evt) + { + // Only manipulate the timelime when paused + if (!self.Settings.IsPaused) + return; + + self.MouseDown = true; + self.LastMouseState = new Mouse.State(evt); + self.TimelineMoved = false; + DOM.Event.StopDefaultAction(evt); + } + + + function OnMouseUp(self, evt) + { + // Only manipulate the timelime when paused + if (!self.Settings.IsPaused) + return; + + var mouse_state = new Mouse.State(evt); + + self.MouseDown = false; + + if (!self.TimelineMoved) + { + // Search for the row being clicked and update its selection + var row_node = DOM.Event.GetNode(evt); + for (var i in self.ThreadRows) + { + var thread_row = self.ThreadRows[i]; + if (thread_row.CanvasNode == row_node) + { + var select = thread_row.UpdateSelectedSample(mouse_state, RowXOffset(self)); + + // Call any selection handlers + if (self.OnSelectedHandler) + self.OnSelectedHandler(thread_row.Name, select); + + break; + } + } + } + } + + + function OnMouseMove(self, evt) + { + // Only manipulate the timelime when paused + if (!self.Settings.IsPaused) + return; + + var mouse_state = new Mouse.State(evt); + + if (self.MouseDown) + { + // Get the time the mouse is over + var x = mouse_state.Position[0] - RowXOffset(self); + var time_us = self.TimeRange.Start_us + x / self.TimeRange.usPerPixel; + + // Shift the visible time range with mouse movement + var time_offset_us = (mouse_state.Position[0] - self.LastMouseState.Position[0]) / self.TimeRange.usPerPixel; + if (time_offset_us) + { + self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us); + self.DrawAllRows(); + self.TimelineMoved = true; + } + } + + else + { + // Highlight any samples the mouse moves over + var row_node = DOM.Event.GetNode(evt); + for (var i in self.ThreadRows) + { + var thread_row = self.ThreadRows[i]; + if (thread_row.CanvasNode == row_node) + { + var hover = thread_row.UpdateHoverSample(mouse_state, RowXOffset(self)); + + if (self.OnHoverHandler) + self.OnHoverHandler(thread_row.Name, hover); + } + else + { + thread_row.SetHoverSample(null, 0); + if (self.OnHoverHandler) + self.OnHoverHandler(thread_row.Name, null); + } + } + } + + self.LastMouseState = mouse_state; + } + + + return TimelineWindow; +})(); + diff --git a/gazebo/common/Remotery/vis/Code/TitleWindow.js b/gazebo/common/Remotery/vis/Code/TitleWindow.js new file mode 100644 index 0000000000..6e19759756 --- /dev/null +++ b/gazebo/common/Remotery/vis/Code/TitleWindow.js @@ -0,0 +1,59 @@ + +TitleWindow = (function() +{ + function TitleWindow(wm, settings, server, connection_address) + { + this.Settings = settings; + + this.Window = wm.AddWindow("     Remotery", 10, 10, 100, 100); + this.Window.ShowNoAnim(); + + this.PingContainer = this.Window.AddControlNew(new WM.Container(4, -13, 10, 10)); + DOM.Node.AddClass(this.PingContainer.Node, "PingContainer"); + + this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address", connection_address)); + + // Setup pause button + this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true })); + this.PauseButton.SetOnClick(Bind(OnPausePressed, this)); + + server.AddMessageHandler("PING", Bind(OnPing, this)); + } + + + TitleWindow.prototype.SetConnectionAddressChanged = function(handler) + { + this.EditBox.SetChangeHandler(handler); + } + + + TitleWindow.prototype.WindowResized = function(width, height) + { + this.Window.SetSize(width - 2 * 10, 50); + this.PauseButton.SetPosition(width - 80, 5); + } + + + function OnPausePressed(self) + { + self.Settings.IsPaused = self.PauseButton.IsPressed(); + if (self.Settings.IsPaused) + self.PauseButton.SetText("Paused"); + else + self.PauseButton.SetText("Pause"); + } + + + function OnPing(self, server) + { + // Set the ping container as active and take it off half a second later + DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive"); + window.setTimeout(Bind(function(self) + { + DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive"); + }, self), 500); + } + + + return TitleWindow; +})(); \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/Code/WebSocketConnection.js b/gazebo/common/Remotery/vis/Code/WebSocketConnection.js new file mode 100644 index 0000000000..95b1208be0 --- /dev/null +++ b/gazebo/common/Remotery/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) + { + var data_view = new DataView(event.data); + + var id = String.fromCharCode( + data_view.getInt8(0), + data_view.getInt8(1), + data_view.getInt8(2), + data_view.getInt8(3)); + + CallMessageHandlers(self, id, data_view); + } + + + return WebSocketConnection; +})(); diff --git a/gazebo/common/Remotery/vis/Styles/Remotery.css b/gazebo/common/Remotery/vis/Styles/Remotery.css new file mode 100644 index 0000000000..5565cc4fe1 --- /dev/null +++ b/gazebo/common/Remotery/vis/Styles/Remotery.css @@ -0,0 +1,265 @@ + +body +{ + /* Take up the full page */ + width: 100%; + height: 100%; + margin: 0px; + + background-color: #AAA; +} + + +.NoSelect +{ + /* Disable text selection so that it doesn't interfere with faux-button clicking */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + /* Stops the text cursor over the label */ + cursor:default; +} + + +/* Override default window styles to remove 3D effect */ +.Window +{ + background: #555; + box-shadow: none; + border-radius: 3px; +} +/*.WindowTitleBar +{ + border-bottom: none; + border-radius: 0px; +} +.WindowBody +{ + border-top: none; +}*/ + + +/* Override default container style to remove 3D effect */ +.Container +{ + border: none; + box-shadow: none; +} + + +/* Override default edit box style to remove 3D effect */ +.EditBox +{ + border: none; + box-shadow: none; + width:200; +} + + + +.ConsoleText +{ + overflow:auto; + color: #BBB; + font: 9px Verdana; + margin: 2px; + white-space: pre; +} + + +.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 +{ + /* Pull .TimelineRowCanvas to the right of the label */ + float:left; + + width: 140px; + height: 14px; +} +.TimelineRowCanvas +{ +} + +/* enable vertical scrollbar in TimelineContainer (useful for many threads) */ +.TimelineContainer +{ + overflow-y: auto; +} diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Animation.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Animation.js new file mode 100644 index 0000000000..516aa9cf80 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Animation.js @@ -0,0 +1,65 @@ + +// +// Very basic linear value animation system, for now. +// + + +namespace("Anim"); + + +Anim.Animation = (function() +{ + var anim_hz = 60; + + + function Animation(anim_func, start_value, end_value, time, end_callback) + { + // Setup initial parameters + this.StartValue = start_value; + this.EndValue = end_value; + this.ValueInc = (end_value - start_value) / (time * anim_hz); + this.Value = start_value; + this.Complete = false; + this.EndCallback = end_callback; + + // Cache the update function to prevent recreating the closure + var self = this; + this.AnimFunc = anim_func; + this.AnimUpdate = function() { Update(self); } + + // Call for the start value + this.AnimUpdate(); + } + + + function Update(self) + { + // Queue up the next frame immediately + var id = window.setTimeout(self.AnimUpdate, 1000 / anim_hz); + + // Linear step the value and check for completion + self.Value += self.ValueInc; + if (Math.abs(self.Value - self.EndValue) < 0.01) + { + self.Value = self.EndValue; + self.Complete = true; + + if (self.EndCallback) + self.EndCallback(); + + window.clearTimeout(id); + } + + // Pass to the animation function + self.AnimFunc(self.Value); + } + + + return Animation; +})(); + + +Anim.Animate = function(anim_func, start_value, end_value, time, end_callback) +{ + return new Anim.Animation(anim_func, start_value, end_value, time, end_callback); +} diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Bind.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Bind.js new file mode 100644 index 0000000000..102ee26f60 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Bind.js @@ -0,0 +1,92 @@ +// +// This will generate a closure for the given function and optionally bind an arbitrary number of +// its initial arguments to specific values. +// +// Parameters: +// +// 0: Either the function scope or the function. +// 1: If 0 is the function scope, this is the function. +// Otherwise it's the start of the optional bound argument list. +// 2: Start of the optional bound argument list if 1 is the function. +// +// Examples: +// +// function GlobalFunction(p0, p1, p2) { } +// function ThisFunction(p0, p1, p2) { } +// +// var a = Bind("GlobalFunction"); +// var b = Bind(this, "ThisFunction"); +// var c = Bind("GlobalFunction", BoundParam0, BoundParam1); +// var d = Bind(this, "ThisFunction", BoundParam0, BoundParam1); +// var e = Bind(GlobalFunction); +// var f = Bind(this, ThisFunction); +// var g = Bind(GlobalFunction, BoundParam0, BoundParam1); +// var h = Bind(this, ThisFunction, BoundParam0, BoundParam1); +// +// a(0, 1, 2); +// b(0, 1, 2); +// c(2); +// d(2); +// e(0, 1, 2); +// f(0, 1, 2); +// g(2); +// h(2); +// +function Bind() +{ + // No closure to define? + if (arguments.length == 0) + return null; + + // Figure out which of the 4 call types is being used to bind + // Locate scope, function and bound parameter start index + + if (typeof(arguments[0]) == "string") + { + var scope = window; + var func = window[arguments[0]]; + var start = 1; + } + + else if (typeof(arguments[0]) == "function") + { + var scope = window; + var func = arguments[0]; + var start = 1; + } + + else if (typeof(arguments[1]) == "string") + { + var scope = arguments[0]; + var func = scope[arguments[1]]; + var start = 2; + } + + else if (typeof(arguments[1]) == "function") + { + var scope = arguments[0]; + var func = arguments[1]; + var start = 2; + } + + else + { + // unknown + console.log("Bind() ERROR: Unknown bind parameter configuration"); + return; + } + + // Convert the arguments list to an array + var arg_array = Array.prototype.slice.call(arguments, start); + start = arg_array.length; + + return function() + { + // Concatenate incoming arguments + for (var i = 0; i < arguments.length; i++) + arg_array[start + i] = arguments[i]; + + // Call the function in the given scope with the new arguments + return func.apply(scope, arg_array); + } +} diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Convert.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Convert.js new file mode 100644 index 0000000000..b1f5461317 --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Core.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Core.js new file mode 100644 index 0000000000..aab35c9207 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Core.js @@ -0,0 +1,20 @@ + +// TODO: requires function for checking existence of dependencies + + +function namespace(name) +{ + // Ensure all nested namespaces are created only once + + var ns_list = name.split("."); + var parent_ns = window; + + for (var i in ns_list) + { + var ns_name = ns_list[i]; + if (!(ns_name in parent_ns)) + parent_ns[ns_name] = { }; + + parent_ns = parent_ns[ns_name]; + } +} \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/DOM.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/DOM.js new file mode 100644 index 0000000000..f11fb192bc --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/DOM.js @@ -0,0 +1,499 @@ + +namespace("DOM.Node"); +namespace("DOM.Event"); +namespace("DOM.Applet"); + + + +// +// ===================================================================================================================== +// ----- DOCUMENT NODE/ELEMENT EXTENSIONS ------------------------------------------------------------------------------ +// ===================================================================================================================== +// + + + +DOM.Node.Get = function(id) +{ + return document.getElementById(id); +} + + +// +// Set node position +// +DOM.Node.SetPosition = function(node, position) +{ + node.style.left = position[0]; + node.style.top = position[1]; +} +DOM.Node.SetX = function(node, x) +{ + node.style.left = x; +} +DOM.Node.SetY = function(node, y) +{ + node.style.top = y; +} + + +// +// Get the absolute position of a HTML element on the page +// +DOM.Node.GetPosition = function(element, account_for_scroll) +{ + // Recurse up through parents, summing offsets from their parent + var x = 0, y = 0; + for (var node = element; node != null; node = node.offsetParent) + { + x += node.offsetLeft; + y += node.offsetTop; + } + + if (account_for_scroll) + { + // Walk up the hierarchy subtracting away any scrolling + for (var node = element; node != document.body; node = node.parentNode) + { + x -= node.scrollLeft; + y -= node.scrollTop; + } + } + + return [x, y]; +} + + +// +// Set node size +// +DOM.Node.SetSize = function(node, size) +{ + node.style.width = size[0]; + node.style.height = size[1]; +} +DOM.Node.SetWidth = function(node, width) +{ + node.style.width = width; +} +DOM.Node.SetHeight = function(node, height) +{ + node.style.height = height; +} + + +// +// Get node OFFSET size: +// clientX includes padding +// offsetX includes padding and borders +// scrollX includes padding, borders and size of contained node +// +DOM.Node.GetSize = function(node) +{ + return [ node.offsetWidth, node.offsetHeight ]; +} +DOM.Node.GetWidth = function(node) +{ + return node.offsetWidth; +} +DOM.Node.GetHeight = function(node) +{ + return node.offsetHeight; +} + + +// +// Set node opacity +// +DOM.Node.SetOpacity = function(node, value) +{ + node.style.opacity = value; +} + + +DOM.Node.SetColour = function(node, colour) +{ + node.style.color = colour; +} + + +// +// Hide a node by completely disabling its rendering (it no longer contributes to document layout) +// +DOM.Node.Hide = function(node) +{ + node.style.display = "none"; +} + + +// +// Show a node by restoring its influcen in document layout +// +DOM.Node.Show = function(node) +{ + node.style.display = "block"; +} + + +// +// Add a CSS class to a HTML element, specified last +// +DOM.Node.AddClass = function(node, class_name) +{ + // Ensure the class hasn't already been added + DOM.Node.RemoveClass(node, class_name); + node.className += " " + class_name; +} + + +// +// Remove a CSS class from a HTML element +// +DOM.Node.RemoveClass = function(node, class_name) +{ + // Remove all variations of where the class name can be in the string list + var regexp = new RegExp("\\b" + class_name + "\\b"); + node.className = node.className.replace(regexp, ""); +} + + + +// +// Check to see if a HTML element contains a class +// +DOM.Node.HasClass = function(node, class_name) +{ + var regexp = new RegExp("\\b" + class_name + "\\b"); + return regexp.test(node.className); +} + + +// +// Recursively search for a node with the given class name +// +DOM.Node.FindWithClass = function(parent_node, class_name, index) +{ + // Search the children looking for a node with the given class name + for (var i in parent_node.childNodes) + { + var node = parent_node.childNodes[i]; + if (DOM.Node.HasClass(node, class_name)) + { + if (index === undefined || index-- == 0) + return node; + } + + // Recurse into children + node = DOM.Node.FindWithClass(node, class_name); + if (node != null) + return node; + } + + return null; +} + + +// +// Check to see if one node logically contains another +// +DOM.Node.Contains = function(node, container_node) +{ + while (node != null && node != container_node) + node = node.parentNode; + return node != null; +} + + +// +// Create the HTML nodes specified in the text passed in +// Assumes there is only one root node in the text +// +DOM.Node.CreateHTML = function(html) +{ + var div = document.createElement("div"); + div.innerHTML = html; + + // First child may be a text node, followed by the created HTML + var child = div.firstChild; + if (child != null && child.nodeType == 3) + child = child.nextSibling; + return child; +} + + +// +// Make a copy of a HTML element, making it visible and clearing its ID to ensure it's not a duplicate +// +DOM.Node.Clone = function(name) +{ + // Get the template element and clone it, making sure it's renderable + var node = DOM.Node.Get(name); + node = node.cloneNode(true); + node.id = null; + node.style.display = "block"; + return node; +} + + +// +// Append an arbitrary block of HTML to an existing node +// +DOM.Node.AppendHTML = function(node, html) +{ + var child = DOM.Node.CreateHTML(html); + node.appendChild(child); + return child; +} + + +// +// Append a div that clears the float style +// +DOM.Node.AppendClearFloat = function(node) +{ + var div = document.createElement("div"); + div.style.clear = "both"; + node.appendChild(div); +} + + +// +// Check to see that the object passed in is an instance of a DOM node +// +DOM.Node.IsNode = function(object) +{ + return object instanceof Element; +} + + +// +// Create an "iframe shim" so that elements within it render over a Java Applet +// http://web.archive.org/web/20110707212850/http://www.oratransplant.nl/2007/10/26/using-iframe-shim-to-partly-cover-a-java-applet/ +// +DOM.Node.CreateShim = function(parent) +{ + var shimmer = document.createElement("iframe"); + + // Position the shimmer so that it's the same location/size as its parent + shimmer.style.position = "fixed"; + shimmer.style.left = parent.style.left; + shimmer.style.top = parent.style.top; + shimmer.style.width = parent.offsetWidth; + shimmer.style.height = parent.offsetHeight; + + // We want the shimmer to be one level below its contents + shimmer.style.zIndex = parent.style.zIndex - 1; + + // Ensure its empty + shimmer.setAttribute("frameborder", "0"); + shimmer.setAttribute("src", ""); + + // Add to the document and the parent + document.body.appendChild(shimmer); + parent.Shimmer = shimmer; + return shimmer; +} + + + +// +// ===================================================================================================================== +// ----- EVENT HANDLING EXTENSIONS ------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Retrieves the event from the first parameter passed into an HTML event +// +DOM.Event.Get = function(evt) +{ + // Internet explorer doesn't pass the event + return window.event || evt; +} + + +// +// Retrieves the element that triggered an event from the event object +// +DOM.Event.GetNode = function(evt) +{ + evt = DOM.Event.Get(evt); + + // Get the target element + var element; + if (evt.target) + element = evt.target; + else if (e.srcElement) + element = evt.srcElement; + + // Default Safari bug + if (element.nodeType == 3) + element = element.parentNode; + + return element; +} + + +// +// Stop default action for an event +// +DOM.Event.StopDefaultAction = function(evt) +{ + if (evt && evt.preventDefault) + evt.preventDefault(); + else if (window.event && window.event.returnValue) + window.event.returnValue = false; +} + + +// +// Stops events bubbling up to parent event handlers +// +DOM.Event.StopPropagation = function(evt) +{ + evt = DOM.Event.Get(evt); + if (evt) + { + evt.cancelBubble = true; + if (evt.stopPropagation) + evt.stopPropagation(); + } +} + + +// +// Stop both event default action and propagation +// +DOM.Event.StopAll = function(evt) +{ + DOM.Event.StopDefaultAction(evt); + DOM.Event.StopPropagation(evt); +} + + +// +// Adds an event handler to an event +// +DOM.Event.AddHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.addEventListener) + obj.addEventListener(evt, func, false); + else if (obj.attachEvent) + obj.attachEvent("on" + evt, func); + } +} + + +// +// Removes an event handler from an event +// +DOM.Event.RemoveHandler = function(obj, evt, func) +{ + if (obj) + { + if (obj.removeEventListener) + obj.removeEventListener(evt, func, false); + else if (obj.detachEvent) + obj.detachEvent("on" + evt, func); + } +} + + +// +// Get the position of the mouse cursor, page relative +// +DOM.Event.GetMousePosition = function(evt) +{ + evt = DOM.Event.Get(evt); + + var px = 0; + var py = 0; + if (evt.pageX || evt.pageY) + { + px = evt.pageX; + py = evt.pageY; + } + else if (evt.clientX || evt.clientY) + { + px = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + py = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return [px, py]; +} + + + +// +// ===================================================================================================================== +// ----- JAVA APPLET EXTENSIONS ---------------------------------------------------------------------------------------- +// ===================================================================================================================== +// + + + +// +// Create an applet element for loading a Java applet, attaching it to the specified node +// +DOM.Applet.Load = function(dest_id, id, code, archive) +{ + // Lookup the applet destination + var dest = DOM.Node.Get(dest_id); + if (!dest) + return; + + // Construct the applet element and add it to the destination + Debug.Log("Injecting applet DOM code"); + var applet = ""; + applet += ""; + 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/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Keyboard.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Keyboard.js new file mode 100644 index 0000000000..f70f4ea035 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Keyboard.js @@ -0,0 +1,149 @@ + +namespace("Keyboard") + + +// ===================================================================================================================== +// Key codes copied from closure-library +// https://code.google.com/p/closure-library/source/browse/closure/goog/events/keycodes.js +// --------------------------------------------------------------------------------------------------------------------- +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +Keyboard.Codes = { + WIN_KEY_FF_LINUX : 0, + MAC_ENTER : 3, + BACKSPACE : 8, + TAB : 9, + NUM_CENTER : 12, // NUMLOCK on FF/Safari Mac + ENTER : 13, + SHIFT : 16, + CTRL : 17, + ALT : 18, + PAUSE : 19, + CAPS_LOCK : 20, + ESC : 27, + SPACE : 32, + PAGE_UP : 33, // also NUM_NORTH_EAST + PAGE_DOWN : 34, // also NUM_SOUTH_EAST + END : 35, // also NUM_SOUTH_WEST + HOME : 36, // also NUM_NORTH_WEST + LEFT : 37, // also NUM_WEST + UP : 38, // also NUM_NORTH + RIGHT : 39, // also NUM_EAST + DOWN : 40, // also NUM_SOUTH + PRINT_SCREEN : 44, + INSERT : 45, // also NUM_INSERT + DELETE : 46, // also NUM_DELETE + ZERO : 48, + ONE : 49, + TWO : 50, + THREE : 51, + FOUR : 52, + FIVE : 53, + SIX : 54, + SEVEN : 55, + EIGHT : 56, + NINE : 57, + FF_SEMICOLON : 59, // Firefox (Gecko) fires this for semicolon instead of 186 + FF_EQUALS : 61, // Firefox (Gecko) fires this for equals instead of 187 + FF_DASH : 173, // Firefox (Gecko) fires this for dash instead of 189 + QUESTION_MARK : 63, // needs localization + A : 65, + B : 66, + C : 67, + D : 68, + E : 69, + F : 70, + G : 71, + H : 72, + I : 73, + J : 74, + K : 75, + L : 76, + M : 77, + N : 78, + O : 79, + P : 80, + Q : 81, + R : 82, + S : 83, + T : 84, + U : 85, + V : 86, + W : 87, + X : 88, + Y : 89, + Z : 90, + META : 91, // WIN_KEY_LEFT + WIN_KEY_RIGHT : 92, + CONTEXT_MENU : 93, + NUM_ZERO : 96, + NUM_ONE : 97, + NUM_TWO : 98, + NUM_THREE : 99, + NUM_FOUR : 100, + NUM_FIVE : 101, + NUM_SIX : 102, + NUM_SEVEN : 103, + NUM_EIGHT : 104, + NUM_NINE : 105, + NUM_MULTIPLY : 106, + NUM_PLUS : 107, + NUM_MINUS : 109, + NUM_PERIOD : 110, + NUM_DIVISION : 111, + F1 : 112, + F2 : 113, + F3 : 114, + F4 : 115, + F5 : 116, + F6 : 117, + F7 : 118, + F8 : 119, + F9 : 120, + F10 : 121, + F11 : 122, + F12 : 123, + NUMLOCK : 144, + SCROLL_LOCK : 145, + + // OS-specific media keys like volume controls and browser controls. + FIRST_MEDIA_KEY : 166, + LAST_MEDIA_KEY : 183, + + SEMICOLON : 186, // needs localization + DASH : 189, // needs localization + EQUALS : 187, // needs localization + COMMA : 188, // needs localization + PERIOD : 190, // needs localization + SLASH : 191, // needs localization + APOSTROPHE : 192, // needs localization + TILDE : 192, // needs localization + SINGLE_QUOTE : 222, // needs localization + OPEN_SQUARE_BRACKET : 219, // needs localization + BACKSLASH : 220, // needs localization + CLOSE_SQUARE_BRACKET: 221, // needs localization + WIN_KEY : 224, + MAC_FF_META : 224, // Firefox (Gecko) fires this for the meta key instead of 91 + MAC_WK_CMD_LEFT : 91, // WebKit Left Command key fired, same as META + MAC_WK_CMD_RIGHT : 93, // WebKit Right Command key fired, different from META + WIN_IME : 229, + + // We've seen users whose machines fire this keycode at regular one + // second intervals. The common thread among these users is that + // they're all using Dell Inspiron laptops, so we suspect that this + // indicates a hardware/bios problem. + // http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx + PHANTOM : 255 +}; +// ===================================================================================================================== diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/LocalStore.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/LocalStore.js new file mode 100644 index 0000000000..7bb8481531 --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Mouse.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Mouse.js new file mode 100644 index 0000000000..a694b80698 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/Mouse.js @@ -0,0 +1,83 @@ + +namespace("Mouse"); + + +Mouse.State =(function() +{ + function State(event) + { + // Get button press states + if (typeof event.buttons != "undefined") + { + // Firefox + this.Left = (event.buttons & 1) != 0; + this.Right = (event.buttons & 2) != 0; + this.Middle = (event.buttons & 4) != 0; + } + else + { + // Chrome + this.Left = (event.button == 0); + this.Middle = (event.button == 1); + this.Right = (event.button == 2); + } + + // Get page-relative mouse position + this.Position = DOM.Event.GetMousePosition(event); + + // Get wheel delta + var delta = 0; + if (event.wheelDelta) + delta = event.wheelDelta / 120; // IE/Opera + else if (event.detail) + delta = -event.detail / 3; // Mozilla + this.WheelDelta = delta; + + // Get the mouse position delta + // Requires Pointer Lock API support + this.PositionDelta = [ + event.movementX || event.mozMovementX || event.webkitMovementX || 0, + event.movementY || event.mozMovementY || event.webkitMovementY || 0 + ]; + } + + return State; +})(); + + +// +// Basic Pointer Lock API support +// https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock +// http://www.chromium.org/developers/design-documents/mouse-lock +// +// Note that API has not been standardised yet so browsers can implement functions with prefixes +// + + +Mouse.PointerLockSupported = function() +{ + return 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document; +} + + +Mouse.RequestPointerLock = function(element) +{ + element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; + if (element.requestPointerLock) + element.requestPointerLock(); +} + + +Mouse.ExitPointerLock = function() +{ + document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; + if (document.exitPointerLock) + document.exitPointerLock(); +} + + +// Can use this element to detect whether pointer lock is enabled (returns non-null) +Mouse.PointerLockElement = function() +{ + return document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement; +} diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/MurmurHash3.js b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/MurmurHash3.js new file mode 100644 index 0000000000..c423d49deb --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/Core/Code/MurmurHash3.js @@ -0,0 +1,68 @@ + +namespace("Hash"); + +/** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author 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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Button.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Button.js new file mode 100644 index 0000000000..12e09815b0 --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/ComboBox.js new file mode 100644 index 0000000000..d199b3abda --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Container.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Container.js new file mode 100644 index 0000000000..2ba1388d14 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Container.js @@ -0,0 +1,34 @@ + +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); + } + + + return Container; +})(); \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/EditBox.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/EditBox.js new file mode 100644 index 0000000000..fd0a039450 --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Grid.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Grid.js new file mode 100644 index 0000000000..84ad52fb4a --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Grid.js @@ -0,0 +1,252 @@ + +namespace("WM"); + + +WM.GridRows = (function() +{ + function GridRows(parent_object) + { + this.ParentObject = parent_object; + + // Array of rows in the order they were added + this.Rows = [ ]; + + // Collection of custom row indexes for fast lookup + this.Indexes = { }; + } + + + GridRows.prototype.AddIndex = function(cell_field_name) + { + var index = { }; + + // Go through existing rows and add to the index + for (var i in this.Rows) + { + var row = this.Rows[i]; + if (cell_field_name in row.CellData) + { + var cell_field = row.CellData[cell_field_name]; + index[cell_field] = row; + } + } + + this.Indexes[cell_field_name] = index; + } + + + GridRows.prototype.ClearIndex = function(index_name) + { + this.Indexes[index_name] = { }; + } + + GridRows.prototype.AddRowToIndex = function(index_name, cell_data, row) + { + this.Indexes[index_name][cell_data] = row; + } + + + GridRows.prototype.Add = function(cell_data, row_classes, cell_classes) + { + var row = new WM.GridRow(this.ParentObject, cell_data, row_classes, cell_classes); + this.Rows.push(row); + return row; + } + + + GridRows.prototype.GetBy = function(cell_field_name, cell_data) + { + var index = this.Indexes[cell_field_name]; + return index[cell_data]; + } + + + GridRows.prototype.Clear = function() + { + // Remove all node references from the parent + for (var i in this.Rows) + { + var row = this.Rows[i]; + row.Parent.BodyNode.removeChild(row.Node); + } + + // Clear all indexes + for (var i in this.Indexes) + this.Indexes[i] = { }; + + this.Rows = [ ]; + } + + + return GridRows; +})(); + + +WM.GridRow = (function() +{ + var template_html = "
"; + + + // + // '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(x, y, width, height) + { + this.Rows = new WM.GridRows(this); + + this.Node = DOM.Node.CreateHTML(template_html); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "GridBody"); + + DOM.Node.SetPosition(this.Node, [ x, y ]); + DOM.Node.SetSize(this.Node, [ width, height ]); + + DOM.Event.AddHandler(this.Node, "dblclick", OnDblClick); + + var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; + DOM.Event.AddHandler(this.Node, mouse_wheel_event, Bind(OnMouseScroll, this)); + } + + + function OnDblClick(evt) + { + // Clicked on a header? + var node = DOM.Event.GetNode(evt); + if (DOM.Node.HasClass(node, "GridRowName")) + { + // Toggle rows open/close + var row = node.parentNode.GridRow; + if (row) + row.Toggle(); + } + } + + + function OnMouseScroll(self, evt) + { + var mouse_state = new Mouse.State(evt); + self.Node.scrollTop -= mouse_state.WheelDelta * 20; + } + + + return Grid; +})(); diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Label.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Label.js new file mode 100644 index 0000000000..9b1d852511 --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Treeview.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Treeview.js new file mode 100644 index 0000000000..66ef80ed1f --- /dev/null +++ b/gazebo/common/Remotery/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/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js new file mode 100644 index 0000000000..ac6133e820 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/TreeviewItem.js @@ -0,0 +1,109 @@ + +namespace("WM"); + + +WM.TreeviewItem = (function() +{ + function TreeviewItem(treeview, name, data, open_image, close_image) + { + // Assign members + this.Treeview = treeview; + this.Label = name; + this.Data = data; + this.OpenImage = open_image; + this.CloseImage = close_image; + + this.Children = [ ]; + + // The HTML node wrapping the item and its children + this.Node = null; + + // The HTML node storing the image for the open/close state feedback + this.ImageNode = null; + + // The HTML node storing just the children + this.ChildrenNode = null; + + // Animation handle for opening and closing the child nodes, only used + // if the tree view item as children + this.AnimHandle = null; + + // Open state of the item + this.IsOpen = true; + } + + + TreeviewItem.prototype.AddItem = function(name, data, open_image, close_image) + { + var item = new WM.TreeviewItem(this.Treeview, name, data, open_image, close_image); + this.Children.push(item); + return item; + } + + + TreeviewItem.prototype.Open = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the open state + this.IsOpen = true; + if (this.ImageNode != null && this.OpenImage != null) + this.ImageNode.src = this.OpenImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var end_height = this.StartHeight; + var treeview = this.Treeview; + + // Reveal the children and animate their height to max + this.ChildrenNode.style.display = "block"; + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + 0, end_height, 0.2, + function() { treeview.UpdateScrollbar(); }); + + // Fade the children in + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 0, 1, 0.2); + } + } + + + TreeviewItem.prototype.Close = function() + { + if (this.AnimHandle == null || this.AnimHandle.Complete) + { + // Swap to the close state + this.IsOpen = false; + if (this.ImageNode != null && this.CloseImage != null) + this.ImageNode.src = this.CloseImage.src; + + // Cache for closure binding + var child_node = this.ChildrenNode; + var treeview = this.Treeview; + + // Mark the height of the item for reload later + this.StartHeight = child_node.offsetHeight; + + // Shrink the height of the children and hide them upon completion + this.AnimHandle = Anim.Animate( + function (val) { DOM.Node.SetHeight(child_node, val) }, + this.ChildrenNode.offsetHeight, 0, 0.2, + function() { child_node.style.display = "none"; treeview.UpdateScrollbar(); }); + + // Fade the children out + Anim.Animate(function(val) { DOM.Node.SetOpacity(child_node, val) }, 1, 0, 0.2); + } + } + + + TreeviewItem.prototype.Toggle = function() + { + if (this.IsOpen) + this.Close(); + else + this.Open(); + } + + + return TreeviewItem; +})(); diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Window.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Window.js new file mode 100644 index 0000000000..e5e986f7d0 --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/Window.js @@ -0,0 +1,243 @@ + +namespace("WM"); + + +WM.Window = (function() +{ + var template_html = " \ +
\ +
\ +
Window Title Bar
\ +
O
\ +
\ +
\ +
\ +
"; + + + function Window(manager, title, x, y, width, height, parent_node) + { + this.Manager = manager; + this.ParentNode = parent_node || document.body; + this.OnMove = null; + this.Visible = false; + this.AnimatedShow = false; + + // Clone the window template and locate key nodes within it + this.Node = DOM.Node.CreateHTML(template_html); + this.TitleBarNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBar"); + this.TitleBarTextNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarText"); + this.TitleBarCloseNode = DOM.Node.FindWithClass(this.Node, "WindowTitleBarClose"); + this.BodyNode = DOM.Node.FindWithClass(this.Node, "WindowBody"); + + // Setup the position and dimensions of the window + this.SetPosition(x, y); + this.SetSize(width, height); + + // Set the title text + this.TitleBarTextNode.innerHTML = title; + + // Hook up event handlers + DOM.Event.AddHandler(this.Node, "mousedown", Bind(this, "SetTop")); + DOM.Event.AddHandler(this.TitleBarNode, "mousedown", Bind(this, "BeginMove")); + DOM.Event.AddHandler(this.TitleBarCloseNode, "mousedown", Bind(this, "Hide")); + + // Create delegates for removable handlers + this.MoveDelegate = Bind(this, "Move"); + this.EndMoveDelegate = Bind(this, "EndMove"); + } + + + Window.prototype.SetOnMove = function(on_move) + { + this.OnMove = on_move; + } + + + Window.prototype.Show = function() + { + if (this.Node.parentNode != this.ParentNode) + { + this.ShowNoAnim(); + Anim.Animate(Bind(this, "OpenAnimation"), 0, 1, 1); + } + } + + + Window.prototype.ShowNoAnim = function() + { + // Add to the document + this.ParentNode.appendChild(this.Node); + this.AnimatedShow = false; + this.Visible = true; + } + + + Window.prototype.Hide = function() + { + if (this.Node.parentNode == this.ParentNode) + { + if (this.AnimatedShow) + { + // Trigger animation that ends with removing the window from the document + Anim.Animate( + Bind(this, "CloseAnimation"), + 0, 1, 0.25, + Bind(this, "HideNoAnim")); + } + else + { + this.HideNoAnim(); + } + } + } + + + Window.prototype.HideNoAnim = function() + { + // Remove node + this.ParentNode.removeChild(this.Node); + this.Visible = false; + } + + + Window.prototype.SetTop = function() + { + this.Manager.SetTopWindow(this); + } + + + + Window.prototype.SetTitle = function(title) + { + this.TitleBarTextNode.innerHTML = title; + } + + + // TODO: Update this + Window.prototype.AddControl = function(control) + { + // Get all arguments to this function and replace the first with this window node + var args = [].slice.call(arguments); + args[0] = this.BodyNode; + + // Create the control and call its Init method with the modified arguments + var instance = new control(); + instance.Init.apply(instance, args); + + return instance; + } + + + Window.prototype.AddControlNew = function(control) + { + control.ParentNode = this.BodyNode; + this.BodyNode.appendChild(control.Node); + return control; + } + + + Window.prototype.Scale = function(t) + { + // Calculate window bounds centre/extents + var ext_x = this.Size[0] / 2; + var ext_y = this.Size[1] / 2; + var mid_x = this.Position[0] + ext_x; + var mid_y = this.Position[1] + ext_y; + + // Scale from the mid-point + DOM.Node.SetPosition(this.Node, [ mid_x - ext_x * t, mid_y - ext_y * t ]); + DOM.Node.SetSize(this.Node, [ this.Size[0] * t, this.Size[1] * t ]); + } + + + Window.prototype.OpenAnimation = function(val) + { + // Power ease in + var t = 1 - Math.pow(1 - val, 8); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, 1 - Math.pow(1 - val, 8)); + this.AnimatedShow = true; + } + + + Window.prototype.CloseAnimation = function(val) + { + // Power ease out + var t = 1 - Math.pow(val, 4); + this.Scale(t); + DOM.Node.SetOpacity(this.Node, t); + } + + + Window.prototype.NotifyChange = function() + { + if (this.OnMove) + { + var pos = DOM.Node.GetPosition(this.Node); + this.OnMove(this, pos); + } + } + + + Window.prototype.BeginMove = function(evt) + { + // Calculate offset of the window from the mouse down position + var mouse_pos = DOM.Event.GetMousePosition(evt); + this.Offset = [ mouse_pos[0] - this.Position[0], mouse_pos[1] - this.Position[1] ]; + + // Dynamically add handlers for movement and release + DOM.Event.AddHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.AddHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.Move = function(evt) + { + // Use the offset at the beginning of movement to drag the window around + var mouse_pos = DOM.Event.GetMousePosition(evt); + var offset = this.Offset; + var pos = [ mouse_pos[0] - offset[0], mouse_pos[1] - offset[1] ]; + this.SetPosition(pos[0], pos[1]); + + if (this.OnMove) + this.OnMove(this, pos); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.EndMove = function(evt) + { + // Remove handlers added during mouse down + DOM.Event.RemoveHandler(document, "mousemove", this.MoveDelegate); + DOM.Event.RemoveHandler(document, "mouseup", this.EndMoveDelegate); + + DOM.Event.StopDefaultAction(evt); + } + + + Window.prototype.SetPosition = function(x, y) + { + this.Position = [ x, y ]; + DOM.Node.SetPosition(this.Node, this.Position); + } + + + Window.prototype.SetSize = function(w, h) + { + this.Size = [ w, h ]; + DOM.Node.SetSize(this.Node, this.Size); + } + + + Window.prototype.GetZIndex = function() + { + return parseInt(this.Node.style.zIndex); + } + + + return Window; +})(); \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js new file mode 100644 index 0000000000..bef33c2f1f --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Code/WindowManager.js @@ -0,0 +1,54 @@ + +namespace("WM"); + + +WM.WindowManager = (function() +{ + function WindowManager() + { + // An empty list of windows under window manager control + this.Windows = [ ]; + } + + + WindowManager.prototype.AddWindow = function(title, x, y, width, height, parent_node) + { + // Create the window and add it to the list of windows + var wnd = new WM.Window(this, title, x, y, width, height, parent_node); + this.Windows.push(wnd); + + // Always bring to the top on creation + wnd.SetTop(); + + return wnd; + } + + + WindowManager.prototype.SetTopWindow = function(top_wnd) + { + // Bring the window to the top of the window list + var top_wnd_index = this.Windows.indexOf(top_wnd); + if (top_wnd_index != -1) + this.Windows.splice(top_wnd_index, 1); + this.Windows.push(top_wnd); + + // Set a CSS z-index for each visible window from the bottom up + for (var i in this.Windows) + { + var wnd = this.Windows[i]; + if (!wnd.Visible) + continue; + + // Ensure there's space between each window for the elements inside to be sorted + var z = (parseInt(i) + 1) * 10; + wnd.Node.style.zIndex = z; + + // Notify window that its z-order has changed + wnd.NotifyChange(); + } + } + + + return WindowManager; + +})(); \ No newline at end of file diff --git a/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css new file mode 100644 index 0000000000..60b1d98e0d --- /dev/null +++ b/gazebo/common/Remotery/vis/extern/BrowserLib/WindowManager/Styles/WindowManager.css @@ -0,0 +1,552 @@ + + +.notextsel +{ + /* Disable text selection so that it doesn't interfere with button-clicking */ + user-select:none; + + /* Stops the text cursor over the label */ + cursor:default; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Window Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Window +{ + /* Allows movement of the window to exceed browser region without triggering scroll bars */ + position:fixed; + + /* Clip all contents to the window border */ + overflow: hidden; + + background: #404040; + + border-radius: 5px; + -moz-border-radius: 5px; + + -webkit-box-shadow: 3px 3px 3px #111, 1px 1px 1px #606060 inset; + box-shadow: 3px 3px 3px #111, 1px 1px 1px #606060 inset; +} + +.Window_Transparent +{ + /* Set transparency changes to fade in/out */ + opacity: 0.5; + transition: opacity 0.5s ease-out; + -moz-transition: opacity 0.5s ease-out; + -webkit-transition: opacity 0.5s ease-out; +} + +.Window_Transparent:hover +{ + opacity: 1; +} + +.WindowTitleBar +{ + height: 17px; + cursor: move; + + border-bottom: 1px solid #303030; + border-radius: 5px; +} + +.WindowTitleBarText +{ + color: #BBB; + font: 9px Verdana; + + padding: 3px; + cursor: move; +} + +.WindowTitleBarClose +{ + color: #999999; + font: 9px Verdana; + + padding: 3px; + cursor: default; +} + +.WindowBody +{ + /* Turns this node into a "positioned node" so that its children can be placed relative to it */ + position: absolute; + + /* Fill the parent window node */ + width: 100%; + height: 100%; + + padding:10px; + border-top: 1px solid #606060; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Container Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Container +{ + /* Position relative to the parent window */ + position: absolute; + + background:#2C2C2C; + + border: 1px black solid; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Treeview Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Treeview +{ + position: absolute; + + background:#2C2C2C; + border: 1px solid black; + overflow:hidden; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewItem +{ + margin:1px; + padding:2px; + border:solid 1px #2C2C2C; + background-color:#2C2C2C; +} + +.TreeviewItemImage +{ + float: left; +} + +.TreeviewItemText +{ + float: left; + margin-left:4px; +} + +.TreeviewItemChildren +{ + overflow: hidden; +} + +.TreeviewItemSelected +{ + background-color:#444; + border-color:#FFF; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +/* Used to populate treeviews that want highlight on hover behaviour */ +.TreeviewItemHover +{ +} + +.TreeviewItemHover:hover +{ + background-color:#111; + border-color:#444; + + -webkit-transition: background-color 0.2s ease-in-out; + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: border-color 0.2s ease-in-out; + -moz-transition: border-color 0.2s ease-in-out; +} + +.TreeviewScrollbarInset +{ + float: right; + + position:relative; + + height: 100%; + + /* CRAZINESS PART A: Trying to get the inset and scrollbar to have 100% height match its container */ + margin: -8px -8px 0 0; + padding: 0 1px 14px 1px; + + width:20px; + background:#2C2C2C; + border: 1px solid black; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + +.TreeviewScrollbar +{ + position:relative; + + background:#2C2C2C; + border: 1px solid black; + + /* CRAZINESS PART B: Trying to get the inset and scrollbar to have 100% height match its container */ + padding: 0 0 10px 0; + margin: 1px 0 0 0; + + width: 18px; + height: 100%; + + border-radius:6px; + border-color:#000; + border-width:1px; + border-style:solid; + + /* The gradient for the button background */ + background-color:#666; + background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#383838)); + background: -moz-linear-gradient(top, #666, #383838); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#666666', endColorstr='#383838'); + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.TreeviewScrollbarHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Treeview Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.EditBoxContainer +{ + position: absolute; + padding:2px 10px 2px 10px; +} + +.EditBoxLabel +{ + float:left; + padding: 3px 4px 4px 4px; + font: 9px Verdana; +} + +.EditBox +{ + float:left; + + background:#666; + border: 1px solid; + border-radius: 6px; + padding: 3px 4px 3px 4px; + height: 20px; + + box-shadow: 1px 1px 1px #222 inset; + + transition: all 0.3s ease-in-out; +} + +.EditBox:focus +{ + background:#FFF; + outline:0; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Label Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Label +{ + /* Position relative to the parent window */ + position:absolute; + + color: #BBB; + font: 9px Verdana; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Combo Box Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.ComboBox +{ + position:absolute; + + /* TEMP! */ + width:90px; + + /* Height is fixed to match the font */ + height:14px; + + /* Align the text within the combo box */ + padding: 1px 0 0 5px; + + /* Solid, rounded border */ + border: 1px solid #111; + border-radius: 5px; + + /* http://www.colorzilla.com/gradient-editor/#e3e3e3+0,c6c6c6+22,b7b7b7+33,afafaf+50,a7a7a7+67,797979+82,414141+100;Custom */ + background: #e3e3e3; + background: -moz-linear-gradient(top, #e3e3e3 0%, #c6c6c6 22%, #b7b7b7 33%, #afafaf 50%, #a7a7a7 67%, #797979 82%, #414141 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e3e3e3), color-stop(22%,#c6c6c6), color-stop(33%,#b7b7b7), color-stop(50%,#afafaf), color-stop(67%,#a7a7a7), color-stop(82%,#797979), color-stop(100%,#414141)); + background: -webkit-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -o-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: -ms-linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + background: linear-gradient(top, #e3e3e3 0%,#c6c6c6 22%,#b7b7b7 33%,#afafaf 50%,#a7a7a7 67%,#797979 82%,#414141 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e3e3e3', endColorstr='#414141',GradientType=0 ); +} + +.ComboBoxPressed +{ + /* The reverse of the default background, simulating depression */ + background: #414141; + background: -moz-linear-gradient(top, #414141 0%, #797979 18%, #a7a7a7 33%, #afafaf 50%, #b7b7b7 67%, #c6c6c6 78%, #e3e3e3 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#414141), color-stop(18%,#797979), color-stop(33%,#a7a7a7), color-stop(50%,#afafaf), color-stop(67%,#b7b7b7), color-stop(78%,#c6c6c6), color-stop(100%,#e3e3e3)); + background: -webkit-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -o-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: -ms-linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + background: linear-gradient(top, #414141 0%,#797979 18%,#a7a7a7 33%,#afafaf 50%,#b7b7b7 67%,#c6c6c6 78%,#e3e3e3 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#414141', endColorstr='#e3e3e3',GradientType=0 ); +} + +.ComboBoxText +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + float:left; +} + +.ComboBoxIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopup +{ + position: fixed; + + background: #CCC; + + border-radius: 5px; + + padding: 1px 0 1px 0; +} + +.ComboBoxPopupItem +{ + /* Text info */ + color: #000; + font: 9px Verdana; + + padding: 1px 1px 1px 5px; + + border-bottom: 1px solid #AAA; + border-top: 1px solid #FFF; +} + +.ComboBoxPopupItemText +{ + float:left; +} + +.ComboBoxPopupItemIcon +{ + /* Push the image to the far right */ + float:right; + + /* Align the image with the combo box */ + padding: 2px 5px 0 0; +} + +.ComboBoxPopupItem:first-child +{ + border-top: 0px; +} + +.ComboBoxPopupItem:last-child +{ + border-bottom: 0px; +} + +.ComboBoxPopupItem:hover +{ + color:#FFF; + background: #2036E1; +} + + + +/* ------------------------------------------------------------------------------------------------------------------ */ +/* Grid Styles */ +/* ------------------------------------------------------------------------------------------------------------------ */ + + + +.Grid +{ + /* Clip contents */ + overflow: hidden; + + position: relative; + + background: #333; + + border-radius: 2px; +} + +.GridBody +{ + 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:6px; + border-color:#000; + border-width:1px; + border-style:solid; + + /* Padding at the top includes 2px for the text drop-shadow */ + padding: 2px 5px 3px 5px; + + color: #BBB; + font: 9px Verdana; + text-shadow: 1px 1px 1px black; + text-align: center; + + /* The gradient for the button background */ + background-color:#666; + background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#383838)); + background: -moz-linear-gradient(top, #666, #383838); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#666666', endColorstr='#383838'); + + /* A box shadow and inset box highlight */ + -webkit-box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; + box-shadow: 1px 1px 1px #222, 1px 1px 1px #777 inset; +} + +.ButtonHeld +{ + /* Reset the gradient to a full-colour background */ + background:#383838; + + /* Two inset box shadows to simulate depressing */ + -webkit-box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; + box-shadow: -1px -1px 1px #222 inset, 1px 1px 1px #222 inset; +} diff --git a/gazebo/common/Remotery/vis/index.html b/gazebo/common/Remotery/vis/index.html new file mode 100644 index 0000000000..3e57256825 --- /dev/null +++ b/gazebo/common/Remotery/vis/index.html @@ -0,0 +1,55 @@ + + + + + + Remotery Viewer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gazebo/common/RemoteryConfig.h.in b/gazebo/common/RemoteryConfig.h.in new file mode 100644 index 0000000000..39825e865c --- /dev/null +++ b/gazebo/common/RemoteryConfig.h.in @@ -0,0 +1,13 @@ +#ifndef REMOTERY__REMOTERYCONFIG_H_ +#define REMOTERY__REMOTERYCONFIG_H_ + +#define RMT_ENABLED @RMT_ENABLED@ +#define RMT_USE_TINYCRT @RMT_USE_TINYCRT@ +#define RMT_USE_CUDA @RMT_USE_CUDA@ +#define RMT_USE_D3D11 @RMT_USE_D3D11@ +#define RMT_USE_OPENGL @RMT_USE_OPENGL@ +#define RMT_USE_METAL @RMT_USE_METAL@ + +#define RMT_DLL + +#endif // REMOTERY__REMOTERYCONFIG_H_ diff --git a/gazebo/common/RemoteryProfilerImpl.cc b/gazebo/common/RemoteryProfilerImpl.cc new file mode 100644 index 0000000000..0086f0b97d --- /dev/null +++ b/gazebo/common/RemoteryProfilerImpl.cc @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2018 Open Source Robotics Foundation + * + * 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. + * + */ + +/* + This is a backport of Ignition Common's profiler +*/ + +#include "RemoteryProfilerImpl.hh" +#include "gazebo/common/Console.hh" + +using namespace gazebo; +using namespace common; + +std::string rmtErrorToString(rmtError error) { + switch (error) { + case RMT_ERROR_NONE: + return "none"; + case RMT_ERROR_RECURSIVE_SAMPLE: + return "Not an error but an internal message to calling code"; + + // System errors + case RMT_ERROR_MALLOC_FAIL: + return "Malloc call within remotery failed"; + case RMT_ERROR_TLS_ALLOC_FAIL: + return "Attempt to allocate thread local storage failed"; + case RMT_ERROR_VIRTUAL_MEMORY_BUFFER_FAIL: + return "Failed to create a virtual memory mirror buffer"; + case RMT_ERROR_CREATE_THREAD_FAIL: + return "Failed to create a thread for the server"; + + // Network TCP/IP socket errors + case RMT_ERROR_SOCKET_INIT_NETWORK_FAIL: + return "Network initialisation failure (e.g. on Win32, WSAStartup fails)"; //NOLINT + case RMT_ERROR_SOCKET_CREATE_FAIL: + return "Can't create a socket for connection to the remote viewer"; + case RMT_ERROR_SOCKET_BIND_FAIL: + return "Can't bind a socket for the server"; + case RMT_ERROR_SOCKET_LISTEN_FAIL: + return "Created server socket failed to enter a listen state"; + case RMT_ERROR_SOCKET_SET_NON_BLOCKING_FAIL: + return "Created server socket failed to switch to a non-blocking state"; + case RMT_ERROR_SOCKET_INVALID_POLL: + return "Poll attempt on an invalid socket"; + case RMT_ERROR_SOCKET_SELECT_FAIL: + return "Server failed to call select on socket"; + case RMT_ERROR_SOCKET_POLL_ERRORS: + return "Poll notified that the socket has errors"; + case RMT_ERROR_SOCKET_ACCEPT_FAIL: + return "Server failed to accept connection from client"; + case RMT_ERROR_SOCKET_SEND_TIMEOUT: + return "Timed out trying to send data"; + case RMT_ERROR_SOCKET_SEND_FAIL: + return "Unrecoverable error occured while client/server tried to send data"; //NOLINT + case RMT_ERROR_SOCKET_RECV_NO_DATA: + return "No data available when attempting a receive"; + case RMT_ERROR_SOCKET_RECV_TIMEOUT: + return "Timed out trying to receive data"; + case RMT_ERROR_SOCKET_RECV_FAILED: + return "Unrecoverable error occured while client/server tried to receive data"; //NOLINT + + // WebSocket errors + case RMT_ERROR_WEBSOCKET_HANDSHAKE_NOT_GET: + return "WebSocket server handshake failed, not HTTP GET"; + case RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_VERSION: + return "WebSocket server handshake failed, can't locate WebSocket version"; //NOLINT + case RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_VERSION: + return "WebSocket server handshake failed, unsupported WebSocket version"; + case RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_HOST: + return "WebSocket server handshake failed, can't locate host"; + case RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_HOST: + return "WebSocket server handshake failed, host is not allowed to connect"; //NOLINT + case RMT_ERROR_WEBSOCKET_HANDSHAKE_NO_KEY: + return "WebSocket server handshake failed, can't locate WebSocket key"; + case RMT_ERROR_WEBSOCKET_HANDSHAKE_BAD_KEY: + return "WebSocket server handshake failed, WebSocket key is ill-formed"; + case RMT_ERROR_WEBSOCKET_HANDSHAKE_STRING_FAIL: + return "WebSocket server handshake failed, internal error, bad string code"; //NOLINT + case RMT_ERROR_WEBSOCKET_DISCONNECTED: + return "WebSocket server received a disconnect request and closed the socket"; //NOLINT + case RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER: + return "Couldn't parse WebSocket frame header"; + case RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_SIZE: + return "Partially received wide frame header size"; + case RMT_ERROR_WEBSOCKET_BAD_FRAME_HEADER_MASK: + return "Partially received frame header data mask"; + case RMT_ERROR_WEBSOCKET_RECEIVE_TIMEOUT: + return "Timeout receiving frame header"; + + case RMT_ERROR_REMOTERY_NOT_CREATED: + return "Remotery object has not been created"; + case RMT_ERROR_SEND_ON_INCOMPLETE_PROFILE: + return "An attempt was made to send an incomplete profile tree to the client"; // NOLINT + + // CUDA error messages + case RMT_ERROR_CUDA_DEINITIALIZED: + return "This indicates that the CUDA driver is in the process of shutting down"; //NOLINT + case RMT_ERROR_CUDA_NOT_INITIALIZED: + return "This indicates that the CUDA driver has not been initialized with cuInit() or that initialization has failed"; //NOLINT + case RMT_ERROR_CUDA_INVALID_CONTEXT: + return "This most frequently indicates that there is no context bound to the current thread"; //NOLINT + case RMT_ERROR_CUDA_INVALID_VALUE: + return "This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values"; //NOLINT + case RMT_ERROR_CUDA_INVALID_HANDLE: + return "This indicates that a resource handle passed to the API call was not valid"; //NOLINT + case RMT_ERROR_CUDA_OUT_OF_MEMORY: + return "The API call failed because it was unable to allocate enough memory to perform the requested operation"; //NOLINT + case RMT_ERROR_ERROR_NOT_READY: + return "This indicates that a resource handle passed to the API call was not valid"; //NOLINT + + // Direct3D 11 error messages + case RMT_ERROR_D3D11_FAILED_TO_CREATE_QUERY: + return "Failed to create query for sample"; + + // OpenGL error messages + case RMT_ERROR_OPENGL_ERROR: + return "Generic OpenGL error, no need to expose detail since app will need an OpenGL error callback registered"; //NOLINT + case RMT_ERROR_CUDA_UNKNOWN: + return "Unknown CUDA error"; + default: + return "Unknown remotery error"; + } +} + +////////////////////////////////////////////////// +RemoteryProfilerImpl::RemoteryProfilerImpl() +{ + this->settings = rmt_Settings(); + + // Always attempt to reuse the port + this->settings->reuse_open_port = RMT_TRUE; + + auto port_c = std::getenv("RMT_PORT"); + if(port_c){ + std::string port = std::string(std::getenv("RMT_PORT")); + if (!port.empty()) + { + this->settings->port = std::stoul(port); + } + } + + auto queue_size_c = std::getenv("RMT_QUEUE_SIZE"); + if(queue_size_c){ + std::string queue_size = std::string(queue_size_c); + if (!queue_size.empty()) + { + this->settings->messageQueueSizeInBytes = std::stoul(queue_size); + } + } else { + this->settings->messageQueueSizeInBytes = 2048 * 2048; + } + + auto messages_per_update_c = std::getenv("RMT_MSGS_PER_UPDATE"); + if(messages_per_update_c){ + std::string messages_per_update = std::string(messages_per_update_c); + if (!messages_per_update.empty()) + { + this->settings->maxNbMessagesPerUpdate = std::stoul(messages_per_update); + } + } else { + this->settings->maxNbMessagesPerUpdate = 10; + } + + auto sleep_between_updates_c = std::getenv("RMT_SLEEP_BETWEEN_UPDATES"); + if (sleep_between_updates_c) { + std::string sleep_between_updates = std::string(sleep_between_updates_c); + if (sleep_between_updates.empty()) + { + this->settings->msSleepBetweenServerUpdates = + std::stoul(sleep_between_updates); + } + } else { + this->settings->msSleepBetweenServerUpdates = 10; + } + + this->settings->input_handler_context = this; + this->settings->input_handler = [](const char *_text, void *_context) { + static_cast(_context)->HandleInput(_text); + }; + + gzmsg << "Staring profiler impl: Remotery" << + " (port: " << this->settings->port << ")" << std::endl; + rmtError error; + error = rmt_CreateGlobalInstance(&this->rmt); + + if (RMT_ERROR_NONE != error) + { + // gzerr << "Error launching Remotery: " << + // rmtErrorToString(error) << std::endl; + this->rmt = nullptr; + } +} + +////////////////////////////////////////////////// +RemoteryProfilerImpl::~RemoteryProfilerImpl() +{ + if (this->rmt) + rmt_DestroyGlobalInstance(this->rmt); +} + +////////////////////////////////////////////////// +std::string RemoteryProfilerImpl::Name() const +{ + return "ign_profiler_remotery"; +} + +////////////////////////////////////////////////// +void RemoteryProfilerImpl::SetThreadName(const char *_name) +{ + rmt_SetCurrentThreadName(_name); +} + +////////////////////////////////////////////////// +void RemoteryProfilerImpl::LogText(const char *_text) +{ + rmt_LogText(_text); +} + +////////////////////////////////////////////////// +void RemoteryProfilerImpl::BeginSample(const char *_name, uint32_t *_hash) +{ + _rmt_BeginCPUSample(_name, RMTSF_Aggregate, _hash); +} + +////////////////////////////////////////////////// +void RemoteryProfilerImpl::EndSample() +{ + rmt_EndCPUSample(); +} + +////////////////////////////////////////////////// +void RemoteryProfilerImpl::HandleInput(const char *_text) +{ + (void) _text; +} diff --git a/gazebo/common/RemoteryProfilerImpl.hh b/gazebo/common/RemoteryProfilerImpl.hh new file mode 100644 index 0000000000..9643a9c25f --- /dev/null +++ b/gazebo/common/RemoteryProfilerImpl.hh @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 Open Source Robotics Foundation + * + * 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. + * + */ + +/* + This is a backport of Ignition Common's profiler +*/ + +#ifndef GAZEBO_COMMON_REMOTERYPROFILERIMPL_HH_ +#define GAZEBO_COMMON_REMOTERYPROFILERIMPL_HH_ + +#include + +#include "gazebo/common/RemoteryConfig.h" +#include + +#include "ProfilerImpl.hh" + +namespace gazebo +{ + namespace common + { + /// \brief Remotery profiler implementation + /// + /// Used to provide Remotery (https://github.com/Celtoys/Remotery) profiler + /// implementation. + /// + /// Remotery will start and open a web socket that will allow users to view + /// live profiling information via the web browser. + /// + /// The Remotery profiler can additionally be configured via environment + /// variables at runtime. + /// + /// * RMT_PORT: Port to listen for incoming connections on. + /// * RMT_QUEUE_SIZE: Size of the internal message queues + /// * RMT_MSGS_PER_UPDATE: Upper limit on messages consumed per loop + /// * RMT_SLEEP_BETWEEN_UPDATES: Controls profile server update rate. + class RemoteryProfilerImpl: public ProfilerImpl + { + /// \brief Constructor. + public: RemoteryProfilerImpl(); + + /// \brief Destructor. + public: ~RemoteryProfilerImpl() final; + + /// \brief Retrieve profiler name. + public: std::string Name() const final; + + /// \brief Set the name of the current thread + /// \param[in] _name Name to set + public: void SetThreadName(const char *_name) final; + + /// \brief Log text to profiler output. + /// Will appear in the Remotery console. + /// \param[in] _text Text to log. + public: void LogText(const char *_text) final; + + /// \brief Begin a named profiling sample. + /// Begins a CPU profiler sample with a given name. Can optionally take + /// a hash parameter to be cached between executions of `BeginSample`, so + /// that hashes don't need to be recomputed. + /// \param[in] _name Name of the sample + /// \param[in,out] _hash An optional hash value that can be cached + /// between executions. + public: void BeginSample(const char *_name, uint32_t *_hash) final; + + /// \brief End a profiling sample. + public: void EndSample() final; + + /// \brief Handle input coming from Remotery web console. + /// \param[in] _text Incoming input. + public: void HandleInput(const char *_text); + + /// \brief Remotery settings. + private: rmtSettings *settings; + + /// \brief Remotery instance. + private: Remotery *rmt; + }; + } +} + +#endif // GAZEBO_COMMON_PROFILERIMPL_HH_ diff --git a/gazebo/physics/World.cc b/gazebo/physics/World.cc index bec7969878..8e3a446f6a 100644 --- a/gazebo/physics/World.cc +++ b/gazebo/physics/World.cc @@ -63,6 +63,7 @@ #include "gazebo/util/Diagnostics.hh" #include "gazebo/util/IntrospectionManager.hh" #include "gazebo/util/LogRecord.hh" +#include "gazebo/common/Profiler.hh" #include "gazebo/physics/Road.hh" #include "gazebo/physics/RayShape.hh" @@ -624,6 +625,8 @@ void World::Step() { DIAG_TIMER_START("World::Step"); + GZ_PROFILE("World::Step"); + GZ_PROFILE_BEGIN("loadPlugins"); /// need this because ODE does not call dxReallocateWorldProcessContext() /// until dWorld.*Step /// Plugins that manipulate joints (and probably other properties) require @@ -634,13 +637,17 @@ void World::Step() this->dataPtr->pluginsLoaded = true; } + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Step", "loadPlugins"); + GZ_PROFILE_BEGIN("publishWorldStats"); // Send statistics about the world simulation this->PublishWorldStats(); + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Step", "publishWorldStats"); + GZ_PROFILE_BEGIN("sleepOffset"); if (this->dataPtr->waitForSensors) this->dataPtr->waitForSensors(this->dataPtr->simTime.Double(), this->dataPtr->physicsEngine->GetMaxStepSize()); @@ -664,8 +671,10 @@ void World::Step() this->dataPtr->sleepOffset = (actualSleep - sleepTime) * 0.01 + this->dataPtr->sleepOffset * 0.99; + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Step", "sleepOffset"); + GZ_PROFILE_BEGIN("worldUpdateMutex"); // throttling update rate, with sleepOffset as tolerance // the tolerance is needed as the sleep time is not exact if (common::Time::GetWallTime() - this->dataPtr->prevStepWallTime + @@ -700,6 +709,9 @@ void World::Step() this->dataPtr->pauseTime += stepTime; } } + GZ_PROFILE_END(); + + GZ_PROFILE_BEGIN("Step"); gazebo::util::IntrospectionManager::Instance()->NotifyUpdates(); @@ -709,6 +721,7 @@ void World::Step() if (g_clearModels) this->ClearModels(); + GZ_PROFILE_END(); } ////////////////////////////////////////////////// @@ -741,6 +754,8 @@ void World::Update() { DIAG_TIMER_START("World::Update"); + GZ_PROFILE("World::Update"); + GZ_PROFILE_BEGIN("needsReset"); if (this->dataPtr->needsReset) { if (this->dataPtr->resetAll) @@ -752,24 +767,29 @@ void World::Update() this->dataPtr->needsReset = false; return; } + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "needsReset"); + GZ_PROFILE_BEGIN("worldUpdateBegin"); this->dataPtr->updateInfo.simTime = this->SimTime(); this->dataPtr->updateInfo.realTime = this->RealTime(); event::Events::worldUpdateBegin(this->dataPtr->updateInfo); - + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "Events::worldUpdateBegin"); + GZ_PROFILE_BEGIN("Update"); // Update all the models (*this.*dataPtr->modelUpdateFunc)(); - + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "Model::Update"); + GZ_PROFILE_BEGIN("UpdateCollision"); // This must be called before PhysicsEngine::UpdatePhysics for ODE. this->dataPtr->physicsEngine->UpdateCollision(); - + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "PhysicsEngine::UpdateCollision"); + GZ_PROFILE_BEGIN("beforePhysicsUpdate"); // Wait for logging to finish, if it's running. if (util::LogRecord::Instance()->Running()) { @@ -790,20 +810,24 @@ void World::Update() this->dataPtr->updateInfo.realTime = this->RealTime(); event::Events::beforePhysicsUpdate(this->dataPtr->updateInfo); + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "Events::beforePhysicsUpdate"); // Update the physics engine if (this->dataPtr->enablePhysicsEngine && this->dataPtr->physicsEngine) { + GZ_PROFILE_BEGIN("UpdatePhysics"); // This must be called directly after PhysicsEngine::UpdateCollision. this->dataPtr->physicsEngine->UpdatePhysics(); + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "PhysicsEngine::UpdatePhysics"); // do this after physics update as // ode --> MoveCallback sets the dirtyPoses // and we need to propagate it into Entity::worldPose { + GZ_PROFILE_BEGIN("SetWorldPose(dirtyPoses)"); // block any other pose updates (e.g. Joint::SetPosition) boost::recursive_mutex::scoped_lock plock( *this->Physics()->GetPhysicsUpdateMutex()); @@ -814,19 +838,24 @@ void World::Update() } this->dataPtr->dirtyPoses.clear(); + GZ_PROFILE_END(); } DIAG_TIMER_LAP("World::Update", "SetWorldPose(dirtyPoses)"); } + GZ_PROFILE_BEGIN("LogRecordNotify"); // Only update state information if logging data. if (util::LogRecord::Instance()->Running()) this->dataPtr->logCondition.notify_one(); + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "LogRecordNotify"); + GZ_PROFILE_BEGIN("PublishContacts"); // Output the contact information this->dataPtr->physicsEngine->GetContactManager()->PublishContacts(); + GZ_PROFILE_END(); DIAG_TIMER_LAP("World::Update", "ContactManager::PublishContacts"); event::Events::worldUpdateEnd(); diff --git a/gazebo/physics/bullet/BulletPhysics.cc b/gazebo/physics/bullet/BulletPhysics.cc index 587874c52c..598861b432 100644 --- a/gazebo/physics/bullet/BulletPhysics.cc +++ b/gazebo/physics/bullet/BulletPhysics.cc @@ -18,6 +18,7 @@ #include #include +#include #include #include "gazebo/physics/bullet/BulletTypes.hh" @@ -384,6 +385,7 @@ void BulletPhysics::Init() ////////////////////////////////////////////////// void BulletPhysics::InitForThread() { + GZ_PROFILE_THREAD_NAME("BullerPhysics"); } ///////////////////////////////////////////////// @@ -485,6 +487,8 @@ void BulletPhysics::OnPhysicsMsg(ConstPhysicsPtr &_msg) ////////////////////////////////////////////////// void BulletPhysics::UpdateCollision() { + GZ_PROFILE("BulletPhysics:UpdateCollision"); + this->contactManager->ResetCount(); if (!this->world->PhysicsEnabled()) @@ -499,21 +503,31 @@ void BulletPhysics::UpdateCollision() // points to happen. So because UpdatePhysics() won't be // called, we have to do this here with // this->dynamicsWorld->performDiscreteCollisionDetection(). + + GZ_PROFILE_BEGIN("performDiscreteCollisionDetection"); this->dynamicsWorld->performDiscreteCollisionDetection(); + GZ_PROFILE_END(); + // In addition, the contacts have to be updated in the contact // manager and for the feedback. + GZ_PROFILE_BEGIN("UpdateContacts"); UpdateContacts(this->dynamicsWorld, this->maxStepSize); + GZ_PROFILE_END(); } } ////////////////////////////////////////////////// void BulletPhysics::UpdatePhysics() { + GZ_PROFILE("BulletPhysics:UpdatePhysics"); + // need to lock, otherwise might conflict with world resetting boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex); + GZ_PROFILE_BEGIN("stepSimulation"); this->dynamicsWorld->stepSimulation( this->maxStepSize, 1, this->maxStepSize); + GZ_PROFILE_END(); } ////////////////////////////////////////////////// diff --git a/gazebo/physics/dart/DARTPhysics.cc b/gazebo/physics/dart/DARTPhysics.cc index 3ae9773c85..4b4f1d6908 100644 --- a/gazebo/physics/dart/DARTPhysics.cc +++ b/gazebo/physics/dart/DARTPhysics.cc @@ -56,6 +56,8 @@ #include "gazebo/physics/dart/DARTPhysicsPrivate.hh" +#include "gazebo/common/Profiler.hh" + using namespace gazebo; using namespace physics; @@ -120,6 +122,7 @@ void DARTPhysics::Reset() ////////////////////////////////////////////////// void DARTPhysics::InitForThread() { + GZ_PROFILE_THREAD_NAME("DARTPhysics"); } @@ -396,6 +399,9 @@ static void RetrieveDARTCollisions( ////////////////////////////////////////////////// void DARTPhysics::UpdateCollision() { + GZ_PROFILE("DARTPhysics::UpdateCollision"); + GZ_PROFILE_BEGIN("UpdateCollision"); + if (!this->world->PhysicsEnabled()) { dart::collision::CollisionResult localResult; @@ -412,11 +418,15 @@ void DARTPhysics::UpdateCollision() RetrieveDARTCollisions(this, &localResult, this->GetContactManager()); } + GZ_PROFILE_END(); } ////////////////////////////////////////////////// void DARTPhysics::UpdatePhysics() { + GZ_PROFILE("DARTPhysics::UpdatePhysics"); + GZ_PROFILE_BEGIN("Update"); + // need to lock, otherwise might conflict with world resetting boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex); @@ -451,6 +461,7 @@ void DARTPhysics::UpdatePhysics() this, &(this->dataPtr->dtWorld->getLastCollisionResult()), this->GetContactManager()); + GZ_PROFILE_END(); } ////////////////////////////////////////////////// @@ -811,4 +822,3 @@ DARTLinkPtr DARTPhysics::FindDARTLink( { return StaticFindDARTLink(this, _dtBodyNode); } - diff --git a/gazebo/physics/ode/ODEPhysics.cc b/gazebo/physics/ode/ODEPhysics.cc index 2601431760..d1c77bdb55 100644 --- a/gazebo/physics/ode/ODEPhysics.cc +++ b/gazebo/physics/ode/ODEPhysics.cc @@ -28,6 +28,7 @@ #include #include +#include #include "gazebo/util/Diagnostics.hh" #include "gazebo/common/Assert.hh" @@ -352,6 +353,7 @@ void ODEPhysics::Init() ////////////////////////////////////////////////// void ODEPhysics::InitForThread() { + GZ_PROFILE_THREAD_NAME("ODEPhysics"); dAllocateODEDataForThread(dAllocateMaskAll); } @@ -359,6 +361,8 @@ void ODEPhysics::InitForThread() void ODEPhysics::UpdateCollision() { DIAG_TIMER_START("ODEPhysics::UpdateCollision"); + GZ_PROFILE("ODEPhysics:UpdateCollision"); + GZ_PROFILE_BEGIN("dSpaceCollide"); boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex); dJointGroupEmpty(this->dataPtr->contactGroup); @@ -374,7 +378,9 @@ void ODEPhysics::UpdateCollision() // Do collision detection; this will add contacts to the contact group dSpaceCollide(this->dataPtr->spaceId, this, CollisionCallback); DIAG_TIMER_LAP("ODEPhysics::UpdateCollision", "dSpaceCollide"); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("collideShapes"); // Generate non-trimesh collisions. for (i = 0; i < this->dataPtr->collidersCount; ++i) { @@ -382,7 +388,10 @@ void ODEPhysics::UpdateCollision() this->dataPtr->colliders[i].second, this->dataPtr->contactCollisions); } DIAG_TIMER_LAP("ODEPhysics::UpdateCollision", "collideShapes"); + GZ_PROFILE_END(); + + GZ_PROFILE_BEGIN("collideTrimeshes"); // Generate trimesh collision. // This must happen in this thread sequentially for (i = 0; i < this->dataPtr->trimeshCollidersCount; ++i) @@ -391,7 +400,8 @@ void ODEPhysics::UpdateCollision() ODECollision *collision2 = this->dataPtr->trimeshColliders[i].second; this->Collide(collision1, collision2, this->dataPtr->contactCollisions); } - DIAG_TIMER_LAP("ODEPhysics::UpdateCollision", "collideTrimeshes"); + DIAG_TIMER_LAP("UpdateCollision", "collideTrimeshes"); + GZ_PROFILE_END(); DIAG_TIMER_STOP("ODEPhysics::UpdateCollision"); } @@ -400,6 +410,7 @@ void ODEPhysics::UpdateCollision() void ODEPhysics::UpdatePhysics() { DIAG_TIMER_START("ODEPhysics::UpdatePhysics"); + GZ_PROFILE("ODEPhysics:UpdatePhysics"); // need to lock, otherwise might conflict with world resetting { @@ -936,6 +947,7 @@ void ODEPhysics::SetGravity(const ignition::math::Vector3d &_gravity) ////////////////////////////////////////////////// void ODEPhysics::CollisionCallback(void *_data, dGeomID _o1, dGeomID _o2) { + GZ_PROFILE("ODEPhysics::CollisionCallback"); dBodyID b1 = dGeomGetBody(_o1); dBodyID b2 = dGeomGetBody(_o2); diff --git a/gazebo/physics/simbody/SimbodyPhysics.cc b/gazebo/physics/simbody/SimbodyPhysics.cc index 2d163f5915..1432e6c3e0 100644 --- a/gazebo/physics/simbody/SimbodyPhysics.cc +++ b/gazebo/physics/simbody/SimbodyPhysics.cc @@ -17,6 +17,8 @@ #include +#include + #include "gazebo/physics/simbody/SimbodyTypes.hh" #include "gazebo/physics/simbody/SimbodyModel.hh" #include "gazebo/physics/simbody/SimbodyLink.hh" @@ -404,11 +406,14 @@ void SimbodyPhysics::InitModel(const physics::ModelPtr _model) ////////////////////////////////////////////////// void SimbodyPhysics::InitForThread() { + GZ_PROFILE_THREAD_NAME("SimbodyPhysics"); } ////////////////////////////////////////////////// void SimbodyPhysics::UpdateCollision() { + GZ_PROFILE("SimbodyPhysics::UpdateCollision"); + GZ_PROFILE_BEGIN("UpdateCollision"); boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex); this->contactManager->ResetCount(); @@ -632,11 +637,15 @@ void SimbodyPhysics::UpdateCollision() } } } + GZ_PROFILE_END(); } ////////////////////////////////////////////////// void SimbodyPhysics::UpdatePhysics() { + GZ_PROFILE("SimbodyPhysics::UpdatePhysics"); + GZ_PROFILE_BEGIN("UpdatePhysics"); + // need to lock, otherwise might conflict with world resetting boost::recursive_mutex::scoped_lock lock(*this->physicsUpdateMutex); @@ -705,6 +714,7 @@ void SimbodyPhysics::UpdatePhysics() // FIXME: this needs to happen before forces are applied for the next step // FIXME: but after we've gotten everything from current state this->discreteForces.clearAllForces(this->integ->updAdvancedState()); + GZ_PROFILE_END(); } ////////////////////////////////////////////////// diff --git a/gazebo/sensors/AltimeterSensor.cc b/gazebo/sensors/AltimeterSensor.cc index 3fcd72fef8..51e628584f 100644 --- a/gazebo/sensors/AltimeterSensor.cc +++ b/gazebo/sensors/AltimeterSensor.cc @@ -15,6 +15,9 @@ * */ #include + +#include "gazebo/common/Profiler.hh" + #include "gazebo/common/common.hh" #include "gazebo/physics/physics.hh" #include "gazebo/transport/transport.hh" @@ -122,6 +125,8 @@ void AltimeterSensor::Init() ////////////////////////////////////////////////// bool AltimeterSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("AltimeterSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); std::lock_guard lock(this->dataPtr->mutex); // Get latest pose information @@ -161,14 +166,15 @@ bool AltimeterSensor::UpdateImpl(const bool /*_force*/) this->dataPtr->altMsg.set_vertical_velocity(altVel.Z()); } } - + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); // Save the time of the measurement msgs::Set(this->dataPtr->altMsg.mutable_time(), this->world->SimTime()); // Publish the message if needed if (this->dataPtr->altPub) this->dataPtr->altPub->Publish(this->dataPtr->altMsg); - + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/CameraSensor.cc b/gazebo/sensors/CameraSensor.cc index 236c124aed..1df3f77e24 100644 --- a/gazebo/sensors/CameraSensor.cc +++ b/gazebo/sensors/CameraSensor.cc @@ -41,6 +41,8 @@ #include "gazebo/sensors/CameraSensorPrivate.hh" #include "gazebo/sensors/CameraSensor.hh" +#include "gazebo/common/Profiler.hh" + using namespace gazebo; using namespace sensors; @@ -260,11 +262,16 @@ void CameraSensor::Render() ////////////////////////////////////////////////// bool CameraSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("CameraSensor::UpdateImpl"); + if (!this->dataPtr->rendered) return false; + GZ_PROFILE_BEGIN("PostRender"); this->camera->PostRender(); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("fillarray"); if ((this->imagePub && this->imagePub->HasConnections()) || this->imagePubIgn.HasConnections()) @@ -310,6 +317,7 @@ bool CameraSensor::UpdateImpl(const bool /*_force*/) } this->dataPtr->rendered = false; + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/ContactSensor.cc b/gazebo/sensors/ContactSensor.cc index 43e6df3090..fbb0c07f1b 100644 --- a/gazebo/sensors/ContactSensor.cc +++ b/gazebo/sensors/ContactSensor.cc @@ -17,6 +17,8 @@ #include #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/common/Exception.hh" #include "gazebo/transport/Node.hh" @@ -131,6 +133,9 @@ void ContactSensor::Init() ////////////////////////////////////////////////// bool ContactSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("ContactSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + std::lock_guard lock(this->dataPtr->mutex); // Don't do anything if there is no new data to process. @@ -186,6 +191,9 @@ bool ContactSensor::UpdateImpl(const bool /*_force*/) } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); + // Clear the incoming contact list. this->dataPtr->incomingContacts.clear(); @@ -200,6 +208,7 @@ bool ContactSensor::UpdateImpl(const bool /*_force*/) this->dataPtr->contactsPub->Publish(this->dataPtr->contactsMsg); } + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/DepthCameraSensor.cc b/gazebo/sensors/DepthCameraSensor.cc index 52134001c9..58b04b5bb4 100644 --- a/gazebo/sensors/DepthCameraSensor.cc +++ b/gazebo/sensors/DepthCameraSensor.cc @@ -16,6 +16,8 @@ */ #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/physics/World.hh" #include "gazebo/rendering/DepthCamera.hh" @@ -134,10 +136,15 @@ void DepthCameraSensor::Init() ////////////////////////////////////////////////// bool DepthCameraSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("DepthCameraSensor::UpdateImpl"); if (!this->Rendered()) return false; + GZ_PROFILE_BEGIN("PostRender"); this->camera->PostRender(); + GZ_PROFILE_END(); + + GZ_PROFILE_BEGIN("fillarray"); if (this->imagePub && this->imagePub->HasConnections() && // check if depth data is available. If not, the depth camera could be @@ -182,6 +189,7 @@ bool DepthCameraSensor::UpdateImpl(const bool /*_force*/) } this->SetRendered(false); + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/ForceTorqueSensor.cc b/gazebo/sensors/ForceTorqueSensor.cc index 275654d2f6..ed6d14c7bd 100644 --- a/gazebo/sensors/ForceTorqueSensor.cc +++ b/gazebo/sensors/ForceTorqueSensor.cc @@ -16,6 +16,8 @@ */ #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/physics/World.hh" #include "gazebo/physics/PhysicsEngine.hh" #include "gazebo/physics/Joint.hh" @@ -200,6 +202,9 @@ ignition::math::Vector3d ForceTorqueSensor::Torque() const ////////////////////////////////////////////////// bool ForceTorqueSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("ForceTorqueSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + std::lock_guard lock(this->dataPtr->mutex); this->lastMeasurementTime = this->world->SimTime(); @@ -259,6 +264,9 @@ bool ForceTorqueSensor::UpdateImpl(const bool /*_force*/) } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); + msgs::Set(this->dataPtr->wrenchMsg.mutable_wrench()->mutable_force(), measuredForce); msgs::Set(this->dataPtr->wrenchMsg.mutable_wrench()->mutable_torque(), @@ -268,6 +276,7 @@ bool ForceTorqueSensor::UpdateImpl(const bool /*_force*/) if (this->dataPtr->wrenchPub) this->dataPtr->wrenchPub->Publish(this->dataPtr->wrenchMsg); + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/GpsSensor.cc b/gazebo/sensors/GpsSensor.cc index 6a4d1608fa..8a0113d716 100644 --- a/gazebo/sensors/GpsSensor.cc +++ b/gazebo/sensors/GpsSensor.cc @@ -16,6 +16,8 @@ */ #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/sensors/SensorFactory.hh" #include "gazebo/common/common.hh" @@ -123,6 +125,9 @@ void GpsSensor::Init() ////////////////////////////////////////////////// bool GpsSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("GpsSensor::UpdateImpl"); + + GZ_PROFILE_BEGIN("Update"); // Get latest pose information if (this->dataPtr->parentLink) { @@ -179,10 +184,12 @@ bool GpsSensor::UpdateImpl(const bool /*_force*/) this->lastMeasurementTime = this->world->SimTime(); msgs::Set(this->dataPtr->lastGpsMsg.mutable_time(), this->lastMeasurementTime); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); if (this->dataPtr->gpsPub) this->dataPtr->gpsPub->Publish(this->dataPtr->lastGpsMsg); - + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/GpuRaySensor.cc b/gazebo/sensors/GpuRaySensor.cc index 076143fa12..8775d2d31c 100644 --- a/gazebo/sensors/GpuRaySensor.cc +++ b/gazebo/sensors/GpuRaySensor.cc @@ -15,6 +15,7 @@ * */ #include +#include "gazebo/common/Profiler.hh" #include #include #include @@ -626,10 +627,15 @@ void GpuRaySensor::Render() ////////////////////////////////////////////////// bool GpuRaySensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("GpuRaySensor::UpdateImpl"); + if (!this->dataPtr->rendered) return false; - + GZ_PROFILE_BEGIN("PostRender"); this->dataPtr->laserCam->PostRender(); + GZ_PROFILE_END(); + + GZ_PROFILE_BEGIN("fillarray"); std::lock_guard lock(this->dataPtr->mutex); @@ -701,7 +707,7 @@ bool GpuRaySensor::UpdateImpl(const bool /*_force*/) this->dataPtr->scanPub->Publish(this->dataPtr->laserMsg); this->dataPtr->rendered = false; - + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/ImuSensor.cc b/gazebo/sensors/ImuSensor.cc index d7ddd55c25..310e89b2a1 100644 --- a/gazebo/sensors/ImuSensor.cc +++ b/gazebo/sensors/ImuSensor.cc @@ -15,6 +15,7 @@ * */ #include +#include "gazebo/common/Profiler.hh" #include #include "gazebo/transport/Node.hh" @@ -300,6 +301,8 @@ void ImuSensor::SetWorldToReferenceOrientation( ////////////////////////////////////////////////// bool ImuSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("ImuSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); msgs::LinkData msg; int readIndex = 0; @@ -430,10 +433,13 @@ bool ImuSensor::UpdateImpl(const bool /*_force*/) break; } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); // Publish the message if (this->dataPtr->pub) this->dataPtr->pub->Publish(this->dataPtr->imuMsg); + GZ_PROFILE_END(); } return true; diff --git a/gazebo/sensors/LogicalCameraSensor.cc b/gazebo/sensors/LogicalCameraSensor.cc index 2879359218..9ce4e8398b 100644 --- a/gazebo/sensors/LogicalCameraSensor.cc +++ b/gazebo/sensors/LogicalCameraSensor.cc @@ -15,6 +15,7 @@ * */ #include +#include "gazebo/common/Profiler.hh" #include "gazebo/transport/transport.hh" #include "gazebo/msgs/msgs.hh" #include "gazebo/physics/World.hh" @@ -139,9 +140,11 @@ void LogicalCameraSensorPrivate::AddVisibleModels( ////////////////////////////////////////////////// bool LogicalCameraSensor::UpdateImpl(const bool _force) { + GZ_PROFILE("LogicalCameraSensor::UpdateImpl"); // Only compute if active, or the update is forced if (_force || this->IsActive()) { + GZ_PROFILE_BEGIN("Update"); std::lock_guard lock(this->dataPtr->mutex); this->dataPtr->msg.clear_model(); @@ -157,9 +160,12 @@ bool LogicalCameraSensor::UpdateImpl(const bool _force) // Recursively check if models and nested models are in the frustum. this->dataPtr->AddVisibleModels(myPose, this->world->Models()); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); // Send the message. this->dataPtr->pub->Publish(this->dataPtr->msg); + GZ_PROFILE_END(); } return true; diff --git a/gazebo/sensors/MagnetometerSensor.cc b/gazebo/sensors/MagnetometerSensor.cc index b9f9ecfbb1..4d5da1be13 100644 --- a/gazebo/sensors/MagnetometerSensor.cc +++ b/gazebo/sensors/MagnetometerSensor.cc @@ -15,6 +15,7 @@ * */ #include +#include "gazebo/common/Profiler.hh" #include #include "gazebo/transport/Node.hh" @@ -127,6 +128,8 @@ void MagnetometerSensor::Init() ////////////////////////////////////////////////// bool MagnetometerSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("MagnetometerSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); std::lock_guard lock(this->dataPtr->mutex); // Get latest pose information @@ -171,10 +174,13 @@ bool MagnetometerSensor::UpdateImpl(const bool /*_force*/) // Save the time of the measurement msgs::Set(this->dataPtr->magMsg.mutable_time(), this->world->SimTime()); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); // Publish the message if needed if (this->dataPtr->magPub) this->dataPtr->magPub->Publish(this->dataPtr->magMsg); + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/MultiCameraSensor.cc b/gazebo/sensors/MultiCameraSensor.cc index 38a009d7ff..9fc591e3d0 100644 --- a/gazebo/sensors/MultiCameraSensor.cc +++ b/gazebo/sensors/MultiCameraSensor.cc @@ -16,6 +16,7 @@ */ #include #include +#include "gazebo/common/Profiler.hh" #include #include "gazebo/common/Exception.hh" @@ -310,6 +311,9 @@ void MultiCameraSensor::Render() ////////////////////////////////////////////////// bool MultiCameraSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("MultiCameraSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + std::lock_guard lock(this->dataPtr->cameraMutex); if (!this->dataPtr->rendered) @@ -333,9 +337,12 @@ bool MultiCameraSensor::UpdateImpl(const bool /*_force*/) image->width() * (*iter)->ImageDepth() * image->height()); } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); if (publish) this->dataPtr->imagePub->Publish(this->dataPtr->msg); + GZ_PROFILE_END(); this->dataPtr->rendered = false; return true; diff --git a/gazebo/sensors/RFIDSensor.cc b/gazebo/sensors/RFIDSensor.cc index ea3316ea5c..223558e32e 100644 --- a/gazebo/sensors/RFIDSensor.cc +++ b/gazebo/sensors/RFIDSensor.cc @@ -14,6 +14,8 @@ * limitations under the License. * */ +#include "gazebo/common/Profiler.hh" + #include "gazebo/msgs/msgs.hh" #include "gazebo/transport/transport.hh" #include "gazebo/physics/World.hh" @@ -115,14 +117,19 @@ void RFIDSensor::Init() ////////////////////////////////////////////////// bool RFIDSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("RFIDSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("EvaluateTags"); this->EvaluateTags(); + GZ_PROFILE_END(); this->lastMeasurementTime = this->world->SimTime(); if (this->dataPtr->scanPub) { + GZ_PROFILE_BEGIN("Publish"); msgs::Pose msg; msgs::Set(&msg, this->dataPtr->entity->WorldPose()); this->dataPtr->scanPub->Publish(msg); + GZ_PROFILE_END(); } return true; diff --git a/gazebo/sensors/RaySensor.cc b/gazebo/sensors/RaySensor.cc index 339f8897ed..56638205ef 100644 --- a/gazebo/sensors/RaySensor.cc +++ b/gazebo/sensors/RaySensor.cc @@ -16,6 +16,8 @@ */ #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/physics/World.hh" #include "gazebo/physics/MultiRayShape.hh" #include "gazebo/physics/PhysicsEngine.hh" @@ -337,6 +339,8 @@ int RaySensor::Fiducial(const unsigned int _index) const ////////////////////////////////////////////////// bool RaySensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("RaySensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); // do the collision checks // this eventually call OnNewScans, so move mutex lock behind it in case // need to move mutex lock after this? or make the OnNewLaserScan connection @@ -491,9 +495,12 @@ bool RaySensor::UpdateImpl(const bool /*_force*/) scan->add_intensities(intensity); } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); if (this->dataPtr->scanPub && this->dataPtr->scanPub->HasConnections()) this->dataPtr->scanPub->Publish(this->dataPtr->laserMsg); + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/Sensor.cc b/gazebo/sensors/Sensor.cc index b34d5ffe0f..68ca997284 100644 --- a/gazebo/sensors/Sensor.cc +++ b/gazebo/sensors/Sensor.cc @@ -14,6 +14,8 @@ * limitations under the License. * */ +#include "gazebo/common/Profiler.hh" + #include "gazebo/transport/transport.hh" #include "gazebo/physics/PhysicsIface.hh" diff --git a/gazebo/sensors/SensorManager.cc b/gazebo/sensors/SensorManager.cc index 6c663ec618..357b610c93 100644 --- a/gazebo/sensors/SensorManager.cc +++ b/gazebo/sensors/SensorManager.cc @@ -14,6 +14,8 @@ * limitations under the License. * */ +#include "gazebo/common/Profiler.hh" + #include #include #include "gazebo/common/Assert.hh" @@ -616,8 +618,12 @@ void SensorManager::SensorContainer::RunLoop() computeMaxUpdateRate(); + GZ_PROFILE_THREAD_NAME("SensorManager"); + while (!this->stop) { + GZ_PROFILE("SensorManager::RunLoop"); + // If all the sensors get deleted, wait here. // Use a while loop since world resets will notify the runCondition. while (this->sensors.empty()) @@ -632,7 +638,9 @@ void SensorManager::SensorContainer::RunLoop() // Get the start time of the update. startTime = world->SimTime(); + GZ_PROFILE_BEGIN("UpdateSensors"); this->Update(false); + GZ_PROFILE_END(); // Compute the time it took to update the sensors. // It's possible that the world time was reset during the Update. This @@ -667,10 +675,12 @@ void SensorManager::SensorContainer::RunLoop() eventTime, &this->runCondition); // This if statement helps prevent deadlock on osx during teardown. + GZ_PROFILE_BEGIN("Sleeping"); if (!this->stop) { this->runCondition.wait(timingLock); } + GZ_PROFILE_END(); } } @@ -687,7 +697,9 @@ void SensorManager::SensorContainer::Update(bool _force) iter != this->sensors.end(); ++iter) { GZ_ASSERT((*iter) != nullptr, "Sensor is null"); + GZ_PROFILE_BEGIN((*iter)->Name().c_str()); (*iter)->Update(_force); + GZ_PROFILE_END(); } } diff --git a/gazebo/sensors/SonarSensor.cc b/gazebo/sensors/SonarSensor.cc index 611ac050c1..9d6ca11896 100644 --- a/gazebo/sensors/SonarSensor.cc +++ b/gazebo/sensors/SonarSensor.cc @@ -16,6 +16,7 @@ */ #include +#include "gazebo/common/Profiler.hh" #include #include "gazebo/physics/World.hh" @@ -252,6 +253,9 @@ double SonarSensor::Range() ////////////////////////////////////////////////// bool SonarSensor::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("SonarSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + std::lock_guard lock(this->dataPtr->mutex); this->lastMeasurementTime = this->world->SimTime(); @@ -313,11 +317,14 @@ bool SonarSensor::UpdateImpl(const bool /*_force*/) // Clear the incoming contact list. this->dataPtr->incomingContacts.clear(); + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); this->dataPtr->update(this->dataPtr->sonarMsg); if (this->dataPtr->sonarPub) this->dataPtr->sonarPub->Publish(this->dataPtr->sonarMsg); + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/WideAngleCameraSensor.cc b/gazebo/sensors/WideAngleCameraSensor.cc index b079ac09c0..d1dc5e23be 100644 --- a/gazebo/sensors/WideAngleCameraSensor.cc +++ b/gazebo/sensors/WideAngleCameraSensor.cc @@ -19,6 +19,8 @@ #include +#include "gazebo/common/Profiler.hh" + #include "gazebo/common/Events.hh" #include "gazebo/common/Exception.hh" #include "gazebo/common/Image.hh" @@ -177,8 +179,14 @@ void WideAngleCameraSensor::Fini() ////////////////////////////////////////////////// bool WideAngleCameraSensor::UpdateImpl(const bool _force) { + GZ_PROFILE("WideAngleCameraSensor::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + if (!CameraSensor::UpdateImpl(_force)) + { + GZ_PROFILE_END(); return false; + } if (this->dataPtr->lensPub && this->dataPtr->lensPub->HasConnections()) { @@ -215,6 +223,7 @@ bool WideAngleCameraSensor::UpdateImpl(const bool _force) this->dataPtr->lensPub->Publish(msg); } + GZ_PROFILE_END(); return true; } diff --git a/gazebo/sensors/WirelessReceiver.cc b/gazebo/sensors/WirelessReceiver.cc index 31ef9c6128..8a1caf43a8 100644 --- a/gazebo/sensors/WirelessReceiver.cc +++ b/gazebo/sensors/WirelessReceiver.cc @@ -14,6 +14,7 @@ * limitations under the License. * */ +#include "gazebo/common/Profiler.hh" #include #include "gazebo/msgs/msgs.hh" @@ -102,6 +103,9 @@ void WirelessReceiver::Load(const std::string &_worldName) ////////////////////////////////////////////////// bool WirelessReceiver::UpdateImpl(const bool /*_force*/) { + GZ_PROFILE("WirelessReceiver::UpdateImpl"); + GZ_PROFILE_BEGIN("Update"); + std::string txEssid; msgs::WirelessNodes msg; double rxPower; @@ -138,10 +142,13 @@ bool WirelessReceiver::UpdateImpl(const bool /*_force*/) wirelessNode->set_signal_level(rxPower); } } + GZ_PROFILE_END(); + GZ_PROFILE_BEGIN("Publish"); if (msg.node_size() > 0) { this->pub->Publish(msg); } + GZ_PROFILE_END(); return true; } diff --git a/gazebo/util/CMakeLists.txt b/gazebo/util/CMakeLists.txt index 2c69f2b250..d2cef4e1fe 100644 --- a/gazebo/util/CMakeLists.txt +++ b/gazebo/util/CMakeLists.txt @@ -97,6 +97,5 @@ if (WIN32) include_directories(IGNITION-MSGS_INCLUDE_DIR) endif() - gz_install_library(gazebo_util) gz_install_includes("util" ${headers} ${CMAKE_CURRENT_BINARY_DIR}/util.hh) diff --git a/worlds/profiler.world b/worlds/profiler.world new file mode 100644 index 0000000000..4aa7ee6b96 --- /dev/null +++ b/worlds/profiler.world @@ -0,0 +1,1397 @@ + + + + + + 0 1 0 + + + + + + + 10 + + 0.05 + 60 + + + 0 + 0.0002 + + + + 30 + + 5 + 20 + + + 0 + 0.03 + + + + + + 0 + 0.03 + + + + + + model://ground_plane + + + model://sun + + + 2 0 0 0 0 0 + + + + 1 + 0 + 0 + 1 + 0 + 1 + + 1.0 + + + + + 1 + + + + + + + 1 + + + + + + + + true + + + 0 0 0 0 0 0 + + + + 1 + 0 + 0 + 1 + 0 + 1 + + 1.0 + + + + + 1 1 1 + + + + + + 1.04719755 + + 640 + 480 + + + 0.1 + 20 + + + + true + 10 + + + true + + + 0 1 0 0 0 0 + + + + 1 + 0 + 0 + 1 + 0 + 1 + + 1.0 + + + + + 1 1 1 + + + + + + 1.047 + + 320 + 240 + + + 0.1 + 100 + + + gnomonical + true + 1.5707 + 512 + + + 1 + 30 + true + + + + true + + + 1 0 0 0 0 0 + + + + 1 + 0 + 0 + 1 + 0 + 1 + + 1.0 + + + + + 1 1 1 + + + + + + 1.04719755 + + 640 + 480 + + + 0.1 + 20 + + + + true + 10 + + + true + + + true + 0 0 0.035 0. 0 0 + + + 0.1 + + + + + model://hokuyo/meshes/hokuyo.dae + + + + + 0 0 -0.0145 0 0 0 + + + 0.05 0.05 0.041 + + + + + 0 0 0.0205 0 0 0 + + + 0.021 + 0.029 + + + + + 1 + 10.000000 + + + + + 0 + 1 + 3 + 1 + 0.5 + + + + + 0 + 1 + 3 + 1 + 1.0 + + + + + + + 0 + 0.1 + 0.1 + 0.1 + 0.1 + + + + + 0 + 0.2 + 0.2 + 0.2 + 0.2 + + + + + + + 0.1 0 0.0175 0 -0 0 + + + + 640 + 1 + -1.396263 + 1.396263 + + + 1 + 1 + 0 + 0 + + + + 0.08 + 10.0 + 0.01 + + + 1 + 100 + true + + + + true + + + 1 + 20.0 + + + + + 1 + 10.0 + + + + + 1 + 10.0 + + + + + 30 + true + + + + + 0 5 1.25 0 0 0 + + walk.dae + 1.0 + + + walk.dae + 1.000000 + true + + + + 0 -5 1.2138 + 1.15 + 1.8 + 5.1 + + + cafe + ground_plane + + + + + + 2 2 0 0 0 0 + + 0 0 0 0 0 0 + + 0 0 0 0 0 0 + 2.0 + + 0.1 + 0 + 0 + 0.1 + 0 + 0.1 + + + + 0 0 0 0 0 0 + + + 0.5 0.5 0.2 + + + + + 0 0 0 0 0 0 + + + 0.5 0.5 0.2 + + + + 0 + 1 + 0 + + + + 0 0.55 0 1.5701 -0 0 + + 0 0 0 0 -0 0 + 0.5 + + 0.001 + 0 + 0 + 0.0001 + 0 + 0.001 + + + + 0 0 0 0 0 0 + + + 0.5 + 0.1 + + + + + 0 0 0 0 0 0 + + + 0.5 + 0.1 + + + + 1 + 0 + + + left_track + base_link + + 0 1 0 + + + 1 + + + + + 0 -0.55 0 -1.5701 0 0 + + 0 0 0 0 0 0 + 0.5 + + 0.001 + 0 + 0 + 0.0001 + 0 + 0.001 + + + + 0 0 0 0 0 0 + + + 0.5 + 0.1 + + + + + 0 0 0 0 0 0 + + + 0.5 + 0.1 + + + + 1 + 0 + + + + right_track + base_link + + 0 1 0 + + + 1 + + + + + base_link + left_track_j + right_track_j + + + + ~/test_vehicle/vel_cmd + 0.25 + 0.2 + + + + + -5 0 0 0 0 0 + + + 0 0 1.5 0 -0 0 + + + 0.5 + 1 + + + + + + + + + + 0.5 + 1 + + + 0 0 1.5 0 0 0 + + 1 + 0 + 0 + + 1 + + 1 + 0 + 0 + 1 + 0 + 1 + + + + + + 0 0 0.5 0 0 0 + + + 1.5 + 1 + + + + + + + + + + 1.5 + 1 + + + 0 0 0.5 0 0 0 + + 1 + 0 + 0 + + 1 + 0 0 0 0 -0 0 + + 1 + 0 + 0 + 1 + 0 + 1 + + + + + base + top + + 0 0 1 + 0 + + 1e-16 + 1e16 + + + 0 0 1 0 -0 0 + + 0 + 1 + + + + actuator_0 + JOINT_0 + 0 + electric_motor + 20 + 6 + 10.0 + + + + + + 5 0 0.7 0 0 0 + + + + 1 + 0 + 0 + 1 + 0 + 1 + + 1.0 + + + 0 0 -0.55 0 0 0 + + + 1 1 0.1 + + + + + + 1 + 1 + + + + + 100 + + + + + + 0 0 -0.55 0 0 0 + + + 1 1 0.1 + + + + + + + + 0 0 0 0 0 0 + + + 0.5 0.5 0.5 + + + + + + 1 + 1 + + + + + 100 + + + + + + 0 0 0 0 0 0 + + + 0.5 0.5 0.5 + + + + + + + + + 0.45 0 -0.5 0 0 0 + + + 0.001 + 0 + 0 + 0.001 + 0 + 0.001 + + 0.1 + + + + + 0.02 1.0 0.02 + + + + + + 1 + 1 + + + + + 100 + + + + + + + + 0.02 1.0 0.02 + + + + + + + + + bar_2 + bar_1 + 0 0.5 0 0 0 0 + + 0 0 1 + true + + + + bar_3 + bar_1 + 0 -0.5 0 0 0 0 + + 0 0 1 + true + + + + 0.5 0.5 -0.5 0 0 0 + + + 0.001 + 0 + 0 + 0.001 + 0 + 0.001 + + 0.1 + + + -0.025 0 0 0 0 0 + + + 0.05 0.02 0.02 + + + + + + 1 + 1 + + + + + 100 + + + + + + -0.025 0 0 0 0 0 + + + 0.05 0.02 0.02 + + + + + + + + + link + bar_2 + 0 0 0 0 0 0 + + 0 0 1 + true + + + + 0.5 -0.5 -0.5 0 0 0 + + + 0.001 + 0 + 0 + 0.001 + 0 + 0.001 + + 0.1 + + + -0.025 0 0 0 0 0 + + + 0.05 0.02 0.02 + + + + + + 1 + 1 + + + + + 100 + + + + + + -0.025 0 0 0 0 0 + + + 0.05 0.02 0.02 + + + + + + + + + link + bar_3 + 0 0 0 0 0 0 + + 0 0 1 + true + + + + 0.5 0.55 -0.5 1.57079633 0 0 + + + 0.01 + 0 + 0 + 0.01 + 0 + 0.01 + + 1.0 + + + + + 0.2 + 0.1 + + + + + + 1 0 0 + 1 + 1 + + + + + 100 + + + + + + + + 0.2 + 0.1 + + + + + + + + + bar_2 + wheel_1 + 0 0 0 0 0 0 + + 0 1 0 + true + + + + 0.5 -0.55 -0.5 1.57079633 0 0 + + + 0.01 + 0 + 0 + 0.01 + 0 + 0.01 + + 1.0 + + + + + 0.2 + 0.1 + + + + + + 1 0 0 + 1 + 1 + + + + + 100 + + + + + + + + 0.2 + 0.1 + + + + + + + + + bar_3 + wheel_2 + 0 0 0 0 0 0 + + 0 1 0 + true + + + + -0.5 0.55 -0.5 1.57079633 0 0 + + + 0.01 + 0 + 0 + 0.01 + 0 + 0.01 + + 1.0 + + + + + 0.2 + 0.1 + + + + + + 1 0 0 + 1 + 1 + + + + + 100 + + + + + + + + 0.2 + 0.1 + + + + + + + + + link + wheel_3 + 0 0 0 0 0 0 + + + 0.0 + 0.0 + + 0 1 0 + true + + + + -0.5 -0.55 -0.5 1.57079633 0 0 + + + 0.01 + 0 + 0 + 0.01 + 0 + 0.01 + + 1.0 + + + + + 0.2 + 0.1 + + + + + + 1 0 0 + 1 + 1 + + + + + 100 + + + + + + + + 0.2 + 0.1 + + + + + + + + + link + wheel_4 + 0 0 0 0 0 0 + + + 0.0 + 0.0 + + 0 1 0 + true + + + + 0 0 0.5 0 0 0 + + + 0.1 + 0 + 0 + 0.1 + 0 + 0.1 + + 10.0 + + + + + 0.25 + + + + + + 1 0 0 + 1 + 1 + + + + + 10 + + + + + + + + 0.25 + + + + + + + + + link + sphere_link + 0 0 0 0 0 0 + + 0 0 1 + true + + + + 0 0 0.25 0 0 0 + + + 0.1 + 0 + 0 + 0.1 + 0 + 0.1 + + 10.0 + + + + + 0.25 + 0.5 + + + + + + 1 + 1 + + + + + 10 + + + + + + + + 0.25 + 0.5 + + + + + + + + + sphere_link + cylinder_link + 0 0 0 0 0 0 + + 0 0 1 + true + + + + bar_2_joint + 10 0.1 0.5 + -10 10 + 0.0 + + 0 + + 100.0 + + wheel_3_joint + 0.1 0.0 0.001 + -.1 .1 + 0 + + 0 + + 0.1 + + wheel_4_joint + 0.1 0.0 0.001 + -.1 .1 + 0 + + 0 + + 0.1 + + + + + -5 -5 0.7 0 0 0 + + + model://cessna + + + + 1.79 0 1.350 0 0 0 + 0 + + + + 0 0 0.495 0 0 0 + 0 + + + + + cessna_c172::propeller_joint + 2500 + cessna_c172::left_aileron_joint + cessna_c172::left_flap_joint + cessna_c172::right_aileron_joint + cessna_c172::right_flap_joint + cessna_c172::elevators_joint + cessna_c172::rudder_joint + 10000 + 0 + 0 + 2000 + 0 + 0 + + + 0.4 + 4.752798721 + 0.6417112299 + 0 + 1.5 + -3.85 + -0.9233984055 + 0 + -0.37 0 0.77 + 0.1 + 1.2041 + 0 -1 0 + 1 0 0 + cessna_c172::propeller + + + 0.4 + 4.752798721 + 0.6417112299 + 0 + 1.5 + -3.85 + -0.9233984055 + 0 + -0.37 0 -0.77 + 0.1 + 1.2041 + 0 1 0 + 1 0 0 + cessna_c172::propeller + + + + 0.05984281113 + 4.752798721 + 0.6417112299 + -1.8 + 0.3391428111 + -3.85 + -0.9233984055 + 0 + -1 2.205 1.5 + 8.08255 + 1.2041 + 1 0 0 + 0 0 1 + cessna_c172::body + cessna_c172::left_aileron_joint + -2.0 + + + + 0.05984281113 + 4.752798721 + 0.6417112299 + -1.8 + 0.3391428111 + -3.85 + -0.9233984055 + 0 + -1 -2.205 1.5 + 8.08255 + 1.2041 + 1 0 0 + 0 0 1 + cessna_c172::body + + cessna_c172::right_aileron_joint + + -2.0 + + + + -0.2 + 4.752798721 + 0.6417112299 + -1.8 + 0.3391428111 + -3.85 + -0.9233984055 + 0 + -5.45 0 0.55 + 2.03458 + 1.2041 + 1 0 0 + 0 0 1 + cessna_c172::body + cessna_c172::elevators_joint + -4.0 + + + + 0 + 4.752798721 + 0.6417112299 + -1.8 + 0.3391428111 + -3.85 + -0.9233984055 + 0 + -6 0 1.55 + 1.5329 + 1.2041 + 1 0 0 + 0 1 0 + cessna_c172::body + cessna_c172::rudder_joint + 4.0 + + + +