diff --git a/src/APILayer/APILayer_loader.cpp b/src/APILayer/APILayer_loader.cpp index 58bf734..412fda5 100644 --- a/src/APILayer/APILayer_loader.cpp +++ b/src/APILayer/APILayer_loader.cpp @@ -320,82 +320,6 @@ static XrResult xrGetInstanceProcAddr( return XR_ERROR_FUNCTION_UNSUPPORTED; } -static void EnumerateExtensions(OpenXRNext* oxr) { - uint32_t extensionCount = 0; - - auto nextResult = oxr->xrEnumerateInstanceExtensionProperties( - nullptr, 0, &extensionCount, nullptr); - - // Workaround for Meta Link PTC as of 2024-07-22: - // - // XR_ERROR_SIZE_SUFFICIENT should *never* be returned for a buffer size of 0: - // https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#buffer-size-parameters - const auto workAroundNonConformantImplementations - = Config::Quirk_Conformance_ExtensionCount - && (nextResult == XR_ERROR_SIZE_INSUFFICIENT) && (extensionCount > 0); - if (workAroundNonConformantImplementations) { - DebugPrint( - "Buggy OpenXR runtime or other API layer; ignoring incorrect " - "XR_ERROR_SIZE_INSUFFICIENT response from " - "xrEnumerateInstanceExtensionProperties(nulllptr, 0, &count, nullptr)"); - } - if (nextResult != XR_SUCCESS && !workAroundNonConformantImplementations) { - DebugPrint("Getting extension count failed: {}", nextResult); - return; - } - - if (extensionCount == 0) { - DebugPrint( - "Runtime supports no extensions, so definitely doesn't support hand " - "tracking. Reporting success but doing nothing."); - return; - } - - std::vector extensions( - extensionCount, XrExtensionProperties {XR_TYPE_EXTENSION_PROPERTIES}); - nextResult = oxr->xrEnumerateInstanceExtensionProperties( - nullptr, extensionCount, &extensionCount, extensions.data()); - if (nextResult != XR_SUCCESS) { - DebugPrint("Enumerating extensions failed: {}", nextResult); - return; - } - - for (const auto& it: extensions) { - const std::string_view name {it.extensionName}; - if (Config::VerboseDebug) { - DebugPrint("Found {}", name); - } - - if (name == XR_EXT_HAND_TRACKING_EXTENSION_NAME) { - Environment::Have_XR_EXT_HandTracking = true; - continue; - } - if ( - name == XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME - && Config::EnableFBOpenXRExtensions) { - Environment::Have_XR_FB_HandTracking_Aim = true; - continue; - } - if (name == XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME) { - Environment::Have_XR_KHR_win32_convert_performance_counter_time = true; - continue; - } - } - - DebugPrint( - "{}: {}", - XR_EXT_HAND_TRACKING_EXTENSION_NAME, - Environment::Have_XR_EXT_HandTracking); - DebugPrint( - "{}: {}", - XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME, - Environment::Have_XR_FB_HandTracking_Aim); - DebugPrint( - "{}: {}", - XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME, - Environment::Have_XR_KHR_win32_convert_performance_counter_time); -} - static XrResult xrCreateApiLayerInstance( const XrInstanceCreateInfo* originalInfo, const struct XrApiLayerCreateInfo* layerInfo, @@ -414,39 +338,6 @@ static XrResult xrCreateApiLayerInstance( // TODO: check version fields etc in layerInfo - XrInstance dummyInstance {}; - { - // While the OpenXR specification says that - // `xrEnumerateInstanceExtensionProperties()` does not require an - // `XrInstance`, if there is a 'next' API layer, it will not be able to - // retrieve the extension list from the runtime or an n+2 API layer unless - // an instance has been created, as it won't have the next - // `xrGetInstanceProcAddr()` pointer yet. - XrInstanceCreateInfo dummyInfo { - .type = XR_TYPE_INSTANCE_CREATE_INFO, - .applicationInfo = { - .applicationVersion = originalInfo->applicationInfo.applicationVersion, - .engineName = "FREDEMMOTT_HTCC", - .engineVersion = 1, - .apiVersion = XR_CURRENT_API_VERSION, - }, - }; - { - auto [it, count] = std::format_to_n( - dummyInfo.applicationInfo.applicationName, - XR_MAX_APPLICATION_NAME_SIZE - 1, - "FREDEMMOTT_HTCC Init: {}", - originalInfo->applicationInfo.applicationName); - *it = '\0'; - } - auto dummyLayerInfo = *layerInfo; - dummyLayerInfo.nextInfo = dummyLayerInfo.nextInfo->next; - layerInfo->nextInfo->nextCreateApiLayerInstance( - &dummyInfo, &dummyLayerInfo, &dummyInstance); - } - - OpenXRNext next(dummyInstance, layerInfo->nextInfo->nextGetInstanceProcAddr); - XrInstanceCreateInfo info {XR_TYPE_INSTANCE_CREATE_INFO}; if (originalInfo) { info = *originalInfo; @@ -454,57 +345,90 @@ static XrResult xrCreateApiLayerInstance( std::vector enabledExtensions; if (Config::Enabled) { - EnumerateExtensions(&next); - if (Environment::Have_XR_EXT_HandTracking) { - DebugPrint("Original extensions:"); - for (auto i = 0; i < originalInfo->enabledExtensionCount; ++i) { - DebugPrint("- {}", originalInfo->enabledExtensionNames[i]); - enabledExtensions.push_back(originalInfo->enabledExtensionNames[i]); - } - - enabledExtensions.push_back(XR_EXT_HAND_TRACKING_EXTENSION_NAME); - // We need 'real time' units to rotate the hand at a known speed for - // MSFS - if (Environment::Have_XR_KHR_win32_convert_performance_counter_time) { - enabledExtensions.push_back( - XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME); - } - // Required for enhanced pinch gestures on Quest - if (Environment::Have_XR_FB_HandTracking_Aim) { - enabledExtensions.push_back(XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME); - } - - { - auto last - = std::unique(enabledExtensions.begin(), enabledExtensions.end()); - enabledExtensions.erase(last, enabledExtensions.end()); - } - - DebugPrint("Requesting extensions:"); - for (const auto& ext: enabledExtensions) { - DebugPrint("- {}", ext); - } - - info.enabledExtensionCount = enabledExtensions.size(); - info.enabledExtensionNames = enabledExtensions.data(); + DebugPrint("Original extensions:"); + for (auto i = 0; i < originalInfo->enabledExtensionCount; ++i) { + DebugPrint("- {}", originalInfo->enabledExtensionNames[i]); + enabledExtensions.push_back(originalInfo->enabledExtensionNames[i]); + } + + enabledExtensions.push_back( + XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME); + enabledExtensions.push_back(XR_EXT_HAND_TRACKING_EXTENSION_NAME); + enabledExtensions.push_back(XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME); + + { + auto last + = std::unique(enabledExtensions.begin(), enabledExtensions.end()); + enabledExtensions.erase(last, enabledExtensions.end()); } + + DebugPrint("Requesting extensions:"); + for (const auto& ext: enabledExtensions) { + DebugPrint("- {}", ext); + } + + info.enabledExtensionCount = enabledExtensions.size(); + info.enabledExtensionNames = enabledExtensions.data(); } - next.check_xrDestroyInstance(dummyInstance); XrApiLayerCreateInfo nextLayerInfo = *layerInfo; nextLayerInfo.nextInfo = layerInfo->nextInfo->next; - auto nextResult = layerInfo->nextInfo->nextCreateApiLayerInstance( - &info, &nextLayerInfo, instance); - if (nextResult != XR_SUCCESS) { - DebugPrint("Next failed."); - return nextResult; + //// Attempt 1: all 3 extensions + { + const auto nextResult = layerInfo->nextInfo->nextCreateApiLayerInstance( + &info, &nextLayerInfo, instance); + if (XR_SUCCEEDED(nextResult)) { + Environment::Have_XR_EXT_HandTracking = true; + Environment::Have_XR_FB_HandTracking_Aim = true; + gNext = std::make_shared( + *instance, layerInfo->nextInfo->nextGetInstanceProcAddr); + DebugPrint("Initialized with all extensions"); + return nextResult; + } + + if (nextResult != XR_ERROR_EXTENSION_NOT_PRESENT) { + DebugPrint("all-in xrCreateApiLayerInstance failed: {}", nextResult); + return nextResult; + } } - gNext = std::make_shared( - *instance, layerInfo->nextInfo->nextGetInstanceProcAddr); + ///// Attempt 2: without XR_FB_hand_tracking_aim + enabledExtensions.erase( + std::ranges::find( + enabledExtensions, XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME)); + info.enabledExtensionCount = enabledExtensions.size(); + info.enabledExtensionNames = enabledExtensions.data(); + { + const auto nextResult = layerInfo->nextInfo->nextCreateApiLayerInstance( + &info, &nextLayerInfo, instance); + if (XR_SUCCEEDED(nextResult)) { + Environment::Have_XR_EXT_HandTracking = true; + Environment::Have_XR_FB_HandTracking_Aim = false; + gNext = std::make_shared( + *instance, layerInfo->nextInfo->nextGetInstanceProcAddr); + DebugPrint( + "Initialized without {}", XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME); + return nextResult; + } + + if (nextResult != XR_ERROR_EXTENSION_NOT_PRESENT) { + DebugPrint( + "bare-minimum xrCreateApiLayerInstance failed: {}", nextResult); + return nextResult; + } + } - return XR_SUCCESS; + ///// Attempt 3: nope, no hand tracking. Just pass through. + const auto nextResult = layerInfo->nextInfo->nextCreateApiLayerInstance( + originalInfo, &nextLayerInfo, instance); + if (XR_SUCCEEDED(nextResult)) { + DebugPrint("No-op passthrough xrCreateAPILayerInstance succeeded"); + } else { + DebugPrint( + "No-op passthrough xrCreateApiLayerInstance failed: {}", nextResult); + } + return nextResult; } }// namespace HandTrackedCockpitClicking::Loader diff --git a/src/APILayer/VirtualControllerSink.cpp b/src/APILayer/VirtualControllerSink.cpp index fd0a210..934d415 100644 --- a/src/APILayer/VirtualControllerSink.cpp +++ b/src/APILayer/VirtualControllerSink.cpp @@ -92,18 +92,7 @@ VirtualControllerSink::VirtualControllerSink( } bool VirtualControllerSink::IsPointerSink() { - if (Config::PointerSink == PointerSink::VirtualVRController) { - if (!Environment::Have_XR_KHR_win32_convert_performance_counter_time) { - // This should pretty much never happen: every runtime supports this - // extension - DebugPrint( - "Configured to use VirtualControllerSink, but don't have {}", - XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME); - return false; - } - return true; - } - return false; + return Config::PointerSink == PointerSink::VirtualVRController; } static bool IsActionSink(ActionSink actionSink) { diff --git a/src/PointCtrlCalibration/PointCtrlCalibration.cpp b/src/PointCtrlCalibration/PointCtrlCalibration.cpp index b1ea781..d0b7412 100644 --- a/src/PointCtrlCalibration/PointCtrlCalibration.cpp +++ b/src/PointCtrlCalibration/PointCtrlCalibration.cpp @@ -1,671 +1,670 @@ -/* - * MIT License - * - * Copyright (c) 2022 Fred Emmott - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#define XR_USE_PLATFORM_WIN32 -#define XR_USE_GRAPHICS_API_D3D11 - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "Config.h" -#include "DebugPrint.h" -#include "Environment.h" -#include "OpenXRNext.h" -#include "PointCtrlSource.h" - -using namespace HandTrackedCockpitClicking; -namespace Config = HandTrackedCockpitClicking::Config; -namespace Environment = HandTrackedCockpitClicking::Environment; - -#define EXTENSION_FUNCTIONS IT(xrGetD3D11GraphicsRequirementsKHR) - -#define IT(x) static PFN_##x ext_##x {nullptr}; -EXTENSION_FUNCTIONS -#undef IT - -constexpr uint32_t TextureHeight = 1024; -constexpr uint32_t TextureWidth = 1024; -// 2 pi radians in a circle, so pi / 18 radians is 10 degrees -constexpr float OffsetInRadians = std::numbers::pi_v / 18; -constexpr float DistanceInMeters = 1.0f; -constexpr float SizeInMeters = 0.25f; - -enum class CalibrationState { - NoInput, - WaitForCenter, - WaitForOffset, - Test, -}; - -static winrt::com_ptr GetDXGIAdapter(LUID luid) { - winrt::com_ptr dxgi; - winrt::check_hresult(CreateDXGIFactory1(IID_PPV_ARGS(dxgi.put()))); - - UINT i = 0; - winrt::com_ptr it; - while (dxgi->EnumAdapters1(i++, it.put()) == S_OK) { - DXGI_ADAPTER_DESC1 desc {}; - winrt::check_hresult(it->GetDesc1(&desc)); - if (memcmp(&luid, &desc.AdapterLuid, sizeof(LUID)) == 0) { - return it; - } - it = {nullptr}; - } - - return {nullptr}; -} - -XrInstance gInstance {}; - -void check_xr(XrResult result) { - if (result == XR_SUCCESS) { - return; - } - - std::string message; - if (gInstance) { - char buffer[XR_MAX_RESULT_STRING_SIZE]; - xrResultToString(gInstance, result, buffer); - message = std::format( - "OpenXR call failed: '{}' ({})", buffer, static_cast(result)); - } else { - message = std::format("OpenXR call failed: {}", static_cast(result)); - } - OutputDebugStringA(message.c_str()); - throw std::runtime_error(message); -} - -struct DrawingResources { - winrt::com_ptr mTexture; - winrt::com_ptr mRT; - winrt::com_ptr mBrush; - winrt::com_ptr mTextFormat; -}; - -static DrawingResources sDrawingResources; - -void InitDrawingResources(ID3D11DeviceContext* context) { - auto& res = sDrawingResources; - if (res.mRT) [[likely]] { - return; - } - - winrt::com_ptr device; - context->GetDevice(device.put()); - D3D11_TEXTURE2D_DESC desc { - .Width = TextureWidth, - .Height = TextureHeight, - .MipLevels = 1, - .ArraySize = 1, - .Format = DXGI_FORMAT_B8G8R8A8_UNORM,// needed for Direct2D - .SampleDesc = {1, 0}, - .BindFlags = D3D11_BIND_RENDER_TARGET, - }; - winrt::check_hresult( - device->CreateTexture2D(&desc, nullptr, res.mTexture.put())); - auto surface = res.mTexture.as(); - - winrt::com_ptr d2d; - winrt::check_hresult( - D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, d2d.put())); - - winrt::check_hresult(d2d->CreateDxgiSurfaceRenderTarget( - surface.get(), - D2D1::RenderTargetProperties( - D2D1_RENDER_TARGET_TYPE_HARDWARE, - D2D1::PixelFormat( - DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)), - res.mRT.put())); - res.mRT->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); - // Don't use cleartype as subpixels won't line up in headset - res.mRT->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - - winrt::check_hresult(res.mRT->CreateSolidColorBrush( - D2D1::ColorF(D2D1::ColorF::Black, 1.0f), res.mBrush.put())); - - winrt::com_ptr dwrite; - winrt::check_hresult(DWriteCreateFactory( - DWRITE_FACTORY_TYPE_ISOLATED, - __uuidof(dwrite), - reinterpret_cast(dwrite.put()))); - - winrt::check_hresult(dwrite->CreateTextFormat( - L"Calibri", - nullptr, - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - 64.0f, - L"", - res.mTextFormat.put())); -} - -void DrawLayer( - CalibrationState state, - ID3D11DeviceContext* context, - ID3D11Texture2D* texture, - XrPosef* layerPose, - const XrVector2f& calibratedRXRY) { - InitDrawingResources(context); - - auto& res = sDrawingResources; - auto rt = res.mRT.get(); - auto brush = res.mBrush.get(); - - rt->BeginDraw(); - rt->Clear(D2D1::ColorF(D2D1::ColorF::White, 1.0f)); - - // draw crosshairs - rt->DrawLine( - {TextureWidth / 2.0, 0}, {TextureWidth / 2.0, TextureHeight}, brush, 5.0f); - rt->DrawLine( - {0, TextureHeight / 2.0}, {TextureWidth, TextureHeight / 2.0}, brush, 5.0f); - - std::wstring_view message; - switch (state) { - case CalibrationState::NoInput: - *layerPose = { - .orientation = XR_POSEF_IDENTITY.orientation, - .position = {0.0f, 0.0f, -DistanceInMeters}, - }; - message - = L"The sensor can't see the LED - press FCU3 to wake it if it's " - L"turned off"; - break; - case CalibrationState::WaitForCenter: - *layerPose = { - .orientation = XR_POSEF_IDENTITY.orientation, - .position = {0.0f, 0.0f, -DistanceInMeters}, - }; - message - = L"Reach for the center of the crosshair, then press FCU button 1"; - break; - case CalibrationState::WaitForOffset: { - const auto o = DirectX::SimpleMath::Quaternion::CreateFromYawPitchRoll( - -OffsetInRadians, OffsetInRadians, 0); - const auto p = DirectX::SimpleMath::Vector3::Transform( - {0.0f, 0.0f, -DistanceInMeters}, o); - - *layerPose = { - .orientation = {o.x, o.y, o.z, o.w}, - .position = {p.x, p.y, p.z}, - }; - - message - = L"Reach for the center of the crosshair, then press FCU button 1"; - break; - } - case CalibrationState::Test: { - auto o = DirectX::SimpleMath::Quaternion::CreateFromYawPitchRoll( - -calibratedRXRY.y, -calibratedRXRY.x, 0); - const auto p = DirectX::SimpleMath::Vector3::Transform( - {0.0f, 0.0f, -DistanceInMeters}, o); - - *layerPose = { - .orientation = {o.x, o.y, o.z, o.w}, - .position = {p.x, p.y, p.z}, - }; - message = L"Press FCU button 1 to confirm, or button 2 to restart"; - break; - } - default: - DebugBreak(); - } - - rt->DrawTextW( - message.data(), - message.size(), - res.mTextFormat.get(), - { - 0.0f, - (TextureHeight / 2.0f) + 7.5f, - (TextureWidth / 2.0f) - 7.5f, - TextureHeight - 5.0f, - }, - brush); - - winrt::check_hresult(rt->EndDraw()); - context->CopyResource(texture, res.mTexture.get()); -} - -int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { - winrt::init_apartment(winrt::apartment_type::single_threaded); - Environment::IsPointCtrlCalibration = true; - - XrInstance instance {}; - { - const std::vector enabledExtensions = { - XR_KHR_D3D11_ENABLE_EXTENSION_NAME, - XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME, - }; - Environment::Have_XR_KHR_win32_convert_performance_counter_time = true; - XrInstanceCreateInfo createInfo { - .type = XR_TYPE_INSTANCE_CREATE_INFO, - .applicationInfo = { - .applicationName = "PointCtrl Calibration", - .applicationVersion = 1, - .apiVersion = XR_CURRENT_API_VERSION, - }, - .enabledExtensionCount = static_cast(enabledExtensions.size()), - .enabledExtensionNames = enabledExtensions.data(), - }; - check_xr(xrCreateInstance(&createInfo, &instance)); - gInstance = instance; - } -#define IT(x) \ - xrGetInstanceProcAddr( \ - instance, #x, reinterpret_cast(&ext_##x)); - EXTENSION_FUNCTIONS -#undef IT - - XrSystemId system {}; - { - XrSystemGetInfo getInfo { - .type = XR_TYPE_SYSTEM_GET_INFO, - .formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY, - }; - while (true) { - xrGetSystem(instance, &getInfo, &system); - if (system) { - break; - } - auto result = MessageBoxW( - NULL, - L"No VR system found; connect your headset, then click retry.", - L"PointCTRL Calibration", - MB_RETRYCANCEL | MB_ICONEXCLAMATION | MB_DEFBUTTON1); - if (result == IDCANCEL) { - return 0; - } - } - } - - winrt::com_ptr device; - winrt::com_ptr context; - { - XrGraphicsRequirementsD3D11KHR d3dRequirements { - XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR}; - ext_xrGetD3D11GraphicsRequirementsKHR(instance, system, &d3dRequirements); - - auto adapter = GetDXGIAdapter(d3dRequirements.adapterLuid); - D3D_FEATURE_LEVEL featureLevels[] - = {std::max(d3dRequirements.minFeatureLevel, D3D_FEATURE_LEVEL_11_0)}; - - UINT flags {D3D11_CREATE_DEVICE_BGRA_SUPPORT}; -#ifndef NDEBUG - flags |= D3D11_CREATE_DEVICE_DEBUG; -#endif - - winrt::check_hresult(D3D11CreateDevice( - adapter.get(), - D3D_DRIVER_TYPE_UNKNOWN, - 0, - flags, - featureLevels, - _countof(featureLevels), - D3D11_SDK_VERSION, - device.put(), - nullptr, - context.put())); - } - - XrSession session {}; - { - XrGraphicsBindingD3D11KHR binding { - .type = XR_TYPE_GRAPHICS_BINDING_D3D11_KHR, - .device = device.get(), - }; - XrSessionCreateInfo createInfo { - .type = XR_TYPE_SESSION_CREATE_INFO, - .next = &binding, - .systemId = system, - }; - check_xr(xrCreateSession(instance, &createInfo, &session)); - } - - XrSpace viewSpace {}; - { - XrReferenceSpaceCreateInfo createInfo { - .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, - .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW, - .poseInReferenceSpace = XR_POSEF_IDENTITY, - }; - xrCreateReferenceSpace(session, &createInfo, &viewSpace); - } - XrSpace localSpace {}; - { - XrReferenceSpaceCreateInfo createInfo { - .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, - .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL, - .poseInReferenceSpace = XR_POSEF_IDENTITY, - }; - xrCreateReferenceSpace(session, &createInfo, &localSpace); - } - const auto openXR - = std::make_shared(instance, &xrGetInstanceProcAddr); - PointCtrlSource pointCtrl; - while (!pointCtrl.IsConnected()) { - const auto result = MessageBoxW( - NULL, - L"PointCTRL device not found; please plug it in, then click retry.", - L"PointCTRL Calibration", - MB_RETRYCANCEL | MB_ICONEXCLAMATION | MB_DEFBUTTON1); - if (result == IDCANCEL) { - return 0; - } - pointCtrl.Update(PointerMode::Direction, {}); - } - - // How to show a non-blocking window without an event loop... :p - { - AllocConsole(); - SetConsoleCtrlHandler(nullptr, false);// allow Ctrl+C to terminate - const auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - freopen("CONOUT$", "w", stdout); - std::cout - << "HTCC PointCTRL Calibration\n\n" - "Put on an FCU, then put on your headset and follow the on-screen\n" - "instructions.\n\n" - "===== TO EXIT =====\n\n" - "Press FCU 3, Ctrl+C, or close this window" - "===== Step 1: Calibration =====\n\n" - "Reach out and try to touch the center of the crosshair - don't\n" - "Once you're as close as you can, press FCU 1.\n\n" - "===== Step 2: Testing =====\n\n" - "Move your hand around in front of you; the cursor should follow\n" - "your hand. If you're happy with the calibration, press FCU 1 to\n" - "save and exit; otherwise, press FCU 2 to re-calibrate." - << std::endl; - } - - bool xrRunning = false; - XrSwapchain swapchain {}; - std::vector swapchainImages; - CalibrationState state {CalibrationState::WaitForCenter}; - PointCtrlSource::RawValues rawValues; - D2D1_POINT_2U centerPoint {}; - D2D1_POINT_2U offsetPoint {}; - XrVector2f radiansPerUnit { - Config::Defaults::PointCtrlRadiansPerUnitX, - Config::Defaults::PointCtrlRadiansPerUnitY, - }; - - bool saveAndExit = false; - XrTime nextDisplayTime {}; - - while (!saveAndExit) { - { - XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER}; - while (xrPollEvent(instance, &event) == XR_SUCCESS) { - switch (event.type) { - case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: - return 0; - case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { - const auto state - = reinterpret_cast(&event) - ->state; - switch (state) { - case XR_SESSION_STATE_READY: { - XrSessionBeginInfo beginInfo { - .type = XR_TYPE_SESSION_BEGIN_INFO, - .primaryViewConfigurationType - = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, - }; - check_xr(xrBeginSession(session, &beginInfo)); - xrRunning = true; - - XrSwapchainCreateInfo createInfo { - .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, - .usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT - | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, - .format = DXGI_FORMAT_B8G8R8A8_UNORM, - .sampleCount = 1, - .width = TextureWidth, - .height = TextureHeight, - .faceCount = 1, - .arraySize = 1, - .mipCount = 1, - }; - check_xr(xrCreateSwapchain(session, &createInfo, &swapchain)); - uint32_t swapchainLength {}; - check_xr(xrEnumerateSwapchainImages( - swapchain, 0, &swapchainLength, nullptr)); - swapchainImages.resize( - swapchainLength, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR}); - check_xr(xrEnumerateSwapchainImages( - swapchain, - swapchainImages.size(), - &swapchainLength, - reinterpret_cast( - swapchainImages.data()))); - break; - } - case XR_SESSION_STATE_STOPPING: - return 0; - case XR_SESSION_STATE_EXITING: - return 0; - case XR_SESSION_STATE_LOSS_PENDING: - return 0; - default: - break; - } - } - default: - break; - } - } - }// event handling - - if (!xrRunning) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; - } - - // FRAME STARTS HERE - XrFrameState frameState {XR_TYPE_FRAME_STATE}; - check_xr(xrWaitFrame(session, nullptr, &frameState)); - check_xr(xrBeginFrame(session, nullptr)); - - nextDisplayTime = frameState.predictedDisplayTime; - - XrCompositionLayerQuad layer { - .type = XR_TYPE_COMPOSITION_LAYER_QUAD, - .layerFlags = XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT, - .space = viewSpace, - .subImage = { - .swapchain = swapchain, - .imageRect = {{0, 0}, {TextureWidth, TextureHeight}}, - .imageArrayIndex = 0, - }, - .size = {SizeInMeters, SizeInMeters}, - }; - - { - uint32_t imageIndex; - check_xr(xrAcquireSwapchainImage(swapchain, nullptr, &imageIndex)); - XrSwapchainImageWaitInfo waitInfo { - .type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, - .timeout = XR_INFINITE_DURATION, - }; - check_xr(xrWaitSwapchainImage(swapchain, &waitInfo)); - - pointCtrl.Update( - PointerMode::Direction, - { - openXR.get(), - instance, - localSpace, - viewSpace, - frameState.predictedDisplayTime, - }); - - const auto newRaw = pointCtrl.GetRawValuesForCalibration(); - if (newRaw.FCU3()) { - return 0; - } - - const auto age = std::chrono::nanoseconds( - frameState.predictedDisplayTime - pointCtrl.GetLastMovedAt()); - if (age > std::chrono::milliseconds(500)) { - DrawLayer( - CalibrationState::NoInput, - context.get(), - swapchainImages.at(imageIndex).texture, - &layer.pose, - {}); - } else { - const auto x = newRaw.mX; - const auto y = newRaw.mY; - - const auto click1 = newRaw.FCU1() && !rawValues.FCU1(); - const auto click2 = newRaw.FCU2() && !rawValues.FCU2(); - rawValues = newRaw; - if (click2) { - state = CalibrationState::WaitForCenter; - } - - if (click1) { - switch (state) { - case CalibrationState::WaitForCenter: - centerPoint = {x, y}; - DebugPrint("Center at ({}, {})", x, y); - // Skip second calibration point as we have angular sensitivity - // of the sensor - // - // state = CalibrationState::WaitForOffset; - state = CalibrationState::Test; - break; - case CalibrationState::WaitForOffset: - offsetPoint = {x, y}; - radiansPerUnit = { - OffsetInRadians / (static_cast(x) - centerPoint.x), - OffsetInRadians / (centerPoint.y - static_cast(y)), - }; - DebugPrint( - "Offset point at ({}, {}); radians per unit: ({}, {}); " - "degrees " - "per unit: ({}, {})", - x, - y, - radiansPerUnit.x, - radiansPerUnit.y, - (radiansPerUnit.x * 180) / std::numbers::pi_v, - (radiansPerUnit.y * 180) / std::numbers::pi_v); - state = CalibrationState::Test; - break; - case CalibrationState::Test: - saveAndExit = true; - break; - } - } - XrVector2f calibratedRotation {}; - - if (state == CalibrationState::Test) { - calibratedRotation = { - (static_cast(y) - centerPoint.y) * radiansPerUnit.y, - (static_cast(x) - centerPoint.x) * radiansPerUnit.x, - }; - } - - DrawLayer( - state, - context.get(), - swapchainImages.at(imageIndex).texture, - &layer.pose, - calibratedRotation); - } - check_xr(xrReleaseSwapchainImage(swapchain, nullptr)); - - XrCompositionLayerQuad* layerPtr = &layer; - XrFrameEndInfo endInfo { - .type = XR_TYPE_FRAME_END_INFO, - .displayTime = frameState.predictedDisplayTime, - .environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE, - .layerCount = 1, - .layers = reinterpret_cast(&layerPtr), - }; - - check_xr(xrEndFrame(session, &endInfo)); - } - } - - Config::SavePointCtrlCenterX(centerPoint.x); - Config::SavePointCtrlCenterY(centerPoint.y); - Config::SavePointCtrlRadiansPerUnitX(radiansPerUnit.x); - Config::SavePointCtrlRadiansPerUnitY(radiansPerUnit.y); - - // Also save the FOV while we're here; this isn't needed when running as - // an OpenXR API layer, but opens the possibility of supporting - // tablet/touchscreen mode without OpenXR - - XrViewLocateInfo viewLocateInfo { - .type = XR_TYPE_VIEW_LOCATE_INFO, - .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, - .displayTime = nextDisplayTime, - .space = viewSpace, - }; - XrViewState viewState {XR_TYPE_VIEW_STATE}; - std::array views; - views.fill({XR_TYPE_VIEW}); - uint32_t viewCount {views.size()}; - { - const auto result = xrLocateViews( - session, - &viewLocateInfo, - &viewState, - viewCount, - &viewCount, - views.data()); - if (result != XR_SUCCESS) { - DebugPrint("Failed to find FOV: {}", static_cast(result)); - return 0; - } - } - - const auto leftFov = views[0].fov; - Config::SaveLeftEyeFOVLeft(leftFov.angleLeft); - Config::SaveLeftEyeFOVRight(leftFov.angleRight); - Config::SaveLeftEyeFOVUp(leftFov.angleUp); - Config::SaveLeftEyeFOVDown(leftFov.angleDown); - - const auto rightFov = views[1].fov; - Config::SaveRightEyeFOVLeft(rightFov.angleLeft); - Config::SaveRightEyeFOVRight(rightFov.angleRight); - Config::SaveRightEyeFOVUp(rightFov.angleUp); - Config::SaveRightEyeFOVDown(rightFov.angleDown); - - Config::SaveHaveSavedFOV(true); - - return 0; -} +/* + * MIT License + * + * Copyright (c) 2022 Fred Emmott + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#define XR_USE_PLATFORM_WIN32 +#define XR_USE_GRAPHICS_API_D3D11 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Config.h" +#include "DebugPrint.h" +#include "Environment.h" +#include "OpenXRNext.h" +#include "PointCtrlSource.h" + +using namespace HandTrackedCockpitClicking; +namespace Config = HandTrackedCockpitClicking::Config; +namespace Environment = HandTrackedCockpitClicking::Environment; + +#define EXTENSION_FUNCTIONS IT(xrGetD3D11GraphicsRequirementsKHR) + +#define IT(x) static PFN_##x ext_##x {nullptr}; +EXTENSION_FUNCTIONS +#undef IT + +constexpr uint32_t TextureHeight = 1024; +constexpr uint32_t TextureWidth = 1024; +// 2 pi radians in a circle, so pi / 18 radians is 10 degrees +constexpr float OffsetInRadians = std::numbers::pi_v / 18; +constexpr float DistanceInMeters = 1.0f; +constexpr float SizeInMeters = 0.25f; + +enum class CalibrationState { + NoInput, + WaitForCenter, + WaitForOffset, + Test, +}; + +static winrt::com_ptr GetDXGIAdapter(LUID luid) { + winrt::com_ptr dxgi; + winrt::check_hresult(CreateDXGIFactory1(IID_PPV_ARGS(dxgi.put()))); + + UINT i = 0; + winrt::com_ptr it; + while (dxgi->EnumAdapters1(i++, it.put()) == S_OK) { + DXGI_ADAPTER_DESC1 desc {}; + winrt::check_hresult(it->GetDesc1(&desc)); + if (memcmp(&luid, &desc.AdapterLuid, sizeof(LUID)) == 0) { + return it; + } + it = {nullptr}; + } + + return {nullptr}; +} + +XrInstance gInstance {}; + +void check_xr(XrResult result) { + if (result == XR_SUCCESS) { + return; + } + + std::string message; + if (gInstance) { + char buffer[XR_MAX_RESULT_STRING_SIZE]; + xrResultToString(gInstance, result, buffer); + message = std::format( + "OpenXR call failed: '{}' ({})", buffer, static_cast(result)); + } else { + message = std::format("OpenXR call failed: {}", static_cast(result)); + } + OutputDebugStringA(message.c_str()); + throw std::runtime_error(message); +} + +struct DrawingResources { + winrt::com_ptr mTexture; + winrt::com_ptr mRT; + winrt::com_ptr mBrush; + winrt::com_ptr mTextFormat; +}; + +static DrawingResources sDrawingResources; + +void InitDrawingResources(ID3D11DeviceContext* context) { + auto& res = sDrawingResources; + if (res.mRT) [[likely]] { + return; + } + + winrt::com_ptr device; + context->GetDevice(device.put()); + D3D11_TEXTURE2D_DESC desc { + .Width = TextureWidth, + .Height = TextureHeight, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_B8G8R8A8_UNORM,// needed for Direct2D + .SampleDesc = {1, 0}, + .BindFlags = D3D11_BIND_RENDER_TARGET, + }; + winrt::check_hresult( + device->CreateTexture2D(&desc, nullptr, res.mTexture.put())); + auto surface = res.mTexture.as(); + + winrt::com_ptr d2d; + winrt::check_hresult( + D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, d2d.put())); + + winrt::check_hresult(d2d->CreateDxgiSurfaceRenderTarget( + surface.get(), + D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_HARDWARE, + D2D1::PixelFormat( + DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)), + res.mRT.put())); + res.mRT->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + // Don't use cleartype as subpixels won't line up in headset + res.mRT->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + + winrt::check_hresult(res.mRT->CreateSolidColorBrush( + D2D1::ColorF(D2D1::ColorF::Black, 1.0f), res.mBrush.put())); + + winrt::com_ptr dwrite; + winrt::check_hresult(DWriteCreateFactory( + DWRITE_FACTORY_TYPE_ISOLATED, + __uuidof(dwrite), + reinterpret_cast(dwrite.put()))); + + winrt::check_hresult(dwrite->CreateTextFormat( + L"Calibri", + nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 64.0f, + L"", + res.mTextFormat.put())); +} + +void DrawLayer( + CalibrationState state, + ID3D11DeviceContext* context, + ID3D11Texture2D* texture, + XrPosef* layerPose, + const XrVector2f& calibratedRXRY) { + InitDrawingResources(context); + + auto& res = sDrawingResources; + auto rt = res.mRT.get(); + auto brush = res.mBrush.get(); + + rt->BeginDraw(); + rt->Clear(D2D1::ColorF(D2D1::ColorF::White, 1.0f)); + + // draw crosshairs + rt->DrawLine( + {TextureWidth / 2.0, 0}, {TextureWidth / 2.0, TextureHeight}, brush, 5.0f); + rt->DrawLine( + {0, TextureHeight / 2.0}, {TextureWidth, TextureHeight / 2.0}, brush, 5.0f); + + std::wstring_view message; + switch (state) { + case CalibrationState::NoInput: + *layerPose = { + .orientation = XR_POSEF_IDENTITY.orientation, + .position = {0.0f, 0.0f, -DistanceInMeters}, + }; + message + = L"The sensor can't see the LED - press FCU3 to wake it if it's " + L"turned off"; + break; + case CalibrationState::WaitForCenter: + *layerPose = { + .orientation = XR_POSEF_IDENTITY.orientation, + .position = {0.0f, 0.0f, -DistanceInMeters}, + }; + message + = L"Reach for the center of the crosshair, then press FCU button 1"; + break; + case CalibrationState::WaitForOffset: { + const auto o = DirectX::SimpleMath::Quaternion::CreateFromYawPitchRoll( + -OffsetInRadians, OffsetInRadians, 0); + const auto p = DirectX::SimpleMath::Vector3::Transform( + {0.0f, 0.0f, -DistanceInMeters}, o); + + *layerPose = { + .orientation = {o.x, o.y, o.z, o.w}, + .position = {p.x, p.y, p.z}, + }; + + message + = L"Reach for the center of the crosshair, then press FCU button 1"; + break; + } + case CalibrationState::Test: { + auto o = DirectX::SimpleMath::Quaternion::CreateFromYawPitchRoll( + -calibratedRXRY.y, -calibratedRXRY.x, 0); + const auto p = DirectX::SimpleMath::Vector3::Transform( + {0.0f, 0.0f, -DistanceInMeters}, o); + + *layerPose = { + .orientation = {o.x, o.y, o.z, o.w}, + .position = {p.x, p.y, p.z}, + }; + message = L"Press FCU button 1 to confirm, or button 2 to restart"; + break; + } + default: + DebugBreak(); + } + + rt->DrawTextW( + message.data(), + message.size(), + res.mTextFormat.get(), + { + 0.0f, + (TextureHeight / 2.0f) + 7.5f, + (TextureWidth / 2.0f) - 7.5f, + TextureHeight - 5.0f, + }, + brush); + + winrt::check_hresult(rt->EndDraw()); + context->CopyResource(texture, res.mTexture.get()); +} + +int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { + winrt::init_apartment(winrt::apartment_type::single_threaded); + Environment::IsPointCtrlCalibration = true; + + XrInstance instance {}; + { + const std::vector enabledExtensions = { + XR_KHR_D3D11_ENABLE_EXTENSION_NAME, + XR_KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME, + }; + XrInstanceCreateInfo createInfo { + .type = XR_TYPE_INSTANCE_CREATE_INFO, + .applicationInfo = { + .applicationName = "PointCtrl Calibration", + .applicationVersion = 1, + .apiVersion = XR_CURRENT_API_VERSION, + }, + .enabledExtensionCount = static_cast(enabledExtensions.size()), + .enabledExtensionNames = enabledExtensions.data(), + }; + check_xr(xrCreateInstance(&createInfo, &instance)); + gInstance = instance; + } +#define IT(x) \ + xrGetInstanceProcAddr( \ + instance, #x, reinterpret_cast(&ext_##x)); + EXTENSION_FUNCTIONS +#undef IT + + XrSystemId system {}; + { + XrSystemGetInfo getInfo { + .type = XR_TYPE_SYSTEM_GET_INFO, + .formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY, + }; + while (true) { + xrGetSystem(instance, &getInfo, &system); + if (system) { + break; + } + auto result = MessageBoxW( + NULL, + L"No VR system found; connect your headset, then click retry.", + L"PointCTRL Calibration", + MB_RETRYCANCEL | MB_ICONEXCLAMATION | MB_DEFBUTTON1); + if (result == IDCANCEL) { + return 0; + } + } + } + + winrt::com_ptr device; + winrt::com_ptr context; + { + XrGraphicsRequirementsD3D11KHR d3dRequirements { + XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR}; + ext_xrGetD3D11GraphicsRequirementsKHR(instance, system, &d3dRequirements); + + auto adapter = GetDXGIAdapter(d3dRequirements.adapterLuid); + D3D_FEATURE_LEVEL featureLevels[] + = {std::max(d3dRequirements.minFeatureLevel, D3D_FEATURE_LEVEL_11_0)}; + + UINT flags {D3D11_CREATE_DEVICE_BGRA_SUPPORT}; +#ifndef NDEBUG + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + winrt::check_hresult(D3D11CreateDevice( + adapter.get(), + D3D_DRIVER_TYPE_UNKNOWN, + 0, + flags, + featureLevels, + _countof(featureLevels), + D3D11_SDK_VERSION, + device.put(), + nullptr, + context.put())); + } + + XrSession session {}; + { + XrGraphicsBindingD3D11KHR binding { + .type = XR_TYPE_GRAPHICS_BINDING_D3D11_KHR, + .device = device.get(), + }; + XrSessionCreateInfo createInfo { + .type = XR_TYPE_SESSION_CREATE_INFO, + .next = &binding, + .systemId = system, + }; + check_xr(xrCreateSession(instance, &createInfo, &session)); + } + + XrSpace viewSpace {}; + { + XrReferenceSpaceCreateInfo createInfo { + .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, + .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW, + .poseInReferenceSpace = XR_POSEF_IDENTITY, + }; + xrCreateReferenceSpace(session, &createInfo, &viewSpace); + } + XrSpace localSpace {}; + { + XrReferenceSpaceCreateInfo createInfo { + .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, + .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL, + .poseInReferenceSpace = XR_POSEF_IDENTITY, + }; + xrCreateReferenceSpace(session, &createInfo, &localSpace); + } + const auto openXR + = std::make_shared(instance, &xrGetInstanceProcAddr); + PointCtrlSource pointCtrl; + while (!pointCtrl.IsConnected()) { + const auto result = MessageBoxW( + NULL, + L"PointCTRL device not found; please plug it in, then click retry.", + L"PointCTRL Calibration", + MB_RETRYCANCEL | MB_ICONEXCLAMATION | MB_DEFBUTTON1); + if (result == IDCANCEL) { + return 0; + } + pointCtrl.Update(PointerMode::Direction, {}); + } + + // How to show a non-blocking window without an event loop... :p + { + AllocConsole(); + SetConsoleCtrlHandler(nullptr, false);// allow Ctrl+C to terminate + const auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + freopen("CONOUT$", "w", stdout); + std::cout + << "HTCC PointCTRL Calibration\n\n" + "Put on an FCU, then put on your headset and follow the on-screen\n" + "instructions.\n\n" + "===== TO EXIT =====\n\n" + "Press FCU 3, Ctrl+C, or close this window" + "===== Step 1: Calibration =====\n\n" + "Reach out and try to touch the center of the crosshair - don't\n" + "Once you're as close as you can, press FCU 1.\n\n" + "===== Step 2: Testing =====\n\n" + "Move your hand around in front of you; the cursor should follow\n" + "your hand. If you're happy with the calibration, press FCU 1 to\n" + "save and exit; otherwise, press FCU 2 to re-calibrate." + << std::endl; + } + + bool xrRunning = false; + XrSwapchain swapchain {}; + std::vector swapchainImages; + CalibrationState state {CalibrationState::WaitForCenter}; + PointCtrlSource::RawValues rawValues; + D2D1_POINT_2U centerPoint {}; + D2D1_POINT_2U offsetPoint {}; + XrVector2f radiansPerUnit { + Config::Defaults::PointCtrlRadiansPerUnitX, + Config::Defaults::PointCtrlRadiansPerUnitY, + }; + + bool saveAndExit = false; + XrTime nextDisplayTime {}; + + while (!saveAndExit) { + { + XrEventDataBuffer event {XR_TYPE_EVENT_DATA_BUFFER}; + while (xrPollEvent(instance, &event) == XR_SUCCESS) { + switch (event.type) { + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: + return 0; + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { + const auto state + = reinterpret_cast(&event) + ->state; + switch (state) { + case XR_SESSION_STATE_READY: { + XrSessionBeginInfo beginInfo { + .type = XR_TYPE_SESSION_BEGIN_INFO, + .primaryViewConfigurationType + = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + }; + check_xr(xrBeginSession(session, &beginInfo)); + xrRunning = true; + + XrSwapchainCreateInfo createInfo { + .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, + .usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT + | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, + .format = DXGI_FORMAT_B8G8R8A8_UNORM, + .sampleCount = 1, + .width = TextureWidth, + .height = TextureHeight, + .faceCount = 1, + .arraySize = 1, + .mipCount = 1, + }; + check_xr(xrCreateSwapchain(session, &createInfo, &swapchain)); + uint32_t swapchainLength {}; + check_xr(xrEnumerateSwapchainImages( + swapchain, 0, &swapchainLength, nullptr)); + swapchainImages.resize( + swapchainLength, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR}); + check_xr(xrEnumerateSwapchainImages( + swapchain, + swapchainImages.size(), + &swapchainLength, + reinterpret_cast( + swapchainImages.data()))); + break; + } + case XR_SESSION_STATE_STOPPING: + return 0; + case XR_SESSION_STATE_EXITING: + return 0; + case XR_SESSION_STATE_LOSS_PENDING: + return 0; + default: + break; + } + } + default: + break; + } + } + }// event handling + + if (!xrRunning) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + // FRAME STARTS HERE + XrFrameState frameState {XR_TYPE_FRAME_STATE}; + check_xr(xrWaitFrame(session, nullptr, &frameState)); + check_xr(xrBeginFrame(session, nullptr)); + + nextDisplayTime = frameState.predictedDisplayTime; + + XrCompositionLayerQuad layer { + .type = XR_TYPE_COMPOSITION_LAYER_QUAD, + .layerFlags = XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT, + .space = viewSpace, + .subImage = { + .swapchain = swapchain, + .imageRect = {{0, 0}, {TextureWidth, TextureHeight}}, + .imageArrayIndex = 0, + }, + .size = {SizeInMeters, SizeInMeters}, + }; + + { + uint32_t imageIndex; + check_xr(xrAcquireSwapchainImage(swapchain, nullptr, &imageIndex)); + XrSwapchainImageWaitInfo waitInfo { + .type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, + .timeout = XR_INFINITE_DURATION, + }; + check_xr(xrWaitSwapchainImage(swapchain, &waitInfo)); + + pointCtrl.Update( + PointerMode::Direction, + { + openXR.get(), + instance, + localSpace, + viewSpace, + frameState.predictedDisplayTime, + }); + + const auto newRaw = pointCtrl.GetRawValuesForCalibration(); + if (newRaw.FCU3()) { + return 0; + } + + const auto age = std::chrono::nanoseconds( + frameState.predictedDisplayTime - pointCtrl.GetLastMovedAt()); + if (age > std::chrono::milliseconds(500)) { + DrawLayer( + CalibrationState::NoInput, + context.get(), + swapchainImages.at(imageIndex).texture, + &layer.pose, + {}); + } else { + const auto x = newRaw.mX; + const auto y = newRaw.mY; + + const auto click1 = newRaw.FCU1() && !rawValues.FCU1(); + const auto click2 = newRaw.FCU2() && !rawValues.FCU2(); + rawValues = newRaw; + if (click2) { + state = CalibrationState::WaitForCenter; + } + + if (click1) { + switch (state) { + case CalibrationState::WaitForCenter: + centerPoint = {x, y}; + DebugPrint("Center at ({}, {})", x, y); + // Skip second calibration point as we have angular sensitivity + // of the sensor + // + // state = CalibrationState::WaitForOffset; + state = CalibrationState::Test; + break; + case CalibrationState::WaitForOffset: + offsetPoint = {x, y}; + radiansPerUnit = { + OffsetInRadians / (static_cast(x) - centerPoint.x), + OffsetInRadians / (centerPoint.y - static_cast(y)), + }; + DebugPrint( + "Offset point at ({}, {}); radians per unit: ({}, {}); " + "degrees " + "per unit: ({}, {})", + x, + y, + radiansPerUnit.x, + radiansPerUnit.y, + (radiansPerUnit.x * 180) / std::numbers::pi_v, + (radiansPerUnit.y * 180) / std::numbers::pi_v); + state = CalibrationState::Test; + break; + case CalibrationState::Test: + saveAndExit = true; + break; + } + } + XrVector2f calibratedRotation {}; + + if (state == CalibrationState::Test) { + calibratedRotation = { + (static_cast(y) - centerPoint.y) * radiansPerUnit.y, + (static_cast(x) - centerPoint.x) * radiansPerUnit.x, + }; + } + + DrawLayer( + state, + context.get(), + swapchainImages.at(imageIndex).texture, + &layer.pose, + calibratedRotation); + } + check_xr(xrReleaseSwapchainImage(swapchain, nullptr)); + + XrCompositionLayerQuad* layerPtr = &layer; + XrFrameEndInfo endInfo { + .type = XR_TYPE_FRAME_END_INFO, + .displayTime = frameState.predictedDisplayTime, + .environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE, + .layerCount = 1, + .layers = reinterpret_cast(&layerPtr), + }; + + check_xr(xrEndFrame(session, &endInfo)); + } + } + + Config::SavePointCtrlCenterX(centerPoint.x); + Config::SavePointCtrlCenterY(centerPoint.y); + Config::SavePointCtrlRadiansPerUnitX(radiansPerUnit.x); + Config::SavePointCtrlRadiansPerUnitY(radiansPerUnit.y); + + // Also save the FOV while we're here; this isn't needed when running as + // an OpenXR API layer, but opens the possibility of supporting + // tablet/touchscreen mode without OpenXR + + XrViewLocateInfo viewLocateInfo { + .type = XR_TYPE_VIEW_LOCATE_INFO, + .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + .displayTime = nextDisplayTime, + .space = viewSpace, + }; + XrViewState viewState {XR_TYPE_VIEW_STATE}; + std::array views; + views.fill({XR_TYPE_VIEW}); + uint32_t viewCount {views.size()}; + { + const auto result = xrLocateViews( + session, + &viewLocateInfo, + &viewState, + viewCount, + &viewCount, + views.data()); + if (result != XR_SUCCESS) { + DebugPrint("Failed to find FOV: {}", static_cast(result)); + return 0; + } + } + + const auto leftFov = views[0].fov; + Config::SaveLeftEyeFOVLeft(leftFov.angleLeft); + Config::SaveLeftEyeFOVRight(leftFov.angleRight); + Config::SaveLeftEyeFOVUp(leftFov.angleUp); + Config::SaveLeftEyeFOVDown(leftFov.angleDown); + + const auto rightFov = views[1].fov; + Config::SaveRightEyeFOVLeft(rightFov.angleLeft); + Config::SaveRightEyeFOVRight(rightFov.angleRight); + Config::SaveRightEyeFOVUp(rightFov.angleUp); + Config::SaveRightEyeFOVDown(rightFov.angleDown); + + Config::SaveHaveSavedFOV(true); + + return 0; +} diff --git a/src/SettingsApp/HTCCSettingsApp.cpp b/src/SettingsApp/HTCCSettingsApp.cpp index 2cbc9d0..049ac45 100644 --- a/src/SettingsApp/HTCCSettingsApp.cpp +++ b/src/SettingsApp/HTCCSettingsApp.cpp @@ -355,20 +355,6 @@ class HTCCSettingsApp { Config::SaveUseHandTrackingAimPointFB(!ignoreAimPose); } } - ImGui::TextWrapped( - "HTCC attempts to detect available features; this may not work with some " - "buggy drivers. You can bypass the detection below - if the features are " - "not actually available, this may make games crash."); - if (ImGui::Checkbox( - "Always enable XR_ext_hand_tracking", - &Config::ForceHaveXRExtHandTracking)) { - Config::SaveForceHaveXRExtHandTracking(); - } - if (ImGui::Checkbox( - "Always enable XR_FB_hand_tracking_aim", - &Config::ForceHaveXRFBHandTrackingAim)) { - Config::SaveForceHaveXRFBHandTrackingAim(); - } ImGui::SeparatorText("About HTCC"); ImGui::TextWrapped("%s", VersionString.c_str()); diff --git a/src/lib/Config.h b/src/lib/Config.h index b4b7779..9f9aedf 100644 --- a/src/lib/Config.h +++ b/src/lib/Config.h @@ -156,9 +156,7 @@ enum class HandTrackingHands : DWORD { IT(uint8_t, GameControllerLWheelDownButton, 0) \ IT(uint8_t, GameControllerRWheelUpButton, 0) \ IT(uint8_t, GameControllerRWheelDownButton, 0) \ - IT(uint32_t, PointCtrlSleepMilliseconds, 20000) \ - IT(bool, ForceHaveXRExtHandTracking, false) \ - IT(bool, ForceHaveXRFBHandTrackingAim, false) + IT(uint32_t, PointCtrlSleepMilliseconds, 20000) #define HandTrackedCockpitClicking_FLOAT_SETTINGS \ IT(PointCtrlRadiansPerUnitX, 3.009e-5f) \ diff --git a/src/lib/Environment.cpp b/src/lib/Environment.cpp index 4ed438a..c9203a3 100644 --- a/src/lib/Environment.cpp +++ b/src/lib/Environment.cpp @@ -29,14 +29,6 @@ namespace HandTrackedCockpitClicking::Environment { void Load() { - if (Config::ForceHaveXRExtHandTracking) { - DebugPrint("Force-enabling XR_EXT_hand_tracking due to config"); - Have_XR_EXT_HandTracking = true; - } - if (Config::ForceHaveXRFBHandTrackingAim) { - DebugPrint("Force-enabling XR_FB_hand_tracking_aim due to config"); - Have_XR_FB_HandTracking_Aim = true; - } } #define IT(native_type, name, default) native_type name {default}; diff --git a/src/lib/Environment.h b/src/lib/Environment.h index f3df10c..df93165 100644 --- a/src/lib/Environment.h +++ b/src/lib/Environment.h @@ -28,7 +28,6 @@ #define HandTrackedCockpitClicking_ENVIRONMENT_INFO \ IT(bool, Have_XR_EXT_HandTracking, false) \ IT(bool, Have_XR_FB_HandTracking_Aim, false) \ - IT(bool, Have_XR_KHR_win32_convert_performance_counter_time, false) \ IT(bool, IsPointCtrlCalibration, false) namespace HandTrackedCockpitClicking::Environment {