diff --git a/doc/changelog.rst b/doc/changelog.rst index 29d34ef3d7..dfaab7666e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -33,6 +33,9 @@ Python bindings Simulate ^^^^^^^^ +- Implemented a workaround for `broken VSync `_ on macOS so that the frame + rate is correctly capped when the Vertical Sync toggle is enabled. + .. image:: images/changelog/contactlabel.png :align: right :width: 400px @@ -42,6 +45,7 @@ Simulate |br| + Version 2.3.2 (February 7, 2023) -------------------------------- diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index af8f2ad43f..b58716a7f3 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -108,6 +108,10 @@ target_sources( PRIVATE glfw_adapter.cc glfw_dispatch.cc platform_ui_adapter.cc ) target_compile_options(platform_ui_adapter PUBLIC ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) +if(APPLE) + target_sources(platform_ui_adapter PUBLIC glfw_corevideo.h PRIVATE glfw_corevideo.mm) + target_link_libraries(platform_ui_adapter PUBLIC "-framework CoreVideo") +endif() target_include_directories( platform_ui_adapter PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} $ diff --git a/simulate/glfw_adapter.cc b/simulate/glfw_adapter.cc index 368a3e4ce4..9bcbfe5d76 100644 --- a/simulate/glfw_adapter.cc +++ b/simulate/glfw_adapter.cc @@ -22,6 +22,10 @@ #include #include "glfw_dispatch.h" +#ifdef __APPLE__ +#include "glfw_corevideo.h" +#endif + namespace mujoco { namespace { int MaybeGlfwInit() { @@ -88,6 +92,12 @@ GlfwAdapter::GlfwAdapter() { }); Glfw().glfwSetWindowRefreshCallback( window_, +[](GLFWwindow* window) { +#ifdef __APPLE__ + auto& core_video = GlfwAdapterFromWindow(window).core_video_; + if (core_video.has_value()) { + core_video->UpdateDisplayLink(); + } +#endif GlfwAdapterFromWindow(window).OnWindowRefresh(); }); Glfw().glfwSetWindowSizeCallback( @@ -142,7 +152,16 @@ void GlfwAdapter::SetClipboardString(const char* text) { } void GlfwAdapter::SetVSync(bool enabled){ +#ifdef __APPLE__ + Glfw().glfwSwapInterval(0); + if (enabled && !core_video_.has_value()) { + core_video_.emplace(window_); + } else if (!enabled && core_video_.has_value()) { + core_video_.reset(); + } +#else Glfw().glfwSwapInterval(enabled); +#endif } void GlfwAdapter::SetWindowTitle(const char* title) { @@ -154,7 +173,16 @@ bool GlfwAdapter::ShouldCloseWindow() const { } void GlfwAdapter::SwapBuffers() { +#ifdef __APPLE__ + if (core_video_.has_value()) { + core_video_->EnqueueSwap(); + core_video_->WaitForSwap(); + } else { + Glfw().glfwSwapBuffers(window_); + } +#else Glfw().glfwSwapBuffers(window_); +#endif } void GlfwAdapter::ToggleFullscreen() { diff --git a/simulate/glfw_adapter.h b/simulate/glfw_adapter.h index 23342dd55f..1490c33334 100644 --- a/simulate/glfw_adapter.h +++ b/simulate/glfw_adapter.h @@ -21,6 +21,11 @@ #include #include "platform_ui_adapter.h" +#ifdef __APPLE__ +#include +#include "glfw_corevideo.h" +#endif + namespace mujoco { class GlfwAdapter : public PlatformUIAdapter { public: @@ -61,6 +66,12 @@ class GlfwAdapter : public PlatformUIAdapter { // store last window information when going to full screen std::pair window_pos_; std::pair window_size_; + +#ifdef __APPLE__ + // Workaround for perpertually broken OpenGL VSync on macOS, + // most recently https://github.com/glfw/glfw/issues/2249. + std::optional core_video_; +#endif }; } // namespace mujoco diff --git a/simulate/glfw_corevideo.h b/simulate/glfw_corevideo.h new file mode 100644 index 0000000000..5b6cb12525 --- /dev/null +++ b/simulate/glfw_corevideo.h @@ -0,0 +1,56 @@ +// Copyright 2023 DeepMind Technologies Limited +// +// 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. + +#ifndef MUJOCO_SIMULATE_GLFW_COREVIDEO_H_ +#define MUJOCO_SIMULATE_GLFW_COREVIDEO_H_ + +#ifndef __APPLE__ +#error "This header only works on macOS." +#endif + +#include +#include + +#include "glfw_dispatch.h" + +#ifdef __OBJC__ +#import +#else +typedef void* CVDisplayLinkRef; +#endif + +// Workaround for perpertually broken OpenGL VSync on macOS, +// most recently https://github.com/glfw/glfw/issues/2249. +namespace mujoco { +class GlfwCoreVideo { + public: + GlfwCoreVideo(GLFWwindow* window); + ~GlfwCoreVideo(); + void EnqueueSwap(); + void WaitForSwap(); + int DisplayLinkCallback(); + void UpdateDisplayLink(); + + private: + GLFWwindow* window_; + CVDisplayLinkRef display_link_; + + bool second_buffer_has_content_; + std::mutex mu_; + std::condition_variable cond_; +}; +} // namespace mujoco + + +#endif // MUJOCO_SIMULATE_GLFW_COREVIDEO_H_ diff --git a/simulate/glfw_corevideo.mm b/simulate/glfw_corevideo.mm new file mode 100644 index 0000000000..571cc991ad --- /dev/null +++ b/simulate/glfw_corevideo.mm @@ -0,0 +1,75 @@ +// Copyright 2023 DeepMind Technologies Limited +// +// 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 "glfw_corevideo.h" + +#include +#include + +#include "glfw_adapter.h" +#include "glfw_dispatch.h" + +// Workaround for perpertually broken OpenGL VSync on macOS, +// most recently https://github.com/glfw/glfw/issues/2249. +namespace mujoco { +namespace { +static int DisplayLinkCallbackTrampoline(CVDisplayLinkRef display_link, + const CVTimeStamp* now, + const CVTimeStamp* output_time, + CVOptionFlags flags_in, + CVOptionFlags* flags_out, + void* user_context) { + return static_cast(user_context)->DisplayLinkCallback(); +} +} // namespace + +GlfwCoreVideo::GlfwCoreVideo(GLFWwindow* window) : window_(window) { + CVDisplayLinkCreateWithActiveCGDisplays(&display_link_); + CVDisplayLinkSetOutputCallback(display_link_, &DisplayLinkCallbackTrampoline, this); + CVDisplayLinkStart(display_link_); +} + +GlfwCoreVideo::~GlfwCoreVideo() { + CVDisplayLinkStop(display_link_); + CVDisplayLinkRelease(display_link_); +} + +void GlfwCoreVideo::EnqueueSwap() { + std::unique_lock lock(mu_); + second_buffer_has_content_ = true; +} + +void GlfwCoreVideo::WaitForSwap() { + if (second_buffer_has_content_) { + std::unique_lock lock(mu_); + cond_.wait(lock, [this]() { return !this->second_buffer_has_content_; }); + } +} + +int GlfwCoreVideo::DisplayLinkCallback() { + if (second_buffer_has_content_) { + std::unique_lock lock(mu_); + Glfw().glfwSwapBuffers(window_); + second_buffer_has_content_ = false; + cond_.notify_one(); + } + return kCVReturnSuccess; +} + +void GlfwCoreVideo::UpdateDisplayLink() { + NSOpenGLContext* nsgl = Glfw().glfwGetNSGLContext(window_); + CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(display_link_, [nsgl CGLContextObj], + [nsgl.pixelFormat CGLPixelFormatObj]); +} +} // namespace mujoco diff --git a/simulate/glfw_dispatch.cc b/simulate/glfw_dispatch.cc index c6a1d75365..cc408e343d 100644 --- a/simulate/glfw_dispatch.cc +++ b/simulate/glfw_dispatch.cc @@ -114,6 +114,10 @@ const struct Glfw& Glfw(void* dlhandle) { mjGLFW_INITIALIZE_SYMBOL(glfwWindowShouldClose); // go/keep-sorted end +#ifdef __APPLE__ + mjGLFW_INITIALIZE_SYMBOL(glfwGetNSGLContext); +#endif + #undef mjGLFW_INITIALIZE_SYMBOL return glfw; diff --git a/simulate/glfw_dispatch.h b/simulate/glfw_dispatch.h index 3a44ead287..f3bec97e28 100644 --- a/simulate/glfw_dispatch.h +++ b/simulate/glfw_dispatch.h @@ -17,6 +17,11 @@ #include +#ifdef __APPLE__ +#define GLFW_EXPOSE_NATIVE_NSGL +#include +#endif + namespace mujoco { // Dynamic dispatch table for GLFW functions required by Simulate. // This allows us to use GLFW without introducing a link-time dependency on the @@ -58,6 +63,11 @@ struct Glfw { mjGLFW_DECLARE_SYMBOL(glfwWindowHint); mjGLFW_DECLARE_SYMBOL(glfwWindowShouldClose); // go/keep-sorted end + +#ifdef __APPLE__ + mjGLFW_DECLARE_SYMBOL(glfwGetNSGLContext); +#endif + #undef mjGLFW_DECLARE_SYMBOL }; diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 403af0398c..1a40196fef 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -1950,6 +1950,9 @@ void Simulate::renderloop() { uiModify(&this->ui0, &this->uistate, &this->platform_ui->mjr_context()); uiModify(&this->ui1, &this->uistate, &this->platform_ui->mjr_context()); + // set VSync to initial value + this->platform_ui->SetVSync(this->vsync); + // run event loop while (!this->platform_ui->ShouldCloseWindow() && !this->exitrequest.load()) { {