diff --git a/python/mujoco/experimental/studio/native_viewer.cc b/python/mujoco/experimental/studio/native_viewer.cc index 77ef189b..fe444dad 100644 --- a/python/mujoco/experimental/studio/native_viewer.cc +++ b/python/mujoco/experimental/studio/native_viewer.cc @@ -26,8 +26,8 @@ #include #include #include +#include #include -#include #include #include #include "structs.h" @@ -119,7 +119,7 @@ class Viewer { width, height, config); ImPlot::CreateContext(); - renderer_ = std::make_unique( + renderer_ = std::make_unique( window_->GetNativeWindowHandle(), config.gfx_mode); } diff --git a/python/mujoco/experimental/studio/renderer.cc b/python/mujoco/experimental/studio/renderer.cc index ba3c98a6..e51c2fab 100644 --- a/python/mujoco/experimental/studio/renderer.cc +++ b/python/mujoco/experimental/studio/renderer.cc @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include #include #include @@ -32,7 +32,7 @@ namespace mujoco::python { class Renderer { public: - using RendererImpl = mujoco::platform::Renderer; + using RendererImpl = mujoco::platform::FilamentRenderer; using GraphicsMode = mujoco::platform::GraphicsMode; Renderer(const std::string& graphics_mode_str) { diff --git a/python/mujoco/experimental/studio/web/web_client.cc b/python/mujoco/experimental/studio/web/web_client.cc index 21146716..eee6ce29 100644 --- a/python/mujoco/experimental/studio/web/web_client.cc +++ b/python/mujoco/experimental/studio/web/web_client.cc @@ -43,7 +43,7 @@ #include #include #include -#include "experimental/platform/hal/renderer.h" +#include "experimental/platform/hal/filament_renderer.h" #include "experimental/platform/hal/window.h" #include "experimental/platform/sim/model_holder.h" #include "experimental/platform/ux/interaction.h" @@ -757,7 +757,7 @@ void StartApp() { 1400, 720, config); ImPlot::CreateContext(); // Needed if the server app uses ImPlot. - g_app.renderer = new mujoco::platform::Renderer( + g_app.renderer = new mujoco::platform::FilamentRenderer( g_app.window->GetNativeWindowHandle(), config.gfx_mode); // Initialize an empty dummy scene so Filament and ImGui are ready to render diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index 5362fdc3..3ff9285e 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -26,11 +26,14 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME} resources.h sys_utils.cc sys_utils.h + hal/classic_renderer.cc + hal/classic_renderer.h hal/egl_utils.cc hal/egl_utils.h + hal/filament_renderer.cc + hal/filament_renderer.h hal/graphics_mode.cc hal/graphics_mode.h - hal/renderer.cc hal/renderer.h hal/window.cc hal/window.h diff --git a/src/experimental/platform/hal/classic_renderer.cc b/src/experimental/platform/hal/classic_renderer.cc new file mode 100644 index 00000000..36c2298e --- /dev/null +++ b/src/experimental/platform/hal/classic_renderer.cc @@ -0,0 +1,202 @@ +// Copyright 2025 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 "experimental/platform/hal/classic_renderer.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#if !defined(__EMSCRIPTEN__) && !defined(__APPLE__) +#include "experimental/platform/hal/egl_utils.h" +#endif +#include "experimental/platform/hal/graphics_mode.h" + +namespace mujoco::platform { + +static void FlipImage(unsigned char* pixels, int width, int height, int bpp) { + const int row_size = width * bpp; + for (int i = 0; i < height / 2; ++i) { + unsigned char* top_row = pixels + i * row_size; + unsigned char* bottom_row = pixels + (height - 1 - i) * row_size; + for (int j = 0; j < row_size; ++j) { + std::swap(top_row[j], bottom_row[j]); + } + } +} + +ClassicRenderer::ClassicRenderer(void* native_window, GraphicsMode gfx) + : gfx_(gfx) { + if (native_window == nullptr) { +#if !defined(__EMSCRIPTEN__) && !defined(__APPLE__) + graphics_api_context_ = CreateEglContext(); +#endif + } + if (ImGui::GetCurrentContext()) { + ImGui_ImplOpenGL3_Init(); + } +} + +ClassicRenderer::~ClassicRenderer() { + if (ImGui::GetCurrentContext()) { + ImGui_ImplOpenGL3_Shutdown(); + } + + Deinit(); + graphics_api_context_.reset(); +} + +void ClassicRenderer::Init(const mjModel* model) { + Deinit(); + if (model) { + mjr_defaultContext(&render_context_); + mjr_makeContext(model, &render_context_, mjFONTSCALE_150); + mjv_defaultScene(&scene_); + mjv_makeScene(model, &scene_, 2000); + initialized_ = true; + } +} + +void ClassicRenderer::Deinit() { + if (initialized_) { + mjv_freeScene(&scene_); + mjr_freeContext(&render_context_); + initialized_ = false; + } +} + +void ClassicRenderer::Render(const mjModel* model, mjData* data, + const mjvPerturb* perturb, mjvCamera* camera, + const mjvOption* vis_option, int width, int height, + std::span pixels, + std::span extra_geoms) { + if (!initialized_) { + return; + } + + mjvCamera default_cam; + if (camera == nullptr) { + if (model) { + mjv_defaultFreeCamera(model, &default_cam); + } else { + mjv_defaultCamera(&default_cam); + } + camera = &default_cam; + } + + mjvOption default_opt; + if (vis_option == nullptr) { + mjv_defaultOption(&default_opt); + vis_option = &default_opt; + } + + mjv_updateScene(model, data, vis_option, perturb, camera, mjCAT_ALL, &scene_); + const int nextra_geoms = + std::min(extra_geoms.size(), scene_.maxgeom - scene_.ngeom); + for (int i = 0; i < nextra_geoms; ++i) { + scene_.geoms[scene_.ngeom++] = extra_geoms[i]; + } + + const bool render_to_texture = !pixels.empty(); + if (render_to_texture) { + // mjr_readPixels reads to a RGB buffer (i.e. 3 bytes per pixel). + if (pixels.size() != width * height * 3) { + mju_error("Offscreen mode requires a pixel buffer of size %d.", + width * height * 3); + } + mjr_resizeOffscreen(width, height, &render_context_); + + framebuffer_mode_ = mjFB_OFFSCREEN; + mjr_setBuffer(mjFB_OFFSCREEN, &render_context_); + } + + const mjrRect viewport = {0, 0, width, height}; + mjr_render(viewport, &scene_, &render_context_); + + // The filament backend knows how to renders the ImGui draw data. For the + // classic backend, we need to render the ImGui draw data ourselves. + if (ImGui::GetCurrentContext()) { + ImGui_ImplOpenGL3_NewFrame(); + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + + if (render_to_texture) { + unsigned char* ptr = reinterpret_cast(pixels.data()); + DoReadPixels(width, height, ptr); + } + + UpdateFps(); +} + +void ClassicRenderer::RenderToTexture(const mjModel* model, mjData* data, + mjvCamera* camera, int width, int height, + std::byte* output) { + if (!initialized_) { + return; + } + + mjv_updateCamera(model, data, camera, &scene_); + unsigned char* ptr = reinterpret_cast(output); + + framebuffer_mode_ = mjFB_OFFSCREEN; + mjr_setBuffer(mjFB_OFFSCREEN, &render_context_); + const mjrRect viewport = {0, 0, width, height}; + mjr_render(viewport, &scene_, &render_context_); + DoReadPixels(width, height, ptr); +} + +int ClassicRenderer::UploadImage(int texture_id, const std::byte* pixels, + int width, int height, int bpp) { + // unsupported + return 0; +} + +void ClassicRenderer::DoReadPixels(int width, int height, unsigned char* rgb) { + if (!rgb) { + return; + } + if (framebuffer_mode_ == mjFB_WINDOW) { + mju_warning("ReadPixels is only supported for offscreen rendering."); + return; + } + + const mjrRect viewport = {0, 0, width, height}; + mjr_readPixels(rgb, nullptr, viewport, &render_context_); + mjr_setBuffer(mjFB_WINDOW, &render_context_); + FlipImage(rgb, viewport.width, viewport.height, 3); + framebuffer_mode_ = mjFB_WINDOW; + mjr_setBuffer(mjFB_WINDOW, &render_context_); +} + +double ClassicRenderer::GetFps() { return fps_; } + +void ClassicRenderer::UpdateFps() { + TimePoint now = std::chrono::steady_clock::now(); + TimePoint::duration delta_time = now - last_fps_update_; + const double interval = std::chrono::duration(delta_time).count(); + ++frames_; + if (interval > 0.2) { // only update FPS stat at most 5 times per second + last_fps_update_ = now; + fps_ = frames_ / interval; + frames_ = 0; + } +} +} // namespace mujoco::platform diff --git a/src/experimental/platform/hal/classic_renderer.h b/src/experimental/platform/hal/classic_renderer.h new file mode 100644 index 00000000..499d4e31 --- /dev/null +++ b/src/experimental/platform/hal/classic_renderer.h @@ -0,0 +1,91 @@ +// Copyright 2025 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_SRC_EXPERIMENTAL_PLATFORM_HAL_CLASSIC_RENDERER_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_CLASSIC_RENDERER_H_ + +#include +#include +#include +#include + +#include +#include "experimental/platform/hal/graphics_mode.h" +#include "experimental/platform/hal/renderer.h" + +namespace mujoco::platform { + +// Renders the mujoco simulation and the imgui state using the Classic MuJoCo +// OpenGL renderer. +class ClassicRenderer : public Renderer { + public: + ClassicRenderer(void* native_window, GraphicsMode gfx_mode); + ~ClassicRenderer(); + + ClassicRenderer(const ClassicRenderer&) = delete; + ClassicRenderer& operator=(const ClassicRenderer&) = delete; + + // Initializes the renderer with the given mjModel. + void Init(const mjModel* model) override; + + // Renders the simulation and ux state. Renders into `pixels` if provided, + // otherwise renders to the `native_window` provided at construction. + void Render(const mjModel* model, mjData* data, const mjvPerturb* perturb, + mjvCamera* camera, const mjvOption* vis_option, int width, + int height, std::span pixels = {}, + std::span extra_geoms = {}) override; + + // Populates the given output buffer with RGB888 pixel data. The size of the + // output buffer must be at least width * height * 3. + void RenderToTexture(const mjModel* model, mjData* data, mjvCamera* camera, + int width, int height, std::byte* output) override; + + // Uploads an image to the backend for GUI rendering, returning the texture + // ID for the texture. The ID can be used in subsequent calls to update the + // texture data. A nullptr pixels argument will free the texture if it exists. + // A texture ID of 0 will create a new texture. + int UploadImage(int texture_id, const std::byte* pixels, int width, + int height, int bpp) override; + + // Rendering flags. + mjtByte* GetRenderFlags() override { return scene_.flags; } + + // Returns the current frame rate. + double GetFps() override; + + private: + using Clock = std::chrono::steady_clock; + using TimePoint = std::chrono::time_point; + + // Resets the renderer; no rendering will occur until Init() is called again. + void Deinit(); + + void UpdateFps(); + + void DoReadPixels(int width, int height, unsigned char* rgb); + + GraphicsMode gfx_ = GraphicsMode::ClassicOpenGl; + std::shared_ptr graphics_api_context_ = nullptr; + mjrContext render_context_; + mjvScene scene_; + TimePoint last_fps_update_; + int framebuffer_mode_ = 0; + int frames_ = 0; + double fps_ = 0; + bool initialized_ = false; +}; + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_CLASSIC_RENDERER_H_ diff --git a/src/experimental/platform/hal/filament_renderer.cc b/src/experimental/platform/hal/filament_renderer.cc new file mode 100644 index 00000000..e1208805 --- /dev/null +++ b/src/experimental/platform/hal/filament_renderer.cc @@ -0,0 +1,289 @@ +// Copyright 2025 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 "experimental/platform/hal/filament_renderer.h" + +#include +#include +#include +#include +#include + +#include +#include +#include "experimental/filament/compat/scene_bridge.h" +#include "experimental/platform/hal/graphics_mode.h" +#include "experimental/platform/ux/imgui_bridge.h" +#include "experimental/platform/ux/imgui_widgets.h" +#include "experimental/platform/ux/plugin.h" +#include "render/filament/mjrfilament_cpp.h" + +namespace mujoco::platform { + +static std::function g_update_gui_callback = nullptr; +static void PluginUpdate(GuiPlugin* plugin) { + if (g_update_gui_callback) { + g_update_gui_callback(); + } +} + +FilamentRenderer::FilamentRenderer(void* native_window, GraphicsMode gfx) + : native_window_(native_window), gfx_(gfx) { + g_update_gui_callback = [this]() { + if (main_scene_) { + mjrf_DEBUG_drawImguiEditor(main_scene_.get()); + } + }; +} + +FilamentRenderer::~FilamentRenderer() { + g_update_gui_callback = nullptr; + Deinit(); +} + +void FilamentRenderer::Init(const mjModel* model) { + Deinit(); + if (model) { + mjrfContextConfig cfg; + mjrf_defaultContextConfig(&cfg); + cfg.native_window = native_window_; + cfg.force_software_rendering = IsSoftware(gfx_); + cfg.graphics_api = IsOpenGl(gfx_) || IsWebGl(gfx_) ? mjGRAPHICS_API_OPENGL + : mjGRAPHICS_API_VULKAN; + filament_context_ = CreateContext(cfg); + + main_scene_ = CreateScene(filament_context_.get(), {}); + ux_scene_ = CreateScene(filament_context_.get(), {}); + scene_bridge_ = std::make_unique(filament_context_.get(), + main_scene_.get(), model); + imgui_bridge_ = + std::make_unique(filament_context_.get(), ux_scene_.get()); + scene_bridge_->SetDrawTextFunction(DrawTextAt); + + mjv_defaultScene(&scene_); + mjv_makeScene(model, &scene_, 2000); + } +} + +void FilamentRenderer::Deinit() { + if (filament_context_) { + mjv_freeScene(&scene_); + scene_bridge_.reset(); + imgui_bridge_.reset(); + ux_scene_.reset(); + main_scene_.reset(); + filament_context_.reset(); + } +} + +void FilamentRenderer::Render(const mjModel* model, mjData* data, + const mjvPerturb* perturb, mjvCamera* camera, + const mjvOption* vis_option, int width, + int height, std::span pixels, + std::span extra_geoms) { + if (!filament_context_) { + return; + } + + mjvCamera default_cam; + if (camera == nullptr) { + if (model) { + mjv_defaultFreeCamera(model, &default_cam); + } else { + mjv_defaultCamera(&default_cam); + } + camera = &default_cam; + } + mjvOption default_opt; + if (vis_option == nullptr) { + mjv_defaultOption(&default_opt); + vis_option = &default_opt; + } + + mjv_updateScene(model, data, vis_option, perturb, camera, mjCAT_ALL, &scene_); + const int nextra_geoms = + std::min(extra_geoms.size(), scene_.maxgeom - scene_.ngeom); + for (int i = 0; i < nextra_geoms; ++i) { + scene_.geoms[scene_.ngeom++] = extra_geoms[i]; + } + + const bool render_to_texture = !pixels.empty(); + if (render_to_texture) { + if (pixels.size() != width * height * 3) { + mju_error("Offscreen mode requires a pixel buffer of size %d.", + width * height * 3); + } + framebuffer_mode_ = mjFB_OFFSCREEN + 1; + } + + DoRender(width, height); + + if (render_to_texture) { + unsigned char* ptr = reinterpret_cast(pixels.data()); + DoReadPixels(width, height, ptr); + } + + UpdateFps(); +} + +void FilamentRenderer::RenderToTexture(const mjModel* model, mjData* data, + mjvCamera* camera, int width, int height, + std::byte* output) { + if (!filament_context_) { + return; + } + + mjv_updateCamera(model, data, camera, &scene_); + unsigned char* ptr = reinterpret_cast(output); + + framebuffer_mode_ = mjFB_OFFSCREEN; + DoRender(width, height); + DoReadPixels(width, height, ptr); +} + +int FilamentRenderer::UploadImage(int texture_id, const std::byte* pixels, + int width, int height, int bpp) { + return imgui_bridge_->UploadImage( + texture_id, reinterpret_cast(pixels), width, height, + bpp); +} + +void FilamentRenderer::DoRender(int width, int height) { + const mjrRect viewport = {0, 0, width, height}; + scene_bridge_->Update(viewport, &scene_); + // Update the UX renderable entity after processing the scene in case there + // are any elements in the scene which generate UX draw calls (e.g. labels). + if (framebuffer_mode_ != 1) { + imgui_bridge_->Update(); + } + if (framebuffer_mode_ == 0) { + mjrDrawMode draw_mode = mjDRAW_MODE_DEFAULT; + if (scene_.flags[mjRND_SEGMENT]) { + if (scene_.flags[mjRND_IDCOLOR]) { + draw_mode = mjDRAW_MODE_SEGMENTATION_BY_ID; + } else { + draw_mode = mjDRAW_MODE_SEGMENTATION_BY_COLOR; + } + } else if (scene_.flags[mjRND_DEPTH]) { + draw_mode = mjDRAW_MODE_DEPTH; + } else if (scene_.flags[mjRND_WIREFRAME]) { + draw_mode = mjDRAW_MODE_WIREFRAME; + } + + mjrfRenderRequest reqs[2]; + + mjrf_defaultRenderRequest(&reqs[0]); + reqs[0].scene = main_scene_.get(); + reqs[0].draw_mode = draw_mode; + reqs[0].camera = scene_bridge_->GetCamera(); + reqs[0].viewport = viewport; + reqs[0].enable_shadows = scene_.flags[mjRND_SHADOW]; + reqs[0].enable_reflections = scene_.flags[mjRND_REFLECTION]; + + mjrf_defaultRenderRequest(&reqs[1]); + reqs[1].scene = ux_scene_.get(); + reqs[1].draw_mode = mjDRAW_MODE_DEFAULT; + reqs[1].camera = imgui_bridge_->GetCamera(viewport.width, viewport.height); + reqs[1].viewport = viewport; + reqs[1].enable_shadows = false; + reqs[1].enable_reflections = false; + reqs[1].enable_post_processing = false; + + mjrf_render(filament_context_.get(), &reqs[0], 2, nullptr, 0); + } +} + +void FilamentRenderer::DoReadPixels(int width, int height, unsigned char* rgb) { + if (!rgb) { + return; + } + if (framebuffer_mode_ == 0) { + mju_warning("ReadPixels is only supported for offscreen rendering."); + return; + } + + const mjrRect viewport = {0, 0, width, height}; + mjrDrawMode draw_mode = mjDRAW_MODE_DEFAULT; + if (scene_.flags[mjRND_SEGMENT]) { + if (scene_.flags[mjRND_IDCOLOR]) { + draw_mode = mjDRAW_MODE_SEGMENTATION_BY_ID; + } else { + draw_mode = mjDRAW_MODE_SEGMENTATION_BY_COLOR; + } + } else if (scene_.flags[mjRND_DEPTH]) { + draw_mode = mjDRAW_MODE_DEPTH; + } else if (scene_.flags[mjRND_WIREFRAME]) { + draw_mode = mjDRAW_MODE_WIREFRAME; + } + + mjrfRenderTargetConfig config; + mjrf_defaultRenderTargetConfig(&config); + config.width = viewport.width; + config.height = viewport.height; + config.color_format = mjPIXEL_FORMAT_RGB8; + config.depth_format = mjPIXEL_FORMAT_DEPTH32F; + auto target = CreateRenderTarget(filament_context_.get(), config); + + mjrfRenderRequest reqs[2]; + mjrf_defaultRenderRequest(&reqs[0]); + reqs[0].scene = main_scene_.get(); + reqs[0].draw_mode = draw_mode; + reqs[0].camera = scene_bridge_->GetCamera(); + reqs[0].target = target.get(); + reqs[0].viewport = viewport; + reqs[0].enable_shadows = scene_.flags[mjRND_SHADOW]; + reqs[0].enable_reflections = scene_.flags[mjRND_REFLECTION]; + + mjrf_defaultRenderRequest(&reqs[1]); + reqs[1].scene = ux_scene_.get(); + reqs[1].draw_mode = mjDRAW_MODE_DEFAULT; + reqs[1].camera = imgui_bridge_->GetCamera(viewport.width, viewport.height); + reqs[1].target = target.get(); + reqs[1].viewport = viewport; + reqs[1].enable_shadows = false; + reqs[1].enable_reflections = false; + reqs[1].enable_post_processing = false; + + mjrfReadPixelsRequest read_request; + mjrf_defaultReadPixelsRequest(&read_request); + read_request.target = target.get(); + read_request.output = rgb; + read_request.num_bytes = viewport.width * viewport.height * 3; + + const int num_requests = (framebuffer_mode_ == 2) ? 2 : 1; + const mjrfFrameHandle frame = mjrf_render(filament_context_.get(), &reqs[0], + num_requests, &read_request, 1); + mjrf_waitForFrame(filament_context_.get(), frame); + framebuffer_mode_ = mjFB_WINDOW; +} + +double FilamentRenderer::GetFps() { return fps_; } + +void FilamentRenderer::UpdateFps() { + mjrfFrameStats stats; + mjrf_defaultFrameStats(&stats); + mjrf_getFrameStats(filament_context_.get(), 0, &stats); + fps_ = stats.frame_rate; +} + +} // namespace mujoco::platform + +mjPLUGIN_LIB_INIT(renderer) { + mujoco::platform::GuiPlugin plugin; + plugin.name = "Filament"; + plugin.update = [](mujoco::platform::GuiPlugin* self) { + mujoco::platform::PluginUpdate(self); + }; + mujoco::platform::RegisterPlugin(plugin); +} diff --git a/src/experimental/platform/hal/filament_renderer.h b/src/experimental/platform/hal/filament_renderer.h new file mode 100644 index 00000000..12ecafd1 --- /dev/null +++ b/src/experimental/platform/hal/filament_renderer.h @@ -0,0 +1,100 @@ +// Copyright 2025 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_SRC_EXPERIMENTAL_PLATFORM_HAL_FILAMENT_RENDERER_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_FILAMENT_RENDERER_H_ + +#include +#include +#include + +#include +#include +#include "experimental/filament/compat/scene_bridge.h" +#include "experimental/platform/hal/graphics_mode.h" +#include "experimental/platform/hal/renderer.h" +#include "experimental/platform/ux/imgui_bridge.h" +#include "render/filament/mjrfilament_cpp.h" + +namespace mujoco::platform { + +// Renders the mujoco simulation and the imgui state using the Filament-based +// MuJoCo renderer. +// +// For OpenGL, two different Filament configurations are available: normal and +// headless. Normal rendering assumes that we will be rendering to an x11 +// window and therefore will use a x11-based context. Headless assumes that we +// will be rendering to a texture and will use an EGL context. Vulkan and WebGL +// have no need for such a distinction. If rendering to a window surface (e.g. +// x11), it requires a pointer to the native window to do so. +class FilamentRenderer : public Renderer { + public: + FilamentRenderer(void* native_window, GraphicsMode gfx_mode); + ~FilamentRenderer(); + + FilamentRenderer(const FilamentRenderer&) = delete; + FilamentRenderer& operator=(const FilamentRenderer&) = delete; + + // Initializes the renderer with the given mjModel. + void Init(const mjModel* model) override; + + // Renders the simulation and ux state. Renders into `pixels` if provided, + // otherwise renders to the `native_window` provided at construction. + void Render(const mjModel* model, mjData* data, const mjvPerturb* perturb, + mjvCamera* camera, const mjvOption* vis_option, int width, + int height, std::span pixels = {}, + std::span extra_geoms = {}) override; + + // Populates the given output buffer with RGB888 pixel data. The size of the + // output buffer must be at least width * height * 3. + void RenderToTexture(const mjModel* model, mjData* data, mjvCamera* camera, + int width, int height, std::byte* output) override; + + // Uploads an image to the backend for GUI rendering, returning the texture + // ID for the texture. The ID can be used in subsequent calls to update the + // texture data. A nullptr pixels argument will free the texture if it exists. + // A texture ID of 0 will create a new texture. + int UploadImage(int texture_id, const std::byte* pixels, int width, + int height, int bpp) override; + + // Rendering flags. + mjtByte* GetRenderFlags() override { return scene_.flags; } + + // Returns the current frame rate. + double GetFps() override; + + private: + // Resets the renderer; no rendering will occur until Init() is called again. + void Deinit(); + + void UpdateFps(); + + void DoRender(int width, int height); + void DoReadPixels(int width, int height, unsigned char* rgb); + + void* native_window_ = nullptr; + GraphicsMode gfx_ = GraphicsMode::FilamentVulkan; + UniquePtr filament_context_{nullptr, nullptr}; + UniquePtr main_scene_{nullptr, nullptr}; + UniquePtr ux_scene_{nullptr, nullptr}; + std::unique_ptr scene_bridge_; + std::unique_ptr imgui_bridge_; + int framebuffer_mode_ = 0; + mjvScene scene_; + double fps_ = 0; +}; + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_FILAMENT_RENDERER_H_ diff --git a/src/experimental/platform/hal/renderer.cc b/src/experimental/platform/hal/renderer.cc deleted file mode 100644 index 080da346..00000000 --- a/src/experimental/platform/hal/renderer.cc +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2025 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 "experimental/platform/hal/renderer.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#if !defined(__EMSCRIPTEN__) && !defined(__APPLE__) -#include "experimental/platform/hal/egl_utils.h" -#endif -#include -#include "experimental/filament/compat/scene_bridge.h" -#include "experimental/platform/hal/graphics_mode.h" -#include "experimental/platform/ux/imgui_bridge.h" -#include "experimental/platform/ux/imgui_widgets.h" -#include "experimental/platform/ux/plugin.h" -#include "render/filament/mjrfilament_cpp.h" - -namespace mujoco::platform { - -static std::function g_update_gui_callback = nullptr; -static void PluginUpdate(GuiPlugin* plugin) { - if (g_update_gui_callback) { - g_update_gui_callback(); - } -} - -static void FlipImage(unsigned char* pixels, int width, int height, int bpp) { - const int row_size = width * bpp; - for (int i = 0; i < height / 2; ++i) { - unsigned char* top_row = pixels + i * row_size; - unsigned char* bottom_row = pixels + (height - 1 - i) * row_size; - for (int j = 0; j < row_size; ++j) { - std::swap(top_row[j], bottom_row[j]); - } - } -} - -Renderer::Renderer(void* native_window, GraphicsMode gfx) - : native_window_(native_window), gfx_(gfx) { - if (IsClassic(gfx_)) { - if (native_window == nullptr) { -#if !defined(__EMSCRIPTEN__) && !defined(__APPLE__) - graphics_api_context_ = CreateEglContext(); -#endif - } - if (ImGui::GetCurrentContext()) { - ImGui_ImplOpenGL3_Init(); - } - } - - g_update_gui_callback = [this]() { - if (main_scene_) { - mjrf_DEBUG_drawImguiEditor(main_scene_.get()); - } - }; -} - -Renderer::~Renderer() { - g_update_gui_callback = nullptr; - if (IsClassic(gfx_)) { - if (ImGui::GetCurrentContext()) { - ImGui_ImplOpenGL3_Shutdown(); - } - } - Deinit(); - graphics_api_context_.reset(); -} - -void Renderer::Init(const mjModel* model) { - Deinit(); - if (model) { - if (IsClassic(gfx_)) { - mjr_defaultContext(&render_context_); - mjr_makeContext(model, &render_context_, mjFONTSCALE_150); - - } else { - mjrfContextConfig cfg; - mjrf_defaultContextConfig(&cfg); - cfg.native_window = native_window_; - cfg.force_software_rendering = IsSoftware(gfx_); - cfg.graphics_api = IsOpenGl(gfx_) || IsWebGl(gfx_) - ? mjGRAPHICS_API_OPENGL - : mjGRAPHICS_API_VULKAN; - filament_context_ = CreateContext(cfg); - main_scene_ = CreateScene(filament_context_.get(), {}); - ux_scene_ = CreateScene(filament_context_.get(), {}); - scene_bridge_ = std::make_unique(filament_context_.get(), - main_scene_.get(), model); - imgui_bridge_ = std::make_unique(filament_context_.get(), - ux_scene_.get()); - scene_bridge_->SetDrawTextFunction(DrawTextAt); - } - - mjv_defaultScene(&scene_); - mjv_makeScene(model, &scene_, 2000); - initialized_ = true; - } -} - -void Renderer::Deinit() { - if (initialized_) { - mjv_freeScene(&scene_); - if (IsClassic(gfx_)) { - mjr_freeContext(&render_context_); - } else { - scene_bridge_.reset(); - imgui_bridge_.reset(); - ux_scene_.reset(); - main_scene_.reset(); - filament_context_.reset(); - } - initialized_ = false; - } -} - -void Renderer::Render(const mjModel* model, mjData* data, - const mjvPerturb* perturb, mjvCamera* camera, - const mjvOption* vis_option, int width, int height, - std::span pixels, - std::span extra_geoms) { - if (!initialized_) { - return; - } - - mjvCamera default_cam; - if (camera == nullptr) { - if (model) { - mjv_defaultFreeCamera(model, &default_cam); - } else { - mjv_defaultCamera(&default_cam); - } - camera = &default_cam; - } - mjvOption default_opt; - if (vis_option == nullptr) { - mjv_defaultOption(&default_opt); - vis_option = &default_opt; - } - - mjv_updateScene(model, data, vis_option, perturb, camera, mjCAT_ALL, &scene_); - const int nextra_geoms = - std::min(extra_geoms.size(), scene_.maxgeom - scene_.ngeom); - for (int i = 0; i < nextra_geoms; ++i) { - scene_.geoms[scene_.ngeom++] = extra_geoms[i]; - } - - const bool render_to_texture = !pixels.empty(); - if (render_to_texture) { - // mjr_readPixels reads to a RGB buffer (i.e. 3 bytes per pixel). - if (pixels.size() != width * height * 3) { - mju_error("Offscreen mode requires a pixel buffer of size %d.", - width * height * 3); - } - if (IsClassic(gfx_)) { - mjr_resizeOffscreen(width, height, &render_context_); - } - - // The filament backend supports two offscreen framebuffers. - // mjFB_OFFSCREEN renders just the mjvScene data. +1 also includes the - // ImGui draw data. - DoSetBuffer(IsClassic(gfx_) ? mjFB_OFFSCREEN : mjFB_OFFSCREEN + 1); - } - - DoRender(width, height); - - // The filament backend knows how to renders the ImGui draw data. For the - // classic backend, we need to render the ImGui draw data ourselves. - if (IsClassic(gfx_) && ImGui::GetCurrentContext()) { - ImGui_ImplOpenGL3_NewFrame(); - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - } - - if (render_to_texture) { - unsigned char* ptr = reinterpret_cast(pixels.data()); - DoReadPixels(width, height, ptr); - } - - UpdateFps(); -} - -void Renderer::RenderToTexture(const mjModel* model, mjData* data, - mjvCamera* camera, int width, int height, - std::byte* output) { - if (!initialized_) { - return; - } - - mjv_updateCamera(model, data, camera, &scene_); - unsigned char* ptr = reinterpret_cast(output); - - DoSetBuffer(mjFB_OFFSCREEN); - DoRender(width, height); - DoReadPixels(width, height, ptr); -} - -int Renderer::UploadImage(int texture_id, const std::byte* pixels, int width, - int height, int bpp) { - if (IsClassic(gfx_)) { - return 0; - } else { - return imgui_bridge_->UploadImage( - texture_id, reinterpret_cast(pixels), width, - height, bpp); - } -} - -void Renderer::DoRender(int width, int height) { - const mjrRect viewport = {0, 0, width, height}; - if (IsClassic(gfx_)) { - mjr_render(viewport, &scene_, &render_context_); - } else { - scene_bridge_->Update(viewport, &scene_); - // Update the UX renderable entity after processing the scene in case there - // are any elements in the scene which generate UX draw calls (e.g. labels). - if (framebuffer_mode_ != 1) { - imgui_bridge_->Update(); - } - if (framebuffer_mode_ == 0) { - mjrDrawMode draw_mode = mjDRAW_MODE_DEFAULT; - if (scene_.flags[mjRND_SEGMENT]) { - if (scene_.flags[mjRND_IDCOLOR]) { - draw_mode = mjDRAW_MODE_SEGMENTATION_BY_ID; - } else { - draw_mode = mjDRAW_MODE_SEGMENTATION_BY_COLOR; - } - } else if (scene_.flags[mjRND_DEPTH]) { - draw_mode = mjDRAW_MODE_DEPTH; - } else if (scene_.flags[mjRND_WIREFRAME]) { - draw_mode = mjDRAW_MODE_WIREFRAME; - } - - mjrfRenderRequest reqs[2]; - - mjrf_defaultRenderRequest(&reqs[0]); - reqs[0].scene = main_scene_.get(); - reqs[0].draw_mode = draw_mode; - reqs[0].camera = scene_bridge_->GetCamera(); - reqs[0].viewport = viewport; - reqs[0].enable_shadows = scene_.flags[mjRND_SHADOW]; - reqs[0].enable_reflections = scene_.flags[mjRND_REFLECTION]; - - mjrf_defaultRenderRequest(&reqs[1]); - reqs[1].scene = ux_scene_.get(); - reqs[1].draw_mode = mjDRAW_MODE_DEFAULT; - reqs[1].camera = imgui_bridge_->GetCamera(viewport.width, viewport.height); - reqs[1].viewport = viewport; - reqs[1].enable_shadows = false; - reqs[1].enable_reflections = false; - reqs[1].enable_post_processing = false; - - mjrf_render(filament_context_.get(), &reqs[0], 2, nullptr, 0); - } - } -} - -void Renderer::DoSetBuffer(int framebuffer) { - framebuffer_mode_ = framebuffer; - if (IsClassic(gfx_)) { - mjr_setBuffer(framebuffer, &render_context_); - } -} - -void Renderer::DoReadPixels(int width, int height, unsigned char* rgb) { - if (!rgb) { - return; - } - if (framebuffer_mode_ == 0) { - mju_warning("ReadPixels is only supported for offscreen rendering."); - return; - } - - const mjrRect viewport = {0, 0, width, height}; - if (IsClassic(gfx_)) { - mjr_readPixels(rgb, nullptr, viewport, &render_context_); - mjr_setBuffer(mjFB_WINDOW, &render_context_); - FlipImage(rgb, viewport.width, viewport.height, 3); - } else { - mjrDrawMode draw_mode = mjDRAW_MODE_DEFAULT; - if (scene_.flags[mjRND_SEGMENT]) { - if (scene_.flags[mjRND_IDCOLOR]) { - draw_mode = mjDRAW_MODE_SEGMENTATION_BY_ID; - } else { - draw_mode = mjDRAW_MODE_SEGMENTATION_BY_COLOR; - } - } else if (scene_.flags[mjRND_DEPTH]) { - draw_mode = mjDRAW_MODE_DEPTH; - } else if (scene_.flags[mjRND_WIREFRAME]) { - draw_mode = mjDRAW_MODE_WIREFRAME; - } - - mjrfRenderTargetConfig config; - mjrf_defaultRenderTargetConfig(&config); - config.width = viewport.width; - config.height = viewport.height; - config.color_format = mjPIXEL_FORMAT_RGB8; - config.depth_format = mjPIXEL_FORMAT_DEPTH32F; - auto target = CreateRenderTarget(filament_context_.get(), config); - - mjrfRenderRequest reqs[2]; - mjrf_defaultRenderRequest(&reqs[0]); - reqs[0].scene = main_scene_.get(); - reqs[0].draw_mode = draw_mode; - reqs[0].camera = scene_bridge_->GetCamera(); - reqs[0].target = target.get(); - reqs[0].viewport = viewport; - reqs[0].enable_shadows = scene_.flags[mjRND_SHADOW]; - reqs[0].enable_reflections = scene_.flags[mjRND_REFLECTION]; - - mjrf_defaultRenderRequest(&reqs[1]); - reqs[1].scene = ux_scene_.get(); - reqs[1].draw_mode = mjDRAW_MODE_DEFAULT; - reqs[1].camera = imgui_bridge_->GetCamera(viewport.width, viewport.height); - reqs[1].target = target.get(); - reqs[1].viewport = viewport; - reqs[1].enable_shadows = false; - reqs[1].enable_reflections = false; - reqs[1].enable_post_processing = false; - - mjrfReadPixelsRequest read_request; - mjrf_defaultReadPixelsRequest(&read_request); - read_request.target = target.get(); - read_request.output = rgb; - read_request.num_bytes = viewport.width * viewport.height * 3; - - const int num_requests = (framebuffer_mode_ == 2) ? 2 : 1; - const mjrfFrameHandle frame = mjrf_render( - filament_context_.get(), &reqs[0], num_requests, &read_request, 1); - mjrf_waitForFrame(filament_context_.get(), frame); - } - DoSetBuffer(mjFB_WINDOW); -} - -double Renderer::GetFps() { return fps_; } - -void Renderer::UpdateFps() { - if (IsClassic(gfx_)) { - TimePoint now = std::chrono::steady_clock::now(); - TimePoint::duration delta_time = now - last_fps_update_; - const double interval = std::chrono::duration(delta_time).count(); - ++frames_; - if (interval > 0.2) { // only update FPS stat at most 5 times per second - last_fps_update_ = now; - fps_ = frames_ / interval; - frames_ = 0; - } - } else { - mjrfFrameStats stats; - mjrf_defaultFrameStats(&stats); - mjrf_getFrameStats(filament_context_.get(), 0, &stats); - fps_ = stats.frame_rate; - } -} - -} // namespace mujoco::platform - -mjPLUGIN_LIB_INIT(renderer) { - mujoco::platform::GuiPlugin plugin; - plugin.name = "Filament"; - plugin.update = [](mujoco::platform::GuiPlugin* self) { - mujoco::platform::PluginUpdate(self); - }; - mujoco::platform::RegisterPlugin(plugin); -} diff --git a/src/experimental/platform/hal/renderer.h b/src/experimental/platform/hal/renderer.h index 4d1e1db8..ff060023 100644 --- a/src/experimental/platform/hal/renderer.h +++ b/src/experimental/platform/hal/renderer.h @@ -15,117 +15,51 @@ #ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_RENDERER_H_ #define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HAL_RENDERER_H_ -#include #include -#include -#include #include -#include #include -#include "experimental/filament/compat/scene_bridge.h" -#include "experimental/platform/hal/graphics_mode.h" -#include "experimental/platform/ux/imgui_bridge.h" -#include "render/filament/mjrfilament_cpp.h" namespace mujoco::platform { // Renders the mujoco simulation and the imgui state. -// -// The Renderer is built around a specific backend configuration -// (see renderer_backend.h). -// -// Currently we support two rendering engines: the Classic MuJoCo OpenGL -// renderer and a Filament-based renderer. -// -// The Classic renderer is implemented using the OpenGL 2.0 fixed function -// pipeline. It assumes that the caller has correctly initialized the OpenGL -// context (e.g. using EGL). -// -// The Filament renderer is a modern physically-based renderer that supports -// OpenGL 3.0, Vulkan, and WebGL (as well as other graphics libraries). Unlike -// the Classic renderer, the Filament engine itself will manage its graphics -// context. If rendering to a window surface (e.g. x11), it requires a pointer -// to the native window to do so. -// -// For OpenGL, two different Filament configurations are available: normal and -// headless. Normal rendering assumes that we will be rendering to an x11 -// window and therefore will use a x11-based context. Headless assumes that we -// will be rendering to a texture and will use an EGL context. Vulkan and WebGL -// have no need for such a distinction. class Renderer { public: - using Clock = std::chrono::steady_clock; - using TimePoint = std::chrono::time_point; - using Seconds = std::chrono::duration; - using Milliseconds = std::chrono::duration; - - Renderer(void* native_window, GraphicsMode gfx_mode); - ~Renderer(); + Renderer() = default; + virtual ~Renderer() = default; Renderer(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete; // Initializes the renderer with the given mjModel. - void Init(const mjModel* model); + virtual void Init(const mjModel* model) = 0; // Renders the simulation and ux state. Renders into `pixels` if provided, // otherwise renders to the `native_window` provided at construction. - void Render(const mjModel* model, mjData* data, const mjvPerturb* perturb, - mjvCamera* camera, const mjvOption* vis_option, int width, - int height, std::span pixels = {}, - std::span extra_geoms = {}); + virtual void Render(const mjModel* model, mjData* data, + const mjvPerturb* perturb, mjvCamera* camera, + const mjvOption* vis_option, int width, int height, + std::span pixels = {}, + std::span extra_geoms = {}) = 0; // Populates the given output buffer with RGB888 pixel data. The size of the // output buffer must be at least width * height * 3. - void RenderToTexture(const mjModel* model, mjData* data, mjvCamera* camera, - int width, int height, std::byte* output); + virtual void RenderToTexture(const mjModel* model, mjData* data, + mjvCamera* camera, int width, int height, + std::byte* output) = 0; // Uploads an image to the backend for GUI rendering, returning the texture // ID for the texture. The ID can be used in subsequent calls to update the // texture data. A nullptr pixels argument will free the texture if it exists. // A texture ID of 0 will create a new texture. - int UploadImage(int texture_id, const std::byte* pixels, int width, - int height, int bpp); + virtual int UploadImage(int texture_id, const std::byte* pixels, int width, + int height, int bpp) = 0; // Rendering flags. - mjtByte* GetRenderFlags() { return scene_.flags; } + virtual mjtByte* GetRenderFlags() = 0; // Returns the current frame rate. - double GetFps(); - - private: - // Resets the renderer; no rendering will occur until Init() is called again. - void Deinit(); - - void UpdateFps(); - - void DoRender(int width, int height); - void DoSetBuffer(int framebuffer); - void DoReadPixels(int width, int height, unsigned char* rgb); - - void* native_window_ = nullptr; - GraphicsMode gfx_ = GraphicsMode::FilamentVulkan; - - // State used by the classic renderer. - std::shared_ptr graphics_api_context_ = nullptr; - mjrContext render_context_; - - // State used by the filament renderer. - UniquePtr filament_context_{nullptr, nullptr}; - UniquePtr main_scene_{nullptr, nullptr}; - UniquePtr ux_scene_{nullptr, nullptr}; - std::unique_ptr scene_bridge_; - std::unique_ptr imgui_bridge_; - - // Common state. - int framebuffer_mode_ = 0; - mjvScene scene_; - bool initialized_ = false; - mjtNum last_update_time_ = -1; - int frames_ = 0; - TimePoint last_fps_update_; - double fps_ = 0; + virtual double GetFps() = 0; }; } // namespace mujoco::platform diff --git a/src/experimental/platform/hal/renderer_test.cc b/src/experimental/platform/hal/renderer_test.cc index 0e020162..af8d5496 100644 --- a/src/experimental/platform/hal/renderer_test.cc +++ b/src/experimental/platform/hal/renderer_test.cc @@ -11,7 +11,7 @@ // 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 "experimental/platform/hal/renderer.h" +#include "experimental/platform/hal/filament_renderer.h" #include #include @@ -50,7 +50,7 @@ class RendererTest : public ::testing::Test { }; TEST_F(RendererTest, OpengGlSoftware) { - Renderer renderer(nullptr, GraphicsMode::FilamentOpenGlSoftware); + FilamentRenderer renderer(nullptr, GraphicsMode::FilamentOpenGlSoftware); renderer.Init(holder_->model()); gl::DriverType driver_type = gl::GetLoadedDriverType(); @@ -69,7 +69,7 @@ TEST_F(RendererTest, OpengGlSoftware) { } TEST_F(RendererTest, OpengGlHeadless) { - Renderer renderer(nullptr, GraphicsMode::FilamentOpenGlHeadless); + FilamentRenderer renderer(nullptr, GraphicsMode::FilamentOpenGlHeadless); renderer.Init(holder_->model()); gl::DriverType driver_type = gl::GetLoadedDriverType(); @@ -92,7 +92,7 @@ TEST_F(RendererTest, OpengGlHeadless) { } TEST_F(RendererTest, VulkanSoftware) { - Renderer renderer(nullptr, GraphicsMode::FilamentVulkanSoftware); + FilamentRenderer renderer(nullptr, GraphicsMode::FilamentVulkanSoftware); renderer.Init(holder_->model()); std::vector pixels(width_ * height_ * 3); @@ -108,7 +108,7 @@ TEST_F(RendererTest, VulkanSoftware) { } TEST_F(RendererTest, VulkanHeadless) { - Renderer renderer(nullptr, GraphicsMode::FilamentVulkan); + FilamentRenderer renderer(nullptr, GraphicsMode::FilamentVulkan); renderer.Init(holder_->model()); std::vector pixels(width_ * height_ * 3); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 134bd91f..4b92367c 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -36,6 +36,8 @@ #include #include #include +#include "experimental/platform/hal/classic_renderer.h" +#include "experimental/platform/hal/filament_renderer.h" #include "experimental/platform/hal/graphics_mode.h" #include "experimental/platform/hal/renderer.h" #include "experimental/platform/hal/window.h" @@ -119,8 +121,14 @@ void App::SwitchGraphicsMode(int width, int height, window_config.gfx_mode = gfx_mode_; window_ = std::make_unique(app_title_, width, height, window_config); - renderer_ = std::make_unique( - window_->GetNativeWindowHandle(), gfx_mode_); + if (platform::IsClassic(gfx_mode_)) { + renderer_ = std::make_unique( + window_->GetNativeWindowHandle(), gfx_mode_); + + } else { + renderer_ = std::make_unique( + window_->GetNativeWindowHandle(), gfx_mode_); + } // TODO: Figure out why this breaks on some platforms. // LoadSettings();