diff --git a/package/Shaders/VR/InSceneOverlay.ps.hlsl b/package/Shaders/VR/InSceneOverlay.ps.hlsl new file mode 100644 index 0000000000..e8c01fe231 --- /dev/null +++ b/package/Shaders/VR/InSceneOverlay.ps.hlsl @@ -0,0 +1,17 @@ +// VR In-Scene Overlay Pixel Shader +// Samples overlay texture with alpha blending support + +Texture2D shaderTexture : register(t0); +SamplerState sampleType : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +float4 main(PS_INPUT input) : SV_TARGET +{ + float4 color = shaderTexture.Sample(sampleType, input.uv); + return color; +} diff --git a/package/Shaders/VR/InSceneOverlay.vs.hlsl b/package/Shaders/VR/InSceneOverlay.vs.hlsl new file mode 100644 index 0000000000..ea5cba5d4b --- /dev/null +++ b/package/Shaders/VR/InSceneOverlay.vs.hlsl @@ -0,0 +1,27 @@ +// VR In-Scene Overlay Vertex Shader +// Simple pass-through shader for rendering overlay quad in VR + +cbuffer MatrixBuffer : register(b0) +{ + matrix wvp; +}; + +struct VS_INPUT +{ + float3 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +PS_INPUT main(VS_INPUT input) +{ + PS_INPUT output; + output.pos = mul(float4(input.pos, 1.0f), wvp); + output.uv = input.uv; + return output; +} diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 47d4f6a9d7..23b547341f 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -1,41 +1,21 @@ #include "VR.h" #include "FeatureConstraints.h" #include "Menu.h" -#include "Menu/Fonts.h" #include "RE/B/BSOpenVR.h" -#include "RE/N/NiPoint3.h" #include "RE/P/PlayerCharacter.h" #include "Upscaling.h" -#include +#include "VR/OpenVRDetection.h" #include "State.h" #include "Utils/D3D.h" -#include "Utils/PerfUtils.h" -#include "Utils/UI.h" #include "Utils/VRUtils.h" -#include -#include -#include + #include #include -#include -#include -#include -#pragma comment(lib, "version.lib") +#include using AttachMode = VR::Settings::OverlayAttachMode; -namespace -{ - bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) - { - return MenuFonts::BeginTabItemWithFont(label, role, flags); - } -} - -constexpr int kOverlayWidth = 1920; -constexpr int kOverlayHeight = 1080; - NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( VR::Settings, EnableDepthBufferCullingInterior, @@ -71,7 +51,6 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void VR::LoadSettings(json& o_json) { settings = o_json.get(); - // Validate and clamp loaded settings to ensure they're within valid ranges settings.ClampToValidRanges(); } @@ -87,21 +66,27 @@ void VR::RestoreDefaultSettings() void VR::SetupResources() { - // Detect OpenVR version and compatibility early to avoid CTDs DetectOpenVRInfo(); - // Log OpenVR information if (openVRInfo.isAvailable) { logger::info("OpenVR DLL detected:"); logger::info(" Path: {}", openVRInfo.dllPath); logger::info(" Version: {}", openVRInfo.version); logger::info(" Size: {} bytes", openVRInfo.fileSize); logger::info(" Modified: {}", openVRInfo.modificationTime); + logger::info(" Runtime: {}", VRDetection::RuntimeTypeToString(openVRInfo.runtimeType)); + logger::info(" Interface probing: {}", openVRInfo.probingSucceeded ? "Passed" : "Failed"); + logger::info(" Overlay (IVROverlay_016): {}", openVRInfo.hasOverlayInterface ? "Yes" : "No"); + logger::info(" System (IVRSystem_017): {}", openVRInfo.hasSystemInterface ? "Yes" : "No"); + logger::info(" Compositor (IVRCompositor_021): {}", openVRInfo.hasCompositorInterface ? "Yes" : "No"); logger::info(" Compatible: {}", openVRInfo.isCompatible ? "Yes" : "No"); if (!openVRInfo.isCompatible) { - logger::info("OpenVR version is incompatible."); - logger::info("Community Shaders VR menus will be disabled for stability"); + if (globals::state->IsDeveloperMode()) { + logger::info("OpenVR not natively compatible, but developer mode is active - VR menus enabled"); + } else { + logger::info("OpenVR version is incompatible. Community Shaders VR menus will be disabled for stability"); + } } } else { logger::info("OpenVR DLL not available in current process"); @@ -112,7 +97,7 @@ void VR::PostPostLoad() { gDepthBufferCulling = reinterpret_cast(REL::Offset(0x1EC6B88).address()); if (!gDepthBufferCulling) { - static bool s_defaultDepthBufferCulling = false; // safe fallback + static bool s_defaultDepthBufferCulling = false; gDepthBufferCulling = &s_defaultDepthBufferCulling; logger::warn("VR: gDepthBufferCulling address not found - using fallback default (false)"); } @@ -124,7 +109,22 @@ void VR::PostPostLoad() logger::warn("VR: gMinOccludeeBoxExtent address not found - using fallback default (10.0)"); } - // Patches BSGeometry::CopyTransformAndBounds to copy the model-bound translation across correctly instead of overwriting it with the bounding sphere centre + // Migration: Fix legacy overlay keybinds + if (settings.VROverlayCloseKeys.size() == 1) { + auto& closeKey = settings.VROverlayCloseKeys[0]; + if (closeKey.GetDevice() == ControllerDevice::Keyboard && closeKey.GetKey() == 32) { + settings.VROverlayCloseKeys[0] = InputCombo::Primary(32); + logger::info("VR: Migrated VROverlayCloseKeys from Keyboard(32) to Primary(32)"); + } + } + if (settings.VROverlayOpenKeys.size() == 1) { + auto& openKey = settings.VROverlayOpenKeys[0]; + if (openKey.GetDevice() == ControllerDevice::Keyboard && openKey.GetKey() == 32) { + settings.VROverlayOpenKeys[0] = InputCombo::Secondary(32); + logger::info("VR: Migrated VROverlayOpenKeys from Keyboard(32) to Secondary(32)"); + } + } + REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xD9) + 0x2, 0x148); REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xE5) + 0x2, 0x14C); REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xF1) + 0x2, 0x150); @@ -132,8 +132,6 @@ void VR::PostPostLoad() void VR::DataLoaded() { - // Initialize occlusion culling based on settings, but force-disable if an external - // upscaler is active (FSR/DLSS) since upscalers may modify the depth buffer. bool desired = settings.EnableDepthBufferCullingExterior; UpdateDepthBufferCulling(desired, { "VR", "EnableDepthBufferCullingExterior" }); @@ -146,8 +144,6 @@ void VR::DataLoaded() void VR::EarlyPrepass() { - // Respect user settings unless an external upscaler is active; if so, force-disable - // depth-buffer culling to avoid incorrect occlusion tests in VR. bool isInterior = RE::TES::GetSingleton()->interiorCell != nullptr; auto settingId = isInterior ? FeatureConstraints::SettingId{ "VR", "EnableDepthBufferCullingInterior" } : FeatureConstraints::SettingId{ "VR", "EnableDepthBufferCullingExterior" }; bool desired = isInterior ? settings.EnableDepthBufferCullingInterior : settings.EnableDepthBufferCullingExterior; @@ -155,2500 +151,99 @@ void VR::EarlyPrepass() } //============================================================================= -// OVERLAY FEATURE OVERRIDES +// OVERLAY SUBMIT AND DEPTH BUFFER CULLING //============================================================================= -void VR::DrawOverlay() +void VR::RecreateOverlayTexturesIfNeeded() { - auto& vr = globals::features::vr; - if (!vr.openVRInfo.isCompatible) - return; - static LARGE_INTEGER overlayShowStart = { 0 }; - static LARGE_INTEGER freq = { 0 }; - - bool shouldShow = settings.kAutoHideSeconds > 0 && globals::game::ui && globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) && globals::menu && !globals::menu->IsEnabled; - - if (!shouldShow) { - overlayShowStart.QuadPart = 0; // Reset timer when overlay is not shown - return; - } - - if (freq.QuadPart == 0) { - QueryPerformanceFrequency(&freq); - } - - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - - if (overlayShowStart.QuadPart == 0) { - overlayShowStart = now; - } - - double elapsed = double(now.QuadPart - overlayShowStart.QuadPart) / double(freq.QuadPart); - const double autoHideSeconds = static_cast(settings.kAutoHideSeconds); - if (elapsed >= autoHideSeconds) { - return; - } - int secondsLeft = int(std::ceil(autoHideSeconds - elapsed)); - - ImGuiIO& io = ImGui::GetIO(); - ImVec2 overlaySize(480, 0); // width, height auto - ImVec2 overlayPos = ImVec2((io.DisplaySize.x - overlaySize.x) * 0.5f, 80.0f); - ImGui::SetNextWindowPos(overlayPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(overlaySize, ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.92f); - - ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); - ImGui::Text("How to Use VR Community Shaders Menu:"); - ImGui::Separator(); - ImGui::Text("You must be in the Main Menu or Tween Menu for these key binds to work."); - ImGui::Spacing(); - ImGui::Text("Open Menu: "); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - ImGui::Text("\nClose Menu: "); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - ImGui::Spacing(); - ImGui::TextDisabled("(This message will auto-disable in %d seconds)", secondsLeft); - ImGui::TextDisabled("(You can disable this message in VR settings > Controller Input Instructions)"); - ImGui::End(); + Util::CreateOverlayTextureAndRTV(globals::d3d::device, Config::kOverlayWidth, Config::kOverlayHeight, menuTexture.put(), menuRTV.put()); } -namespace +void VR::SubmitOverlayFrame() { - void DrawControllerInputInstructions(); - void DrawGeneralVRSettings(); - void DrawMenuSettings(); - void DrawMouseSettings(); - void DrawDragSettings(); - void DrawKeyBindings(); - void DrawDebugSection(); -} + InstallSubmitHook(); -void VR::DrawSettings() -{ - auto menu = globals::menu; - if (!menu) + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) { return; - if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { - // General Settings Tab - if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { - DrawGeneralVRSettings(); - DrawControllerInputInstructions(); - DrawMenuSettings(); - DrawMouseSettings(); - DrawDragSettings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Key Bindings Tab - if (openVRInfo.isCompatible) { - if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { - DrawKeyBindings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - } - // Debug Tab (existing debug functionality) - if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { - DrawDebugSection(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); } - // Combo recording popup - if (this->isCapturingCombo) { - ImGui::OpenPopup("Record Combo"); - ImGui::SetNextWindowSize(ImVec2(400, 200), ImGuiCond_FirstUseEver); - if (ImGui::BeginPopupModal("Record Combo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - // Helper function to get button name - auto GetButtonName = [](uint32_t key) -> const char* { - switch (key) { - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTrigger): - return "Trigger"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip): - return "Grip"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTouchpadClick): - return "Touchpad"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger): - return "Stick Click"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kXA): - return "A/X"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kBY): - return "B/Y"; - default: - return "Unknown"; - } - }; - - ImGui::Text("Recording combo for: %s", this->currentComboName ? this->currentComboName : "Unknown"); - ImGui::Spacing(); - - ImGui::TextDisabled("(During recording, any controller's buttons can be used. Requirement is only enforced during use.)"); - - ImGui::Spacing(); - - // Show countdown timer with color - double remainingTime = this->comboTimeout - (Util::GetNowSecs() - this->comboStartTime); - ImVec4 timerColor = remainingTime > 2.0 ? Util::Colors::GetTimerGood() : - remainingTime > 1.0 ? Util::Colors::GetTimerWarning() : - Util::Colors::GetTimerCritical(); - ImGui::TextColored(timerColor, "Time remaining: %.1f seconds", remainingTime); - - ImGui::Spacing(); - - // Show recorded buttons - if (this->recordedCombo.empty()) { - ImGui::Text("Press buttons to record combo..."); - } else { - ImGui::Text("Recorded buttons:"); - // Create a sorted list of decoded buttons for consistent display - std::vector sortedRecordedCombos; - for (size_t i = 0; i < this->recordedCombo.size(); ++i) { - sortedRecordedCombos.push_back(this->recordedCombo[i]); - } - std::sort(sortedRecordedCombos.begin(), sortedRecordedCombos.end(), - [](const ButtonCombo& a, const ButtonCombo& b) { - return a.GetKey() < b.GetKey(); - }); - - Util::DrawButtonCombo(sortedRecordedCombos, false); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - // Instructions - ImGui::Text("Press ENTER to accept, ESC to cancel"); - - // Handle button recording - // Check for VR controller button presses - record them (any controller allowed during recording) - bool buttonPressed = false; - uint32_t pressedKey = 0; - ControllerDevice pressedDevice = ControllerDevice::Both; // Default to Both, will set below - - // Check primary controller buttons - for (const auto& [keyCode, buttonState] : primaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Primary; - break; - } - } - - // Check secondary controller buttons if primary didn't have any - if (!buttonPressed) { - for (const auto& [keyCode, buttonState] : secondaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Secondary; - break; - } - } - } - - // Record button press - if (buttonPressed) { - // Check if this button is already in the combo (avoid duplicates) - auto it = recordingButtonControllers.find(pressedKey); - if (it == recordingButtonControllers.end()) { - // Not yet recorded, add with the current device - recordingButtonControllers[pressedKey] = pressedDevice; - } else { - // Already recorded, if the other controller is now pressed, set to BOTH - if (it->second != pressedDevice && it->second != ControllerDevice::Both) { - it->second = ControllerDevice::Both; - } - } - // Update the recordedCombo vector to match the map - this->recordedCombo.clear(); - for (const auto& [key, device] : recordingButtonControllers) { - this->recordedCombo.push_back(ButtonCombo(device, key)); - } - } - - // Handle ENTER key to accept combo - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))) { - if (!this->recordedCombo.empty()) { - // Apply the recorded combo to the correct settings vector - switch (this->currentComboType) { - case VR::ComboType::MenuOpen: - settings.VRMenuOpenKeys = this->recordedCombo; - break; - case VR::ComboType::MenuClose: - settings.VRMenuCloseKeys = this->recordedCombo; - break; - case VR::ComboType::OverlayOpen: - settings.VROverlayOpenKeys = this->recordedCombo; - break; - case VR::ComboType::OverlayClose: - settings.VROverlayCloseKeys = this->recordedCombo; - break; - default: - break; - } - } - - // Reset recording state - this->isCapturingCombo = false; - this->currentComboType = VR::ComboType::None; - this->currentComboName = nullptr; - this->recordedCombo.clear(); - this->comboStartTime = 0.0; - recordingButtonControllers.clear(); - ImGui::CloseCurrentPopup(); - } + auto& enabled = globals::menu->IsEnabled; + auto& overlayVisible = globals::menu->overlayVisible; - // Handle ESC key to cancel - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) { - // Reset recording state - this->isCapturingCombo = false; - this->currentComboType = VR::ComboType::None; - this->currentComboName = nullptr; - this->recordedCombo.clear(); - this->comboStartTime = 0.0; - recordingButtonControllers.clear(); - ImGui::CloseCurrentPopup(); - } + if ((enabled || overlayVisible || settings.kAutoHideSeconds > 0) && menuTexture.get() && menuRTV.get()) { + UpdateFixedWorldPositioning(); + UpdateOverlayDrag(); - // Handle timeout - auto-accept if buttons were pressed, auto-cancel if not - if (remainingTime <= 0.0) { - if (!this->recordedCombo.empty()) { - // Auto-accept if buttons were pressed - apply to correct settings vector - switch (this->currentComboType) { - case VR::ComboType::MenuOpen: - settings.VRMenuOpenKeys = this->recordedCombo; - break; - case VR::ComboType::MenuClose: - settings.VRMenuCloseKeys = this->recordedCombo; - break; - case VR::ComboType::OverlayOpen: - settings.VROverlayOpenKeys = this->recordedCombo; - break; - case VR::ComboType::OverlayClose: - settings.VROverlayCloseKeys = this->recordedCombo; - break; - default: - break; - } - } - // Auto-cancel if no buttons were pressed (do nothing, just close) + ID3D11RenderTargetView* oldRTV = nullptr; + globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); + ID3D11RenderTargetView* menuRTVPtr = menuRTV.get(); + globals::d3d::context->OMSetRenderTargets(1, &menuRTVPtr, nullptr); + float clearColor[4] = { 0, 0, 0, 0 }; + globals::d3d::context->ClearRenderTargetView(menuRTV.get(), clearColor); + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); - // Reset recording state - this->isCapturingCombo = false; - this->currentComboType = VR::ComboType::None; - this->currentComboName = nullptr; - this->recordedCombo.clear(); - this->comboStartTime = 0.0; - recordingButtonControllers.clear(); - ImGui::CloseCurrentPopup(); - } + bool beingDragged = settings.EnableDragToReposition && overlayDragState.dragging; + Util::ApplyHighlightTintToTexture(menuTexture.get(), beingDragged, settings.dragHighlightColor); - ImGui::EndPopup(); - } + if (oldRTV) + oldRTV->Release(); } } -namespace +void VR::UpdateDepthBufferCulling(bool desired, const FeatureConstraints::SettingId& settingId) { - void DrawControllerInputInstructions() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.openVRInfo.isCompatible) - return; - if (ImGui::CollapsingHeader("Controller Input Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderInt("Auto-hide Welcome overlay timeout", &settings.kAutoHideSeconds, 0, VR::Config::kMaxAutoHideSeconds, - settings.kAutoHideSeconds <= 0 ? "Hidden" : "%d seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Set to 0 to hide the overlay, or a positive value to show it for that many seconds"); - } - ImGui::TextWrapped("Menu (while in the main menu or tween menu):"); - if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Overlay (while in the main menu or tween menu):"); - if (ImGui::BeginTable("OverlayInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Menu Controller Input:"); - if (ImGui::BeginTable("ControllerInputTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Trigger (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Left mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Grip (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Right mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Touchpad Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Stick Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "A/X (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Enter"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "B/Y (Primary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Tab"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "B/Y (Secondary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Shift+Tab"); - ImGui::EndTable(); - } - // Thumbstick instructions - bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || settings.attachMode == VR::Settings::OverlayAttachMode::Both); - if (ImGui::BeginTable("ThumbstickInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - if (useAttachedControllerForCursor) { - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - } - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (HMD mode)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } - ImGui::EndTable(); - } - } - } - - void DrawGeneralVRSettings() - { - auto& vr = globals::features::vr; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - // Use constraint-aware checkboxes that automatically handle disabling - // and showing tooltips when other features constrain these settings - Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Exteriors", - &settings.EnableDepthBufferCullingExterior, - { "VR", "EnableDepthBufferCullingExterior" }); - // Show normal tooltip when not constrained - auto exteriorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingExterior" }); - if (!exteriorConstraint.isConstrained) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in exteriors, recommended ON."); - } - } - - Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Interiors", - &settings.EnableDepthBufferCullingInterior, - { "VR", "EnableDepthBufferCullingInterior" }); - // Show normal tooltip when not constrained - auto interiorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingInterior" }); - if (!interiorConstraint.isConstrained) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); - } - } - - if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) - *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Minimum bounding box dimensions for object occlusion culling. Lower values improve performance but may result in visual artifacts."); - } - } - } - - void DrawMenuSettings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.openVRInfo.isCompatible) - return; - if (ImGui::CollapsingHeader("Menu Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Menu Scale", &settings.VRMenuScale, VR::Config::kMinMenuScale, VR::Config::kMaxMenuScale, "%.2f"); - const char* positioningMethods[] = { "HMD Relative", "Fixed World Position" }; - ImGui::Combo("Menu Positioning Method", &settings.VRMenuPositioningMethod, positioningMethods, IM_ARRAYSIZE(positioningMethods)); - const char* attachModes[] = { "HMD Only", "Controller Only", "Both" }; - int attachModeInt = static_cast(settings.attachMode); - if (ImGui::Combo("Attach Mode", &attachModeInt, attachModes, IM_ARRAYSIZE(attachModes))) { - settings.attachMode = static_cast(attachModeInt); - } - - // Controller-specific settings (only show when controller mode is active) - if (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || - settings.attachMode == VR::Settings::OverlayAttachMode::Both) { - const char* attachControllers[] = { "Primary Controller", "Secondary Controller" }; - int attachControllerInt = static_cast(settings.VRMenuAttachController); - if (ImGui::Combo("Attach to Controller", &attachControllerInt, attachControllers, IM_ARRAYSIZE(attachControllers))) { - settings.VRMenuAttachController = static_cast(attachControllerInt); - } - - ImGui::Separator(); - ImGui::Text("Controller Offset Settings"); - ImGui::SliderFloat("Controller Offset X", &settings.VRMenuControllerOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Y", &settings.VRMenuControllerOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Z", &settings.VRMenuControllerOffsetZ, -2.0f, 2.0f, "%.2f"); - } - - // HMD-specific settings (only show when HMD mode is active) - if (settings.attachMode == VR::Settings::OverlayAttachMode::HMDOnly || - settings.attachMode == VR::Settings::OverlayAttachMode::Both) { - ImGui::Separator(); - ImGui::Text("HMD Offset Settings"); - ImGui::SliderFloat("HMD Offset X", &settings.VRMenuOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Y", &settings.VRMenuOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Z", &settings.VRMenuOffsetZ, -2.0f, 2.0f, "%.2f"); - } - - // Fixed World Position: show auto reset distance and manual reset button - if (settings.VRMenuPositioningMethod == 1) { // 1 = Fixed World Position - ImGui::Separator(); - ImGui::Text("Fixed World Position Settings"); - ImGui::SliderFloat("Auto Reset Distance (game units)", &settings.VRMenuAutoResetDistance, 100.0f, 5000.0f, "%.0f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("If you move farther than this distance from the menu, it will automatically reset to your HMD position. %s", Util::Units::FormatDistance(settings.VRMenuAutoResetDistance).c_str()); - } - if (ImGui::Button("Reset Menu to HMD Position")) { - vr.SetFixedOverlayToCurrentHMD(); - } - } - } - } - - void DrawMouseSettings() - { - auto& vr = globals::features::vr; - if (!vr.openVRInfo.isCompatible) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Input Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - // Wand pointing settings - if (ImGui::Checkbox("Enable Wand Pointing", &settings.EnableWandPointing)) { - // Reset wand state when toggling - vr.wandState.isIntersecting = false; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Use controller ray-casting to point at UI elements"); - } - ImGui::Separator(); - ImGui::Text("Joystick Settings"); - ImGui::SliderFloat("Mouse Deadzone", &settings.mouseDeadzone, 0.0f, 1.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Thumbstick deadzone for joystick cursor movement"); - } - ImGui::SliderFloat("Mouse Speed", &settings.mouseSpeed, 0.1f, 50.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Speed multiplier for joystick cursor movement"); - } - } - } - - void DrawDragSettings() - { - auto& vr = globals::features::vr; - if (!vr.openVRInfo.isCompatible) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Drag Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::CollapsingHeader("Drag Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Overlay Positioning (Grip + Drag):"); - ImGui::BulletText("Fixed World Position: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("HMD Relative: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("Controller Attached: Only the opposite hand can drag the controller overlay"); - } - ImGui::Checkbox("Enable drag to reposition overlays", &settings.EnableDragToReposition); - ImGui::BeginDisabled(!settings.EnableDragToReposition); - ImGui::ColorEdit4("Drag Highlight Color", settings.dragHighlightColor.data()); - ImGui::EndDisabled(); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Color used to highlight draggable overlays in VR."); - } - } - } - void DrawKeyBindings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - - // Combo Settings - if (ImGui::CollapsingHeader("Combo Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Combo Timeout", &settings.comboTimeout, 1.0f, 10.0f, "%.1f seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time limit for recording button combinations."); - } - } - ImGui::Separator(); - // Combo box for selecting which combo to record - const char* comboTypes[] = { - "Open Community Shaders Menu", - "Close Community Shaders Menu", - "Open VR Overlay", - "Close VR Overlay" - }; - static int selectedComboIndex = 0; - ImGui::Text("Select Combo to Record:"); - ImGui::SameLine(); - if (ImGui::Combo("##ComboSelector", &selectedComboIndex, comboTypes, IM_ARRAYSIZE(comboTypes))) { - // Reset recording state when changing selection - vr.isCapturingCombo = false; - vr.currentComboType = VR::ComboType::None; - vr.recordedCombo.clear(); - } - if (ImGui::Button("Record Selected Combo")) { - // Start recording the selected combo - vr.isCapturingCombo = true; - vr.currentComboType = static_cast(selectedComboIndex + 1); - vr.currentComboName = comboTypes[selectedComboIndex]; - vr.recordedCombo.clear(); - vr.comboStartTime = Util::GetNowSecs(); - vr.recordingButtonControllers.clear(); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Clear")) { - // Clear the selected combo - switch (selectedComboIndex) { - case 0: - settings.VRMenuOpenKeys.clear(); - break; - case 1: - settings.VRMenuCloseKeys.clear(); - break; - case 2: - settings.VROverlayOpenKeys.clear(); - break; - case 3: - settings.VROverlayCloseKeys.clear(); - break; - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to start recording a new button combination for the selected action."); - } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - // Table for displaying current key bindings - if (ImGui::BeginTable("##VRBindingsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Action"); - ImGui::TableSetupColumn("Current Binding"); - ImGui::TableSetupColumn("Description"); - ImGui::TableHeadersRow(); - // Define VR key binding configurations - struct VRKeyBindingConfig - { - const char* label; - std::vector& combos; - const char* description; - const char* controllerRequirement; - }; - std::vector keyBindingConfigs = { - { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, - { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, - { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, - { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } - }; - for (size_t row = 0; row < keyBindingConfigs.size(); ++row) { - const auto& config = keyBindingConfigs[row]; - ImGui::TableNextRow(); - // Highlight the selected row - if (row == static_cast(selectedComboIndex)) { - ImU32 highlight = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.15f)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, highlight); - } - // Make row selectable - ImGui::TableSetColumnIndex(0); - char selectableId[64]; - snprintf(selectableId, sizeof(selectableId), "##combo_row_%zu", row); - bool rowSelected = (row == static_cast(selectedComboIndex)); - if (ImGui::Selectable(selectableId, rowSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap, ImVec2(0, 0))) { - selectedComboIndex = static_cast(row); - } - ImGui::SameLine(0, 0); - ImGui::Text("%s", config.label); - // Current Binding column - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(config.combos, false); - // Description column - ImGui::TableSetColumnIndex(2); - ImGui::Text("%s", config.description); - } - ImGui::EndTable(); - } - ImGui::Spacing(); - // Reset to defaults button - if (ImGui::Button("Reset to Defaults")) { - // Use InputCombo structure for cleaner defaults - settings.VRMenuOpenKeys = { - InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kXA)), - InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kBY)) - }; - settings.VRMenuCloseKeys = { - InputCombo::Both(static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip)) - }; - settings.VROverlayOpenKeys = { - InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) - }; - settings.VROverlayCloseKeys = { - InputCombo::Secondary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) - }; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Reset all VR key bindings to their default values."); - } - } - void DrawDebugSection() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - auto menu = globals::menu; - - // OpenVR Version Information - if (ImGui::CollapsingHeader("OpenVR Information", ImGuiTreeNodeFlags_DefaultOpen)) { - auto& info = vr.openVRInfo; - if (info.isAvailable) { - ImGui::Text("OpenVR System: %s", info.isCompatible ? "Active & Compatible" : "Active but INCOMPATIBLE"); - if (!info.isCompatible) { - std::string reason = std::format("{} {}", "OpenVR version is incompatible.", "VR menus disabled."); - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Reason: %s", reason.c_str()); - } - ImGui::Text("DLL Path: %s", info.dllPath.c_str()); - ImGui::Text("DLL Version: %s", info.version.c_str()); - ImGui::Text("DLL Size: %llu bytes", info.fileSize); - ImGui::Text("Modified: %s", info.modificationTime.c_str()); - } else { - ImGui::Text("OpenVR system not available"); - } - } - - // Controller Diagnostics Section - if (ImGui::CollapsingHeader("Controller Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Test Mode: Disable controller menu input (except scroll controller and triggers)", &settings.VRMenuControllerDiagnosticsTestMode)) { - ImGui::SetScrollHereY(0.0f); // Scroll to top of the window when toggled - } - ImGui::SeparatorText("Button State"); - double nowSecs = Util::GetNowSecs(); - // Get highlight color from theme - ImVec4 highlightColor = menu->GetTheme().StatusPalette.InfoColor; - ImU32 highlightColorU32 = ImGui::ColorConvertFloat4ToU32(highlightColor); - - // Determine display order based on handedness - bool isLeftHanded = vr.lastKnownLeftHandedMode; // Use cached handedness - - if (ImGui::BeginTable("vr_input_state_table", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Button"); - if (isLeftHanded) { - // Left-handed: Primary (left hand) on left, Secondary (right hand) on right - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - } else { - // Right-handed: Secondary (left hand) on left, Primary (right hand) on right - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - } - ImGui::TableHeadersRow(); - // Helper for button type text - auto DrawButtonType = [](const RE::ButtonState& state) { - if (!state.isPressed) { - if (state.IsClick()) - ImGui::TextUnformatted("Click"); - else if (state.IsHold()) - ImGui::TextUnformatted("Hold"); - else - ImGui::TextUnformatted("-"); - } else { - ImGui::TextUnformatted("Held"); - } - }; - // Helper for printing a row with left/right cell highlight - auto printRow = [&](const char* label, const RE::ButtonState& left, const RE::ButtonState& right) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(left.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(2); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", left.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(3); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(left); - ImGui::TableSetColumnIndex(4); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(right.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(5); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", right.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(6); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(right); - }; - - // Helper to determine the correct order for display based on handedness - auto printRowWithHandedness = [&](const char* label, auto key) { - auto& primary = vr.primaryControllerState[key]; - auto& secondary = vr.secondaryControllerState[key]; - if (isLeftHanded) { - // Left-handed: Primary (left hand) on left, Secondary (right hand) on right - printRow(label, primary, secondary); - } else { - // Right-handed: Secondary (left hand) on left, Primary (right hand) on right - printRow(label, secondary, primary); - } - }; - - printRowWithHandedness("Trigger", RE::BSOpenVRControllerDevice::Keys::kTrigger); - printRowWithHandedness("Grip", RE::BSOpenVRControllerDevice::Keys::kGrip); - printRowWithHandedness("GripAlt", RE::BSOpenVRControllerDevice::Keys::kGripAlt); - printRowWithHandedness("Stick Click", RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger); - printRowWithHandedness("Touchpad Click", RE::BSOpenVRControllerDevice::Keys::kTouchpadClick); - printRowWithHandedness("Touchpad Alt", RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt); - printRowWithHandedness("B/Y", RE::BSOpenVRControllerDevice::Keys::kBY); - printRowWithHandedness("A/X", RE::BSOpenVRControllerDevice::Keys::kXA); - ImGui::EndTable(); - } - ImGui::SeparatorText("VR Thumbstick State"); - // Helper to draw a thumbstick quadrant visualization (returns ImVec2 for label alignment) - auto DrawThumbstickPad = [&](float x, float y, ImU32 highlightCol) -> ImVec2 { - ImVec2 padSize = ImVec2(80, 80); - ImVec2 cursor = ImGui::GetCursorScreenPos(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 center = ImVec2(cursor.x + padSize.x / 2, cursor.y + padSize.y / 2); - float radius = padSize.x / 2 - 4; - ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border); - ImU32 axisCol = ImGui::GetColorU32(ImGuiCol_TextDisabled); - ImU32 dotCol = ImGui::GetColorU32(ImGuiCol_Text); - // Draw background - drawList->AddRectFilled(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), ImGui::GetColorU32(ImGuiCol_FrameBg)); - // Draw border - drawList->AddRect(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), borderCol, 4.0f, 0, 2.0f); - // Draw axes - drawList->AddLine(ImVec2(center.x, cursor.y + 4), ImVec2(center.x, cursor.y + padSize.y - 4), axisCol, 1.0f); - drawList->AddLine(ImVec2(cursor.x + 4, center.y), ImVec2(cursor.x + padSize.x - 4, center.y), axisCol, 1.0f); - // Determine quadrant - int quad = 0; - if (x > 0 && y > 0) - quad = 1; // top-right - else if (x < 0 && y > 0) - quad = 2; // top-left - else if (x < 0 && y < 0) - quad = 3; // bottom-left - else if (x > 0 && y < 0) - quad = 4; // bottom-right - // Highlight quadrant - if (quad != 0) { - ImVec2 q0 = center; - ImVec2 q1 = center; - ImVec2 q2 = center; - ImVec2 q3 = center; - if (quad == 1) { // top-right - q1.x += radius; - q1.y -= radius; - q2.x += radius; - q2.y += 0; - q3.x += 0; - q3.y -= radius; - } else if (quad == 2) { // top-left - q1.x -= radius; - q1.y -= radius; - q2.x -= radius; - q2.y += 0; - q3.x += 0; - q3.y -= radius; - } else if (quad == 3) { // bottom-left - q1.x -= radius; - q1.y += radius; - q2.x -= radius; - q2.y += 0; - q3.x += 0; - q3.y += radius; - } else if (quad == 4) { // bottom-right - q1.x += radius; - q1.y += radius; - q2.x += radius; - q2.y += 0; - q3.x += 0; - q3.y += radius; - } - ImVec2 poly[4] = { center, q1, q2, q3 }; - drawList->AddConvexPolyFilled(poly, 4, highlightCol); - } - // Draw stick position dot - ImVec2 dot = ImVec2(center.x + x * radius, center.y - y * radius); - drawList->AddCircleFilled(dot, 5.0f, dotCol); - // Return size for label alignment - return padSize; - }; - ImU32 highlightCol = ImGui::ColorConvertFloat4ToU32(menu->GetTheme().StatusPalette.InfoColor); - if (ImGui::BeginTable("##VRThumbstickTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - if (isLeftHanded) { - // Left-handed: Primary (left hand) on left, Secondary (right hand) on right - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } else { - // Right-handed: Secondary (left hand) on left, Primary (right hand) on right - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } - ImGui::TableHeadersRow(); - - // Left column content - ImGui::TableSetColumnIndex(0); - ImGui::BeginGroup(); - if (isLeftHanded) { - // Left-handed: Show primary controller in left column - ImVec2 padSizeL = DrawThumbstickPad(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, highlightCol); - ImGui::Dummy(padSizeL); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, RE::GetQuadrantName(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y)); - } else { - // Right-handed: Show secondary controller in left column - ImVec2 padSizeL = DrawThumbstickPad(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, highlightCol); - ImGui::Dummy(padSizeL); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, RE::GetQuadrantName(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y)); - } - ImGui::EndGroup(); - - // Right column content - ImGui::TableSetColumnIndex(1); - ImGui::BeginGroup(); - if (isLeftHanded) { - // Left-handed: Show secondary controller in right column - ImVec2 padSizeR = DrawThumbstickPad(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, highlightCol); - ImGui::Dummy(padSizeR); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, RE::GetQuadrantName(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y)); - } else { - // Right-handed: Show primary controller in right column - ImVec2 padSizeR = DrawThumbstickPad(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, highlightCol); - ImGui::Dummy(padSizeR); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, RE::GetQuadrantName(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y)); - } - ImGui::EndGroup(); - ImGui::EndTable(); - } - ImGui::SeparatorText("Recent VR Controller Events"); - ImGui::TextDisabled("Note: For thumbstick events, KeyCode/Value columns show X/Y floats."); - if (ImGui::BeginTable("eventlog", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthFixed, 60.0f); - ImGui::TableSetupColumn("KeyCode/X", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Value/Y", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Pressed", ImGuiTableColumnFlags_WidthFixed, 70.0f); - ImGui::TableSetupColumn("Known Mapping", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Event Type", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableHeadersRow(); - for (const auto& e : vr.vrControllerEventLog) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%d", e.device); - ImGui::TableSetColumnIndex(1); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickX); - } else { - ImGui::Text("%d", e.keyCode); - } - ImGui::TableSetColumnIndex(2); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickY); - } else { - ImGui::Text("%d", e.value); - } - ImGui::TableSetColumnIndex(3); - ImGui::Text("%s", e.pressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(4); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted(e.controllerRole.c_str()); - } else { - ImGui::TextUnformatted(RE::GetOpenVRButtonName(e.keyCode)); - } - ImGui::TableSetColumnIndex(5); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted("-"); - } else { - // Show click/hold for release events if available - if (!e.pressed) { - if (e.heldTime > 0.0) { - if (e.heldTime < 0.5) { - ImGui::Text("Click (%.2fs)", e.heldTime); - } else { - ImGui::Text("Hold (%.2fs)", e.heldTime); - } - } else { - ImGui::Text("Release"); - } - } else if (e.pressed) { - if (e.heldTime > 0.0) { - ImGui::Text("Held for %.2fs", e.heldTime); - } else { - ImGui::Text("Press"); - } - } - } - } - ImGui::EndTable(); - } - - // Wand Pointing Diagnostics - ImGui::SeparatorText("Wand Pointing State"); - if (ImGui::BeginTable("##WandPointingState", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableHeadersRow(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Wand Pointing Enabled"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%s", settings.EnableWandPointing ? "Yes" : "No"); + auto constraint = FeatureConstraints::GetConstraints(settingId); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Intersecting Overlay"); - ImGui::TableSetColumnIndex(1); - if (vr.wandState.isIntersecting) { - ImGui::TextColored(menu->GetTheme().StatusPalette.InfoColor, "YES"); - } else { - ImGui::Text("No"); + if (constraint.isConstrained) { + if (auto* forcedValuePtr = std::get_if(&constraint.forcedValue)) { + bool forcedValue = *forcedValuePtr; + if (gDepthBufferCulling && *gDepthBufferCulling != forcedValue) { + *gDepthBufferCulling = forcedValue; + for (const auto& src : constraint.sources) { + logger::info("{} forcing depth buffer culling {}: {}", + src.featureName, + forcedValue ? "ON" : "OFF", + src.reason); } - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("UV Coordinates"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.3f, %.3f)", vr.wandState.uvCoordinates.x, vr.wandState.uvCoordinates.y); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Controller Index"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%u", vr.wandState.controllerIndex); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Origin"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayOrigin.x, vr.wandState.rayOrigin.y, vr.wandState.rayOrigin.z); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Direction"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayDirection.x, vr.wandState.rayDirection.y, vr.wandState.rayDirection.z); - - ImGui::EndTable(); } + } else { + logger::warn("VR::UpdateDepthBufferCulling: Constraint on {} has non-bool forced value, ignoring", settingId.settingPath); } - - // Debugging addresses for copy/paste - if (ImGui::CollapsingHeader("OpenVR Addresses")) { - auto openvr = RE::BSOpenVR::GetSingleton(); - auto overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - auto vrSystem = openvr ? openvr->vrSystem : nullptr; - ADDRESS_NODE(openvr) - ADDRESS_NODE(overlay) - ADDRESS_NODE(vrSystem) + } else { + if (gDepthBufferCulling && *gDepthBufferCulling != desired) { + *gDepthBufferCulling = desired; + logger::info("VR depth buffer culling set to {}", desired); } } -} // namespace +} //============================================================================= -// VR-SPECIFIC PUBLIC API +// OPENVR VERSION DETECTION AND COMPATIBILITY //============================================================================= -void VR::UpdateVROverlayPosition() -{ - Util::OpenVRContext ctx; - if (!ctx.HasOverlay()) - return; - - if (menuOverlayHandle == vr::k_ulOverlayHandleInvalid) { - return; - } - - // Determine positioning strategy based on settings - bool showOnController = (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both); - bool showOnHMD = (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both); - - // Texture size - float aspect = static_cast(kOverlayHeight) / kOverlayWidth; - float baseWidth = 1.0f; - float overlayWidth = baseWidth * settings.VRMenuScale; - float overlayHeight = overlayWidth * aspect; - float offsetX = settings.VRMenuOffsetX; - float offsetY = settings.VRMenuOffsetY; - float offsetZ = settings.VRMenuOffsetZ; - - static int lastPositioningMethod = -1; - bool justSwitchedToFixed = (lastPositioningMethod != 1 && settings.VRMenuPositioningMethod == 1); - lastPositioningMethod = settings.VRMenuPositioningMethod; - - // Handle HMD positioning - if (showOnHMD) { - if (settings.VRMenuPositioningMethod == 0) { - // HMD Relative positioning - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - return; - - if (hmdPose.bPoseIsValid) { - // Calculate position in front of HMD using offsets directly - float height = 0.0f; - - // Create transform matrix - start with identity - vr::HmdMatrix34_t hmdTransform; - hmdTransform.m[0][0] = 1.0f; - hmdTransform.m[0][1] = 0.0f; - hmdTransform.m[0][2] = 0.0f; - hmdTransform.m[0][3] = 0.0f; - hmdTransform.m[1][0] = 0.0f; - hmdTransform.m[1][1] = 1.0f; - hmdTransform.m[1][2] = 0.0f; - hmdTransform.m[1][3] = 0.0f; - hmdTransform.m[2][0] = 0.0f; - hmdTransform.m[2][1] = 0.0f; - hmdTransform.m[2][2] = 1.0f; - hmdTransform.m[2][3] = 0.0f; - - // Copy HMD position - hmdTransform.m[0][3] = hmdPose.mDeviceToAbsoluteTracking.m[0][3]; - hmdTransform.m[1][3] = hmdPose.mDeviceToAbsoluteTracking.m[1][3]; - hmdTransform.m[2][3] = hmdPose.mDeviceToAbsoluteTracking.m[2][3]; - - // Copy HMD orientation - hmdTransform.m[0][0] = hmdPose.mDeviceToAbsoluteTracking.m[0][0]; - hmdTransform.m[0][1] = hmdPose.mDeviceToAbsoluteTracking.m[0][1]; - hmdTransform.m[0][2] = hmdPose.mDeviceToAbsoluteTracking.m[0][2]; - hmdTransform.m[1][0] = hmdPose.mDeviceToAbsoluteTracking.m[1][0]; - hmdTransform.m[1][1] = hmdPose.mDeviceToAbsoluteTracking.m[1][1]; - hmdTransform.m[1][2] = hmdPose.mDeviceToAbsoluteTracking.m[1][2]; - hmdTransform.m[2][0] = hmdPose.mDeviceToAbsoluteTracking.m[2][0]; - hmdTransform.m[2][1] = hmdPose.mDeviceToAbsoluteTracking.m[2][1]; - hmdTransform.m[2][2] = hmdPose.mDeviceToAbsoluteTracking.m[2][2]; - - // Apply HMD offset positions directly (in HMD local space) - hmdTransform.m[0][3] += hmdTransform.m[0][0] * offsetX + hmdTransform.m[0][1] * offsetY + hmdTransform.m[0][2] * offsetZ; - hmdTransform.m[1][3] += hmdTransform.m[1][0] * offsetX + hmdTransform.m[1][1] * offsetY + hmdTransform.m[1][2] * offsetZ; - hmdTransform.m[2][3] += hmdTransform.m[2][0] * offsetX + hmdTransform.m[2][1] * offsetY + hmdTransform.m[2][2] * offsetZ; - - // Move up by height (Y axis in HMD space) - hmdTransform.m[0][3] += hmdTransform.m[0][1] * height; - hmdTransform.m[1][3] += hmdTransform.m[1][1] * height; - hmdTransform.m[2][3] += hmdTransform.m[2][1] * height; - - // Scale the overlay based on width/height - hmdTransform.m[0][0] *= overlayWidth; - hmdTransform.m[1][1] *= overlayHeight; - - Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); - ctx.overlay->SetOverlayTransformAbsolute(menuOverlayHandle, vr::TrackingUniverseStanding, &hmdTransform); - ctx.overlay->SetOverlayWidthInMeters(menuOverlayHandle, baseWidth * settings.VRMenuScale); - - } else { - logger::debug("HMD pose invalid, falling back to fixed positioning"); - settings.VRMenuPositioningMethod = 1; // Fall back to fixed positioning - } - } - - if (settings.VRMenuPositioningMethod == 1) { - // Fixed World Position - // Cache player position once per frame - RE::NiPoint3 playerPos = savedPlayerWorldPos; - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) { - playerPos = player->GetPosition(); - } - - if (justSwitchedToFixed) { - SetFixedOverlayToCurrentHMD(); - // Save player position when switching to Fixed World Position - savedPlayerWorldPos = playerPos; - } - - // --- Auto reset logic using player world position --- - float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); - float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; - if (sqDist > thresholdSq) { - SetFixedOverlayToCurrentHMD(); - // Update saved position after reset - savedPlayerWorldPos = playerPos; - } - - // Scale the overlay based on width/height (same as relative HMD mode) - vr::HmdMatrix34_t fixedTransform = Util::MatrixToHmdMatrix34(fixedWorldOverlayPosition.m); - fixedTransform.m[0][0] *= overlayWidth; - fixedTransform.m[1][1] *= overlayHeight; - - Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); - ctx.overlay->SetOverlayTransformAbsolute(menuOverlayHandle, vr::TrackingUniverseStanding, &fixedTransform); - ctx.overlay->SetOverlayWidthInMeters(menuOverlayHandle, baseWidth * settings.VRMenuScale); - } - } - - // Handle controller positioning separately (can be shown alongside HMD) - if (showOnController) { - // Get the VR controller overlay handle from Menu.cpp - if (menuControllerOverlayHandle == vr::k_ulOverlayHandleInvalid) { - return; - } - - // Attach to controller - vr::TrackedDeviceIndex_t controllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (controllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - // Position relative to controller using offset settings - vr::HmdMatrix34_t transform = Util::CreateControllerOverlayTransform( - settings.VRMenuControllerOffsetX, - settings.VRMenuControllerOffsetY, - settings.VRMenuControllerOffsetZ, - overlayWidth, - overlayHeight); - - Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); - ctx.overlay->SetOverlayTransformTrackedDeviceRelative(menuControllerOverlayHandle, controllerIndex, &transform); - - // Update the overlay width to match the calculated size - ctx.overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, overlayWidth); - - // Update controller overlay flags for input interaction - Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); - } - } - - // Update overlay flags for input interaction - Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); -} - -void VR::UpdateVROverlayControllerPosition() +void VR::DetectOpenVRInfo() { - Util::OpenVRContext ctx; - if (!ctx.HasOverlay()) - return; - - // Get the VR controller overlay handle from Menu.cpp - if (menuControllerOverlayHandle == vr::k_ulOverlayHandleInvalid) { - return; - } - - // Texture size based on preset - float aspect = static_cast(kOverlayHeight) / kOverlayWidth; - float baseWidth = 1.0f; - float overlayWidth = baseWidth * settings.VRMenuScale; - float overlayHeight = overlayWidth * aspect; - - // Find the appropriate controller for the controller overlay - vr::TrackedDeviceIndex_t controllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (controllerIndex == vr::k_unTrackedDeviceIndexInvalid) { - ctx.overlay->HideOverlay(menuControllerOverlayHandle); - return; - } - - // Position relative to controller using offset settings - vr::HmdMatrix34_t transform = Util::CreateControllerOverlayTransform( - settings.VRMenuControllerOffsetX, - settings.VRMenuControllerOffsetY, - settings.VRMenuControllerOffsetZ, - overlayWidth, - overlayHeight); - - Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); - ctx.overlay->SetOverlayTransformTrackedDeviceRelative(menuControllerOverlayHandle, controllerIndex, &transform); + openVRInfo = {}; - // Update the overlay width to match the calculated size - ctx.overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, overlayWidth); + auto result = VRDetection::Detect(); - // Update controller overlay flags for input interaction - Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); + openVRInfo.isAvailable = result.isAvailable; + openVRInfo.isCompatible = result.isCompatible; + openVRInfo.dllPath = result.dllPath; + openVRInfo.version = result.version; + openVRInfo.fileSize = result.fileSize; + openVRInfo.modificationTime = result.modificationTime; + openVRInfo.hasOverlayInterface = result.hasOverlayInterface; + openVRInfo.hasSystemInterface = result.hasSystemInterface; + openVRInfo.hasCompositorInterface = result.hasCompositorInterface; + openVRInfo.runtimeType = result.runtimeType; + openVRInfo.probingSucceeded = result.probingSucceeded; } -// Add overlay management methods for VR menu overlays -void VR::EnsureOverlayInitialized() +bool VR::IsOpenVRCompatible() const { - // Check OpenVR compatibility first - if (!openVRInfo.isCompatible) { - logger::warn("OpenVR version is incompatible."); - return; - } - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - logger::debug("BSOpenVR: 0x{:X}", reinterpret_cast(openvr)); - if (!openvr) { - logger::error("BSOpenVR::GetSingleton() returned nullptr"); - return; - } - auto* vrSystem = openvr->vrSystem; - auto* overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - logger::debug("openVR->vrSystem: 0x{:X}", reinterpret_cast(vrSystem)); - logger::debug("openVR->vrContext: 0x{:X}", reinterpret_cast(&openvr->vrContext)); - logger::debug("openVR->vrContext.vrOverlay: 0x{:X}", reinterpret_cast(openvr->vrContext.vrOverlay)); - logger::debug("openVR->hmdDeviceType: {} ({})", static_cast(openvr->hmdDeviceType), magic_enum::enum_name(openvr->hmdDeviceType)); - for (int i = 0; i < RE::BSVRInterface::Hand::kTotal; ++i) { - logger::debug("openVR->controllerNodes[{}]: 0x{:X}", i, reinterpret_cast(openvr->controllerNodes[i].get())); - if (openvr->controllerNodes[i] && reinterpret_cast(openvr->controllerNodes[i].get()) < 0x1000) { - logger::warn("controllerNodes[{}] is suspiciously low (0x{:X})", i, reinterpret_cast(openvr->controllerNodes[i].get())); - } - } - logger::debug("menuOverlayHandle: 0x{:X}", menuOverlayHandle); - logger::debug("menuControllerOverlayHandle: 0x{:X}", menuControllerOverlayHandle); - if (!overlay) { - logger::error("IVROverlay is nullptr after GetIVROverlay"); - return; - } - Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuTexture.put(), menuRTV.put()); - std::string key = "communityshaders.menu"; - std::string name = "Community Shaders Menu"; - vr::EVROverlayError err = overlay->CreateOverlay(key.c_str(), name.c_str(), &menuOverlayHandle); - if (err == vr::VROverlayError_None) { - logger::debug("CreateOverlay succeeded for menuOverlayHandle: 0x{:X}", menuOverlayHandle); - Util::SetOverlayInputFlags(overlay, menuOverlayHandle); - overlay->SetOverlayWidthInMeters(menuOverlayHandle, 1.0f); - } else { - logger::error("CreateOverlay failed: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } - // Controller overlay - std::string controllerKey = "communityshaders.menu.controller"; - std::string controllerName = "Community Shaders Menu (Controller)"; - err = overlay->CreateOverlay(controllerKey.c_str(), controllerName.c_str(), &menuControllerOverlayHandle); - if (err == vr::VROverlayError_None) { - logger::debug("CreateOverlay succeeded for menuControllerOverlayHandle: 0x{:X}", menuControllerOverlayHandle); - Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuControllerTexture.put(), menuControllerRTV.put()); - Util::SetOverlayInputFlags(overlay, menuControllerOverlayHandle); - overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, 1.0f); - } else { - logger::error("CreateOverlay failed: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } -} - -//============================================================================= -// PRIVATE IMPLEMENTATION -//============================================================================= - -void VR::DestroyOverlay() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - if (!overlay) { - logger::error("DestroyOverlay: IVROverlay is nullptr"); - return; - } - if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { - overlay->DestroyOverlay(menuOverlayHandle); - menuOverlayHandle = vr::k_ulOverlayHandleInvalid; - } - if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { - overlay->DestroyOverlay(menuControllerOverlayHandle); - menuControllerOverlayHandle = vr::k_ulOverlayHandleInvalid; - } -} - -void VR::RecreateOverlayTexturesIfNeeded() -{ - // Smart pointers automatically release existing resources when put() assigns new ones - Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuTexture.put(), menuRTV.put()); - Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuControllerTexture.put(), menuControllerRTV.put()); -} - -void VR::SubmitOverlayFrame() -{ - // Skip overlay operations if OpenVR is incompatible - if (!openVRInfo.isCompatible) { - return; - } - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* gameOverlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - auto* cleanOverlay = RE::BSOpenVR::GetCleanIVROverlay(); - - static bool cleanOverlayLogged = false; - if (!cleanOverlayLogged) { - if (cleanOverlay) { - logger::debug("VR: Successfully acquired clean IVROverlay interface via CommonLib: 0x{:X}", reinterpret_cast(cleanOverlay)); - } else { - logger::error("VR: Failed to get clean IVROverlay interface via CommonLib"); - } - cleanOverlayLogged = true; - } - - if (!gameOverlay || !cleanOverlay) { - return; - } - - if (!openvr || !openvr->vrSystem) { - logger::error("SubmitOverlayFrame: BSOpenVR or vrSystem is nullptr"); - return; - } - - // Update drag logic for all modes - only when overlay is visible - auto& enabled = globals::menu->IsEnabled; - auto& overlayVisible = globals::menu->overlayVisible; - if ((enabled || overlayVisible || settings.kAutoHideSeconds > 0) && menuOverlayHandle != vr::k_ulOverlayHandleInvalid && menuTexture.get() && menuRTV.get()) { - // Update drag logic only when overlay is active - UpdateOverlayDrag(); - // Copy ImGui output to overlay texture - ID3D11RenderTargetView* oldRTV = nullptr; - globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); - ID3D11RenderTargetView* menuRTVPtr = menuRTV.get(); - globals::d3d::context->OMSetRenderTargets(1, &menuRTVPtr, nullptr); - float clearColor[4] = { 0, 0, 0, 0 }; - globals::d3d::context->ClearRenderTargetView(menuRTV.get(), clearColor); - // Re-render ImGui for HMD overlay - ImGui::Render(); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); - - // Apply highlight tint to HMD overlay if it's being dragged - bool hmdBeingDragged = settings.EnableDragToReposition && overlayDragState.dragging && - (overlayDragState.mode == OverlayDragState::DragMode::HMD || - overlayDragState.mode == OverlayDragState::DragMode::FixedWorld); - Util::ApplyHighlightTintToTexture(menuTexture.get(), hmdBeingDragged, settings.dragHighlightColor); - - // Update overlay position and submit to SteamVR - UpdateVROverlayPosition(); - vr::Texture_t tex = { menuTexture.get(), vr::TextureType_DirectX, vr::ColorSpace_Auto }; - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - Util::SetOverlayInputFlags(gameOverlay, menuOverlayHandle); - vr::EVROverlayError err = cleanOverlay->SetOverlayTexture(menuOverlayHandle, &tex); - if (err != vr::VROverlayError_None) { - logger::error("SetOverlayTexture failed for menu overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } - err = gameOverlay->ShowOverlay(menuOverlayHandle); - if (err != vr::VROverlayError_None) { - logger::error("ShowOverlay failed for menu overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } - } else if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { - gameOverlay->HideOverlay(menuOverlayHandle); - } - // Controller overlay - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - // Copy the same ImGui output to controller overlay texture - ID3D11RenderTargetView* menuControllerRTVPtr = menuControllerRTV.get(); - globals::d3d::context->OMSetRenderTargets(1, &menuControllerRTVPtr, nullptr); - globals::d3d::context->ClearRenderTargetView(menuControllerRTV.get(), clearColor); - // Re-render ImGui for controller overlay - ImGui::Render(); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); - - // Apply highlight tint to controller overlay if it's being dragged - bool controllerBeingDragged = overlayDragState.dragging && - overlayDragState.mode == OverlayDragState::DragMode::Controller; - Util::ApplyHighlightTintToTexture(menuControllerTexture.get(), controllerBeingDragged, settings.dragHighlightColor); - - // Position controller overlay and submit - UpdateVROverlayControllerPosition(); - - vr::Texture_t controllerTex = { menuControllerTexture.get(), vr::TextureType_DirectX, vr::ColorSpace_Auto }; - Util::SetOverlayInputFlags(gameOverlay, menuControllerOverlayHandle); - vr::EVROverlayError err = cleanOverlay->SetOverlayTexture(menuControllerOverlayHandle, &controllerTex); - if (err != vr::VROverlayError_None) { - logger::error("SetOverlayTexture failed for controller overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } - err = gameOverlay->ShowOverlay(menuControllerOverlayHandle); - if (err != vr::VROverlayError_None) { - logger::error("ShowOverlay failed for controller overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); - } - } else if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { - gameOverlay->HideOverlay(menuControllerOverlayHandle); - } - - // Release oldRTV after all usage is complete to prevent use-after-free - if (oldRTV) - oldRTV->Release(); - } else { - if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { - gameOverlay->HideOverlay(menuOverlayHandle); - } - if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { - gameOverlay->HideOverlay(menuControllerOverlayHandle); - } - } -} - -// Helper to centralize VR depth buffer culling logic, reducing duplication between DataLoaded and EarlyPrepass. -void VR::UpdateDepthBufferCulling(bool desired, const FeatureConstraints::SettingId& settingId) -{ - // Check if any feature is constraining this setting - auto constraint = FeatureConstraints::GetConstraints(settingId); - - if (constraint.isConstrained) { - // Use std::get_if to safely extract bool value and avoid std::bad_variant_access - if (auto* forcedValuePtr = std::get_if(&constraint.forcedValue)) { - bool forcedValue = *forcedValuePtr; - if (gDepthBufferCulling && *gDepthBufferCulling != forcedValue) { - *gDepthBufferCulling = forcedValue; - for (const auto& src : constraint.sources) { - logger::info("{} forcing depth buffer culling {}: {}", - src.featureName, - forcedValue ? "ON" : "OFF", - src.reason); - } - } - } else { - // Constraint has non-bool value type - log warning and skip - logger::warn("VR::UpdateDepthBufferCulling: Constraint on {} has non-bool forced value, ignoring", settingId.settingPath); - } - } else { - if (gDepthBufferCulling && *gDepthBufferCulling != desired) { - *gDepthBufferCulling = desired; - logger::info("VR depth buffer culling set to {}", desired); - } - } -} - -// Handles overlay/menu open/close logic based on controller input state -void VR::UpdateOverlayMenuStateFromInput() -{ - // Disable menu interactions during combo recording - if (this->isCapturingCombo) { - return; - } - - bool& isEnabled = globals::menu->IsEnabled; - bool& overlayEnabled = globals::menu->overlayVisible; - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - - // Auto-disable test mode if user leaves VR section or closes menu - if (testMode) { - // Check if we're still in the VR section or if menu is closed - if (!isEnabled) { - settings.VRMenuControllerDiagnosticsTestMode = false; - return; - } - // In test mode, only allow basic input processing - return; - } - - // Compute whether the game's UI menus we care about are open. Do this early so - // downstream logic can reuse the result and we only check the UI once. - bool uiMenusOpen = globals::game::ui && - (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); - - // Valid menu state means either one of those UI menus is open, or our menu is - // enabled (but only if the game's UI system is present). - bool inValidMenuState = uiMenusOpen || (globals::game::ui && isEnabled); - - if (!inValidMenuState) - return; - - // Define menu state mappings. The `allowWhenUIMenusClosed` flag controls whether - // a mapping is allowed to run when our menu is enabled but the game's UI menus - // are not reported open. This prevents 'open' controls from firing in that state - // while still allowing 'close' actions. - struct MenuStateMapping - { - std::function condition; - std::function action; - bool allowWhenUIMenusClosed = false; - }; - - // Generic combo checking function - makes the system truly extensible - auto CheckCombo = [&](const std::vector& combos) -> bool { - if (combos.empty()) - return false; - - // Check all configured buttons in the combo - for (size_t i = 0; i < combos.size(); ++i) { - const auto& combo = combos[i]; - - bool buttonPressed = false; - - switch (combo.GetDevice()) { - case ControllerDevice::Both: - // Check if this button is pressed on BOTH controllers - buttonPressed = primaryControllerState[combo.GetKey()].isPressed && - secondaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Primary: - // Check if this button is pressed on PRIMARY controller only - buttonPressed = primaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Secondary: - // Check if this button is pressed on SECONDARY controller only - buttonPressed = secondaryControllerState[combo.GetKey()].isPressed; - break; - } - - if (!buttonPressed) { - return false; // Any button not pressed means combo fails - } - } - - // All configured buttons are pressed according to requirements - return true; - }; - - // Define the menu state mappings with extensible lambda array - std::vector mappings = { - // Open Community Shaders menu when closed - { [&]() { - return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; - }, - [&]() { isEnabled = true; } }, - - // Close Community Shaders menu when open - { [&]() { - return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; - }, - [&]() { isEnabled = false; }, - true }, - - // Open VR overlay when closed - { [&]() { - return CheckCombo(settings.VROverlayOpenKeys) && !overlayEnabled; - }, - [&]() { overlayEnabled = true; } }, - - // Close VR overlay when open - { [&]() { - return CheckCombo(settings.VROverlayCloseKeys) && overlayEnabled; - }, - [&]() { overlayEnabled = false; } } - }; - - // Process mappings in order. If our menu is enabled but the game's UI menus - // are not open, only allow mappings explicitly marked with - // allowWhenUIMenusClosed (close actions). - bool onlyAllowClose = isEnabled && !uiMenusOpen; - - for (const auto& mapping : mappings) { - if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) - continue; - - if (mapping.condition()) { - mapping.action(); - break; // Only execute one action per frame - } - } -} - -void VR::ProcessVREvents(std::vector& vrEvents) -{ - // Check for handedness changes and reset controller states if needed - bool currentLeftHandedMode = RE::BSOpenVRControllerDevice::IsLeftHandedMode(); - static bool firstCall = true; - if (firstCall || currentLeftHandedMode != lastKnownLeftHandedMode) { - if (!firstCall) { - logger::debug("VR handedness changed: {} -> {}", lastKnownLeftHandedMode ? "Left" : "Right", currentLeftHandedMode ? "Left" : "Right"); - } - firstCall = false; - lastKnownLeftHandedMode = currentLeftHandedMode; - // Reset controller states so they get repopulated with correct roles - primaryControllerState = {}; - secondaryControllerState = {}; - } - - double nowSecs = Util::GetNowSecs(); - for (auto& event : vrEvents) { - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - struct VRButtonDescriptor - { - const char* name; - bool (*isButton)(std::uint32_t); - std::uint32_t keyCode; - }; - static const VRButtonDescriptor kVRButtons[] = { - { "Grip", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGrip }, - { "GripAlt", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGripAlt }, - { "Trigger", RE::BSOpenVRControllerDevice::IsTriggerButton, RE::BSOpenVRControllerDevice::Keys::kTrigger }, - { "Stick Click", RE::BSOpenVRControllerDevice::IsStickClick, RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger }, - { "Touchpad Click", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadClick }, - { "Touchpad Alt", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt }, - { "A/X", RE::BSOpenVRControllerDevice::IsAButton, RE::BSOpenVRControllerDevice::Keys::kXA }, - { "B/Y", RE::BSOpenVRControllerDevice::IsBButton, RE::BSOpenVRControllerDevice::Keys::kBY }, - }; - for (const auto& desc : kVRButtons) { - if (desc.isButton(event.keyCode)) { - RE::ButtonState* state = isPrimary ? &primaryControllerState[desc.keyCode] : isSecondary ? &secondaryControllerState[desc.keyCode] : - nullptr; - if (state) { - state->OnEvent(event.IsPressed(), nowSecs); - } - break; - } - } - // Do not log events here; logging is handled in the event-specific handler - switch (event.eventType) { - case RE::INPUT_EVENT_TYPE::kButton: - ProcessVRButtonEvent(event); - break; - case RE::INPUT_EVENT_TYPE::kThumbstick: - UpdateControllerState(event); - break; - default: - break; - } - } -} - -void VR::ProcessVRButtonEvent(const Menu::KeyEvent& event) -{ - // Disable menu interactions during combo recording - if (this->isCapturingCombo) { - return; - } - - ImGuiIO& io = ImGui::GetIO(); - (void)event; - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - constexpr size_t kNumTriggerMappings = 1; - - // Process button mappings for the current controller - if (isPrimary || isSecondary) { - // Define mappings for both controllers (only B/Y differs) - constexpr size_t kNumMappings = 6; - RE::ButtonMapping mappings[kNumMappings] = { - { RE::BSOpenVRControllerDevice::Keys::kTrigger, ImGuiMouseButton_Left, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kGrip, ImGuiMouseButton_Right, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kTouchpadClick, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kBY, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_TAB), isSecondary }, // Shift+Tab for secondary - { RE::BSOpenVRControllerDevice::Keys::kXA, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_RETURN), false }, - }; - - // Use separate state arrays for each controller - static bool prevPrimaryStates[kNumMappings] = {}; - static bool prevSecondaryStates[kNumMappings] = {}; - bool* prevStates = isPrimary ? prevPrimaryStates : prevSecondaryStates; - - // Get the appropriate controller state - RE::InputDeviceState& controllerState = isPrimary ? primaryControllerState : secondaryControllerState; - - size_t limit = testMode ? kNumTriggerMappings : kNumMappings; // Only trigger mappings in test mode - - for (size_t i = 0; i < limit; ++i) { - RE::ButtonState* state = &controllerState[mappings[i].keyCode]; - bool curr = state ? state->isPressed : false; - if (curr != prevStates[i]) { - if (mappings[i].isKeyEvent) { - if (mappings[i].isShift) - io.AddKeyEvent(ImGuiMod_Shift, curr); - io.AddKeyEvent(static_cast(mappings[i].key), curr); - } else { - io.AddMouseButtonEvent(mappings[i].logicalButton, curr); - } - prevStates[i] = curr; - } - } - } - // Log the button event after state is updated - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "button"; - logEntry.thumbstickX = 0.0f; - logEntry.thumbstickY = 0.0f; - logEntry.controllerRole = isPrimary ? "Primary" : isSecondary ? "Secondary" : - "Unknown"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -void VR::UpdateControllerState(const Menu::KeyEvent& event) -{ - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - - // Update thumbstick state for diagnostics display and later input processing - if (isPrimary) { - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x = event.thumbstickX; - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y = event.thumbstickY; - } else if (isSecondary) { - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x = event.thumbstickX; - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y = event.thumbstickY; - } - - // Log the thumbstick event - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "thumbstick"; - logEntry.thumbstickX = event.thumbstickX; - logEntry.thumbstickY = event.thumbstickY; - logEntry.controllerRole = isPrimary ? "Primary" : "Secondary"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -// Helper function to process thumbstick input for scrolling -void VR::ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io) -{ - bool usingScrollStickX = (std::abs(controllerState.thumbsticks[thumbstickIndex].x) > deadzone); - bool usingScrollStickY = (std::abs(controllerState.thumbsticks[thumbstickIndex].y) > deadzone); - - if (usingScrollStickX || usingScrollStickY) { - // Per-controller scroll accumulation to prevent interference between controllers - struct ScrollAccum - { - float x = 0.0f; - float y = 0.0f; - }; - static std::unordered_map scrollAccums; - ScrollAccum& accum = scrollAccums[thumbstickIndex]; - - // Accumulate scroll input with sensitivity scaling - accum.x += controllerState.thumbsticks[thumbstickIndex].x * 0.1f; - accum.y += controllerState.thumbsticks[thumbstickIndex].y * 0.1f; - - // Send scroll events when accumulated enough input - float scrollEventX = 0.0f; - float scrollEventY = 0.0f; - - if (std::abs(accum.x) > 0.3f) { - scrollEventX = accum.x > 0 ? 1.0f : -1.0f; - accum.x = 0.0f; - } - if (std::abs(accum.y) > 0.3f) { - scrollEventY = accum.y > 0 ? 1.0f : -1.0f; - accum.y = 0.0f; - } - - // Send both horizontal and vertical scroll events if needed - if (scrollEventX != 0.0f || scrollEventY != 0.0f) { - io.AddMouseWheelEvent(scrollEventX, scrollEventY); - } - } -} - -// Converts VR controller input to ImGui mouse and scroll events for the overlay UI -// Supports both modern wand pointing and traditional thumbstick control -void VR::ProcessControllerInputForImGui() -{ - if (!globals::menu->IsEnabled) - return; - bool testMode = settings.VRMenuControllerDiagnosticsTestMode; - float mouseDeadzone = settings.mouseDeadzone; - float mouseSpeed = settings.mouseSpeed; - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; - io.WantSetMousePos = false; - - // Try wand pointing first - bool wandHandledCursor = false; - if (!testMode && settings.EnableWandPointing) { - UpdateCursorFromWandPointing(); - wandHandledCursor = wandState.isIntersecting; - } - - if (!testMode) { - // When wand is handling cursor, BOTH thumbsticks become scroll controls - // When wand is NOT active, use traditional cursor/scroll assignment - if (wandHandledCursor) { - // Wand mode: Both thumbsticks scroll - ProcessThumbstickScroll(primaryControllerState, static_cast(RE::ControllerRole::Primary), mouseDeadzone, io); - ProcessThumbstickScroll(secondaryControllerState, static_cast(RE::ControllerRole::Secondary), mouseDeadzone, io); - } else { - // Traditional mode: Determine which controller handles cursor vs scrolling - bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || - settings.attachMode == VR::Settings::OverlayAttachMode::Both); - - RE::VRControllerState* cursorController = nullptr; - RE::VRControllerState* scrollController = nullptr; - - if (useAttachedControllerForCursor) { - // When attached to controller: attached controller = cursor, other controller = scrolling - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } else { - cursorController = &secondaryControllerState; - scrollController = &primaryControllerState; - } - } else { - // HMD mode: primary = cursor, secondary = scroll (traditional) - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } - - // Cursor movement (from determined cursor controller) - if (cursorController) { - size_t thumbstickIndex = (cursorController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - - float thumbstickX = cursorController->thumbsticks[thumbstickIndex].x; - float thumbstickY = cursorController->thumbsticks[thumbstickIndex].y; - bool usingCursorStick = (std::abs(thumbstickX) > mouseDeadzone || std::abs(thumbstickY) > mouseDeadzone); - - if (usingCursorStick) { - ImVec2 mousePos = io.MousePos; - mousePos.x += thumbstickX * mouseSpeed; - mousePos.y -= thumbstickY * mouseSpeed; - mousePos.x = std::clamp(mousePos.x, 0.0f, io.DisplaySize.x); - mousePos.y = std::clamp(mousePos.y, 0.0f, io.DisplaySize.y); - io.MousePos = mousePos; - io.AddMousePosEvent(mousePos.x, mousePos.y); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); - } - } - - // Scrolling (from determined scroll controller) - if (scrollController) { - size_t thumbstickIndex = (scrollController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - ProcessThumbstickScroll(*scrollController, thumbstickIndex, mouseDeadzone, io); - } - } - } -} - -// --- File-scope static helpers for drag logic --- -static bool CanStartAny(vr::ETrackedControllerRole role) -{ - return role != vr::TrackedControllerRole_Invalid; -} - -void VR::UpdateOverlayDrag() -{ - if (!CanPerformDrag()) { - return; - } - - if (overlayDragState.dragging) { - UpdateActiveDrag(); - } else { - TryStartNewDrag(); - } -} - -bool VR::CanPerformDrag() -{ - if (!settings.EnableDragToReposition) - return false; - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return false; - - // Check if test mode is active - disable all dragging - if (settings.VRMenuControllerDiagnosticsTestMode) { - return false; - } - - return true; -} - -void VR::UpdateActiveDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - // Helper to get grip state for a controller based on actual hand position - auto getGripPressed = [&](bool isLeft, bool isRight) { - bool isLeftHanded = lastKnownLeftHandedMode; - - if (isLeft) { - // Left hand: primary if left-handed, secondary if right-handed - if (isLeftHanded) { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - if (isRight) { - // Right hand: secondary if left-handed, primary if right-handed - if (isLeftHanded) { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - return false; - }; - - // Helper to reset drag state - auto resetDragState = [&]() { - overlayDragState.dragging = false; - overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; - overlayDragState.isPrimary = false; - overlayDragState.isSecondary = false; - }; - - float rawMatrix[3][4]; - if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - - // Update drag based on current mode - switch (overlayDragState.mode) { - case OverlayDragState::DragMode::Controller: - { - // Get current attached controller transform to convert world deltas to local space - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::TrackedDevicePose_t controllerPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &controllerPose, 1)) - break; - if (controllerPose.bPoseIsValid) { - Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); - - // Calculate world-space delta - Vector3 worldDelta( - controllerMatrix._14 - overlayDragState.initialControllerMatrix._14, - controllerMatrix._24 - overlayDragState.initialControllerMatrix._24, - controllerMatrix._34 - overlayDragState.initialControllerMatrix._34); - - // Transform world delta to attached controller local space (use transpose for correct direction) - Vector3 localDelta = Vector3::Transform(worldDelta, attachedControllerMatrix); - - // Apply local delta to offsets - settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; - settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; - settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; - UpdateVROverlayPosition(); - } - } - break; - } - case OverlayDragState::DragMode::FixedWorld: - { - Matrix delta = controllerMatrix * overlayDragState.initialControllerMatrix.Invert(); - fixedWorldOverlayPosition.m = delta * overlayDragState.initialOverlayMatrix; - break; - } - case OverlayDragState::DragMode::HMD: - { - // Get current HMD transform to convert world deltas to local space - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - break; - if (hmdPose.bPoseIsValid) { - Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - - // Calculate world-space delta - Vector3 worldDelta( - controllerMatrix._14 - overlayDragState.initialControllerMatrix._14, - controllerMatrix._24 - overlayDragState.initialControllerMatrix._24, - controllerMatrix._34 - overlayDragState.initialControllerMatrix._34); - - // Transform world delta to HMD local space (use transpose for correct direction) - Vector3 localDelta = Vector3::Transform(worldDelta, hmdMatrix); - - // Apply local delta to offsets - settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; - settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; - settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; - UpdateVROverlayPosition(); - } - break; - } - } - } - - bool gripPressed = getGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); - if (!gripPressed) { - resetDragState(); - } -} - -void VR::TryStartNewDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - // Helper to get grip state for a controller based on actual hand position - auto getGripPressed = [&](bool isLeft, bool isRight) { - bool isLeftHanded = lastKnownLeftHandedMode; - - if (isLeft) { - // Left hand: primary if left-handed, secondary if right-handed - if (isLeftHanded) { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - if (isRight) { - // Right hand: secondary if left-handed, primary if right-handed - if (isLeftHanded) { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - return false; - }; - - // --- Strict mutually exclusive drag mode selection --- - struct DragMode - { - OverlayDragState::DragMode mode; - bool isActive; - std::function canStart; - std::function onInit; - }; - - std::vector dragModes; - - // Controller mode - only for opposite hand (highest priority) - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - // Controller drag - only opposite hand can drag the controller overlay - dragModes.push_back({ OverlayDragState::DragMode::Controller, - true, - [&](vr::ETrackedControllerRole role) { - // Get the attached controller index - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) { - return false; // No attached controller found - } - - // Get the opposite controller index - ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); - if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) { - return false; // No opposite controller found - } - - // Check if the current controller (the one doing the dragging) is the opposite controller - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); - if (deviceRole == role && i == oppositeControllerIndex) { - return true; // This is the opposite controller, can drag - } - } - } - return false; // Not the opposite controller - }, - [&]() { - overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; - overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; - overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - // Fixed world mode - only for attached controller when in "Both" mode - if (settings.VRMenuPositioningMethod == 1) { - // In "Both" mode, only the attached controller can adjust fixed world position - // In HMD-only mode, any controller can adjust fixed world position - std::function fixedWorldCanStart; - if (settings.attachMode == AttachMode::Both) { - // In "Both" mode, only the attached controller can adjust fixed world position - fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { - // Find the actual attached controller using helper function - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - // Only allow the attached controller to drag fixed world - return role == actualAttachedRole; - } - return false; - }; - } else { - // In HMD-only mode, any controller can adjust fixed world position - fixedWorldCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, - true, - fixedWorldCanStart, - [&]() { - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; - } }); - } - - // HMD mode - for attached controller when both modes active, or any controller otherwise - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - // In "Both" mode, only the attached controller can adjust HMD position - // In HMD-only mode, any controller can adjust HMD position - std::function hmdCanStart; - if (settings.attachMode == AttachMode::Both) { - // In "Both" mode, only the attached controller can adjust HMD position - hmdCanStart = [&](vr::ETrackedControllerRole role) { - // Find the actual attached controller using helper function - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - // Only allow the attached controller to drag HMD - return role == actualAttachedRole; - } - return false; - }; - } else { - // In HMD-only mode, any controller can adjust HMD - hmdCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::HMD, - true, - hmdCanStart, - [&]() { - overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; - overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; - overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - // Try to start a new drag - use first available mode - for (const auto& mode : dragModes) { - if (!mode.isActive) - continue; - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) - continue; - vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); - bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); - bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (!mode.canStart(role)) - continue; - bool gripPressed = getGripPressed(isLeft, isRight); - if (!gripPressed) - continue; - float rawMatrix[3][4]; - if (!Util::GetControllerWorldMatrix(i, rawMatrix)) - continue; - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - overlayDragState.dragging = true; - overlayDragState.mode = mode.mode; - overlayDragState.controllerIndex = i; - overlayDragState.isPrimary = isLeft; - overlayDragState.isSecondary = isRight; - overlayDragState.startControllerMatrix = controllerMatrix; - mode.onInit(); - - // Send haptic pulse to the controller that started the drag (only if overlay is visible) - if (system && globals::menu->IsEnabled) { - // Find the controller device index for the hand that started the drag - for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { - if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); - bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (isRightController == isRight) { - // Use BSOpenVR's haptic pulse method instead of direct OpenVR call - openvr->TriggerHapticPulse(isRightController, 25.0f); // 100ms pulse - break; - } - } - } - } - - return; - } - } -} - -void VR::SetFixedOverlayToCurrentHMD() -{ - vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( - settings.VRMenuOffsetX, - settings.VRMenuOffsetY, - settings.VRMenuOffsetZ); - fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); -} - -//============================================================================= -// OPENVR VERSION DETECTION AND COMPATIBILITY -//============================================================================= - -void VR::DetectOpenVRInfo() -{ - // Reset info - openVRInfo = {}; - - // Find the OpenVR DLL module - HMODULE hModule = GetModuleHandleA("openvr_api.dll"); - if (!hModule) { - openVRInfo.isAvailable = false; - return; - } - - openVRInfo.isAvailable = true; - - // Get the full path to the DLL - char dllPath[MAX_PATH]; - if (GetModuleFileNameA(hModule, dllPath, MAX_PATH) == 0) { - openVRInfo.isCompatible = false; - return; - } - - openVRInfo.dllPath = dllPath; - - // Get file version information - DWORD dwSize = GetFileVersionInfoSizeA(dllPath, nullptr); - if (dwSize > 0) { - std::vector buffer(dwSize); - if (GetFileVersionInfoA(dllPath, 0, dwSize, buffer.data())) { - VS_FIXEDFILEINFO* pFileInfo = nullptr; - UINT len = 0; - if (VerQueryValueA(buffer.data(), "\\", (LPVOID*)&pFileInfo, &len)) { - DWORD major = HIWORD(pFileInfo->dwFileVersionMS); - DWORD minor = LOWORD(pFileInfo->dwFileVersionMS); - DWORD build = HIWORD(pFileInfo->dwFileVersionLS); - DWORD revision = LOWORD(pFileInfo->dwFileVersionLS); - openVRInfo.version = std::format("{}.{}.{}.{}", major, minor, build, revision); - } - } - } - - if (openVRInfo.version.empty()) { - openVRInfo.version = "Unknown"; - } - - // Get file size and timestamp - WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(dllPath, &findData); - if (hFind != INVALID_HANDLE_VALUE) { - FindClose(hFind); - ULARGE_INTEGER fileSize; - fileSize.LowPart = findData.nFileSizeLow; - fileSize.HighPart = findData.nFileSizeHigh; - openVRInfo.fileSize = fileSize.QuadPart; - - // Convert file time to readable format - SYSTEMTIME st; - FileTimeToSystemTime(&findData.ftLastWriteTime, &st); - openVRInfo.modificationTime = std::format("{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); - } - - // Check compatibility - openVRInfo.isCompatible = IsOpenVRCompatible(); -} - -bool VR::IsOpenVRCompatible() const -{ - if (!openVRInfo.isAvailable) { - return false; - } - - // Whitelist: Only allow explicitly known compatible versions - struct WhitelistedVersion - { - std::string version; - uint64_t fileSize; - std::string modificationTime; - }; - - static const std::vector whitelist = { - { "1.0.10.0", 598816, "2022-04-18 00:47:59" }, - // Add more known compatible versions here - }; - - for (const auto& entry : whitelist) { - if (openVRInfo.version == entry.version) { - return true; - } - } - - return false; // Not compatible unless explicitly whitelisted -} - -//============================================================================= -// WAND POINTING IMPLEMENTATION -//============================================================================= - -bool VR::ComputeWandIntersection(vr::VROverlayHandle_t overlayHandle, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) -{ - Util::OpenVRContext ctx; - if (!ctx.HasOverlay()) - return false; - - // Use utility function for core intersection logic - bool intersected = Util::ComputeWandIntersection(ctx.overlay, overlayHandle, controllerIndex, outUV); - - if (intersected) { - // Update wand state for debugging and visualization - wandState.isIntersecting = true; - wandState.uvCoordinates = outUV; - wandState.controllerIndex = controllerIndex; - - // Get controller pose for ray geometry (for debugging/laser visualization) - vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; - if (Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) { - if (poses[controllerIndex].bPoseIsValid) { - wandState.rayOrigin = Vector3( - poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][3], - poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][3], - poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][3]); - wandState.rayDirection = Vector3( - -poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][2], - -poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][2], - -poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][2]); - } - } - } else { - wandState.isIntersecting = false; - } - - return intersected; -} - -void VR::UpdateCursorFromWandPointing() -{ - // Only update cursor when menu is active (ImGui must be initialized) - if (!settings.EnableWandPointing || !globals::menu->IsEnabled) - return; - - ImGuiIO& io = ImGui::GetIO(); - - // Determine which controller should be used for pointing - // Use non-attached controller (free hand points at menu on other hand) - vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; - - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - // If menu is attached to a controller, use the OTHER controller for pointing - ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); - } else { - // HMD-only mode: use primary (dominant) hand for pointing - pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); - } - - if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { - wandState.isIntersecting = false; - return; - } - - // Try to get intersection with active overlay - ImVec2 uv; - bool intersected = false; - - // Check HMD overlay first - if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { - if (ComputeWandIntersection(menuOverlayHandle, pointingController, uv)) { - intersected = true; - } - } - - // If HMD overlay didn't intersect, try controller overlay - if (!intersected && menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { - if (ComputeWandIntersection(menuControllerOverlayHandle, pointingController, uv)) { - intersected = true; - } - } - - // Update cursor position from intersection - if (intersected) { - // Convert UV (0-1) to screen pixel coordinates - // Invert Y to match laser visualization (OpenVR UV has 0 at bottom) - float screenX = uv.x * io.DisplaySize.x; - float screenY = (1.0f - uv.y) * io.DisplaySize.y; - - // Clamp to screen bounds (allow full display size, consistent with thumbstick cursor) - screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); - screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); - - io.MousePos = ImVec2(screenX, screenY); - io.AddMousePosEvent(screenX, screenY); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - } else { - // Ensure wand state is cleared if no intersection - wandState.isIntersecting = false; - } + return openVRInfo.isCompatible; } diff --git a/src/Features/VR.h b/src/Features/VR.h index 67f02de31d..7fbfcf2f68 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -3,6 +3,7 @@ #include "Menu.h" #include "OverlayFeature.h" #include "Utils/Input.h" +#include "VR/OpenVRDetection.h" // In Features/VR/ #include #include #include @@ -60,9 +61,14 @@ struct VR : OverlayFeature */ struct Config { + // Overlay texture dimensions + static constexpr int kOverlayWidth = 1920; ///< Overlay texture width in pixels + static constexpr int kOverlayHeight = 1080; ///< Overlay texture height in pixels + static constexpr float kOverlayAspect = static_cast(kOverlayHeight) / static_cast(kOverlayWidth); ///< Aspect ratio (height/width) + static constexpr float kDefaultMenuScale = 1.0f; ///< Default overlay scale factor - static constexpr float kMinMenuScale = 0.5f; ///< Minimum allowed overlay scale - static constexpr float kMaxMenuScale = 2.0f; ///< Maximum allowed overlay scale + static constexpr float kMinMenuScale = 0.1f; ///< Minimum allowed overlay scale + static constexpr float kMaxMenuScale = 5.0f; ///< Maximum allowed overlay scale static constexpr float kDefaultComboTimeout = 3.0f; ///< Default timeout for button combos (seconds) static constexpr float kDefaultMouseDeadzone = 0.1f; ///< Default thumbstick deadzone for mouse input static constexpr float kDefaultMouseSpeed = 10.0f; ///< Default mouse speed multiplier @@ -70,14 +76,14 @@ struct VR : OverlayFeature static constexpr int kMaxAutoHideSeconds = 300; ///< Maximum auto-hide timeout (5 minutes) // Default HMD overlay offset values (in meters, relative to HMD) - static constexpr float kDefaultHMDOffsetX = 0.26f; ///< Default horizontal offset from HMD - static constexpr float kDefaultHMDOffsetY = -0.04f; ///< Default vertical offset from HMD - static constexpr float kDefaultHMDOffsetZ = -0.41f; ///< Default depth offset from HMD + static constexpr float kDefaultHMDOffsetX = 0.195f; ///< Default horizontal offset from HMD + static constexpr float kDefaultHMDOffsetY = -0.375f; ///< Default vertical offset from HMD + static constexpr float kDefaultHMDOffsetZ = -1.355f; ///< Default depth offset from HMD // Default controller overlay offset values (in meters, relative to controller) - static constexpr float kDefaultControllerOffsetX = 0.22f; ///< Default horizontal offset from controller - static constexpr float kDefaultControllerOffsetY = 0.15f; ///< Default vertical offset from controller - static constexpr float kDefaultControllerOffsetZ = 0.20f; ///< Default depth offset from controller + static constexpr float kDefaultControllerOffsetX = 0.295f; ///< Default horizontal offset from controller + static constexpr float kDefaultControllerOffsetY = 0.211f; ///< Default vertical offset from controller + static constexpr float kDefaultControllerOffsetZ = 0.063f; ///< Default depth offset from controller }; //============================================================================= @@ -91,10 +97,11 @@ struct VR : OverlayFeature return { "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", { "Depth buffer culling optimization for VR performance", + "In-scene overlay menu with HMD/Controller/Fixed World attach modes", + "VR controller input with customizable button mappings", + "Grip-to-drag overlay positioning with depth control", "Configurable occlusion culling parameters", - "VR-specific rendering pipeline improvements", - "Performance optimizations for dual-eye rendering", - "Enhanced VR compatibility across all shader features" } + "Enhanced VR compatibility with SteamVR and OpenComposite" } }; } @@ -121,7 +128,7 @@ struct VR : OverlayFeature //============================================================================= virtual void DrawOverlay() override; - virtual bool IsOverlayVisible() const override { return openVRInfo.isCompatible && settings.kAutoHideSeconds > 0 && !globals::menu->IsEnabled; } + virtual bool IsOverlayVisible() const override { return IsOpenVRCompatible() && settings.kAutoHideSeconds > 0 && globals::menu && !globals::menu->IsEnabled; } //============================================================================= // SETTINGS STRUCTURE @@ -153,7 +160,8 @@ struct VR : OverlayFeature { HMDOnly = 0, ///< Overlay attached to HMD only ControllerOnly = 1, ///< Overlay attached to controller only - Both = 2 ///< Overlay can be attached to both HMD and controller + Both = 2, ///< Overlay can be attached to both HMD and controller + None = 3 ///< Overlay display disabled }; OverlayAttachMode attachMode = OverlayAttachMode::HMDOnly; ///< Current overlay attachment mode ControllerDevice VRMenuAttachController = ControllerDevice::Secondary; ///< Which controller to attach overlay to @@ -216,7 +224,7 @@ struct VR : OverlayFeature */ bool IsAttachModeValid() const { - return attachMode >= OverlayAttachMode::HMDOnly && attachMode <= OverlayAttachMode::Both; + return attachMode >= OverlayAttachMode::HMDOnly && attachMode <= OverlayAttachMode::None; } /** @@ -242,13 +250,16 @@ struct VR : OverlayFeature // VR-SPECIFIC PUBLIC API //============================================================================= - void UpdateVROverlayPosition(); - void UpdateVROverlayControllerPosition(); - void ProcessVREvents(std::vector& vrEvents); // Wand pointing methods - bool ComputeWandIntersection(vr::VROverlayHandle_t overlayHandle, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); + enum class OverlayType + { + HMD, + Controller + }; + bool ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); + bool ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); void UpdateCursorFromWandPointing(); void UpdateOverlayMenuStateFromInput(); void ProcessVRButtonEvent(const Menu::KeyEvent& event); @@ -256,8 +267,6 @@ struct VR : OverlayFeature void ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io); void ProcessControllerInputForImGui(); - void EnsureOverlayInitialized(); - void DestroyOverlay(); void RecreateOverlayTexturesIfNeeded(); void SubmitOverlayFrame(); @@ -309,6 +318,7 @@ struct VR : OverlayFeature void UpdateActiveDrag(); void TryStartNewDrag(); void SetFixedOverlayToCurrentHMD(); + void UpdateFixedWorldPositioning(); bool ShouldHighlightOverlayWindow() const { return overlayDragState.dragging; } //============================================================================= @@ -349,6 +359,7 @@ struct VR : OverlayFeature struct OverlayWorldPosition { Matrix m = Matrix::Identity; + bool initialized = false; } fixedWorldOverlayPosition; struct OverlayDragState @@ -372,6 +383,7 @@ struct VR : OverlayFeature Vector3 initialHMDOffset = Vector3::Zero; Vector3 initialControllerOffset = Vector3::Zero; + float initialHMDScale = 1.0f; Matrix startControllerMatrix = Matrix::Identity; } overlayDragState; @@ -413,6 +425,15 @@ struct VR : OverlayFeature std::string version; uint64_t fileSize = 0; std::string modificationTime; + + // Interface probing results + bool hasOverlayInterface = false; + bool hasSystemInterface = false; + bool hasCompositorInterface = false; + + // Detection metadata + VRDetection::RuntimeType runtimeType = VRDetection::RuntimeType::Unknown; + bool probingSucceeded = false; } openVRInfo; RE::NiPoint3 savedPlayerWorldPos = RE::NiPoint3(); // Used for auto-reset distance check @@ -427,11 +448,44 @@ struct VR : OverlayFeature Vector3 rayDirection = Vector3::Zero; } wandState; -public: - //============================================================================= - // PRIVATE IMPLEMENTATION - //============================================================================= + // In-Scene Overlay Rendering Resources (Fallback for incompatible runtimes) + struct InSceneResources + { + winrt::com_ptr vs; + winrt::com_ptr ps; + winrt::com_ptr vb; + winrt::com_ptr ib; + winrt::com_ptr cb; + winrt::com_ptr inputLayout; + winrt::com_ptr blendState; + winrt::com_ptr depthState; + winrt::com_ptr sampler; + winrt::com_ptr rasterizerState; + + // Cached SRV to avoid creating every frame + winrt::com_ptr menuSRV; + ID3D11Texture2D* cachedMenuTexture = nullptr; + + bool initialized = false; + } inSceneResources; + + struct InSceneCB + { + Matrix wvp; + }; + void InitInSceneResources(); + void RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds); + void InstallSubmitHook(); void DetectOpenVRInfo(); bool IsOpenVRCompatible() const; + +private: + //============================================================================= + // PRIVATE HELPERS + //============================================================================= + + bool GetGripPressed(bool isLeft, bool isRight) const; + void ResetComboRecording(); + void ApplyRecordedCombo(); }; diff --git a/src/Features/VR/InSceneOverlay.cpp b/src/Features/VR/InSceneOverlay.cpp new file mode 100644 index 0000000000..f079659966 --- /dev/null +++ b/src/Features/VR/InSceneOverlay.cpp @@ -0,0 +1,611 @@ +#include "Features/VR.h" +#include "Globals.h" +#include "Hooks.h" +#include "Menu.h" +#include "Util.h" +#include "Utils/VRUtils.h" +#include +#include +#include +#include +#include +#include + +using namespace DirectX; +using namespace DirectX::SimpleMath; + +using AttachMode = VR::Settings::OverlayAttachMode; + +// Helper to create aspect-corrected scale matrix for overlay +inline Matrix CreateOverlayScaleMatrix(float scale) +{ + return Matrix::CreateScale(scale, scale * VR::Config::kOverlayAspect, scale); +} + +//============================================================================= +// IN-SCENE OVERLAY RENDERING VIA SUBMIT HOOK +//============================================================================= + +namespace +{ + struct IVRCompositor_Submit + { + static vr::EVRCompositorError thunk(vr::IVRCompositor* _this, vr::EVREye eEye, const vr::Texture_t* pTexture, const vr::VRTextureBounds_t* pBounds, vr::EVRSubmitFlags nSubmitFlags) + { + auto& vr = globals::features::vr; + // Only process DirectX textures - skip OpenGL/Vulkan to avoid undefined behavior + if (pTexture && pTexture->handle && pTexture->eType == vr::TextureType_DirectX) { + vr.RenderInSceneOverlay(eEye, (ID3D11Texture2D*)pTexture->handle, pBounds); + } + return func(_this, eEye, pTexture, pBounds, nSubmitFlags); + } + static inline REL::Relocation func; + }; +} + +void VR::InitInSceneResources() +{ + if (inSceneResources.initialized) + return; + + InSceneResources temp = {}; + + auto device = globals::d3d::device; + + // 1. Compile shaders - compile VS to get bytecode for input layout, PS separately + ID3DBlob* vsBlob = nullptr; + ID3DBlob* psBlob = nullptr; + ID3DBlob* errorBlob = nullptr; + + // Compile vertex shader + if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.vs.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, + "main", "vs_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob))) { + if (errorBlob) { + logger::error("VR InScene VS compile error: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + return; + } + if (errorBlob) { + errorBlob->Release(); + errorBlob = nullptr; + } + + // Compile pixel shader + if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.ps.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, + "main", "ps_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &psBlob, &errorBlob))) { + if (errorBlob) { + logger::error("VR InScene PS compile error: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + if (vsBlob) + vsBlob->Release(); + return; + } + if (errorBlob) { + errorBlob->Release(); + errorBlob = nullptr; + } + + // Create shader objects from bytecode + ID3D11VertexShader* vs = nullptr; + ID3D11PixelShader* ps = nullptr; + if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &vs)) || + FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &ps))) { + logger::error("VR: Failed to create shader objects"); + if (vs) + vs->Release(); + if (ps) + ps->Release(); + if (vsBlob) + vsBlob->Release(); + if (psBlob) + psBlob->Release(); + return; + } + + temp.vs.attach(vs); + temp.ps.attach(ps); + if (psBlob) + psBlob->Release(); // Don't need PS blob anymore + + // 2. Input Layout + D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; + polygonLayout[0].SemanticName = "POSITION"; + polygonLayout[0].SemanticIndex = 0; + polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; + polygonLayout[0].InputSlot = 0; + polygonLayout[0].AlignedByteOffset = 0; + polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; + polygonLayout[0].InstanceDataStepRate = 0; + + polygonLayout[1].SemanticName = "TEXCOORD"; + polygonLayout[1].SemanticIndex = 0; + polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; + polygonLayout[1].InputSlot = 0; + polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; + polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; + polygonLayout[1].InstanceDataStepRate = 0; + + if (FAILED(device->CreateInputLayout(polygonLayout, 2, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), temp.inputLayout.put()))) { + logger::error("VR: Failed to create input layout"); + vsBlob->Release(); + return; + } + + vsBlob->Release(); + + // 3. Buffers + // Quad Vertices (XY plane, z=0, size=1) + struct VertexType + { + XMFLOAT3 position; + XMFLOAT2 texture; + }; + VertexType vertices[4] = { + { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT2(0.0f, 1.0f) }, // Bottom Left + { XMFLOAT3(-0.5f, 0.5f, 0.0f), XMFLOAT2(0.0f, 0.0f) }, // Top Left + { XMFLOAT3(0.5f, 0.5f, 0.0f), XMFLOAT2(1.0f, 0.0f) }, // Top Right + { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT2(1.0f, 1.0f) } // Bottom Right + }; + + D3D11_BUFFER_DESC vertexBufferDesc = {}; + vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; + vertexBufferDesc.ByteWidth = sizeof(VertexType) * 4; + vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + D3D11_SUBRESOURCE_DATA vertexData = {}; + vertexData.pSysMem = vertices; + if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, temp.vb.put()))) { + logger::error("VR: Failed to create vertex buffer"); + return; + } + + unsigned long indices[6] = { 0, 1, 2, 0, 2, 3 }; + D3D11_BUFFER_DESC indexBufferDesc = {}; + indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; + indexBufferDesc.ByteWidth = sizeof(unsigned long) * 6; + indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; + D3D11_SUBRESOURCE_DATA indexData = {}; + indexData.pSysMem = indices; + if (FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, temp.ib.put()))) { + logger::error("VR: Failed to create index buffer"); + return; + } + + D3D11_BUFFER_DESC matrixBufferDesc = {}; + matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; + matrixBufferDesc.ByteWidth = sizeof(InSceneCB); + matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(device->CreateBuffer(&matrixBufferDesc, nullptr, temp.cb.put()))) { + logger::error("VR: Failed to create constant buffer"); + return; + } + + // 4. States + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F; + if (FAILED(device->CreateBlendState(&blendDesc, temp.blendState.put()))) { + logger::error("VR: Failed to create blend state"); + return; + } + + D3D11_DEPTH_STENCIL_DESC depthDesc = {}; + depthDesc.DepthEnable = FALSE; // Always on top + depthDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + depthDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; + if (FAILED(device->CreateDepthStencilState(&depthDesc, temp.depthState.put()))) { + logger::error("VR: Failed to create depth stencil state"); + return; + } + + D3D11_RASTERIZER_DESC rasterDesc = {}; + rasterDesc.FillMode = D3D11_FILL_SOLID; + rasterDesc.CullMode = D3D11_CULL_NONE; + rasterDesc.FrontCounterClockwise = FALSE; + rasterDesc.DepthClipEnable = TRUE; + if (FAILED(device->CreateRasterizerState(&rasterDesc, temp.rasterizerState.put()))) { + logger::error("VR: Failed to create rasterizer state"); + return; + } + + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device->CreateSamplerState(&samplerDesc, temp.sampler.put()))) { + logger::error("VR: Failed to create sampler state"); + return; + } + + inSceneResources = std::move(temp); + inSceneResources.initialized = true; + logger::debug("VR: In-Scene Overlay resources initialized."); +} + +void VR::RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds) +{ + auto context = globals::d3d::context; + winrt::com_ptr perf; + context->QueryInterface(__uuidof(ID3DUserDefinedAnnotation), perf.put_void()); + + std::wstring eventName = L"VR In-Scene Overlay (Eye " + std::to_wstring((int)eye) + L")"; + if (perf) + perf->BeginEvent(eventName.c_str()); + + if (!inSceneResources.initialized) + InitInSceneResources(); + if (!inSceneResources.initialized) { + if (perf) + perf->EndEvent(); + return; + } + + // Only render if overlay should be visible + if (!globals::menu || !(globals::menu->IsEnabled || globals::menu->overlayVisible || settings.kAutoHideSeconds > 0)) { + if (perf) + perf->EndEvent(); + return; + } + if (!menuTexture) { + if (perf) + perf->EndEvent(); + return; + } + + // Skip rendering when attach mode is None (disabled) + if (settings.attachMode == AttachMode::None) { + if (perf) + perf->EndEvent(); + return; + } + + // We can't render if we don't have HMD pose + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) { + if (perf) + perf->EndEvent(); + return; + } + + // Get HMD Pose and Eye matrices + vr::TrackedDevicePose_t hmdPose; + vr::TrackedDevicePose_t renderPose[vr::k_unMaxTrackedDeviceCount]; + + RE::BSOpenVR::GetIVRCompositor()->GetLastPoses(renderPose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + hmdPose = renderPose[vr::k_unTrackedDeviceIndex_Hmd]; + if (!hmdPose.bPoseIsValid) { + if (perf) + perf->EndEvent(); + return; + } + + Matrix hmdWorld = Matrix::Identity; + Matrix eyeToHead = Matrix::Identity; + Matrix proj = Matrix::Identity; + Matrix vpHeadSpace = Matrix::Identity; // For HMD-relative rendering (head space) + Matrix vpWorldSpace = Matrix::Identity; // For world/controller rendering (world space) + + // Always get Eye and Projection matrices + eyeToHead = Util::HmdMatrix34ToMatrix(openvr->vrSystem->GetEyeToHeadTransform(eye)); + + // Use GetProjectionRaw to build a DirectX-compatible projection matrix (Depth [0, 1]) + // IMPORTANT: OpenVR GetProjectionRaw has a known bug (Valve issue #110, open since 2016): + // The 3rd parameter (named "pTop") actually returns the BOTTOM tangent, and + // the 4th parameter (named "pBottom") actually returns the TOP tangent. + // We name our variables to match the ACTUAL values, not the misleading parameter names. + float left, right, bottom, top; + openvr->vrSystem->GetProjectionRaw(eye, &left, &right, &bottom, &top); + float nearZ = 0.1f; + float farZ = 1000.0f; + + proj = DirectX::XMMatrixPerspectiveOffCenterRH(left * nearZ, right * nearZ, bottom * nearZ, top * nearZ, nearZ, farZ); + + // Log projection values once per eye + static bool projLogged[2] = { false, false }; + if (!projLogged[(int)eye]) { + logger::debug("VR Projection Eye {}: L={:.4f} R={:.4f} B={:.4f} T={:.4f}, EyeX={:.4f}", + (int)eye, left, right, bottom, top, eyeToHead._41); + projLogged[(int)eye] = true; + } + + // Head-space VP (for HMD-relative mode) + vpHeadSpace = eyeToHead.Invert() * proj; + + // World-space VP (for controller attach and fixed world position modes) + if (hmdPose.bPoseIsValid) { + hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + Matrix eyeToWorld = hmdWorld * eyeToHead; + vpWorldSpace = eyeToWorld.Invert() * proj; + } + + // Create RTV for the target texture + winrt::com_ptr rtv; + D3D11_TEXTURE2D_DESC texDesc; + targetTexture->GetDesc(&texDesc); + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = texDesc.Format; + + if (texDesc.ArraySize > 1) { + if (texDesc.SampleDesc.Count > 1) { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY; + rtvDesc.Texture2DMSArray.FirstArraySlice = (UINT)eye; + rtvDesc.Texture2DMSArray.ArraySize = 1; + } else { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; + rtvDesc.Texture2DArray.FirstArraySlice = (UINT)eye; + rtvDesc.Texture2DArray.ArraySize = 1; + rtvDesc.Texture2DArray.MipSlice = 0; + } + } else if (texDesc.SampleDesc.Count > 1) { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS; + } else { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + } + + HRESULT hr = globals::d3d::device->CreateRenderTargetView(targetTexture, &rtvDesc, rtv.put()); + + if (FAILED(hr)) { + logger::error("VR: Failed to create RTV for eye texture (Format: {}, Samples: {}). HRESULT: {:x}", + (uint32_t)texDesc.Format, texDesc.SampleDesc.Count, (uint32_t)hr); + if (perf) + perf->EndEvent(); + return; + } + + // Save State + ID3D11RenderTargetView* oldRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT]; + ID3D11DepthStencilView* oldDSV; + context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, &oldDSV); + + D3D11_VIEWPORT oldViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; + UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + context->RSGetViewports(&numViewports, oldViewports); + + ID3D11RasterizerState* oldRS = nullptr; + context->RSGetState(&oldRS); + + // Setup Render + ID3D11RenderTargetView* rtvPtr = rtv.get(); + context->OMSetRenderTargets(1, &rtvPtr, nullptr); // No DSV + + // Viewport: Use bounds if provided (for SBS textures), otherwise use full texture + D3D11_VIEWPORT vpDesc = {}; + if (bounds) { + vpDesc.TopLeftX = bounds->uMin * texDesc.Width; + vpDesc.TopLeftY = bounds->vMin * texDesc.Height; + vpDesc.Width = (bounds->uMax - bounds->uMin) * texDesc.Width; + vpDesc.Height = (bounds->vMax - bounds->vMin) * texDesc.Height; + } else { + vpDesc.TopLeftX = 0.0f; + vpDesc.TopLeftY = 0.0f; + vpDesc.Width = (float)texDesc.Width; + vpDesc.Height = (float)texDesc.Height; + } + vpDesc.MinDepth = 0.0f; + vpDesc.MaxDepth = 1.0f; + context->RSSetViewports(1, &vpDesc); + + // Log texture and viewport details once per session + static bool textureInfoLogged = false; + if (!textureInfoLogged) { + logger::debug("VR Submit Texture Info:"); + logger::debug(" Texture Size: {}x{}, Format: {}, ArraySize: {}, SampleCount: {}", + texDesc.Width, texDesc.Height, (uint32_t)texDesc.Format, texDesc.ArraySize, texDesc.SampleDesc.Count); + if (bounds) { + logger::debug(" Bounds for Eye {}: uMin={:.3f}, vMin={:.3f}, uMax={:.3f}, vMax={:.3f}", + (int)eye, bounds->uMin, bounds->vMin, bounds->uMax, bounds->vMax); + logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", + vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); + } else { + logger::debug(" No bounds provided (full texture per eye, or texture array)"); + logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", + vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); + } + logger::debug(" RTV Dimension: {}", + rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DARRAY ? "Texture2DArray (per-eye slice)" : + rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2D ? "Texture2D (single)" : + rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DMS ? "Texture2DMS" : + rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY ? "Texture2DMSArray" : + "Unknown"); + // Log again for the other eye + if (eye == vr::Eye_Right) + textureInfoLogged = true; + } + + // Helper to draw the overlay quad with a given WVP matrix + auto drawOverlayQuad = [&](ID3D11DeviceContext* ctx, const InSceneCB& cbData) { + D3D11_MAPPED_SUBRESOURCE mappedResource; + if (SUCCEEDED(ctx->Map(inSceneResources.cb.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource))) { + memcpy(mappedResource.pData, &cbData, sizeof(InSceneCB)); + ctx->Unmap(inSceneResources.cb.get(), 0); + } + + ctx->VSSetShader(inSceneResources.vs.get(), nullptr, 0); + ctx->PSSetShader(inSceneResources.ps.get(), nullptr, 0); + ID3D11Buffer* cb = inSceneResources.cb.get(); + ctx->VSSetConstantBuffers(0, 1, &cb); + + struct VT + { + XMFLOAT3 p; + XMFLOAT2 t; + }; + UINT stride = sizeof(VT); + UINT offset = 0; + ID3D11Buffer* vb = inSceneResources.vb.get(); + ctx->IASetVertexBuffers(0, 1, &vb, &stride, &offset); + ctx->IASetIndexBuffer(inSceneResources.ib.get(), DXGI_FORMAT_R32_UINT, 0); + ctx->IASetInputLayout(inSceneResources.inputLayout.get()); + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + ctx->OMSetBlendState(inSceneResources.blendState.get(), nullptr, 0xFFFFFFFF); + ctx->OMSetDepthStencilState(inSceneResources.depthState.get(), 0); + ctx->RSSetState(inSceneResources.rasterizerState.get()); + + // Cache SRV to avoid creating every frame + if (menuTexture.get() != inSceneResources.cachedMenuTexture) { + inSceneResources.menuSRV = nullptr; + if (FAILED(globals::d3d::device->CreateShaderResourceView(menuTexture.get(), nullptr, inSceneResources.menuSRV.put()))) { + logger::error("VR: Failed to create menu texture SRV"); + return; + } + inSceneResources.cachedMenuTexture = menuTexture.get(); + } + ID3D11ShaderResourceView* srvPtr = inSceneResources.menuSRV.get(); + ctx->PSSetShaderResources(0, 1, &srvPtr); + + ID3D11SamplerState* sampler = inSceneResources.sampler.get(); + ctx->PSSetSamplers(0, 1, &sampler); + + ctx->DrawIndexed(6, 0, 0); + }; + + // --- Render HMD Overlay --- + if ((settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) && menuTexture) { + InSceneCB cbData; + + Matrix modelMatrix; + Matrix vp; + if (settings.VRMenuPositioningMethod == 1) { // Fixed World Position + modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * fixedWorldOverlayPosition.m; + vp = vpWorldSpace; + } else { // HMD Relative + Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); + modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * offset; + vp = vpHeadSpace; + } + cbData.wvp = (modelMatrix * vp).Transpose(); + + drawOverlayQuad(context, cbData); + } + + // --- Render Controller Overlay --- + if ((settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) && menuTexture) { + vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachIndex != vr::k_unTrackedDeviceIndexInvalid && attachIndex < vr::k_unMaxTrackedDeviceCount) { + vr::TrackedDevicePose_t controllerPose = renderPose[attachIndex]; + if (controllerPose.bPoseIsValid) { + Matrix controllerWorld = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); + Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); + Matrix modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * offset * controllerWorld; + + // Backface culling: hide overlay when viewed from behind + // Use the unscaled controller+offset transform for correct normal direction + Matrix overlayTransform = offset * controllerWorld; + Vector3 overlayNormal(overlayTransform._31, overlayTransform._32, overlayTransform._33); + overlayNormal.Normalize(); + Matrix eyeWorld = hmdWorld * eyeToHead; + Vector3 eyePos = eyeWorld.Translation(); + Vector3 overlayPos = overlayTransform.Translation(); + Vector3 toEye = eyePos - overlayPos; + toEye.Normalize(); + // Quad front face is +Z in local space (D3D default CW winding). + // Render when eye is on the +Z side of the overlay (dot > 0). + float dot = overlayNormal.Dot(toEye); + if (dot > 0.0f) { + InSceneCB cbData; + cbData.wvp = (modelMatrix * vpWorldSpace).Transpose(); + drawOverlayQuad(context, cbData); + } + } + } + } + + // Restore State + context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, oldDSV); + context->RSSetViewports(numViewports, oldViewports); + if (oldRS) { + context->RSSetState(oldRS); + oldRS->Release(); + } + for (int i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) + if (oldRTVs[i]) + oldRTVs[i]->Release(); + if (oldDSV) + oldDSV->Release(); + + if (perf) + perf->EndEvent(); +} + +void VR::InstallSubmitHook() +{ + static bool installed = false; + if (installed) + return; + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (openvr && RE::BSOpenVR::GetIVRCompositor()) { + logger::info("VR: Installing IVRCompositor::Submit hook for in-scene overlay rendering"); + + // Log comprehensive VR system parameters (debug only) + logger::debug("=== VR System Configuration ==="); + + // Get and log IPD + float ipd = Util::GetIPDFromHMD(); + logger::debug("IPD: {:.4f} meters ({:.2f} mm)", ipd, ipd * 1000.0f); + + // Get and log eye transforms + if (openvr->vrSystem) { + vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); + vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); + + logger::debug("Left Eye Transform:"); + logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", + leftEye.m[0][3], leftEye.m[1][3], leftEye.m[2][3]); + logger::debug("Right Eye Transform:"); + logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", + rightEye.m[0][3], rightEye.m[1][3], rightEye.m[2][3]); + logger::debug("Calculated Eye Separation: {:.4f} meters ({:.2f} mm)", + std::abs(leftEye.m[0][3] - rightEye.m[0][3]), + std::abs(leftEye.m[0][3] - rightEye.m[0][3]) * 1000.0f); + + // Get projection matrices + vr::HmdMatrix44_t leftProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); + vr::HmdMatrix44_t rightProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Right, 0.1f, 1000.0f); + + logger::debug("Projection Matrices (near=0.1, far=1000.0):"); + logger::debug(" Left [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", + leftProj.m[0][0], leftProj.m[1][1], leftProj.m[0][2]); + logger::debug(" Right [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", + rightProj.m[0][0], rightProj.m[1][1], rightProj.m[0][2]); + } + + logger::debug("Convergence Formula Info:"); + logger::debug(" Formula: stereoShift = (IPD/2) / (depth * tan(hFOV/2))"); + logger::debug(" - Shift is independent of scale (scale only controls size)"); + logger::debug(" - Depth is controlled by OffsetZ (negative = in front)"); + float halfIPD = ipd / 2.0f; + if (openvr->vrSystem) { + vr::HmdMatrix44_t proj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); + float tanHFOV = 1.0f / proj.m[0][0]; + logger::debug(" tan(hFOV/2) = {:.4f}", tanHFOV); + logger::debug(" Example: At depth 1.0m, shift={:.6f}", halfIPD / (1.0f * tanHFOV)); + logger::debug(" Example: At depth 2.0m, shift={:.6f}", halfIPD / (2.0f * tanHFOV)); + logger::debug(" Example: At depth 5.0m, shift={:.6f}", halfIPD / (5.0f * tanHFOV)); + } + logger::debug("================================"); + + // IVRCompositor::Submit is index 5 + stl::detour_vfunc<5, IVRCompositor_Submit>(RE::BSOpenVR::GetIVRCompositor()); + installed = true; + + logger::info("VR: In-scene overlay initialized"); + } else { + logger::warn("VR: Failed to install IVRCompositor::Submit hook - Interface not available"); + } +} diff --git a/src/Features/VR/Input.cpp b/src/Features/VR/Input.cpp new file mode 100644 index 0000000000..8105b42e30 --- /dev/null +++ b/src/Features/VR/Input.cpp @@ -0,0 +1,376 @@ +#include "Features/VR.h" +#include "Menu.h" +#include "Utils/PerfUtils.h" +#include "Utils/VRUtils.h" + +#include + +using AttachMode = VR::Settings::OverlayAttachMode; + +void VR::UpdateOverlayMenuStateFromInput() +{ + if (this->isCapturingCombo) { + return; + } + + if (globals::menu == nullptr) + return; + + bool& isEnabled = globals::menu->IsEnabled; + bool& overlayEnabled = globals::menu->overlayVisible; + bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; + + if (testMode) { + if (!isEnabled) { + settings.VRMenuControllerDiagnosticsTestMode = false; + return; + } + return; + } + + bool uiMenusOpen = globals::game::ui && + (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); + + bool inValidMenuState = uiMenusOpen || (globals::game::ui && (isEnabled || overlayEnabled)); + + if (!inValidMenuState) + return; + + struct MenuStateMapping + { + std::function condition; + std::function action; + bool allowWhenUIMenusClosed = false; + }; + + auto CheckCombo = [&](const std::vector& combos) -> bool { + if (combos.empty()) + return false; + + for (size_t i = 0; i < combos.size(); ++i) { + const auto& combo = combos[i]; + bool buttonPressed = false; + + switch (combo.GetDevice()) { + case ControllerDevice::Both: + buttonPressed = primaryControllerState[combo.GetKey()].isPressed && + secondaryControllerState[combo.GetKey()].isPressed; + break; + case ControllerDevice::Primary: + buttonPressed = primaryControllerState[combo.GetKey()].isPressed; + break; + case ControllerDevice::Secondary: + buttonPressed = secondaryControllerState[combo.GetKey()].isPressed; + break; + } + + if (!buttonPressed) { + return false; + } + } + + return true; + }; + + std::vector mappings = { + // Open Community Shaders menu when closed + { [&]() { + return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; + }, + [&]() { isEnabled = true; } }, + + // Close Community Shaders menu when open + { [&]() { + return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; + }, + [&]() { + isEnabled = false; + overlayDragState.dragging = false; + }, + true }, + + // Open VR overlay when closed (only when CS menu is open) + { [&]() { + return CheckCombo(settings.VROverlayOpenKeys) && !overlayEnabled && isEnabled; + }, + [&]() { overlayEnabled = true; } }, + + // Close VR overlay when open (only when CS menu is open) + { [&]() { + return CheckCombo(settings.VROverlayCloseKeys) && overlayEnabled && isEnabled; + }, + [&]() { overlayEnabled = false; } } + }; + + bool onlyAllowClose = isEnabled && !uiMenusOpen; + + for (const auto& mapping : mappings) { + if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) + continue; + + if (mapping.condition()) { + mapping.action(); + break; + } + } +} + +void VR::ProcessVREvents(std::vector& vrEvents) +{ + bool currentLeftHandedMode = RE::BSOpenVRControllerDevice::IsLeftHandedMode(); + static bool firstCall = true; + if (firstCall || currentLeftHandedMode != lastKnownLeftHandedMode) { + if (!firstCall) { + logger::debug("VR handedness changed: {} -> {}", lastKnownLeftHandedMode ? "Left" : "Right", currentLeftHandedMode ? "Left" : "Right"); + } + firstCall = false; + lastKnownLeftHandedMode = currentLeftHandedMode; + primaryControllerState = {}; + secondaryControllerState = {}; + } + + double nowSecs = Util::GetNowSecs(); + for (auto& event : vrEvents) { + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + struct VRButtonDescriptor + { + const char* name; + bool (*isButton)(std::uint32_t); + std::uint32_t keyCode; + }; + static const VRButtonDescriptor kVRButtons[] = { + { "Grip", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGrip }, + { "GripAlt", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGripAlt }, + { "Trigger", RE::BSOpenVRControllerDevice::IsTriggerButton, RE::BSOpenVRControllerDevice::Keys::kTrigger }, + { "Stick Click", RE::BSOpenVRControllerDevice::IsStickClick, RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger }, + { "Touchpad Click", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadClick }, + { "Touchpad Alt", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt }, + { "A/X", RE::BSOpenVRControllerDevice::IsAButton, RE::BSOpenVRControllerDevice::Keys::kXA }, + { "B/Y", RE::BSOpenVRControllerDevice::IsBButton, RE::BSOpenVRControllerDevice::Keys::kBY }, + }; + for (const auto& desc : kVRButtons) { + if (event.keyCode == desc.keyCode) { + RE::ButtonState* state = isPrimary ? &primaryControllerState[desc.keyCode] : isSecondary ? &secondaryControllerState[desc.keyCode] : + nullptr; + if (state) { + state->OnEvent(event.IsPressed(), nowSecs); + } + break; + } + } + switch (event.eventType) { + case RE::INPUT_EVENT_TYPE::kButton: + ProcessVRButtonEvent(event); + break; + case RE::INPUT_EVENT_TYPE::kThumbstick: + UpdateControllerState(event); + break; + default: + break; + } + } +} + +void VR::ProcessVRButtonEvent(const Menu::KeyEvent& event) +{ + if (this->isCapturingCombo) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; + constexpr size_t kNumTriggerMappings = 1; + + if (isPrimary || isSecondary) { + constexpr size_t kNumMappings = 6; + RE::ButtonMapping mappings[kNumMappings] = { + { RE::BSOpenVRControllerDevice::Keys::kTrigger, ImGuiMouseButton_Left, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kGrip, ImGuiMouseButton_Right, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kTouchpadClick, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kBY, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_TAB), isSecondary }, + { RE::BSOpenVRControllerDevice::Keys::kXA, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_RETURN), false }, + }; + + static bool prevPrimaryStates[kNumMappings] = {}; + static bool prevSecondaryStates[kNumMappings] = {}; + bool* prevStates = isPrimary ? prevPrimaryStates : prevSecondaryStates; + + RE::InputDeviceState& controllerState = isPrimary ? primaryControllerState : secondaryControllerState; + + size_t limit = testMode ? kNumTriggerMappings : kNumMappings; + + for (size_t i = 0; i < limit; ++i) { + RE::ButtonState* state = &controllerState[mappings[i].keyCode]; + bool curr = state ? state->isPressed : false; + if (curr != prevStates[i]) { + if (mappings[i].isKeyEvent) { + if (mappings[i].isShift) + io.AddKeyEvent(ImGuiMod_Shift, curr); + io.AddKeyEvent(static_cast(mappings[i].key), curr); + } else { + io.AddMouseButtonEvent(mappings[i].logicalButton, curr); + } + prevStates[i] = curr; + } + } + } + + VRControllerEventLog logEntry; + logEntry.device = static_cast(event.device); + logEntry.keyCode = event.keyCode; + logEntry.value = static_cast(event.value); + logEntry.pressed = event.IsPressed(); + logEntry.heldTime = 0.0; + logEntry.heldSource = "button"; + logEntry.thumbstickX = 0.0f; + logEntry.thumbstickY = 0.0f; + logEntry.controllerRole = isPrimary ? "Primary" : isSecondary ? "Secondary" : + "Unknown"; + vrControllerEventLog.push_back(logEntry); + if (vrControllerEventLog.size() > 32) { + vrControllerEventLog.erase(vrControllerEventLog.begin()); + } +} + +void VR::UpdateControllerState(const Menu::KeyEvent& event) +{ + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + + if (isPrimary) { + primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x = event.thumbstickX; + primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y = event.thumbstickY; + } else if (isSecondary) { + secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x = event.thumbstickX; + secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y = event.thumbstickY; + } + + VRControllerEventLog logEntry; + logEntry.device = static_cast(event.device); + logEntry.keyCode = event.keyCode; + logEntry.value = static_cast(event.value); + logEntry.pressed = event.IsPressed(); + logEntry.heldTime = 0.0; + logEntry.heldSource = "thumbstick"; + logEntry.thumbstickX = event.thumbstickX; + logEntry.thumbstickY = event.thumbstickY; + logEntry.controllerRole = isPrimary ? "Primary" : "Secondary"; + vrControllerEventLog.push_back(logEntry); + if (vrControllerEventLog.size() > 32) { + vrControllerEventLog.erase(vrControllerEventLog.begin()); + } +} + +void VR::ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io) +{ + bool usingScrollStickX = (std::abs(controllerState.thumbsticks[thumbstickIndex].x) > deadzone); + bool usingScrollStickY = (std::abs(controllerState.thumbsticks[thumbstickIndex].y) > deadzone); + + if (usingScrollStickX || usingScrollStickY) { + struct ScrollAccum + { + float x = 0.0f; + float y = 0.0f; + }; + static std::unordered_map scrollAccums; + ScrollAccum& accum = scrollAccums[thumbstickIndex]; + + accum.x += controllerState.thumbsticks[thumbstickIndex].x * 0.1f; + accum.y += controllerState.thumbsticks[thumbstickIndex].y * 0.1f; + + float scrollEventX = 0.0f; + float scrollEventY = 0.0f; + + if (std::abs(accum.x) > 0.3f) { + scrollEventX = accum.x > 0 ? 1.0f : -1.0f; + accum.x = 0.0f; + } + if (std::abs(accum.y) > 0.3f) { + scrollEventY = accum.y > 0 ? 1.0f : -1.0f; + accum.y = 0.0f; + } + + if (scrollEventX != 0.0f || scrollEventY != 0.0f) { + io.AddMouseWheelEvent(-scrollEventX, scrollEventY); + } + } +} + +void VR::ProcessControllerInputForImGui() +{ + if (!globals::menu || !globals::menu->IsEnabled) + return; + bool testMode = settings.VRMenuControllerDiagnosticsTestMode; + float mouseDeadzone = settings.mouseDeadzone; + float mouseSpeed = settings.mouseSpeed; + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; + io.WantSetMousePos = false; + + bool wandHandledCursor = false; + if (!testMode && settings.EnableWandPointing) { + UpdateCursorFromWandPointing(); + wandHandledCursor = wandState.isIntersecting; + } + + if (!testMode) { + bool isDragging = overlayDragState.dragging; + + if (wandHandledCursor && !isDragging) { + ProcessThumbstickScroll(primaryControllerState, static_cast(RE::ControllerRole::Primary), mouseDeadzone, io); + ProcessThumbstickScroll(secondaryControllerState, static_cast(RE::ControllerRole::Secondary), mouseDeadzone, io); + } else if (!isDragging) { + bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || + settings.attachMode == VR::Settings::OverlayAttachMode::Both); + + RE::VRControllerState* cursorController = nullptr; + RE::VRControllerState* scrollController = nullptr; + + if (useAttachedControllerForCursor) { + if (settings.VRMenuAttachController == ControllerDevice::Primary) { + cursorController = &primaryControllerState; + scrollController = &secondaryControllerState; + } else { + cursorController = &secondaryControllerState; + scrollController = &primaryControllerState; + } + } else { + cursorController = &primaryControllerState; + scrollController = &secondaryControllerState; + } + + if (cursorController) { + size_t thumbstickIndex = (cursorController == &primaryControllerState) ? + static_cast(RE::ControllerRole::Primary) : + static_cast(RE::ControllerRole::Secondary); + + float thumbstickX = cursorController->thumbsticks[thumbstickIndex].x; + float thumbstickY = cursorController->thumbsticks[thumbstickIndex].y; + bool usingCursorStick = (std::abs(thumbstickX) > mouseDeadzone || std::abs(thumbstickY) > mouseDeadzone); + + if (usingCursorStick) { + ImVec2 mousePos = io.MousePos; + mousePos.x += thumbstickX * mouseSpeed; + mousePos.y -= thumbstickY * mouseSpeed; + mousePos.x = std::clamp(mousePos.x, 0.0f, io.DisplaySize.x); + mousePos.y = std::clamp(mousePos.y, 0.0f, io.DisplaySize.y); + io.MousePos = mousePos; + io.AddMousePosEvent(mousePos.x, mousePos.y); + io.MouseDrawCursor = true; + io.WantSetMousePos = true; + } + } + + if (scrollController) { + size_t thumbstickIndex = (scrollController == &primaryControllerState) ? + static_cast(RE::ControllerRole::Primary) : + static_cast(RE::ControllerRole::Secondary); + ProcessThumbstickScroll(*scrollController, thumbstickIndex, mouseDeadzone, io); + } + } + } +} diff --git a/src/Features/VR/OpenVRDetection.cpp b/src/Features/VR/OpenVRDetection.cpp new file mode 100644 index 0000000000..78223d28b9 --- /dev/null +++ b/src/Features/VR/OpenVRDetection.cpp @@ -0,0 +1,136 @@ +#include "OpenVRDetection.h" +#include +#include +#include +#include +#include +#pragma comment(lib, "version.lib") + +namespace VRDetection +{ + const char* RuntimeTypeToString(RuntimeType type) + { + switch (type) { + case RuntimeType::SteamVR: + return "SteamVR"; + case RuntimeType::OpenComposite: + return "OpenComposite"; + default: + return "Unknown"; + } + } + + bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result) + { + HMODULE hModule = GetModuleHandleA("openvr_api.dll"); + if (!hModule) + return false; + + using pfnIsValid = bool(__cdecl*)(const char*); + auto IsValid = reinterpret_cast(GetProcAddress(hModule, "VR_IsInterfaceVersionValid")); + if (!IsValid) + return false; + + result.hasOverlayInterface = IsValid(vr::IVROverlay_Version); + result.hasSystemInterface = IsValid(vr::IVRSystem_Version); + result.hasCompositorInterface = IsValid(vr::IVRCompositor_Version); + + result.probingSucceeded = result.hasOverlayInterface && result.hasSystemInterface && result.hasCompositorInterface; + return result.probingSucceeded; + } + + void GatherDLLInfo(OpenVRDetectionResult& result) + { + HMODULE hModule = GetModuleHandleA("openvr_api.dll"); + if (!hModule) { + result.isAvailable = false; + return; + } + + result.isAvailable = true; + + char dllPath[MAX_PATH]; + DWORD fileLength = GetModuleFileNameA(hModule, dllPath, MAX_PATH); + if (fileLength == 0 || (fileLength == MAX_PATH && GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { + result.isAvailable = false; + return; + } + + result.dllPath = dllPath; + + DWORD dwSize = GetFileVersionInfoSizeA(dllPath, nullptr); + if (dwSize > 0) { + std::vector buffer(dwSize); + if (GetFileVersionInfoA(dllPath, 0, dwSize, buffer.data())) { + VS_FIXEDFILEINFO* pFileInfo = nullptr; + UINT len = 0; + if (VerQueryValueA(buffer.data(), "\\", reinterpret_cast(&pFileInfo), &len)) { + DWORD major = HIWORD(pFileInfo->dwFileVersionMS); + DWORD minor = LOWORD(pFileInfo->dwFileVersionMS); + DWORD build = HIWORD(pFileInfo->dwFileVersionLS); + DWORD revision = LOWORD(pFileInfo->dwFileVersionLS); + result.version = std::format("{}.{}.{}.{}", major, minor, build, revision); + } + } + } + + if (result.version.empty()) + result.version = "Unknown"; + + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(dllPath, &findData); + if (hFind != INVALID_HANDLE_VALUE) { + FindClose(hFind); + ULARGE_INTEGER fileSize; + fileSize.LowPart = findData.nFileSizeLow; + fileSize.HighPart = findData.nFileSizeHigh; + result.fileSize = fileSize.QuadPart; + + SYSTEMTIME st; + FileTimeToSystemTime(&findData.ftLastWriteTime, &st); + result.modificationTime = std::format("{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + } + } + + RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize) + { + // OpenComposite DLLs are typically small (~600KB) with version 1.0.10.0 + if (version == "1.0.10.0" && fileSize < 700000) + return RuntimeType::OpenComposite; + + // Check path for OpenComposite indicators + std::string lowerPath = dllPath; + for (auto& c : lowerPath) + c = static_cast(std::tolower(static_cast(c))); + + if (lowerPath.find("opencomposite") != std::string::npos) + return RuntimeType::OpenComposite; + + // SteamVR DLLs are typically larger and have higher version numbers + if (lowerPath.find("steamvr") != std::string::npos || lowerPath.find("steam") != std::string::npos) + return RuntimeType::SteamVR; + + // Higher version numbers suggest SteamVR + if (!version.empty() && version != "Unknown" && version != "1.0.10.0") + return RuntimeType::SteamVR; + + return RuntimeType::Unknown; + } + + OpenVRDetectionResult Detect() + { + OpenVRDetectionResult result; + + GatherDLLInfo(result); + if (!result.isAvailable) + return result; + + result.runtimeType = DetectRuntimeType(result.dllPath, result.version, result.fileSize); + + // Detect compatibility via runtime interface probing + result.isCompatible = ProbeRuntimeInterfaces(result); + + return result; + } +} diff --git a/src/Features/VR/OpenVRDetection.h b/src/Features/VR/OpenVRDetection.h new file mode 100644 index 0000000000..1eaa411272 --- /dev/null +++ b/src/Features/VR/OpenVRDetection.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include + +namespace VRDetection +{ + enum class RuntimeType + { + Unknown, + SteamVR, + OpenComposite + }; + + struct OpenVRDetectionResult + { + bool isAvailable = false; + bool isCompatible = false; + + // Interface probing results + bool hasOverlayInterface = false; + bool hasSystemInterface = false; + bool hasCompositorInterface = false; + + // File-based info + std::string dllPath; + std::string version; + uint64_t fileSize = 0; + std::string modificationTime; + + // Detection metadata + RuntimeType runtimeType = RuntimeType::Unknown; + bool probingSucceeded = false; + }; + + // Runtime interface probing via VR_IsInterfaceVersionValid + bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result); + + // Gather DLL metadata (path, version, size, timestamp) + void GatherDLLInfo(OpenVRDetectionResult& result); + + // Detect runtime type (SteamVR vs OpenComposite) + RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize); + + // Full detection via interface probing + OpenVRDetectionResult Detect(); + + const char* RuntimeTypeToString(RuntimeType type); +} diff --git a/src/Features/VR/OverlayDrag.cpp b/src/Features/VR/OverlayDrag.cpp new file mode 100644 index 0000000000..e5912e8206 --- /dev/null +++ b/src/Features/VR/OverlayDrag.cpp @@ -0,0 +1,405 @@ +#include "Features/VR.h" +#include "RE/B/BSOpenVR.h" +#include "RE/P/PlayerCharacter.h" +#include "Utils/VRUtils.h" + +#include +#include +#include +#include +#include + +using namespace DirectX::SimpleMath; +using AttachMode = VR::Settings::OverlayAttachMode; + +bool VR::GetGripPressed(bool isLeft, bool isRight) const +{ + bool isLeftHanded = lastKnownLeftHandedMode; + + if (isLeft) { + if (isLeftHanded) { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + if (isRight) { + if (isLeftHanded) { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + return false; +} + +static bool CanStartAny(vr::ETrackedControllerRole role) +{ + return role != vr::TrackedControllerRole_Invalid; +} + +void VR::UpdateOverlayDrag() +{ + if (!CanPerformDrag()) { + return; + } + + if (overlayDragState.dragging) { + UpdateActiveDrag(); + } else { + TryStartNewDrag(); + } +} + +bool VR::CanPerformDrag() +{ + if (!settings.EnableDragToReposition) + return false; + + if (!globals::menu || !globals::menu->IsEnabled) + return false; + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return false; + + if (settings.VRMenuControllerDiagnosticsTestMode) { + return false; + } + + return true; +} + +void VR::UpdateActiveDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + auto resetDragState = [&]() { + overlayDragState.dragging = false; + overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; + overlayDragState.isPrimary = false; + overlayDragState.isSecondary = false; + }; + + float rawMatrix[3][4]; + if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + + switch (overlayDragState.mode) { + case OverlayDragState::DragMode::Controller: + { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::TrackedDevicePose_t controllerPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &controllerPose, 1)) + break; + if (controllerPose.bPoseIsValid) { + Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); + + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + + Matrix worldToLocal = attachedControllerMatrix.Invert(); + Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); + + settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; + settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; + settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; + } + } + break; + } + case OverlayDragState::DragMode::FixedWorld: + { + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + Matrix translated = overlayDragState.initialOverlayMatrix; + translated._41 += worldDelta.x; + translated._42 += worldDelta.y; + translated._43 += worldDelta.z; + fixedWorldOverlayPosition.m = translated; + break; + } + case OverlayDragState::DragMode::HMD: + { + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + break; + if (hmdPose.bPoseIsValid) { + Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + + Matrix worldToLocal = hmdMatrix.Invert(); + Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); + + static auto lastDeltaLog = std::chrono::steady_clock::now(); + auto nowDelta = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(nowDelta - lastDeltaLog).count() > 500) { + logger::debug("VR Drag Delta - Local: ({:.3f}, {:.3f}, {:.3f})", localDelta.x, localDelta.y, localDelta.z); + lastDeltaLog = nowDelta; + } + + settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; + settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; + settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; + settings.VRMenuScale = overlayDragState.initialHMDScale; + + static std::chrono::steady_clock::time_point lastLog = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastLog).count() > 500) { + logger::debug("VR Dragging (3D Mode): Offset ({:.2f}, {:.2f}, {:.2f}), Scale {:.2f}", + settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ, settings.VRMenuScale); + lastLog = now; + } + } + break; + } + default: + break; + } + } + + // Joystick depth control during grip + if (overlayDragState.dragging) { + RE::VRControllerState* gripController = nullptr; + size_t thumbIdx = 0; + if (overlayDragState.isPrimary) { + if (lastKnownLeftHandedMode) { + gripController = &primaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Primary); + } else { + gripController = &secondaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Secondary); + } + } else if (overlayDragState.isSecondary) { + if (lastKnownLeftHandedMode) { + gripController = &secondaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Secondary); + } else { + gripController = &primaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Primary); + } + } + + if (gripController) { + float thumbY = gripController->thumbsticks[thumbIdx].y; + const float deadzone = settings.mouseDeadzone; + const float depthSpeed = 0.02f; + if (std::abs(thumbY) > deadzone) { + float depthDelta = -thumbY * depthSpeed; + if (overlayDragState.mode == OverlayDragState::DragMode::HMD) { + overlayDragState.initialHMDOffset.z += depthDelta; + overlayDragState.initialHMDOffset.z = std::clamp(overlayDragState.initialHMDOffset.z, -10.0f, 10.0f); + } else if (overlayDragState.mode == OverlayDragState::DragMode::Controller) { + overlayDragState.initialControllerOffset.z += depthDelta; + overlayDragState.initialControllerOffset.z = std::clamp(overlayDragState.initialControllerOffset.z, -10.0f, 10.0f); + } + } + } + } + + bool gripPressed = GetGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); + if (!gripPressed) { + resetDragState(); + } +} + +void VR::TryStartNewDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + struct DragMode + { + OverlayDragState::DragMode mode; + bool isActive; + std::function canStart; + std::function onInit; + }; + + std::vector dragModes; + + // Controller mode - only for opposite hand (highest priority) + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + dragModes.push_back({ OverlayDragState::DragMode::Controller, + true, + [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); + if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); + if (deviceRole == role && i == oppositeControllerIndex) + return true; + } + } + return false; + }, + [&]() { + overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; + overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; + overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + // Fixed world mode + if (settings.VRMenuPositioningMethod == 1) { + std::function fixedWorldCanStart; + if (settings.attachMode == AttachMode::Both) { + fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + return role == actualAttachedRole; + } + return false; + }; + } else { + fixedWorldCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, + true, + fixedWorldCanStart, + [&]() { + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; + } }); + } + + // HMD mode + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + std::function hmdCanStart; + if (settings.attachMode == AttachMode::Both) { + hmdCanStart = [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + return role == actualAttachedRole; + } + return false; + }; + } else { + hmdCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::HMD, + true, + hmdCanStart, + [&]() { + overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; + overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; + overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; + overlayDragState.initialHMDScale = settings.VRMenuScale; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + for (const auto& mode : dragModes) { + if (!mode.isActive) + continue; + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) + continue; + vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); + bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); + bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (!mode.canStart(role)) + continue; + bool gripPressed = GetGripPressed(isLeft, isRight); + if (!gripPressed) + continue; + float rawMatrix[3][4]; + if (!Util::GetControllerWorldMatrix(i, rawMatrix)) + continue; + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + overlayDragState.dragging = true; + overlayDragState.mode = mode.mode; + overlayDragState.controllerIndex = i; + overlayDragState.isPrimary = isLeft; + overlayDragState.isSecondary = isRight; + overlayDragState.startControllerMatrix = controllerMatrix; + mode.onInit(); + + if (system && globals::menu->IsEnabled) { + for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { + if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); + bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (isRightController == isRight) { + openvr->TriggerHapticPulse(isRightController, 25.0f); + break; + } + } + } + } + + return; + } + } +} + +void VR::SetFixedOverlayToCurrentHMD() +{ + vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( + settings.VRMenuOffsetX, + settings.VRMenuOffsetY, + settings.VRMenuOffsetZ); + fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); +} + +void VR::UpdateFixedWorldPositioning() +{ + if (settings.VRMenuPositioningMethod != 1) + return; + + if (!fixedWorldOverlayPosition.initialized) { + fixedWorldOverlayPosition.initialized = true; + SetFixedOverlayToCurrentHMD(); + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) { + savedPlayerWorldPos = player->GetPosition(); + } + return; + } + + if (settings.VRMenuAutoResetDistance > 0.0f) { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) { + RE::NiPoint3 playerPos = player->GetPosition(); + float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); + float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; + if (sqDist > thresholdSq) { + SetFixedOverlayToCurrentHMD(); + savedPlayerWorldPos = playerPos; + } + } + } +} diff --git a/src/Features/VR/SettingsUI.cpp b/src/Features/VR/SettingsUI.cpp new file mode 100644 index 0000000000..33fd4aa76f --- /dev/null +++ b/src/Features/VR/SettingsUI.cpp @@ -0,0 +1,1026 @@ +#include "FeatureConstraints.h" +#include "Features/VR.h" +#include "Menu.h" +#include "Menu/Fonts.h" +#include "RE/B/BSOpenVR.h" +#include "RE/P/PlayerCharacter.h" +#include "Utils/PerfUtils.h" +#include "Utils/UI.h" +#include "Utils/VRUtils.h" + +#include + +using AttachMode = VR::Settings::OverlayAttachMode; + +namespace +{ + bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) + { + return MenuFonts::BeginTabItemWithFont(label, role, flags); + } +} + +//============================================================================= +// COMBO RECORDING HELPERS +//============================================================================= + +void VR::ResetComboRecording() +{ + isCapturingCombo = false; + currentComboType = ComboType::None; + currentComboName = nullptr; + recordedCombo.clear(); + comboStartTime = 0.0; + recordingButtonControllers.clear(); +} + +void VR::ApplyRecordedCombo() +{ + if (recordedCombo.empty()) + return; + + switch (currentComboType) { + case ComboType::MenuOpen: + settings.VRMenuOpenKeys = recordedCombo; + break; + case ComboType::MenuClose: + settings.VRMenuCloseKeys = recordedCombo; + break; + case ComboType::OverlayOpen: + settings.VROverlayOpenKeys = recordedCombo; + break; + case ComboType::OverlayClose: + settings.VROverlayCloseKeys = recordedCombo; + break; + default: + break; + } +} + +//============================================================================= +// OVERLAY (WELCOME MESSAGE) +//============================================================================= + +void VR::DrawOverlay() +{ + auto& vr = globals::features::vr; + if (!vr.IsOpenVRCompatible()) + return; + static LARGE_INTEGER overlayShowStart = { 0 }; + static LARGE_INTEGER freq = { 0 }; + + bool shouldShow = settings.kAutoHideSeconds > 0 && globals::game::ui && globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) && globals::menu && !globals::menu->IsEnabled; + + if (!shouldShow) { + overlayShowStart.QuadPart = 0; + return; + } + + if (freq.QuadPart == 0) { + QueryPerformanceFrequency(&freq); + } + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + + if (overlayShowStart.QuadPart == 0) { + overlayShowStart = now; + } + + double elapsed = double(now.QuadPart - overlayShowStart.QuadPart) / double(freq.QuadPart); + const double autoHideSeconds = static_cast(settings.kAutoHideSeconds); + if (elapsed >= autoHideSeconds) { + return; + } + int secondsLeft = int(std::ceil(autoHideSeconds - elapsed)); + + ImGuiIO& io = ImGui::GetIO(); + ImVec2 overlaySize(520, 0); + ImVec2 overlayPos = ImVec2((io.DisplaySize.x - overlaySize.x) * 0.5f, (io.DisplaySize.y * 0.35f)); + ImGui::SetNextWindowPos(overlayPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(overlaySize, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.95f); + + ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); + + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f); + ImGui::TextWrapped("How to Use VR Community Shaders Menu:"); + ImGui::Separator(); + ImGui::TextWrapped("You must open the Main Menu or Tween Menu before VR controls work."); + ImGui::Spacing(); + ImGui::PopTextWrapPos(); + + ImGui::Text("Open Menu: "); + ImGui::SameLine(); + Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); + + ImGui::Text("Close Menu: "); + ImGui::SameLine(); + Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); + + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f); + ImGui::TextWrapped("Grip + Thumbstick: Adjust overlay depth (closer/farther)"); + ImGui::Spacing(); + ImGui::TextWrapped("Tip: Disable this VR overlay by setting Attach Mode to 'None' in VR settings."); + ImGui::Spacing(); + ImGui::TextWrapped("(This welcome message will auto-hide in %d seconds)", secondsLeft); + ImGui::TextWrapped("(Disable in: VR settings > Controller Input Instructions)"); + ImGui::PopTextWrapPos(); + + ImGui::End(); +} + +//============================================================================= +// ANONYMOUS NAMESPACE: SETTINGS PANEL DRAW FUNCTIONS +//============================================================================= + +namespace +{ + void DrawControllerInputInstructions() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + if (!vr.IsOpenVRCompatible()) + return; + if (ImGui::CollapsingHeader("Controller Input Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderInt("Auto-hide Welcome overlay timeout", &settings.kAutoHideSeconds, 0, VR::Config::kMaxAutoHideSeconds, + settings.kAutoHideSeconds <= 0 ? "Hidden" : "%d seconds"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Set to 0 to hide the overlay, or a positive value to show it for that many seconds"); + } + ImGui::TextWrapped("Menu (while in the main menu or tween menu):"); + if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Open Community Shaders Menu:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Close Community Shaders Menu:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); + ImGui::EndTable(); + } + ImGui::TextWrapped("Overlay (while in the main menu or tween menu):"); + if (ImGui::BeginTable("OverlayInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Open Overlay:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VROverlayOpenKeys, true); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Close Overlay:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VROverlayCloseKeys, true); + ImGui::EndTable(); + } + ImGui::TextWrapped("Menu Controller Input:"); + if (ImGui::BeginTable("ControllerInputTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Trigger (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Left mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Grip (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Right mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Touchpad Click (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Middle mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Stick Click (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Middle mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "A/X (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Enter"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "B/Y (Primary Controller)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Tab"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "B/Y (Secondary Controller)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Shift+Tab"); + ImGui::EndTable(); + } + bool useAttachedControllerForCursor = (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both); + if (ImGui::BeginTable("ThumbstickInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (useAttachedControllerForCursor) { + if (settings.VRMenuAttachController == ControllerDevice::Primary) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (attached controller)"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + } else { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (attached controller)"); + } + } else { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (HMD mode)"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + } + ImGui::EndTable(); + } + } + } + + void DrawGeneralVRSettings() + { + auto& vr = globals::features::vr; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Exteriors", + &settings.EnableDepthBufferCullingExterior, + { "VR", "EnableDepthBufferCullingExterior" }); + auto exteriorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingExterior" }); + if (!exteriorConstraint.isConstrained) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in exteriors, recommended ON."); + } + } + + Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Interiors", + &settings.EnableDepthBufferCullingInterior, + { "VR", "EnableDepthBufferCullingInterior" }); + auto interiorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingInterior" }); + if (!interiorConstraint.isConstrained) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); + } + } + + if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) + *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Minimum bounding box dimensions for object occlusion culling. Lower values improve performance but may result in visual artifacts."); + } + } + } + + void DrawMenuSettings() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + if (!vr.IsOpenVRCompatible()) + return; + if (ImGui::CollapsingHeader("Menu Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + float maxScale = VR::Config::kMaxMenuScale; + ImGui::SliderFloat("Menu Scale", &settings.VRMenuScale, VR::Config::kMinMenuScale, maxScale, "%.2f"); + const char* positioningMethods[] = { "HMD Relative", "Fixed World Position" }; + int prevMethod = settings.VRMenuPositioningMethod; + if (ImGui::Combo("Menu Positioning Method", &settings.VRMenuPositioningMethod, positioningMethods, IM_ARRAYSIZE(positioningMethods))) { + if (prevMethod != 1 && settings.VRMenuPositioningMethod == 1) { + vr.SetFixedOverlayToCurrentHMD(); + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) + vr.savedPlayerWorldPos = player->GetPosition(); + } + } + const char* attachModes[] = { "HMD Only", "Controller Only", "Both", "None (Disabled)" }; + int attachModeInt = static_cast(settings.attachMode); + if (ImGui::Combo("Attach Mode", &attachModeInt, attachModes, IM_ARRAYSIZE(attachModes))) { + settings.attachMode = static_cast(attachModeInt); + } + + if (settings.attachMode == AttachMode::ControllerOnly || + settings.attachMode == AttachMode::Both) { + const char* attachControllers[] = { "Primary Controller", "Secondary Controller" }; + int attachControllerInt = static_cast(settings.VRMenuAttachController); + if (ImGui::Combo("Attach to Controller", &attachControllerInt, attachControllers, IM_ARRAYSIZE(attachControllers))) { + settings.VRMenuAttachController = static_cast(attachControllerInt); + } + + ImGui::Separator(); + ImGui::Text("Controller Offset Settings"); + ImGui::SliderFloat("Controller Offset X", &settings.VRMenuControllerOffsetX, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Controller Offset Y", &settings.VRMenuControllerOffsetY, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Controller Offset Z", &settings.VRMenuControllerOffsetZ, -2.0f, 2.0f, "%.2f"); + } + + if (settings.attachMode == AttachMode::HMDOnly || + settings.attachMode == AttachMode::Both) { + ImGui::Separator(); + ImGui::Text("HMD Offset Settings"); + ImGui::SliderFloat("HMD Offset X", &settings.VRMenuOffsetX, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("HMD Offset Y", &settings.VRMenuOffsetY, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("HMD Offset Z", &settings.VRMenuOffsetZ, -4.0f, 1.0f, "%.2f"); + } + + if (settings.VRMenuPositioningMethod == 1) { + ImGui::Separator(); + ImGui::Text("Fixed World Position Settings"); + ImGui::SliderFloat("Auto Reset Distance (game units)", &settings.VRMenuAutoResetDistance, 100.0f, 5000.0f, "%.0f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("If you move farther than this distance from the menu, it will automatically reset to your HMD position. %s", Util::Units::FormatDistance(settings.VRMenuAutoResetDistance).c_str()); + } + if (ImGui::Button("Reset Menu to HMD Position")) { + vr.SetFixedOverlayToCurrentHMD(); + } + } + } + } + + void DrawMouseSettings() + { + auto& vr = globals::features::vr; + if (!vr.IsOpenVRCompatible()) + return; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("Input Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Checkbox("Enable Wand Pointing", &settings.EnableWandPointing)) { + vr.wandState.isIntersecting = false; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Use controller ray-casting to point at UI elements"); + } + ImGui::Separator(); + ImGui::Text("Joystick Settings"); + ImGui::SliderFloat("Mouse Deadzone", &settings.mouseDeadzone, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Thumbstick deadzone for joystick cursor movement"); + } + ImGui::SliderFloat("Mouse Speed", &settings.mouseSpeed, 0.1f, 50.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Speed multiplier for joystick cursor movement"); + } + } + } + + void DrawDragSettings() + { + auto& vr = globals::features::vr; + if (!vr.IsOpenVRCompatible()) + return; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("Drag Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("Drag Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextWrapped("Overlay Positioning (Grip + Drag):"); + ImGui::BulletText("Fixed World Position: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); + ImGui::BulletText("HMD Relative: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); + ImGui::BulletText("Controller Attached: Only the opposite hand can drag the controller overlay"); + ImGui::Spacing(); + ImGui::TextWrapped("Depth Adjustment (Grip + Thumbstick):"); + ImGui::BulletText("While gripping to drag, use the thumbstick on the same hand to adjust depth"); + ImGui::BulletText("Thumbstick forward: Push overlay farther away"); + ImGui::BulletText("Thumbstick back: Pull overlay closer"); + } + ImGui::Checkbox("Enable drag to reposition overlays", &settings.EnableDragToReposition); + ImGui::BeginDisabled(!settings.EnableDragToReposition); + ImGui::ColorEdit4("Drag Highlight Color", settings.dragHighlightColor.data()); + ImGui::EndDisabled(); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Color used to highlight draggable overlays in VR."); + } + } + } + + void DrawKeyBindings() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + + if (ImGui::CollapsingHeader("Combo Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("Combo Timeout", &settings.comboTimeout, 1.0f, 10.0f, "%.1f seconds"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Time limit for recording button combinations."); + } + } + ImGui::Separator(); + const char* comboTypes[] = { + "Open Community Shaders Menu", + "Close Community Shaders Menu", + "Open VR Overlay", + "Close VR Overlay" + }; + static int selectedComboIndex = 0; + ImGui::Text("Select Combo to Record:"); + ImGui::SameLine(); + if (ImGui::Combo("##ComboSelector", &selectedComboIndex, comboTypes, IM_ARRAYSIZE(comboTypes))) { + vr.isCapturingCombo = false; + vr.currentComboType = VR::ComboType::None; + vr.recordedCombo.clear(); + } + if (ImGui::Button("Record Selected Combo")) { + vr.isCapturingCombo = true; + vr.currentComboType = static_cast(selectedComboIndex + 1); + vr.currentComboName = comboTypes[selectedComboIndex]; + vr.recordedCombo.clear(); + vr.comboStartTime = Util::GetNowSecs(); + vr.recordingButtonControllers.clear(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { + switch (selectedComboIndex) { + case 0: + settings.VRMenuOpenKeys.clear(); + break; + case 1: + settings.VRMenuCloseKeys.clear(); + break; + case 2: + settings.VROverlayOpenKeys.clear(); + break; + case 3: + settings.VROverlayCloseKeys.clear(); + break; + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Click to start recording a new button combination for the selected action."); + } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + if (ImGui::BeginTable("##VRBindingsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Action"); + ImGui::TableSetupColumn("Current Binding"); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + struct VRKeyBindingConfig + { + const char* label; + std::vector& combos; + const char* description; + const char* controllerRequirement; + }; + std::vector keyBindingConfigs = { + { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, + { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, + { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, + { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } + }; + for (size_t row = 0; row < keyBindingConfigs.size(); ++row) { + const auto& config = keyBindingConfigs[row]; + ImGui::TableNextRow(); + if (row == static_cast(selectedComboIndex)) { + ImU32 highlight = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.15f)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, highlight); + } + ImGui::TableSetColumnIndex(0); + char selectableId[64]; + snprintf(selectableId, sizeof(selectableId), "##combo_row_%zu", row); + bool rowSelected = (row == static_cast(selectedComboIndex)); + if (ImGui::Selectable(selectableId, rowSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap, ImVec2(0, 0))) { + selectedComboIndex = static_cast(row); + } + ImGui::SameLine(0, 0); + ImGui::Text("%s", config.label); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(config.combos, false); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%s", config.description); + } + ImGui::EndTable(); + } + ImGui::Spacing(); + if (ImGui::Button("Reset to Defaults")) { + VR::Settings defaults; + settings.VRMenuOpenKeys = defaults.VRMenuOpenKeys; + settings.VRMenuCloseKeys = defaults.VRMenuCloseKeys; + settings.VROverlayOpenKeys = defaults.VROverlayOpenKeys; + settings.VROverlayCloseKeys = defaults.VROverlayCloseKeys; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Reset all VR key bindings to their default values."); + } + } + + void DrawThumbstickColumn(VR& vr, bool showPrimary, ImU32 highlightCol) + { + auto& state = showPrimary ? vr.primaryControllerState : vr.secondaryControllerState; + auto role = showPrimary ? RE::ControllerRole::Primary : RE::ControllerRole::Secondary; + float x = state.thumbsticks[static_cast(role)].x; + float y = state.thumbsticks[static_cast(role)].y; + + ImVec2 padSize = ImVec2(80, 80); + ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2(cursor.x + padSize.x / 2, cursor.y + padSize.y / 2); + float radius = padSize.x / 2 - 4; + ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border); + ImU32 axisCol = ImGui::GetColorU32(ImGuiCol_TextDisabled); + ImU32 dotCol = ImGui::GetColorU32(ImGuiCol_Text); + + drawList->AddRectFilled(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), ImGui::GetColorU32(ImGuiCol_FrameBg)); + drawList->AddRect(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), borderCol, 4.0f, 0, 2.0f); + drawList->AddLine(ImVec2(center.x, cursor.y + 4), ImVec2(center.x, cursor.y + padSize.y - 4), axisCol, 1.0f); + drawList->AddLine(ImVec2(cursor.x + 4, center.y), ImVec2(cursor.x + padSize.x - 4, center.y), axisCol, 1.0f); + + int quad = 0; + if (x > 0 && y > 0) + quad = 1; + else if (x < 0 && y > 0) + quad = 2; + else if (x < 0 && y < 0) + quad = 3; + else if (x > 0 && y < 0) + quad = 4; + + if (quad != 0) { + ImVec2 q0 = center, q1 = center, q2 = center, q3 = center; + if (quad == 1) { + q1 = { center.x + radius, center.y - radius }; + q2 = { center.x + radius, center.y }; + q3 = { center.x, center.y - radius }; + } else if (quad == 2) { + q1 = { center.x - radius, center.y - radius }; + q2 = { center.x - radius, center.y }; + q3 = { center.x, center.y - radius }; + } else if (quad == 3) { + q1 = { center.x - radius, center.y + radius }; + q2 = { center.x - radius, center.y }; + q3 = { center.x, center.y + radius }; + } else if (quad == 4) { + q1 = { center.x + radius, center.y + radius }; + q2 = { center.x + radius, center.y }; + q3 = { center.x, center.y + radius }; + } + ImVec2 poly[4] = { center, q1, q2, q3 }; + drawList->AddConvexPolyFilled(poly, 4, highlightCol); + } + + ImVec2 dot = ImVec2(center.x + x * radius, center.y - y * radius); + drawList->AddCircleFilled(dot, 5.0f, dotCol); + + ImGui::Dummy(padSize); + ImGui::SetNextItemWidth(160.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", x, y, RE::GetQuadrantName(x, y)); + } + + void DrawDebugSection() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + auto menu = globals::menu; + + if (ImGui::CollapsingHeader("OpenVR Information", ImGuiTreeNodeFlags_DefaultOpen)) { + auto& info = vr.openVRInfo; + if (info.isAvailable) { + if (vr.IsOpenVRCompatible()) { + ImGui::Text("OpenVR System: Active & Compatible"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "OpenVR System: Active but INCOMPATIBLE"); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "VR overlay menus disabled."); + } + + ImGui::Text("Runtime: %s", VRDetection::RuntimeTypeToString(info.runtimeType)); + ImGui::Text("DLL Path: %s", info.dllPath.c_str()); + ImGui::Text("DLL Version: %s", info.version.c_str()); + ImGui::Text("DLL Size: %llu bytes", info.fileSize); + ImGui::Text("Modified: %s", info.modificationTime.c_str()); + + ImGui::Separator(); + ImGui::Text("Detection Method:"); + ImGui::Text(" Interface Probing: %s", info.probingSucceeded ? "Passed" : "Failed"); + ImGui::Text(" IVROverlay_016: %s", info.hasOverlayInterface ? "OK" : "Missing"); + ImGui::Text(" IVRSystem_017: %s", info.hasSystemInterface ? "OK" : "Missing"); + ImGui::Text(" IVRCompositor_021: %s", info.hasCompositorInterface ? "OK" : "Missing"); + ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), " Rendering: In-scene overlay (submit hook)"); + + } else { + ImGui::Text("OpenVR system not available"); + } + } + + if (ImGui::CollapsingHeader("Controller Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Checkbox("Test Mode: Disable controller menu input (except scroll controller and triggers)", &settings.VRMenuControllerDiagnosticsTestMode)) { + ImGui::SetScrollHereY(0.0f); + } + ImGui::SeparatorText("Button State"); + double nowSecs = Util::GetNowSecs(); + ImVec4 highlightColor = menu->GetTheme().StatusPalette.InfoColor; + ImU32 highlightColorU32 = ImGui::ColorConvertFloat4ToU32(highlightColor); + + bool isLeftHanded = vr.lastKnownLeftHandedMode; + + if (ImGui::BeginTable("vr_input_state_table", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Button"); + if (isLeftHanded) { + ImGui::TableSetupColumn("Primary State"); + ImGui::TableSetupColumn("Primary Held (s)"); + ImGui::TableSetupColumn("Primary Type"); + ImGui::TableSetupColumn("Secondary State"); + ImGui::TableSetupColumn("Secondary Held (s)"); + ImGui::TableSetupColumn("Secondary Type"); + } else { + ImGui::TableSetupColumn("Secondary State"); + ImGui::TableSetupColumn("Secondary Held (s)"); + ImGui::TableSetupColumn("Secondary Type"); + ImGui::TableSetupColumn("Primary State"); + ImGui::TableSetupColumn("Primary Held (s)"); + ImGui::TableSetupColumn("Primary Type"); + } + ImGui::TableHeadersRow(); + + auto DrawButtonType = [](const RE::ButtonState& state) { + if (!state.isPressed) { + if (state.IsClick()) + ImGui::TextUnformatted("Click"); + else if (state.IsHold()) + ImGui::TextUnformatted("Hold"); + else + ImGui::TextUnformatted("-"); + } else { + ImGui::TextUnformatted("Held"); + } + }; + + auto printRow = [&](const char* label, const RE::ButtonState& left, const RE::ButtonState& right) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::TextUnformatted(left.isPressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(2); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::Text("%.2f", left.GetCurrentHeldTime(nowSecs)); + ImGui::TableSetColumnIndex(3); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + DrawButtonType(left); + ImGui::TableSetColumnIndex(4); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::TextUnformatted(right.isPressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(5); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::Text("%.2f", right.GetCurrentHeldTime(nowSecs)); + ImGui::TableSetColumnIndex(6); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + DrawButtonType(right); + }; + + auto printRowWithHandedness = [&](const char* label, auto key) { + auto& primary = vr.primaryControllerState[key]; + auto& secondary = vr.secondaryControllerState[key]; + if (isLeftHanded) { + printRow(label, primary, secondary); + } else { + printRow(label, secondary, primary); + } + }; + + printRowWithHandedness("Trigger", RE::BSOpenVRControllerDevice::Keys::kTrigger); + printRowWithHandedness("Grip", RE::BSOpenVRControllerDevice::Keys::kGrip); + printRowWithHandedness("GripAlt", RE::BSOpenVRControllerDevice::Keys::kGripAlt); + printRowWithHandedness("Stick Click", RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger); + printRowWithHandedness("Touchpad Click", RE::BSOpenVRControllerDevice::Keys::kTouchpadClick); + printRowWithHandedness("Touchpad Alt", RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt); + printRowWithHandedness("B/Y", RE::BSOpenVRControllerDevice::Keys::kBY); + printRowWithHandedness("A/X", RE::BSOpenVRControllerDevice::Keys::kXA); + ImGui::EndTable(); + } + + ImGui::SeparatorText("VR Thumbstick State"); + ImU32 highlightCol = ImGui::ColorConvertFloat4ToU32(menu->GetTheme().StatusPalette.InfoColor); + if (ImGui::BeginTable("##VRThumbstickTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + if (isLeftHanded) { + ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + } else { + ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + } + ImGui::TableHeadersRow(); + + // Left column + ImGui::TableSetColumnIndex(0); + ImGui::BeginGroup(); + DrawThumbstickColumn(vr, isLeftHanded, highlightCol); + ImGui::EndGroup(); + + // Right column + ImGui::TableSetColumnIndex(1); + ImGui::BeginGroup(); + DrawThumbstickColumn(vr, !isLeftHanded, highlightCol); + ImGui::EndGroup(); + ImGui::EndTable(); + } + + ImGui::SeparatorText("Recent VR Controller Events"); + ImGui::TextDisabled("Note: For thumbstick events, KeyCode/Value columns show X/Y floats."); + if (ImGui::BeginTable("eventlog", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("KeyCode/X", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Value/Y", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Pressed", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("Known Mapping", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Event Type", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableHeadersRow(); + for (const auto& e : vr.vrControllerEventLog) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%d", e.device); + ImGui::TableSetColumnIndex(1); + if (e.heldSource == "thumbstick") { + ImGui::Text("%.3f", e.thumbstickX); + } else { + ImGui::Text("%d", e.keyCode); + } + ImGui::TableSetColumnIndex(2); + if (e.heldSource == "thumbstick") { + ImGui::Text("%.3f", e.thumbstickY); + } else { + ImGui::Text("%d", e.value); + } + ImGui::TableSetColumnIndex(3); + ImGui::Text("%s", e.pressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(4); + if (e.heldSource == "thumbstick") { + ImGui::TextUnformatted(e.controllerRole.c_str()); + } else { + ImGui::TextUnformatted(RE::GetOpenVRButtonName(e.keyCode)); + } + ImGui::TableSetColumnIndex(5); + if (e.heldSource == "thumbstick") { + ImGui::TextUnformatted("-"); + } else { + if (!e.pressed) { + if (e.heldTime > 0.0) { + if (e.heldTime < 0.5) { + ImGui::Text("Click (%.2fs)", e.heldTime); + } else { + ImGui::Text("Hold (%.2fs)", e.heldTime); + } + } else { + ImGui::Text("Release"); + } + } else if (e.pressed) { + if (e.heldTime > 0.0) { + ImGui::Text("Held for %.2fs", e.heldTime); + } else { + ImGui::Text("Press"); + } + } + } + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("Wand Pointing State"); + if (ImGui::BeginTable("##WandPointingState", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Wand Pointing Enabled"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%s", settings.EnableWandPointing ? "Yes" : "No"); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Intersecting Overlay"); + ImGui::TableSetColumnIndex(1); + if (vr.wandState.isIntersecting) { + ImGui::TextColored(menu->GetTheme().StatusPalette.InfoColor, "YES"); + } else { + ImGui::Text("No"); + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("UV Coordinates"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.3f, %.3f)", vr.wandState.uvCoordinates.x, vr.wandState.uvCoordinates.y); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Controller Index"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%u", vr.wandState.controllerIndex); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Ray Origin"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayOrigin.x, vr.wandState.rayOrigin.y, vr.wandState.rayOrigin.z); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Ray Direction"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayDirection.x, vr.wandState.rayDirection.y, vr.wandState.rayDirection.z); + + ImGui::EndTable(); + } + } + + if (ImGui::CollapsingHeader("OpenVR Addresses")) { + auto openvr = RE::BSOpenVR::GetSingleton(); + auto overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; + auto vrSystem = openvr ? openvr->vrSystem : nullptr; + ADDRESS_NODE(openvr) + ADDRESS_NODE(overlay) + ADDRESS_NODE(vrSystem) + } + } +} // namespace + +//============================================================================= +// DRAW SETTINGS (main entry point) +//============================================================================= + +void VR::DrawSettings() +{ + auto menu = globals::menu; + if (!menu) + return; + if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { + if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { + DrawGeneralVRSettings(); + DrawControllerInputInstructions(); + DrawMenuSettings(); + DrawMouseSettings(); + DrawDragSettings(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + if (IsOpenVRCompatible()) { + if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { + DrawKeyBindings(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + } + + if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { + DrawDebugSection(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + // Combo recording popup + if (this->isCapturingCombo) { + ImGui::OpenPopup("Record Combo"); + ImGui::SetNextWindowSize(ImVec2(400, 200), ImGuiCond_FirstUseEver); + if (ImGui::BeginPopupModal("Record Combo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + auto GetButtonName = [](uint32_t key) -> const char* { + switch (key) { + case static_cast(RE::BSOpenVRControllerDevice::Keys::kTrigger): + return "Trigger"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip): + return "Grip"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kTouchpadClick): + return "Touchpad"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger): + return "Stick Click"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kXA): + return "A/X"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kBY): + return "B/Y"; + default: + return "Unknown"; + } + }; + + ImGui::Text("Recording combo for: %s", this->currentComboName ? this->currentComboName : "Unknown"); + ImGui::Spacing(); + + ImGui::TextDisabled("(During recording, any controller's buttons can be used. Requirement is only enforced during use.)"); + + ImGui::Spacing(); + + double remainingTime = settings.comboTimeout - (Util::GetNowSecs() - this->comboStartTime); + ImVec4 timerColor = remainingTime > 2.0 ? Util::Colors::GetTimerGood() : + remainingTime > 1.0 ? Util::Colors::GetTimerWarning() : + Util::Colors::GetTimerCritical(); + ImGui::TextColored(timerColor, "Time remaining: %.1f seconds", remainingTime); + + ImGui::Spacing(); + + if (this->recordedCombo.empty()) { + ImGui::Text("Press buttons to record combo..."); + } else { + ImGui::Text("Recorded buttons:"); + std::vector sortedRecordedCombos; + for (size_t i = 0; i < this->recordedCombo.size(); ++i) { + sortedRecordedCombos.push_back(this->recordedCombo[i]); + } + std::sort(sortedRecordedCombos.begin(), sortedRecordedCombos.end(), + [](const ButtonCombo& a, const ButtonCombo& b) { + return a.GetKey() < b.GetKey(); + }); + + Util::DrawButtonCombo(sortedRecordedCombos, false); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("Press ENTER to accept, ESC to cancel"); + + // Handle button recording + bool buttonPressed = false; + uint32_t pressedKey = 0; + ControllerDevice pressedDevice = ControllerDevice::Both; + + for (const auto& [keyCode, buttonState] : primaryControllerState.GetActiveButtons()) { + if (buttonState->isPressed) { + pressedKey = keyCode; + buttonPressed = true; + pressedDevice = ControllerDevice::Primary; + break; + } + } + + if (!buttonPressed) { + for (const auto& [keyCode, buttonState] : secondaryControllerState.GetActiveButtons()) { + if (buttonState->isPressed) { + pressedKey = keyCode; + buttonPressed = true; + pressedDevice = ControllerDevice::Secondary; + break; + } + } + } + + if (buttonPressed) { + auto it = recordingButtonControllers.find(pressedKey); + if (it == recordingButtonControllers.end()) { + recordingButtonControllers[pressedKey] = pressedDevice; + } else { + if (it->second != pressedDevice && it->second != ControllerDevice::Both) { + it->second = ControllerDevice::Both; + } + } + this->recordedCombo.clear(); + for (const auto& [key, device] : recordingButtonControllers) { + this->recordedCombo.push_back(ButtonCombo(device, key)); + } + } + + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))) { + ApplyRecordedCombo(); + ResetComboRecording(); + ImGui::CloseCurrentPopup(); + } + + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) { + ResetComboRecording(); + ImGui::CloseCurrentPopup(); + } + + if (remainingTime <= 0.0) { + ApplyRecordedCombo(); + ResetComboRecording(); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + } +} diff --git a/src/Features/VR/WandPointing.cpp b/src/Features/VR/WandPointing.cpp new file mode 100644 index 0000000000..5a76019721 --- /dev/null +++ b/src/Features/VR/WandPointing.cpp @@ -0,0 +1,144 @@ +#include "Features/VR.h" +#include "RE/B/BSOpenVR.h" +#include "Utils/VRUtils.h" + +#include +#include +#include + +using namespace DirectX::SimpleMath; +using AttachMode = VR::Settings::OverlayAttachMode; + +bool VR::ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) +{ + float controllerM[3][4]; + if (!Util::GetControllerWorldMatrix(controllerIndex, controllerM)) { + return false; + } + Matrix controllerWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(controllerM)); + Vector3 rayOrigin = controllerWorld.Translation(); + Vector3 rayDir = controllerWorld.Forward(); + + // Update debug state + wandState.rayOrigin = rayOrigin; + wandState.rayDirection = rayDir; + Matrix overlayWorld; + if (type == OverlayType::HMD) { + if (settings.VRMenuPositioningMethod == 1) { // Fixed World + overlayWorld = fixedWorldOverlayPosition.m; + } else { // HMD Relative + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + return false; + if (!hmdPose.bPoseIsValid) + return false; + Matrix hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); + overlayWorld = offset * hmdWorld; + } + } else { // Controller Relative + vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + float attachM[3][4]; + if (!Util::GetControllerWorldMatrix(attachIndex, attachM)) + return false; + Matrix attachWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachM)); + + Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); + overlayWorld = offset * attachWorld; + } + + if (settings.VRMenuScale < 1e-4f) + return false; + overlayWorld = Matrix::CreateScale(settings.VRMenuScale) * overlayWorld; + + Matrix worldToOverlay = overlayWorld.Invert(); + Vector3 localOrigin = Vector3::Transform(rayOrigin, worldToOverlay); + Vector3 localDir = Vector3::TransformNormal(rayDir, worldToOverlay); + + if (std::abs(localDir.z) < 1e-6f) + return false; + + float t = -localOrigin.z / localDir.z; + if (t < 0.0f) + return false; + + Vector3 hit = localOrigin + t * localDir; + + if (hit.x < -0.5f || hit.x > 0.5f || hit.y < -0.5f || hit.y > 0.5f) + return false; + + outUV.x = hit.x + 0.5f; + outUV.y = 0.5f - hit.y; + + return true; +} + +bool VR::ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) +{ + bool intersected = false; + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + if (ComputeWandIntersectionForOverlayType(OverlayType::HMD, controllerIndex, outUV)) { + intersected = true; + } + } + if (!intersected && (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both)) { + if (ComputeWandIntersectionForOverlayType(OverlayType::Controller, controllerIndex, outUV)) { + intersected = true; + } + } + + if (intersected) { + wandState.isIntersecting = true; + wandState.uvCoordinates = outUV; + wandState.controllerIndex = controllerIndex; + } else { + wandState.isIntersecting = false; + } + + return intersected; +} + +void VR::UpdateCursorFromWandPointing() +{ + if (!settings.EnableWandPointing || !globals::menu || !globals::menu->IsEnabled) + return; + + ImGuiIO& io = ImGui::GetIO(); + + vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; + + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); + } else { + pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); + } + + if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { + wandState.isIntersecting = false; + return; + } + + ImVec2 uv; + bool intersected = ComputeWandIntersection(pointingController, uv); + + if (intersected) { + float screenX = uv.x * io.DisplaySize.x; + float screenY = uv.y * io.DisplaySize.y; + + screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); + screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); + + io.MousePos = ImVec2(screenX, screenY); + io.AddMousePosEvent(screenX, screenY); + io.MouseDrawCursor = true; + io.WantSetMousePos = true; + } else { + wandState.isIntersecting = false; + } +} diff --git a/src/Menu.cpp b/src/Menu.cpp index ac195c9433..07789f44e3 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -232,8 +232,6 @@ Menu::~Menu() ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); dxgiAdapter3 = nullptr; - - globals::features::vr.DestroyOverlay(); } void Menu::Load(json& o_json) @@ -651,10 +649,6 @@ void Menu::Init() BuildCategoryCounts(); - if (globals::features::vr.IsOpenVRCompatible()) { - globals::features::vr.EnsureOverlayInitialized(); - } - initialized = true; } diff --git a/src/Utils/VRUtils.cpp b/src/Utils/VRUtils.cpp index 28daf4c586..23ac293156 100644 --- a/src/Utils/VRUtils.cpp +++ b/src/Utils/VRUtils.cpp @@ -105,35 +105,6 @@ namespace Util return transform; } - vr::HmdMatrix34_t CreateControllerOverlayTransform(float offsetX, float offsetY, float offsetZ, float width, float height) - { - vr::HmdMatrix34_t transform; - transform.m[0][0] = width; - transform.m[0][1] = 0.0f; - transform.m[0][2] = 0.0f; - transform.m[0][3] = offsetX; - transform.m[1][0] = 0.0f; - transform.m[1][1] = height; - transform.m[1][2] = 0.0f; - transform.m[1][3] = offsetY; - transform.m[2][0] = 0.0f; - transform.m[2][1] = 0.0f; - transform.m[2][2] = 1.0f; - transform.m[2][3] = offsetZ; - return transform; - } - - void SetOverlayInputFlags(vr::IVROverlay* overlay, vr::VROverlayHandle_t handle) - { - if (!overlay || handle == vr::k_ulOverlayHandleInvalid) - return; - - overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRScrollEvents, true); - overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRTouchpadEvents, true); - overlay->SetOverlayFlag(handle, vr::VROverlayFlags_AcceptsGamepadEvents, true); - overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true); - } - //============================================================================= // NEW ACTIVE FUNCTIONS FROM VR.CPP //============================================================================= @@ -153,30 +124,6 @@ namespace Util } } - ImVec4 GetControllerDeviceColor(InputDeviceType device, bool isRecording) - { - // UI color constants from VR.cpp - constexpr ImVec4 Primary = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green - constexpr ImVec4 Secondary = ImVec4(0.0f, 0.6f, 1.0f, 1.0f); // Blue - constexpr ImVec4 Both = ImVec4(0.5f, 0.0f, 0.5f, 1.0f); // Purple - constexpr ImVec4 Recording = ImVec4(1.0f, 0.65f, 0.0f, 1.0f); // Orange - constexpr ImVec4 Default = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White - - if (isRecording && device == InputDeviceType::Both) { - return Recording; // Orange for recording mode - } - switch (device) { - case InputDeviceType::Primary: - return Primary; - case InputDeviceType::Secondary: - return Secondary; - case InputDeviceType::Both: - return Both; - default: - return Default; - } - } - vr::TrackedDeviceIndex_t GetControllerIndexForDevice(InputDeviceType device, bool isLeftHanded) { OpenVRContext ctx; @@ -291,48 +238,4 @@ namespace Util return false; } - //============================================================================= - // WAND POINTING IMPLEMENTATION - //============================================================================= - - bool ComputeWandIntersection(vr::IVROverlay* overlay, vr::VROverlayHandle_t overlayHandle, - vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) - { - if (!overlay || overlayHandle == vr::k_ulOverlayHandleInvalid || controllerIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - // Bounds check to prevent array out-of-bounds access - if (controllerIndex >= vr::k_unMaxTrackedDeviceCount) - return false; - - // Get controller pose - vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; - if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) - return false; - - if (!poses[controllerIndex].bPoseIsValid) - return false; - - // Compute intersection using OpenVR's built-in ray-casting - vr::VROverlayIntersectionParams_t params; - params.eOrigin = vr::TrackingUniverseStanding; - params.vSource.v[0] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][3]; - params.vSource.v[1] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][3]; - params.vSource.v[2] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][3]; - - // Ray direction is the -Z axis of the controller (forward vector) - params.vDirection.v[0] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][2]; - params.vDirection.v[1] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][2]; - params.vDirection.v[2] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][2]; - - vr::VROverlayIntersectionResults_t results; - if (overlay->ComputeOverlayIntersection(overlayHandle, ¶ms, &results)) { - // Convert UV coordinates (0-1 range) to output - outUV.x = results.vUVs.v[0]; - outUV.y = results.vUVs.v[1]; - return true; - } - - return false; - } } \ No newline at end of file diff --git a/src/Utils/VRUtils.h b/src/Utils/VRUtils.h index ef4786fb62..0e52b283d6 100644 --- a/src/Utils/VRUtils.h +++ b/src/Utils/VRUtils.h @@ -54,19 +54,6 @@ namespace Util */ void DrawButtonCombo(const std::vector& combo, bool showControllerLabels); - /** - * @brief Sets standard input flags for a VR overlay - * @param overlay Pointer to the OpenVR overlay interface - * @param handle Handle to the VR overlay - * - * This function configures an overlay to accept VR input events including: - * - Scroll events - * - Touchpad events - * - Gamepad events - * - Dashboard visibility - */ - void SetOverlayInputFlags(vr::IVROverlay* overlay, vr::VROverlayHandle_t handle); - /** * @brief Computes a transformation matrix for positioning an overlay relative to the HMD * @param offsetX Horizontal offset from HMD in meters (positive = right) @@ -79,20 +66,6 @@ namespace Util */ vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ); - /** - * @brief Creates a transformation matrix for controller-relative overlay positioning - * @param offsetX Horizontal offset from controller in meters - * @param offsetY Vertical offset from controller in meters - * @param offsetZ Depth offset from controller in meters - * @param width Width of the overlay in meters - * @param height Height of the overlay in meters - * @return Transformation matrix for controller-relative positioning - * - * This function creates a transformation matrix that positions an overlay - * relative to a VR controller with the specified dimensions and offsets. - */ - vr::HmdMatrix34_t CreateControllerOverlayTransform(float offsetX, float offsetY, float offsetZ, float width, float height); - /** * @brief Common OpenVR system access pattern with validation * @@ -127,17 +100,6 @@ namespace Util bool HasOverlay() const { return IsValid() && overlay; } }; - /** - * @brief Get UI color for controller device type - * @param device The controller device type - * @param isRecording Whether the device is in recording mode (affects color) - * @return ImVec4 color value for UI rendering - * - * This function provides consistent color coding for different controller - * device types in the UI, with special handling for recording mode. - */ - ImVec4 GetControllerDeviceColor(ControllerDevice device, bool isRecording = false); - /** * @brief Get controller index for our ControllerDevice enum * @param device The controller device type (Primary/Secondary) @@ -188,11 +150,14 @@ namespace Util */ inline Matrix HmdMatrix34ToMatrix(const vr::HmdMatrix34_t& m) { + // OpenVR matrices are row-major but designed for column-vector math (M * v). + // DirectX SimpleMath uses row-vector math (v * M). + // We need to transpose the rotation and move translation to the bottom row. return Matrix( - m.m[0][0], m.m[0][1], m.m[0][2], m.m[0][3], - m.m[1][0], m.m[1][1], m.m[1][2], m.m[1][3], - m.m[2][0], m.m[2][1], m.m[2][2], m.m[2][3], - 0, 0, 0, 1); + m.m[0][0], m.m[1][0], m.m[2][0], 0.0f, + m.m[0][1], m.m[1][1], m.m[2][1], 0.0f, + m.m[0][2], m.m[1][2], m.m[2][2], 0.0f, + m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); } /** @@ -206,18 +171,19 @@ namespace Util inline vr::HmdMatrix34_t MatrixToHmdMatrix34(const Matrix& mat) { vr::HmdMatrix34_t m{}; + // Transpose rotation back (row-vector → column-vector) and extract translation from row 4 m.m[0][0] = mat._11; - m.m[0][1] = mat._12; - m.m[0][2] = mat._13; - m.m[0][3] = mat._14; - m.m[1][0] = mat._21; + m.m[0][1] = mat._21; + m.m[0][2] = mat._31; + m.m[0][3] = mat._41; + m.m[1][0] = mat._12; m.m[1][1] = mat._22; - m.m[1][2] = mat._23; - m.m[1][3] = mat._24; - m.m[2][0] = mat._31; - m.m[2][1] = mat._32; + m.m[1][2] = mat._32; + m.m[1][3] = mat._42; + m.m[2][0] = mat._13; + m.m[2][1] = mat._23; m.m[2][2] = mat._33; - m.m[2][3] = mat._34; + m.m[2][3] = mat._43; return m; } @@ -238,24 +204,45 @@ namespace Util return mat; } - //============================================================================= - // WAND POINTING UTILITIES - //============================================================================= - /** - * @brief Computes controller ray intersection with a VR overlay - * @param overlay OpenVR overlay interface - * @param overlayHandle Handle to the overlay to test intersection with - * @param controllerIndex Tracked device index of the controller - * @param outUV Output UV coordinates (0-1 range) of intersection point - * @return true if the controller ray intersects the overlay, false otherwise + * @brief Gets the Inter-Pupillary Distance (IPD) from the HMD + * @return IPD in meters, or 0.064 (average human IPD) as fallback * - * This function uses OpenVR's ComputeOverlayIntersection to perform ray-casting - * from the controller's position and forward direction to detect if it's pointing - * at the specified overlay. If an intersection is found, the UV coordinates are - * returned in the outUV parameter. + * Tries multiple methods to determine IPD: + * 1. Query Prop_UserIpdMeters_Float property directly + * 2. Calculate from eye-to-head transforms + * 3. Fallback to average human IPD (64mm) */ - bool ComputeWandIntersection(vr::IVROverlay* overlay, vr::VROverlayHandle_t overlayHandle, - vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); + inline float GetIPDFromHMD() + { + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) + return 0.064f; // Default fallback IPD in meters + + // Method 1: Query IPD property directly + vr::ETrackedPropertyError error = vr::TrackedProp_UnknownProperty; + float ipd = openvr->vrSystem->GetFloatTrackedDeviceProperty( + vr::k_unTrackedDeviceIndex_Hmd, + vr::Prop_UserIpdMeters_Float, + &error); + + if (error == vr::TrackedProp_Success && ipd > 0.0f && ipd < 0.1f) { + return ipd; + } + + // Method 2: Calculate from eye-to-head transforms + vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); + vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); + + // Eye separation is in the X translation component (m[0][3]) + float eyeSeparation = std::abs(leftEye.m[0][3] - rightEye.m[0][3]); + + if (eyeSeparation > 0.0f && eyeSeparation < 0.1f) { + return eyeSeparation; + } + + // Fallback to average human IPD + return 0.064f; + } } \ No newline at end of file