diff --git a/src/experimental/filament/filament/filament_platform_factory.cc b/src/experimental/filament/filament/filament_platform_factory.cc index a7dc9709..9d9863cf 100644 --- a/src/experimental/filament/filament/filament_platform_factory.cc +++ b/src/experimental/filament/filament/filament_platform_factory.cc @@ -14,6 +14,7 @@ #include "experimental/filament/filament/filament_platform_factory.h" +#include #include #include diff --git a/src/experimental/platform/hal/graphics_mode.cc b/src/experimental/platform/hal/graphics_mode.cc index d992f1f4..87547e5a 100644 --- a/src/experimental/platform/hal/graphics_mode.cc +++ b/src/experimental/platform/hal/graphics_mode.cc @@ -28,6 +28,7 @@ bool IsClassic(GraphicsMode gfx_mode) { bool IsFilament(GraphicsMode gfx_mode) { return gfx_mode == GraphicsMode::FilamentOpenGl || gfx_mode == GraphicsMode::FilamentVulkan || + gfx_mode == GraphicsMode::FilamentVulkanSoftware || gfx_mode == GraphicsMode::FilamentWebGl || gfx_mode == GraphicsMode::FilamentOpenGlHeadless || gfx_mode == GraphicsMode::FilamentOpenGlSoftware; @@ -42,7 +43,8 @@ bool IsOpenGl(GraphicsMode gfx_mode) { } bool IsVulkan(GraphicsMode gfx_mode) { - return gfx_mode == GraphicsMode::FilamentVulkan; + return gfx_mode == GraphicsMode::FilamentVulkan || + gfx_mode == GraphicsMode::FilamentVulkanSoftware; } bool IsWebGl(GraphicsMode gfx_mode) { @@ -56,7 +58,8 @@ bool IsHeadless(GraphicsMode gfx_mode) { } bool IsSoftware(GraphicsMode gfx_mode) { - return gfx_mode == GraphicsMode::FilamentOpenGlSoftware; + return gfx_mode == GraphicsMode::FilamentOpenGlSoftware || + gfx_mode == GraphicsMode::FilamentVulkanSoftware; } GraphicsMode GraphicsModeFromString(std::string_view str, @@ -69,6 +72,8 @@ GraphicsMode GraphicsModeFromString(std::string_view str, return GraphicsMode::FilamentOpenGl; } else if (str == "vulkan") { return GraphicsMode::FilamentVulkan; + } else if (str == "vulkan_software") { + return GraphicsMode::FilamentVulkanSoftware; } else if (str == "webgl") { return GraphicsMode::FilamentWebGl; } else if (str == "opengl_headless") { diff --git a/src/experimental/platform/hal/graphics_mode.h b/src/experimental/platform/hal/graphics_mode.h index d81dfa96..9b8045fc 100644 --- a/src/experimental/platform/hal/graphics_mode.h +++ b/src/experimental/platform/hal/graphics_mode.h @@ -33,6 +33,9 @@ enum class GraphicsMode { // The Filament-based renderer running on Vulkan. FilamentVulkan, + // The Filament-based renderer running on Vulkan using software rendering. + FilamentVulkanSoftware, + // The Filament-based renderer running on WebGL. FilamentWebGl, diff --git a/src/experimental/platform/hal/renderer_test.cc b/src/experimental/platform/hal/renderer_test.cc new file mode 100644 index 00000000..0e020162 --- /dev/null +++ b/src/experimental/platform/hal/renderer_test.cc @@ -0,0 +1,127 @@ +// Copyright 2026 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 "third_party/mujoco/google/gfx/opengl_dynamic_loader.h" +#include +#include "experimental/platform/hal/graphics_mode.h" +#include "experimental/platform/sim/model_holder.h" + +#include "testing/base/public/gunit.h" + +namespace mujoco::platform { +namespace { + +class RendererTest : public ::testing::Test { + public: + void SetUp() { + mjSpec* spec = mj_makeSpec(); + // Set a clear color. + const double clear_color[] = {1.0, 1.0, 1.0, 1.0}; + mjsNumeric* numeric = mjs_addNumeric(spec); + mjs_setName(numeric->element, "filament.clearColor"); + numeric->size = 4; + mjs_setDouble(numeric->data, clear_color, numeric->size); + holder_ = ModelHolder::FromSpec(spec); + holder_->model()->vis.global.offwidth = width_; + holder_->model()->vis.global.offheight = height_; + } + + void Test(); + + int width_ = 2; + int height_ = 2; + std::unique_ptr holder_; +}; + +TEST_F(RendererTest, OpengGlSoftware) { + Renderer renderer(nullptr, GraphicsMode::FilamentOpenGlSoftware); + renderer.Init(holder_->model()); + + gl::DriverType driver_type = gl::GetLoadedDriverType(); + ASSERT_EQ(driver_type, gl::DriverType::kOsMesa); + + std::vector pixels(width_ * height_ * 3); + renderer.Render(holder_->model(), holder_->data(), nullptr, nullptr, nullptr, + width_, height_, pixels); + // We set the clear color to white, but we don't know the exact color due to + // post processing, but it should definitely not be black. + for (int i = 0; i < pixels.size(); i += 3) { + EXPECT_NE((int)pixels[i + 0], 0); + EXPECT_NE((int)pixels[i + 1], 0); + EXPECT_NE((int)pixels[i + 2], 0); + } +} + +TEST_F(RendererTest, OpengGlHeadless) { + Renderer renderer(nullptr, GraphicsMode::FilamentOpenGlHeadless); + renderer.Init(holder_->model()); + + gl::DriverType driver_type = gl::GetLoadedDriverType(); + #if TEST_HAS_GPU + ASSERT_EQ(driver_type, gl::DriverType::kEgl); + #else + ASSERT_EQ(driver_type, gl::DriverType::kOsMesa); + #endif + + std::vector pixels(width_ * height_ * 3); + renderer.Render(holder_->model(), holder_->data(), nullptr, nullptr, nullptr, + width_, height_, pixels); + // We set the clear color to white, but we don't know the exact color due to + // post processing, but it should definitely not be black. + for (int i = 0; i < pixels.size(); i += 3) { + EXPECT_NE((int)pixels[i + 0], 0); + EXPECT_NE((int)pixels[i + 1], 0); + EXPECT_NE((int)pixels[i + 2], 0); + } +} + +TEST_F(RendererTest, VulkanSoftware) { + Renderer renderer(nullptr, GraphicsMode::FilamentVulkanSoftware); + renderer.Init(holder_->model()); + + std::vector pixels(width_ * height_ * 3); + renderer.Render(holder_->model(), holder_->data(), nullptr, nullptr, nullptr, + width_, height_, pixels); + // We set the clear color to white, but we don't know the exact color due to + // post processing, but it should definitely not be black. + for (int i = 0; i < pixels.size(); i += 3) { + EXPECT_NE((int)pixels[i + 0], 0); + EXPECT_NE((int)pixels[i + 1], 0); + EXPECT_NE((int)pixels[i + 2], 0); + } +} + +TEST_F(RendererTest, VulkanHeadless) { + Renderer renderer(nullptr, GraphicsMode::FilamentVulkan); + renderer.Init(holder_->model()); + + std::vector pixels(width_ * height_ * 3); + renderer.Render(holder_->model(), holder_->data(), nullptr, nullptr, nullptr, + width_, height_, pixels); + // We set the clear color to white, but we don't know the exact color due to + // post processing, but it should definitely not be black. + for (int i = 0; i < pixels.size(); i += 3) { + EXPECT_NE((int)pixels[i + 0], 0); + EXPECT_NE((int)pixels[i + 1], 0); + EXPECT_NE((int)pixels[i + 2], 0); + } +} + +} // namespace +} // namespace mujoco::platform diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index f3cf2f62..1bed7a85 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -1712,6 +1712,11 @@ void App::MainMenuGui() { gfx_mode_ == platform::GraphicsMode::FilamentVulkan)) { mode = platform::GraphicsMode::FilamentVulkan; } + if (ImGui::MenuItem( + "Filament Vulkan Software", nullptr, + gfx_mode_ == platform::GraphicsMode::FilamentVulkanSoftware)) { + mode = platform::GraphicsMode::FilamentVulkanSoftware; + } if (mode.has_value()) { pending_op_ = [=, this]() { const int width = window_->GetWidth();