// OpenXR Simulator configurator. // // A small GLFW + Dear ImGui tool that edits the runtime's JSON config file. The runtime DLL reads // the same file (resolved via XRSIM_CONFIG, else /openxr_simulator_config.json). #define GLFW_INCLUDE_NONE #ifdef _WIN32 # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif # include #endif #include #include #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include "Config.h" #include "HeadsetPresets.h" #include #include namespace { int PresentModeIndex( xrsim::PresentMode mode ) { switch ( mode ) { case xrsim::PresentMode::Mailbox: return 0; case xrsim::PresentMode::Immediate: return 1; case xrsim::PresentMode::Fifo: return 2; } return 0; } int DisplayModeIndex( xrsim::DisplayMode mode ) { switch ( mode ) { case xrsim::DisplayMode::SideBySide: return 0; case xrsim::DisplayMode::SingleEyeLeft: return 1; case xrsim::DisplayMode::SingleEyeRight: return 2; } return 0; } } // namespace int main( int, char ** ) { if ( glfwInit() != GLFW_TRUE ) { return 1; } const char * glslVersion = "#version 130"; glfwWindowHint( GLFW_CONTEXT_VERSION_MAJOR, 3 ); glfwWindowHint( GLFW_CONTEXT_VERSION_MINOR, 0 ); GLFWwindow * window = glfwCreateWindow( 920, 720, "OpenXR Simulator Configurator", nullptr, nullptr ); if ( window == nullptr ) { glfwTerminate(); return 1; } glfwMakeContextCurrent( window ); glfwSwapInterval( 1 ); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL( window, true ); ImGui_ImplOpenGL3_Init( glslVersion ); std::string configPath = xrsim::SimulatorConfig::ResolvePath(); xrsim::SimulatorConfig config = xrsim::SimulatorConfig::LoadFrom( configPath ); std::string status; while ( glfwWindowShouldClose( window ) == 0 ) { glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); const ImGuiViewport * viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos( viewport->WorkPos ); ImGui::SetNextWindowSize( viewport->WorkSize ); ImGui::Begin( "OpenXR Simulator Config", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize ); ImGui::TextWrapped( "These settings are read by openxr_simulator.dll at runtime startup. Restart the " "application after saving." ); ImGui::Separator(); static char pathBuffer[1024] = {}; if ( pathBuffer[0] == '\0' ) { std::strncpy( pathBuffer, configPath.c_str(), sizeof( pathBuffer ) - 1 ); } if ( ImGui::InputText( "Config path", pathBuffer, sizeof( pathBuffer ) ) ) { configPath = pathBuffer; } if ( ImGui::Button( "Reload" ) ) { config = xrsim::SimulatorConfig::LoadFrom( configPath ); status = "Reloaded."; } ImGui::SameLine(); if ( ImGui::Button( "Save" ) ) { status = config.Save( configPath ) ? "Saved." : "Failed to save!"; } ImGui::SameLine(); if ( ImGui::Button( "Reset to defaults" ) ) { config = xrsim::SimulatorConfig{}; status = "Reset to defaults (not saved yet)."; } if ( !status.empty() ) { ImGui::SameLine(); ImGui::TextColored( ImVec4( 0.5f, 0.9f, 0.5f, 1.0f ), "%s", status.c_str() ); } ImGui::Separator(); { constexpr int presetCount = static_cast( xrsim::kHeadsetPresets.size() ); static const char * presetItems[presetCount + 1] = {}; if ( presetItems[0] == nullptr ) { presetItems[0] = "Custom"; for ( int i = 0; i < presetCount; ++i ) { presetItems[i + 1] = xrsim::kHeadsetPresets[i].name; } } static int presetIndex = 0; if ( ImGui::Combo( "Headset preset", &presetIndex, presetItems, presetCount + 1 ) && presetIndex > 0 ) { const xrsim::HeadsetPreset & preset = xrsim::kHeadsetPresets[presetIndex - 1]; config.systemName = preset.name; config.eyeWidth = preset.eyeWidth; config.eyeHeight = preset.eyeHeight; config.refreshRateHz = preset.refreshRateHz; config.fovLeftDeg = preset.fovLeftDeg; config.fovRightDeg = preset.fovRightDeg; config.fovUpDeg = preset.fovUpDeg; config.fovDownDeg = preset.fovDownDeg; config.ipdMeters = preset.ipdMeters; status = std::string( "Applied preset: " ) + preset.name; } } ImGui::Separator(); if ( ImGui::CollapsingHeader( "Headset", ImGuiTreeNodeFlags_DefaultOpen ) ) { char nameBuffer[256]; std::strncpy( nameBuffer, config.systemName.c_str(), sizeof( nameBuffer ) - 1 ); nameBuffer[sizeof( nameBuffer ) - 1] = '\0'; if ( ImGui::InputText( "System name", nameBuffer, sizeof( nameBuffer ) ) ) { config.systemName = nameBuffer; } ImGui::InputScalar( "Vendor id", ImGuiDataType_U32, &config.vendorId ); } if ( ImGui::CollapsingHeader( "Display", ImGuiTreeNodeFlags_DefaultOpen ) ) { ImGui::InputScalar( "Eye width", ImGuiDataType_U32, &config.eyeWidth ); ImGui::InputScalar( "Eye height", ImGuiDataType_U32, &config.eyeHeight ); ImGui::InputScalar( "Refresh rate (Hz)", ImGuiDataType_U32, &config.refreshRateHz ); ImGui::SliderFloat( "IPD (m)", &config.ipdMeters, 0.04f, 0.09f, "%.3f" ); ImGui::InputScalar( "Swapchain images", ImGuiDataType_U32, &config.swapchainImageCount ); } if ( ImGui::CollapsingHeader( "Field of view (half-angle degrees)", ImGuiTreeNodeFlags_DefaultOpen ) ) { ImGui::SliderFloat( "Left", &config.fovLeftDeg, 20.0f, 80.0f, "%.1f" ); ImGui::SliderFloat( "Right", &config.fovRightDeg, 20.0f, 80.0f, "%.1f" ); ImGui::SliderFloat( "Up", &config.fovUpDeg, 20.0f, 80.0f, "%.1f" ); ImGui::SliderFloat( "Down", &config.fovDownDeg, 20.0f, 80.0f, "%.1f" ); if ( ImGui::Button( "Symmetric" ) ) { config.fovRightDeg = config.fovLeftDeg; config.fovUpDeg = config.fovLeftDeg; config.fovDownDeg = config.fovLeftDeg; } } if ( ImGui::CollapsingHeader( "Compositor window", ImGuiTreeNodeFlags_DefaultOpen ) ) { char titleBuffer[256]; std::strncpy( titleBuffer, config.windowTitle.c_str(), sizeof( titleBuffer ) - 1 ); titleBuffer[sizeof( titleBuffer ) - 1] = '\0'; if ( ImGui::InputText( "Window title", titleBuffer, sizeof( titleBuffer ) ) ) { config.windowTitle = titleBuffer; } ImGui::InputInt( "Window width", &config.windowWidth ); ImGui::InputInt( "Window height", &config.windowHeight ); int presentIndex = PresentModeIndex( config.presentMode ); const char * presentItems[] = { "Mailbox (uncapped)", "Immediate (uncapped, tearing)", "FIFO (vsync)" }; if ( ImGui::Combo( "Present mode", &presentIndex, presentItems, IM_ARRAYSIZE( presentItems ) ) ) { config.presentMode = static_cast( presentIndex ); } int displayIndex = DisplayModeIndex( config.displayMode ); const char * displayItems[] = { "Side by side", "Single eye (left)", "Single eye (right)" }; if ( ImGui::Combo( "Display mode", &displayIndex, displayItems, IM_ARRAYSIZE( displayItems ) ) ) { config.displayMode = static_cast( displayIndex ); } } ImGui::Separator(); ImGui::TextDisabled( "Note: 'Mailbox' avoids capping the application to the monitor refresh rate." ); ImGui::End(); ImGui::Render(); int displayWidth = 0, displayHeight = 0; glfwGetFramebufferSize( window, &displayWidth, &displayHeight ); glViewport( 0, 0, displayWidth, displayHeight ); glClearColor( 0.10f, 0.10f, 0.12f, 1.0f ); glClear( GL_COLOR_BUFFER_BIT ); ImGui_ImplOpenGL3_RenderDrawData( ImGui::GetDrawData() ); glfwSwapBuffers( window ); } ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow( window ); glfwTerminate(); return 0; }