From 62467b1c748bdcfa9907e86e85d2cfd71b0458d1 Mon Sep 17 00:00:00 2001 From: Rylie Pavlik Date: Tue, 5 Mar 2024 09:53:34 -0600 Subject: [PATCH] OpenXR CTS 1.0.34.0 (2024-02-29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Conformance Tests - Fix: Correct the warning for when Wrist Z variance is above the 14 degree threshold. (internal MR 3043) - Improvement: Code cleanup and documentation in the conformance layer. (internal MR 3044) - Improvement: Make the conformance layer throw a distinct error when it encounters a handle whose creation it did not wrap. (internal MR 3089) - Improvement: Mention in the instructions/README that the conformance automation extension may not be used for conformance submissions, and write a comment about this to the XML output when it is in use for easier identification. (internal MR 3143) - Improvement: Reduce the maximum time allowed for transitioning session state in debug mode from 1 hour to 1 minute, and add a notice message in debug mode explaining this. (internal MR 3151) - New test: Validate that XrEventDataInteractionProfileChanged is only queued during xrSyncActions using the conformance layer. (internal MR 3044, internal issue 1883, internal MR 3211) - New test: “SpaceOffset” interactive test validates the results of calling xrLocateSpace on spaces created with a non-identity pose. This tests some of the same math that Interactive Throw is intended to test, but with automatic pass/fail detection and better troubleshooting assistance and debugging visualization. (internal MR 3058, internal issue 1855) GitOrigin-RevId: 9488157f6ddbecc8c536ebd6ab13eaf1295a4ba4 --- CHANGELOG.CTS.md | 32 + README.md | 3 +- changes/conformance/mr.3043.gl.md | 1 - changes/conformance/mr.3044.gl.1.md | 1 - changes/conformance/mr.3044.gl.md | 4 - changes/conformance/mr.3151.gl.md | 1 - .../conformance_layer/ConformanceHooks.h | 4 +- .../conformance_layer/HandleState.cpp | 4 +- .../conformance_layer/HandleState.h | 7 + .../conformance_layer/Instance.cpp | 56 +- src/conformance/conformance_layer/Session.cpp | 8 +- src/conformance/conformance_test/readme.md | 3 +- .../test_InteractiveThrow.cpp | 53 +- .../conformance_test/test_SpaceOffsets.cpp | 605 ++++++++++++++++++ .../test_XR_EXT_user_presence.cpp | 88 +++ .../framework/conformance_framework.cpp | 2 +- src/conformance/framework/graphics_plugin.h | 19 +- .../framework/graphics_plugin_d3d11.cpp | 3 +- .../framework/graphics_plugin_d3d12.cpp | 3 +- .../framework/graphics_plugin_opengl.cpp | 8 +- .../framework/graphics_plugin_opengles.cpp | 8 +- .../framework/graphics_plugin_vulkan.cpp | 15 +- .../framework/vulkan_shaders/vert.glsl | 3 +- .../framework/vulkan_shaders/vert.spv | 109 ++-- .../framework/xml_test_environment.cpp | 3 + src/conformance/utilities/CMakeLists.txt | 1 + src/conformance/utilities/Geometry.cpp | 18 +- src/conformance/utilities/Geometry.h | 6 +- src/conformance/utilities/ballistics.cpp | 54 ++ src/conformance/utilities/ballistics.h | 21 + src/conformance/utilities/d3d_common.h | 4 +- src/conformance/utilities/vulkan_utils.h | 11 +- src/loader/manifest_file.cpp | 24 +- src/scripts/template_gen_dispatch.cpp | 8 +- 34 files changed, 1032 insertions(+), 158 deletions(-) delete mode 100644 changes/conformance/mr.3043.gl.md delete mode 100644 changes/conformance/mr.3044.gl.1.md delete mode 100644 changes/conformance/mr.3044.gl.md delete mode 100644 changes/conformance/mr.3151.gl.md create mode 100644 src/conformance/conformance_test/test_SpaceOffsets.cpp create mode 100644 src/conformance/conformance_test/test_XR_EXT_user_presence.cpp create mode 100644 src/conformance/utilities/ballistics.cpp create mode 100644 src/conformance/utilities/ballistics.h diff --git a/CHANGELOG.CTS.md b/CHANGELOG.CTS.md index 6ec26798..67036b49 100644 --- a/CHANGELOG.CTS.md +++ b/CHANGELOG.CTS.md @@ -17,6 +17,38 @@ particular, since it is primarily software, pull requests may be integrated as they are accepted even between periodic updates. However, versions that are not signed tags on the `approved` branch are not valid for conformance submission. +## OpenXR CTS 1.0.34.0 (2024-02-29) + +- Conformance Tests + - Fix: Correct the warning for when Wrist Z variance is above the 14 degree + threshold. + ([internal MR 3043](https://gitlab.khronos.org/openxr/openxr/merge_requests/3043)) + - Improvement: Code cleanup and documentation in the conformance layer. + ([internal MR 3044](https://gitlab.khronos.org/openxr/openxr/merge_requests/3044)) + - Improvement: Make the conformance layer throw a distinct error when it + encounters a handle whose creation it did not wrap. + ([internal MR 3089](https://gitlab.khronos.org/openxr/openxr/merge_requests/3089)) + - Improvement: Mention in the instructions/README that the conformance automation + extension may not be used for conformance submissions, and write a comment + about this to the XML output when it is in use for easier identification. + ([internal MR 3143](https://gitlab.khronos.org/openxr/openxr/merge_requests/3143)) + - Improvement: Reduce the maximum time allowed for transitioning session state in + debug mode from 1 hour to 1 minute, and add a notice message in debug mode + explaining this. + ([internal MR 3151](https://gitlab.khronos.org/openxr/openxr/merge_requests/3151)) + - New test: Validate that `XrEventDataInteractionProfileChanged` is only queued + during xrSyncActions using the conformance layer. + ([internal MR 3044](https://gitlab.khronos.org/openxr/openxr/merge_requests/3044), + [internal issue 1883](https://gitlab.khronos.org/openxr/openxr/issues/1883), + [internal MR 3211](https://gitlab.khronos.org/openxr/openxr/merge_requests/3211)) + - New test: "SpaceOffset" interactive test validates the results of calling + xrLocateSpace on spaces created with a non-identity pose. This tests some of + the same math that Interactive Throw is intended to test, but with automatic + pass/fail detection and better troubleshooting assistance and debugging + visualization. + ([internal MR 3058](https://gitlab.khronos.org/openxr/openxr/merge_requests/3058), + [internal issue 1855](https://gitlab.khronos.org/openxr/openxr/issues/1855)) + ## OpenXR CTS 1.0.33.0 (2024-01-18) - Conformance Tests diff --git a/README.md b/README.md index d4d45a1f..cf00a543 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,8 @@ Conformance Criteria A conformance run is considered passing if all tests finish with allowed result codes, and all warnings are acceptably explained to describe why they are not a -conformance failure. Test results are contained in the output XML files, which +conformance failure. XR_EXT_conformance_automation may not be used for conformance +submission. Test results are contained in the output XML files, which are an extension of the common "*Unit" schema with some custom elements. Each test case leaf section is reached by a run of its own, and is recorded with a `testcase` tag, e.g.: diff --git a/changes/conformance/mr.3043.gl.md b/changes/conformance/mr.3043.gl.md deleted file mode 100644 index 9ff33140..00000000 --- a/changes/conformance/mr.3043.gl.md +++ /dev/null @@ -1 +0,0 @@ -Fix: Corrects the CTS warning for when Wrist Z variance is above the 14 degree threshold. diff --git a/changes/conformance/mr.3044.gl.1.md b/changes/conformance/mr.3044.gl.1.md deleted file mode 100644 index 106cdb44..00000000 --- a/changes/conformance/mr.3044.gl.1.md +++ /dev/null @@ -1 +0,0 @@ -Improvement: Code cleanup and documentation in the conformance layer. diff --git a/changes/conformance/mr.3044.gl.md b/changes/conformance/mr.3044.gl.md deleted file mode 100644 index 34c2b057..00000000 --- a/changes/conformance/mr.3044.gl.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -- issue.1883.gl ---- -New test: Validate that `XrEventDataInteractionProfileChanged` is only queued during xrSyncActions using the conformance layer. diff --git a/changes/conformance/mr.3151.gl.md b/changes/conformance/mr.3151.gl.md deleted file mode 100644 index 870e79ad..00000000 --- a/changes/conformance/mr.3151.gl.md +++ /dev/null @@ -1 +0,0 @@ -Improvement: Reduce the maximum time allowed for transitioning session state in debug mode from 1 hour to 1 minute, and add a notice message in debug mode explaining this. diff --git a/src/conformance/conformance_layer/ConformanceHooks.h b/src/conformance/conformance_layer/ConformanceHooks.h index 3ab2a21c..aa18eb8a 100644 --- a/src/conformance/conformance_layer/ConformanceHooks.h +++ b/src/conformance/conformance_layer/ConformanceHooks.h @@ -40,6 +40,8 @@ struct ConformanceHooks : ConformanceHooksBase //XrResult xrDestroyInstance(XrInstance instance) override; XrResult xrPollEvent(XrInstance instance, XrEventDataBuffer* eventData) override; + XrResult xrGetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProperties* properties) override; + // Defined in Session.cpp XrResult xrCreateSession(XrInstance instance, const XrSessionCreateInfo* createInfo, XrSession* session) override; //XrResult xrDestroySession(XrSession session) override; @@ -107,7 +109,6 @@ struct ConformanceHooks : ConformanceHooksBase XrResult xrResultToString(XrInstance instance, XrResult value, char buffer[XR_MAX_RESULT_STRING_SIZE]) override; XrResult xrStructureTypeToString(XrInstance instance, XrStructureType value, char buffer[XR_MAX_STRUCTURE_NAME_SIZE]) override; XrResult xrGetSystem(XrInstance instance, const XrSystemGetInfo* getInfo, XrSystemId* systemId) override; - XrResult xrGetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProperties* properties) override; XrResult xrEnumerateEnvironmentBlendModes(XrInstance instance, XrSystemId systemId, XrViewConfigurationType viewConfigurationType, uint32_t environmentBlendModeCapacityInput, uint32_t* environmentBlendModeCountOutput, XrEnvironmentBlendMode* environmentBlendModes) override; XrResult xrGetReferenceSpaceBoundsRect(XrSession session, XrReferenceSpaceType referenceSpaceType, XrExtent2Df* bounds) override; XrResult xrEnumerateViewConfigurations(XrInstance instance, XrSystemId systemId, uint32_t viewConfigurationTypeCapacityInput, uint32_t* viewConfigurationTypeCountOutput, XrViewConfigurationType* viewConfigurationTypes) override; @@ -140,4 +141,5 @@ struct ConformanceHooks : ConformanceHooksBase void checkEventPayload(const XrEventDataVisibilityMaskChangedKHR* data); void checkEventPayload(const XrEventDataPerfSettingsEXT* data); void checkEventPayload(const XrEventDataSpatialAnchorCreateCompleteFB* data); + void checkEventPayload(const XrEventDataUserPresenceChangedEXT* data); }; diff --git a/src/conformance/conformance_layer/HandleState.cpp b/src/conformance/conformance_layer/HandleState.cpp index 93acaaf8..b9ef292d 100644 --- a/src/conformance/conformance_layer/HandleState.cpp +++ b/src/conformance/conformance_layer/HandleState.cpp @@ -92,8 +92,8 @@ HandleState* GetHandleState(HandleStateKey key) std::unique_lock lock(g_handleStatesMutex); auto it = g_handleStates.find(key); if (it == g_handleStates.end()) { - throw HandleException(std::string("Encountered unknown ") + to_string(key.second) + " handle with value " + - std::to_string(key.first)); + throw HandleNotFoundException(std::string("Encountered unknown ") + to_string(key.second) + " handle with value " + + std::to_string(key.first)); } return it->second.get(); } diff --git a/src/conformance/conformance_layer/HandleState.h b/src/conformance/conformance_layer/HandleState.h index 2ca99bd7..b7843c76 100644 --- a/src/conformance/conformance_layer/HandleState.h +++ b/src/conformance/conformance_layer/HandleState.h @@ -82,6 +82,13 @@ struct HandleException : public std::runtime_error } }; +struct HandleNotFoundException : public HandleException +{ + HandleNotFoundException(const std::string& message) : HandleException(message) + { + } +}; + using HandleStateKey = std::pair; void UnregisterHandleStateInternal(std::unique_lock& lockProof, HandleStateKey key); diff --git a/src/conformance/conformance_layer/Instance.cpp b/src/conformance/conformance_layer/Instance.cpp index 108491dc..f3a640a7 100644 --- a/src/conformance/conformance_layer/Instance.cpp +++ b/src/conformance/conformance_layer/Instance.cpp @@ -56,9 +56,15 @@ XrResult ConformanceHooks::xrPollEvent(XrInstance instance, XrEventDataBuffer* e // caveat: it is technically possible but unlikely that an entire xrSyncActions has happened // since this function forwarded the xrPollEvent call session::SyncActionsState exchangeIfState = session::SyncActionsState::CALLED_SINCE_QUEUE_EXHAUST; - customSessionState->syncActionsState.compare_exchange_strong( - exchangeIfState, session::SyncActionsState::NOT_CALLED_SINCE_QUEUE_EXHAUST, // - std::memory_order::memory_order_seq_cst, std::memory_order::memory_order_seq_cst); + customSessionState->syncActionsState.compare_exchange_strong(exchangeIfState, + session::SyncActionsState::NOT_CALLED_SINCE_QUEUE_EXHAUST, // +#if __cplusplus >= 202000L + std::memory_order_seq_cst, std::memory_order_seq_cst +#else + std::memory_order::memory_order_seq_cst, + std::memory_order::memory_order_seq_cst +#endif // __cpluscplus >= 202000L + ); } } @@ -165,3 +171,47 @@ void ConformanceHooks::checkEventPayload(const XrEventDataSpatialAnchorCreateCom (void)data; // Event data used in gen_dispatch.cpp } + +void ConformanceHooks::checkEventPayload(const XrEventDataUserPresenceChangedEXT* data) +{ + VALIDATE_EVENT_XRBOOL32(data->isUserPresent); +} + +XrResult ConformanceHooks::xrGetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProperties* properties) +{ + const XrResult result = ConformanceHooksBase::xrGetSystemProperties(instance, systemId, properties); + + if (result == XR_SUCCESS) { + // validate some structs? + auto nextStruct = properties->next; + + ForEachExtension(properties->next, [&](const XrBaseInStructure* ext) { + switch (ext->type) { + case XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT: { + auto testStruct = reinterpret_cast(ext); + // The runtime is only required to validate this if EXT_hand_tracking is enabled + // but we don't supply invalid values for this in our tests so this is a + // a reasonable thing to check here. + VALIDATE_EVENT_XRBOOL32(testStruct->supportsHandTracking); + break; + } + case XR_TYPE_SYSTEM_USER_PRESENCE_PROPERTIES_EXT: { + // The runtime is only required to validate this if EXT_hand_tracking is enabled + // but we don't supply invalidate values for this in our tests so this is a + // a reasonable thing to check here. + + auto testStruct = reinterpret_cast(ext); + // The runtime is only required to validate this if EXT_user_presence is enabled + // but we don't supply invalid values for this in the tests so this is a + // a reasonable thing to check here. + VALIDATE_EVENT_XRBOOL32(testStruct->supportsUserPresence); + break; + } + default: + break; + } + }); + } + + return result; +} diff --git a/src/conformance/conformance_layer/Session.cpp b/src/conformance/conformance_layer/Session.cpp index 01962fb7..50d2a497 100644 --- a/src/conformance/conformance_layer/Session.cpp +++ b/src/conformance/conformance_layer/Session.cpp @@ -132,7 +132,13 @@ namespace session session::CustomSessionState* const customSessionState = GetCustomSessionState(interactionProfileChanged->session); // Cannot clear here because you may have gotten several of these events queued. // Not very useful, but the spec doesn't forbid it. - session::SyncActionsState syncActionsState = customSessionState->syncActionsState.load(std::memory_order::memory_order_seq_cst); + session::SyncActionsState syncActionsState = customSessionState->syncActionsState.load( +#if __cplusplus >= 202000L + std::memory_order_seq_cst +#else + std::memory_order::memory_order_seq_cst +#endif // __cpluscplus >= 202000L + ); if (syncActionsState == SyncActionsState::NOT_CALLED_SINCE_QUEUE_EXHAUST) { conformanceHooks->ConformanceFailure( XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT, "xrPollEvent", diff --git a/src/conformance/conformance_test/readme.md b/src/conformance/conformance_test/readme.md index d4d45a1f..cf00a543 100644 --- a/src/conformance/conformance_test/readme.md +++ b/src/conformance/conformance_test/readme.md @@ -320,7 +320,8 @@ Conformance Criteria A conformance run is considered passing if all tests finish with allowed result codes, and all warnings are acceptably explained to describe why they are not a -conformance failure. Test results are contained in the output XML files, which +conformance failure. XR_EXT_conformance_automation may not be used for conformance +submission. Test results are contained in the output XML files, which are an extension of the common "*Unit" schema with some custom elements. Each test case leaf section is reached by a run of its own, and is recorded with a `testcase` tag, e.g.: diff --git a/src/conformance/conformance_test/test_InteractiveThrow.cpp b/src/conformance/conformance_test/test_InteractiveThrow.cpp index 4570eab9..d05929ec 100644 --- a/src/conformance/conformance_test/test_InteractiveThrow.cpp +++ b/src/conformance/conformance_test/test_InteractiveThrow.cpp @@ -17,6 +17,7 @@ #include "common/xr_linear.h" #include "composition_utils.h" #include "conformance_framework.h" +#include "utilities/ballistics.h" #include "utilities/throw_helpers.h" #include "utilities/utils.h" @@ -150,14 +151,7 @@ namespace Conformance throwSpaces.push_back(std::move(handThrowSpaces)); } - struct ThrownCube - { - XrSpaceVelocity velocity; // Velocity of space that was captured when a throw happened. - XrPosef pose; - XrTime updateTime; - XrTime createTime; - }; - std::vector thrownCubes; + std::vector thrownCubes; // Three fixed cubes which must be reached by the thrown cubes to pass the test. std::vector targetCubes{{-1, -1, -3.0f}, {1, -1, -4.0f}, {0, 1.0f, -5.0f}}; @@ -202,39 +196,8 @@ namespace Conformance ++it; } - // Apply velocities to thrown cubes. - auto simulateThrownCubeAtTime = [](ThrownCube& thrownCube, XrTime predictedDisplayTime) { - const XrDuration timeSinceLastTick = predictedDisplayTime - thrownCube.updateTime; - CHECK_MSG(timeSinceLastTick > 0, "Unexpected old frame state predictedDisplayTime or future action state lastChangeTime"); - thrownCube.updateTime = predictedDisplayTime; - - const float secondSinceLastTick = timeSinceLastTick / (float)1'000'000'000; - - // Apply gravity to velocity. - thrownCube.velocity.linearVelocity.y += -9.8f * secondSinceLastTick; - - // Apply velocity to position. - XrVector3f deltaVelocity; - XrVector3f_Scale(&deltaVelocity, &thrownCube.velocity.linearVelocity, secondSinceLastTick); - XrVector3f_Add(&thrownCube.pose.position, &thrownCube.pose.position, &deltaVelocity); - - // Convert angular velocity to quaternion with the appropriate amount of rotation for the delta time. - XrQuaternionf angularRotation; - { - const float radiansPerSecond = XrVector3f_Length(&thrownCube.velocity.angularVelocity); - XrVector3f angularAxis = thrownCube.velocity.angularVelocity; - XrVector3f_Normalize(&angularAxis); - XrQuaternionf_CreateFromAxisAngle(&angularRotation, &angularAxis, radiansPerSecond * secondSinceLastTick); - } - - // Update the orientation given the computed angular rotation. - XrQuaternionf newOrientation; - XrQuaternionf_Multiply(&newOrientation, &thrownCube.pose.orientation, &angularRotation); - thrownCube.pose.orientation = newOrientation; - }; - - for (ThrownCube& thrownCube : thrownCubes) { - simulateThrownCubeAtTime(thrownCube, frameState.predictedDisplayTime); + for (BodyInMotion& thrownCube : thrownCubes) { + thrownCube.doSimulationStep({0.f, -9.8f, 0.f}, frameState.predictedDisplayTime); cubes.push_back({thrownCube.pose, activateCubeScale}); // Remove any target cubes which are hit by the thrown cube. @@ -279,11 +242,11 @@ namespace Conformance // Draw an instantaneous indication of the linear & angular velocity if (spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { auto gnomonTime = frameState.predictedDisplayTime; - ThrownCube gnomon{spaceVelocity, spaceLocation.pose, gnomonTime, gnomonTime}; + BodyInMotion gnomon{spaceVelocity, spaceLocation.pose, gnomonTime, gnomonTime}; for (int step = 1; step < 20; ++step) { auto predictedDisplayTimeAtStep = frameState.predictedDisplayTime + step * frameState.predictedDisplayPeriod; - simulateThrownCubeAtTime(gnomon, predictedDisplayTimeAtStep); + gnomon.doSimulationStep({0.f, -9.8f, 0.f}, predictedDisplayTimeAtStep); meshes.push_back(MeshDrawable{gnomonMesh, gnomon.pose, gnomonScale}); } } @@ -299,8 +262,8 @@ namespace Conformance releaseSpaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT && releaseSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT && releaseSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { - thrownCubes.emplace_back(ThrownCube{releaseSpaceVelocity, releaseSpaceLocation.pose, - boolState.lastChangeTime, boolState.lastChangeTime}); + thrownCubes.emplace_back(BodyInMotion{releaseSpaceVelocity, releaseSpaceLocation.pose, + boolState.lastChangeTime, boolState.lastChangeTime}); } } } diff --git a/src/conformance/conformance_test/test_SpaceOffsets.cpp b/src/conformance/conformance_test/test_SpaceOffsets.cpp new file mode 100644 index 00000000..568413f3 --- /dev/null +++ b/src/conformance/conformance_test/test_SpaceOffsets.cpp @@ -0,0 +1,605 @@ +// Copyright (c) 2019-2024, The Khronos Group Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "catch2/catch_approx.hpp" +#include "catch2/catch_message.hpp" +#include "common/xr_linear.h" +#include "composition_utils.h" +#include "conformance_framework.h" +#include "utilities/ballistics.h" +#include "utilities/throw_helpers.h" +#include "utilities/types_and_constants.h" + +#include +#include + +#include +#include +#include +#include +#include + +using namespace Conformance; + +namespace Conformance +{ + constexpr XrVector3f Up{0, 1, 0}; + + // Calculate the correct XrSpaceVelocity for a space which is rigidly attached to another space via a known pose offset + XrSpaceVelocity adjustVelocitiesForPose(XrSpaceLocation locationWithoutOffset, XrSpaceVelocity velocityWithoutOffset, + XrPosef relativePose) + { + XrSpaceVelocity adjustedVelocity = {XR_TYPE_SPACE_VELOCITY}; + + if (velocityWithoutOffset.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { + adjustedVelocity.velocityFlags |= XR_SPACE_VELOCITY_LINEAR_VALID_BIT; + XrVector3f_Add(&adjustedVelocity.linearVelocity, &adjustedVelocity.linearVelocity, &velocityWithoutOffset.linearVelocity); + } + + if (velocityWithoutOffset.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT) { + adjustedVelocity.velocityFlags |= XR_SPACE_VELOCITY_ANGULAR_VALID_BIT; + // Can't easily add angular velocities, and there's only one, so apply directly to result. + adjustedVelocity.angularVelocity = velocityWithoutOffset.angularVelocity; + } + + if (velocityWithoutOffset.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT) { + adjustedVelocity.velocityFlags |= XR_SPACE_VELOCITY_ANGULAR_VALID_BIT; + adjustedVelocity.velocityFlags |= XR_SPACE_VELOCITY_LINEAR_VALID_BIT; + XrVector3f leverArmVelocity; + + XrVector3f leverArmInBaseSpace; + XrQuaternionf_RotateVector3f(&leverArmInBaseSpace, &locationWithoutOffset.pose.orientation, &relativePose.position); + + XrVector3f_Cross(&leverArmVelocity, &velocityWithoutOffset.angularVelocity, &leverArmInBaseSpace); + XrVector3f_Add(&adjustedVelocity.linearVelocity, &adjustedVelocity.linearVelocity, &leverArmVelocity); + } + + // velocities are in base space reference frame, so they do not need to be rotated based on the pose of the space + + return adjustedVelocity; + }; + + // Purpose: Verify that the linear and angular velocities returned by the runtime are self-consistent, + // and that spaces offset from pose actions display correct behavior with pose and velocities. + TEST_CASE("SpaceOffsets", "[scenario][interactive][no_auto]") + { + const char* instructions = + "Wave the controller(s) around. To freeze time, press [select]." + " The red-tint gnomons (runtime-reported velocities) should match" + " the green-tint gnomons (calculated by the CTS).\n\n" + "The test will automatically pass when the following criteria are met:"; + // the remainder of the instructions are populated based on `criteria`. + + const char* failureInstructions = + "The test has failed. The failing state is shown frozen in time." + " For debugging, you may press [select] to un-freeze time until another failure is detected." + " Press [menu] when you are ready to end the test.\n\n" + "The paths of the space pose that exceeded the failure thresholds are not greyed out:" + " The red/green/blue gnomons are past poses. The red and cyan tinted trails" + " are future poses based on the runtime-provided and CTS-calculated velocities" + " respectively. Failure here suggests that either your reported angular velocities" + " or your velocity calculations for offset spaces are incorrect."; + + struct SuccessCriterion + { + const char* description; + XrVector3f linearVelocityComponent; + float linearVelocityMagnitude; + XrVector3f angularVelocityComponent; + float angularVelocityMagnitude; + bool satisfied; + }; + + SuccessCriterion criteria[] = { + {"X linear velocity", {1.0f, 0.0f, 0.0f}, 0.5f, {}, 0}, // + {"Y linear velocity", {0.0f, 1.0f, 0.0f}, 0.5f, {}, 0}, // + {"Z linear velocity", {0.0f, 0.0f, 1.0f}, 0.5f, {}, 0}, // + {"X angular velocity", {}, 0, {1.0f, 0.0f, 0.0f}, 6.0f}, // + {"Y angular velocity", {}, 0, {0.0f, 1.0f, 0.0f}, 6.0f}, // + {"Z angular velocity", {}, 0, {0.0f, 0.0f, 1.0f}, 6.0f}, // + }; + + CompositionHelper compositionHelper("Space Offsets"); + + const XrSpace localSpace = compositionHelper.CreateReferenceSpace(XR_REFERENCE_SPACE_TYPE_LOCAL, XrPosefCPP{}); + + // Set up composition projection layer and swapchains (one swapchain per view). + std::vector swapchains; + XrCompositionLayerProjection* const projLayer = compositionHelper.CreateProjectionLayer(localSpace); + { + const std::vector viewProperties = compositionHelper.EnumerateConfigurationViews(); + for (uint32_t j = 0; j < projLayer->viewCount; j++) { + const XrSwapchain swapchain = compositionHelper.CreateSwapchain(compositionHelper.DefaultColorSwapchainCreateInfo( + viewProperties[j].recommendedImageRectWidth, viewProperties[j].recommendedImageRectHeight)); + const_cast(projLayer->views[j].subImage) = compositionHelper.MakeDefaultSubImage(swapchain, 0); + swapchains.push_back(swapchain); + } + } + + const std::vector subactionPaths{StringToPath(compositionHelper.GetInstance(), "/user/hand/left"), + StringToPath(compositionHelper.GetInstance(), "/user/hand/right")}; + XrActionSet actionSet; + XrAction freezeAction, failAction, gripPoseAction; + { + XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; + strcpy(actionSetInfo.actionSetName, "interaction_test"); + strcpy(actionSetInfo.localizedActionSetName, "Interaction Test"); + XRC_CHECK_THROW_XRCMD(xrCreateActionSet(compositionHelper.GetInstance(), &actionSetInfo, &actionSet)); + + XrActionCreateInfo actionInfo{XR_TYPE_ACTION_CREATE_INFO}; + actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + strcpy(actionInfo.actionName, "complete_test"); + strcpy(actionInfo.localizedActionName, "Complete test"); + XRC_CHECK_THROW_XRCMD(xrCreateAction(actionSet, &actionInfo, &failAction)); + + // Remainder of actions use subaction. + actionInfo.subactionPaths = subactionPaths.data(); + actionInfo.countSubactionPaths = (uint32_t)subactionPaths.size(); + + actionInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + strcpy(actionInfo.actionName, "freeze"); + strcpy(actionInfo.localizedActionName, "Freeze time"); + XRC_CHECK_THROW_XRCMD(xrCreateAction(actionSet, &actionInfo, &freezeAction)); + + actionInfo.actionType = XR_ACTION_TYPE_POSE_INPUT; + strcpy(actionInfo.actionName, "grip_pose"); + strcpy(actionInfo.localizedActionName, "Grip pose"); + actionInfo.subactionPaths = subactionPaths.data(); + actionInfo.countSubactionPaths = (uint32_t)subactionPaths.size(); + XRC_CHECK_THROW_XRCMD(xrCreateAction(actionSet, &actionInfo, &gripPoseAction)); + } + + const std::vector bindings = { + {freezeAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/left/input/select/click")}, + {freezeAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/right/input/select/click")}, + {failAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/left/input/menu/click")}, + {failAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/right/input/menu/click")}, + {gripPoseAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/left/input/grip/pose")}, + {gripPoseAction, StringToPath(compositionHelper.GetInstance(), "/user/hand/right/input/grip/pose")}, + }; + + XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; + suggestedBindings.interactionProfile = StringToPath(compositionHelper.GetInstance(), "/interaction_profiles/khr/simple_controller"); + suggestedBindings.suggestedBindings = bindings.data(); + suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); + XRC_CHECK_THROW_XRCMD(xrSuggestInteractionProfileBindings(compositionHelper.GetInstance(), &suggestedBindings)); + + XrSessionActionSetsAttachInfo attachInfo{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO}; + attachInfo.actionSets = &actionSet; + attachInfo.countActionSets = 1; + XRC_CHECK_THROW_XRCMD(xrAttachSessionActionSets(compositionHelper.GetSession(), &attachInfo)); + + compositionHelper.BeginSession(); + + // Create the instructional quad layer placed to the left. + + XrCompositionLayerQuad* instructionsQuad = nullptr; + auto updateInstructions = [&](bool failed) { + if (instructionsQuad != nullptr && instructionsQuad->subImage.swapchain != XR_NULL_HANDLE) { + compositionHelper.DestroySwapchain(instructionsQuad->subImage.swapchain); + } + std::ostringstream oss; + if (failed) { + oss << failureInstructions << '\n'; + } + else { + oss << instructions << '\n'; + for (const SuccessCriterion& criterion : criteria) { + oss << "[" << (criterion.satisfied ? "x" : " ") << "] " << criterion.description << '\n'; + } + } + instructionsQuad = compositionHelper.CreateQuadLayer( + compositionHelper.CreateStaticSwapchainImage(CreateTextImage(1024, 780, oss.str().c_str(), 48)), localSpace, 1, + {Quat::Identity, {-1.5f, 0, -0.3f}}); + XrQuaternionf_CreateFromAxisAngle(&instructionsQuad->pose.orientation, &Up, 70 * MATH_PI / 180); + }; + updateInstructions(false); + + // Spaces attached to the hand (subaction). + struct HandSpace + { + XrPosef poseInActionSpace; + XrSpace space; + std::deque pastPosesInLocalSpace; + XrSpaceLocation lastReportedLocation{XR_TYPE_SPACE_LOCATION}; + XrSpaceVelocity lastReportedVelocity{XR_TYPE_SPACE_VELOCITY}; + XrSpaceLocation lastPredictedLocation{XR_TYPE_SPACE_LOCATION}; + XrSpaceVelocity lastPredictedVelocity{XR_TYPE_SPACE_VELOCITY}; + + bool failed; // for visualisation only + + HandSpace(XrPosef poseInActionSpace, XrSpace space) : poseInActionSpace(poseInActionSpace), space(space){}; + }; + + // Spaces attached to the hand (subaction). + struct HandSpaces + { + XrPath subactionPath; + XrSpace spaceWithoutOffset; + std::vector spaces; + }; + std::vector spaces; + + // Create XrSpaces at various spaces around the grip poses. + + XrPosef handRelativePoses[] = { + XrPosefCPP(), + {Quat::FromAxisAngle({1, 0, 0}, Math::DegToRad(135)), {0, 0, 0}}, + {Quat::FromAxisAngle({1, 0, 0}, Math::DegToRad(45)), {0.25, 0, 0}}, + {Quat::FromAxisAngle({1, 0, 0}, Math::DegToRad(45)), {-0.25, 0, 0}}, + {Quat::FromAxisAngle({1, 0, 0}, Math::DegToRad(30)), {0, 0, -0.25}}, + {Quat::Identity, {0, 0, -0.5}}, + {Quat::FromAxisAngle({1, 1, 1}, Math::DegToRad(127)), {-0.25, 0, -0.5}}, + {Quat::FromAxisAngle({1, -1, -1}, Math::DegToRad(38)), {0.25, 0, -0.5}}, + }; + + for (XrPath subactionPath : subactionPaths) { + HandSpaces handSpaces; + handSpaces.subactionPath = subactionPath; + + XrActionSpaceCreateInfo spaceCreateInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; + spaceCreateInfo.action = gripPoseAction; + spaceCreateInfo.subactionPath = subactionPath; + + spaceCreateInfo.poseInActionSpace = XrPosefCPP(); + XRC_CHECK_THROW_XRCMD(xrCreateActionSpace(compositionHelper.GetSession(), &spaceCreateInfo, &handSpaces.spaceWithoutOffset)); + + for (XrPosef pose : handRelativePoses) { + spaceCreateInfo.poseInActionSpace = pose; + XrSpace handSpace; + XRC_CHECK_THROW_XRCMD(xrCreateActionSpace(compositionHelper.GetSession(), &spaceCreateInfo, &handSpace)); + handSpaces.spaces.emplace_back(pose, handSpace); + } + spaces.push_back(std::move(handSpaces)); + } + + constexpr XrVector3f gnomonScale{0.025f, 0.025f, 0.025f}; + constexpr XrColor4f reportedGnomonTint{1.0f, 0.0f, 0.0f, 0.5f}; + constexpr XrColor4f predictedGnomonTint{0.0f, 1.0f, 0.0f, 0.5f}; + constexpr XrVector3f liveCubeScale{0.05f, 0.05f, 0.05f}; + + MeshHandle pastGnomonMesh = GetGlobalData().graphicsPlugin->MakeGnomonMesh(1.0f, 0.1f); + MeshHandle predictedGnomonMesh = GetGlobalData().graphicsPlugin->MakeGnomonMesh(0.9f, 0.1f); + MeshHandle reportedGnomonMesh = GetGlobalData().graphicsPlugin->MakeGnomonMesh(1.0f, 0.08f); + + bool testFailed = false; + bool frozen = false; + bool postFailureUnfreeze = false; + bool updatedSinceLastFailure = false; + + auto update = [&](const XrFrameState& frameState) { + std::vector cubes; + std::vector meshes; + + const std::array activeActionSets = {{{actionSet, XR_NULL_PATH}}}; + XrActionsSyncInfo syncInfo{XR_TYPE_ACTIONS_SYNC_INFO}; + syncInfo.activeActionSets = activeActionSets.data(); + syncInfo.countActiveActionSets = (uint32_t)activeActionSets.size(); + XRC_CHECK_THROW_XRCMD(xrSyncActions(compositionHelper.GetSession(), &syncInfo)); + + // Check if user has requested to fail the test. + { + XrActionStateGetInfo completeActionGetInfo{XR_TYPE_ACTION_STATE_GET_INFO}; + completeActionGetInfo.action = failAction; + XrActionStateBoolean completeActionState{XR_TYPE_ACTION_STATE_BOOLEAN}; + XRC_CHECK_THROW_XRCMD( + xrGetActionStateBoolean(compositionHelper.GetSession(), &completeActionGetInfo, &completeActionState)); + if (completeActionState.currentState == XR_TRUE && completeActionState.changedSinceLastSync) { + testFailed = true; + return false; + } + } + + XrActionStateGetInfo freezeActionGetInfo{XR_TYPE_ACTION_STATE_GET_INFO}; + freezeActionGetInfo.action = freezeAction; + XrActionStateBoolean freezeActionState{XR_TYPE_ACTION_STATE_BOOLEAN}; + XRC_CHECK_THROW_XRCMD(xrGetActionStateBoolean(compositionHelper.GetSession(), &freezeActionGetInfo, &freezeActionState)); + + if (testFailed) { + postFailureUnfreeze = freezeActionState.currentState; + if (freezeActionState.currentState) { + frozen = false; + } + } + else if (freezeActionState.changedSinceLastSync) { + frozen = freezeActionState.currentState; + } + + if (!frozen) { + // Locate space without offset and each offset space. Calculate linear and angular velocities based on moment arm, + // and check that runtime-provided values are close to the ones we calculated ourselves. + bool frameFailed = false; + updatedSinceLastFailure = true; + + for (auto& subactionSpaces : spaces) { + for (HandSpace& space : subactionSpaces.spaces) { + space.failed = false; + CAPTURE(space.poseInActionSpace); + + // xrLocateSpace on the base space every time to get up to date values + XrSpaceVelocity velocityWithoutOffset[2] = {{XR_TYPE_SPACE_VELOCITY}, {XR_TYPE_SPACE_VELOCITY}}; + XrSpaceLocation locationWithoutOffset[2] = {{XR_TYPE_SPACE_LOCATION, &velocityWithoutOffset[0]}, + {XR_TYPE_SPACE_LOCATION, &velocityWithoutOffset[1]}}; + XRC_CHECK_THROW_XRCMD(xrLocateSpace(subactionSpaces.spaceWithoutOffset, localSpace, frameState.predictedDisplayTime, + &locationWithoutOffset[0])); + + XrSpaceVelocity spaceVelocity{XR_TYPE_SPACE_VELOCITY}; + XrSpaceLocation spaceLocation{XR_TYPE_SPACE_LOCATION, &spaceVelocity}; + XRC_CHECK_THROW_XRCMD(xrLocateSpace(space.space, localSpace, frameState.predictedDisplayTime, &spaceLocation)); + + XRC_CHECK_THROW_XRCMD(xrLocateSpace(subactionSpaces.spaceWithoutOffset, localSpace, frameState.predictedDisplayTime, + &locationWithoutOffset[1])); + + // run the checks once to see if the space fails with both no-offset locates. + bool dryRun = true; + bool failed[2] = {false, false}; + const char* withoutOffsetWasCalled[2] = {"before", "after"}; + for (int i = 0; i < 2; ++i) { + CAPTURE(withoutOffsetWasCalled[i]); + space.lastReportedLocation = spaceLocation; + space.lastReportedVelocity = spaceVelocity; + if (spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + if (space.pastPosesInLocalSpace.size() >= 8) { + space.pastPosesInLocalSpace.pop_back(); + } + space.pastPosesInLocalSpace.push_front(spaceLocation.pose); + } + + XrSpaceVelocity predictedVelocity = + adjustVelocitiesForPose(locationWithoutOffset[i], velocityWithoutOffset[i], space.poseInActionSpace); + + if (locationWithoutOffset[i].locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + space.lastPredictedLocation = locationWithoutOffset[i]; + XrPosef_Multiply(&space.lastPredictedLocation.pose, &locationWithoutOffset[i].pose, + &space.poseInActionSpace); + + space.lastPredictedVelocity = predictedVelocity; + } + + CAPTURE(locationWithoutOffset[i].pose); + CAPTURE(space.lastPredictedLocation.pose); + CAPTURE(spaceLocation.pose); + + CAPTURE(velocityWithoutOffset[i].linearVelocity); + CAPTURE(predictedVelocity.linearVelocity); + CAPTURE(spaceVelocity.linearVelocity); + CAPTURE(velocityWithoutOffset[i].angularVelocity); + CAPTURE(predictedVelocity.angularVelocity); + CAPTURE(spaceVelocity.angularVelocity); + + CAPTURE(XrVector3f_Length(&spaceVelocity.linearVelocity)); + CAPTURE(XrVector3f_Length(&predictedVelocity.linearVelocity)); + + XrVector3f predictedLeverArmVelocity; + XrVector3f reportedLeverArmVelocity; + XrVector3f_Sub(&predictedLeverArmVelocity, // + &predictedVelocity.linearVelocity, &velocityWithoutOffset[i].linearVelocity); + XrVector3f_Sub(&reportedLeverArmVelocity, // + &spaceVelocity.linearVelocity, &velocityWithoutOffset[i].linearVelocity); + + CAPTURE(XrVector3f_Length(&predictedLeverArmVelocity)); + CAPTURE(XrVector3f_Length(&reportedLeverArmVelocity)); + + CAPTURE(XrVector3f_Length(&spaceVelocity.angularVelocity)); + CAPTURE(XrVector3f_Length(&predictedVelocity.angularVelocity)); + + bool anyVelocityInvalid = ~(velocityWithoutOffset[i].velocityFlags & spaceVelocity.velocityFlags) & + (XR_SPACE_VELOCITY_ANGULAR_VALID_BIT | XR_SPACE_VELOCITY_LINEAR_VALID_BIT); + if (!anyVelocityInvalid) { +#define CHECK_WITH_SET_FAILED(...) \ + failed[i] |= !(__VA_ARGS__); \ + if (!dryRun && !postFailureUnfreeze) { \ + CHECK(__VA_ARGS__); \ + } + // constants subject to adjustment based on errors found in correct runtimes + { + constexpr float pm = 0.01f; // 10mm + constexpr float pe = 0.1f; // 10% error is always tolerated + CHECK_WITH_SET_FAILED( + spaceLocation.pose.position.x == + Catch::Approx(space.lastPredictedLocation.pose.position.x).margin(pm).epsilon(pe)); + CHECK_WITH_SET_FAILED( + spaceLocation.pose.position.y == + Catch::Approx(space.lastPredictedLocation.pose.position.y).margin(pm).epsilon(pe)); + CHECK_WITH_SET_FAILED( + spaceLocation.pose.position.z == + Catch::Approx(space.lastPredictedLocation.pose.position.z).margin(pm).epsilon(pe)); + + constexpr float rm = 0.05f; // five percentiles + constexpr float re = 0.1f; // 10% error is always tolerated + // Quaternions that have the same value but opposite sign on all components are considered equal. + // This does prevent the assertion from having a nice message, but relevant data is CAPTUREd above. + CHECK_WITH_SET_FAILED( + ((spaceLocation.pose.orientation.x == + Catch::Approx(space.lastPredictedLocation.pose.orientation.x).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.y == + Catch::Approx(space.lastPredictedLocation.pose.orientation.y).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.z == + Catch::Approx(space.lastPredictedLocation.pose.orientation.z).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.w == + Catch::Approx(space.lastPredictedLocation.pose.orientation.w).margin(rm).epsilon(re)) || + (spaceLocation.pose.orientation.x == + Catch::Approx(-space.lastPredictedLocation.pose.orientation.x).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.y == + Catch::Approx(-space.lastPredictedLocation.pose.orientation.y).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.z == + Catch::Approx(-space.lastPredictedLocation.pose.orientation.z).margin(rm).epsilon(re) && + spaceLocation.pose.orientation.w == + Catch::Approx(-space.lastPredictedLocation.pose.orientation.w).margin(rm).epsilon(re)))); + } + { + constexpr float am = 0.1f; // 0.1 radians/sec + constexpr float ae = 0.1f; // 10% error is always tolerated + CHECK_WITH_SET_FAILED(spaceVelocity.angularVelocity.x == + Catch::Approx(predictedVelocity.angularVelocity.x).margin(am).epsilon(ae)); + CHECK_WITH_SET_FAILED(spaceVelocity.angularVelocity.y == + Catch::Approx(predictedVelocity.angularVelocity.y).margin(am).epsilon(ae)); + CHECK_WITH_SET_FAILED(spaceVelocity.angularVelocity.z == + Catch::Approx(predictedVelocity.angularVelocity.z).margin(am).epsilon(ae)); + } + { + float angularSpeed = XrVector3f_Length(&spaceVelocity.angularVelocity); + + float lm = 0.01f // 10 mm/s + + angularSpeed * 0.20f; // + lever arm speed at 20cm (~40% of lever arm effect at 50cm) + constexpr float le = 0.1f; // 10% error is always tolerated + CHECK_WITH_SET_FAILED(spaceVelocity.linearVelocity.x == + Catch::Approx(predictedVelocity.linearVelocity.x).margin(lm).epsilon(le)); + CHECK_WITH_SET_FAILED(spaceVelocity.linearVelocity.y == + Catch::Approx(predictedVelocity.linearVelocity.y).margin(lm).epsilon(le)); + CHECK_WITH_SET_FAILED(spaceVelocity.linearVelocity.z == + Catch::Approx(predictedVelocity.linearVelocity.z).margin(lm).epsilon(le)); + } +#undef CHECK_WITH_SET_FAILED + // Only update criteria if predictions were successful, to be safe + if (!testFailed && !failed[i]) { + for (SuccessCriterion& criterion : criteria) { + if (criterion.satisfied) { + continue; + } + float linearMagnitude = std::abs( + XrVector3f_Dot(&velocityWithoutOffset->linearVelocity, &criterion.linearVelocityComponent)); + bool linearSatisfied = linearMagnitude >= criterion.linearVelocityMagnitude; + float angularMagnitude = std::abs( + XrVector3f_Dot(&velocityWithoutOffset->angularVelocity, &criterion.angularVelocityComponent)); + bool angularSatisfied = angularMagnitude >= criterion.angularVelocityMagnitude; + if (linearSatisfied && angularSatisfied) { + criterion.satisfied = true; + + bool allSatisfied = true; + for (const SuccessCriterion& otherCriterion : criteria) { + if (!otherCriterion.satisfied) { + allSatisfied = false; + break; + } + } + if (allSatisfied) { + // Test has completed successfully + return false; + } + updateInstructions(testFailed); + } + } + } + } + + // reset the loop, actually calling CHECK this time + if (dryRun && failed[0] && failed[1]) { + dryRun = false; + i = 0; + space.failed = true; + frameFailed = true; + if (!testFailed) { + updateInstructions(true); + } + testFailed = true; + } + } + + if (spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + cubes.push_back(Cube{spaceLocation.pose, liveCubeScale}); + } + } + } + + if (frameFailed) { + frozen = true; + updatedSinceLastFailure = false; + } + } + + for (auto& subactionSpaces : spaces) { + for (HandSpace& space : subactionSpaces.spaces) { + auto dimNonFailed = [&](XrColor4f tint = {0, 0, 0, 0}) { + if (testFailed && !updatedSinceLastFailure && !space.failed) { + return XrColor4f{0.3f, 0.3f, 0.3f, 0.9f}; + } + return tint; + }; + for (const auto& pastPose : space.pastPosesInLocalSpace) { + meshes.push_back(MeshDrawable{pastGnomonMesh, pastPose, gnomonScale, dimNonFailed()}); + } + + struct predictionTrail + { + MeshHandle mesh; + XrSpaceLocation spaceLocation; + XrSpaceVelocity spaceVelocity; + XrColor4f tint; + }; + predictionTrail trails[] = { + {reportedGnomonMesh, space.lastReportedLocation, space.lastReportedVelocity, reportedGnomonTint}, + {predictedGnomonMesh, space.lastPredictedLocation, space.lastPredictedVelocity, predictedGnomonTint}, + }; + for (predictionTrail& trail : trails) { + // Draw an instantaneous indication of the linear & angular velocity + if (trail.spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT && + trail.spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { + auto gnomonTime = frameState.predictedDisplayTime; + BodyInMotion gnomon{trail.spaceVelocity, trail.spaceLocation.pose, gnomonTime, gnomonTime}; + for (int step = 1; step < 20; ++step) { + auto predictedDisplayTimeAtStep = + frameState.predictedDisplayTime + step * frameState.predictedDisplayPeriod; + gnomon.doSimulationStep({0.f, 0.f, 0.f}, predictedDisplayTimeAtStep); + meshes.push_back(MeshDrawable{ + trail.mesh, + gnomon.pose, + gnomonScale, + dimNonFailed(trail.tint), + }); + } + } + } + } + } + + auto viewData = compositionHelper.LocateViews(localSpace, frameState.predictedDisplayTime); + const auto& viewState = std::get(viewData); + + std::vector layers; + if (viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT && + viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { + const auto& views = std::get>(viewData); + + // Render into each view port of the wide swapchain using the projection layer view fov and pose. + for (size_t view = 0; view < views.size(); view++) { + compositionHelper.AcquireWaitReleaseImage(swapchains[view], [&](const XrSwapchainImageBaseHeader* swapchainImage) { + GetGlobalData().graphicsPlugin->ClearImageSlice(swapchainImage); + const_cast(projLayer->views[view].fov) = views[view].fov; + const_cast(projLayer->views[view].pose) = views[view].pose; + GetGlobalData().graphicsPlugin->RenderView(projLayer->views[view], swapchainImage, + RenderParams().Draw(cubes).Draw(meshes)); + }); + } + + layers.push_back({reinterpret_cast(projLayer)}); + } + + layers.push_back({reinterpret_cast(instructionsQuad)}); + + compositionHelper.EndFrame(frameState.predictedDisplayTime, layers); + + return compositionHelper.PollEvents(); + }; + + RenderLoop(compositionHelper.GetSession(), update).Loop(); + + // The render loop will end if the user waves the controller or if the user presses menu. + if (testFailed) { + FAIL("User has failed the test"); + } + } +} // namespace Conformance diff --git a/src/conformance/conformance_test/test_XR_EXT_user_presence.cpp b/src/conformance/conformance_test/test_XR_EXT_user_presence.cpp new file mode 100644 index 00000000..99d9644b --- /dev/null +++ b/src/conformance/conformance_test/test_XR_EXT_user_presence.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2019-2024, The Khronos Group Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "utilities/utils.h" +#include "conformance_framework.h" +#include "conformance_utils.h" +#include "utilities/system_properties_helper.h" +#include +#include +#include + +using namespace Conformance; + +namespace Conformance +{ + static const auto SystemSupportsUserPresence = + MakeSystemPropertiesBoolChecker(XrSystemUserPresencePropertiesEXT{XR_TYPE_SYSTEM_USER_PRESENCE_PROPERTIES_EXT}, + &XrSystemUserPresencePropertiesEXT::supportsUserPresence); + + TEST_CASE("XR_EXT_user_presence", "[XR_EXT_user_presence]") + { + GlobalData& globalData = GetGlobalData(); + if (!globalData.IsInstanceExtensionSupported(XR_EXT_USER_PRESENCE_EXTENSION_NAME)) { + SKIP(XR_EXT_USER_PRESENCE_EXTENSION_NAME " not supported"); + } + + AutoBasicInstance instance({XR_EXT_USER_PRESENCE_EXTENSION_NAME}, AutoBasicInstance::createSystemId); + + if (!SystemSupportsUserPresence(instance, instance.systemId)) { + // If the system does not support user presence sensing, the runtime must: + // return ename:XR_FALSE for pname:supportsUserPresence and must: not queue the + // slink:XrEventDataUserPresenceChangedEXT event for any session on this + // system. + + SKIP("System does not support user presence sensing."); + } + + AutoBasicSession session(AutoBasicSession::createSession, instance); + + FrameIterator frameIterator(&session); + frameIterator.RunToSessionState(XR_SESSION_STATE_READY); + + XrSessionBeginInfo sessionBeginInfo{XR_TYPE_SESSION_BEGIN_INFO}; + sessionBeginInfo.primaryViewConfigurationType = GetGlobalData().GetOptions().viewConfigurationValue; + REQUIRE(XR_SUCCESS == xrBeginSession(session, &sessionBeginInfo)); + + // The runtime must: queue this event upon a successful call to the + // flink:xrBeginSession function, regardless of the value of + // pname:isUserPresent, so that the application can be in sync on the state + // when a session begins running. + + bool foundUserPresenceEvent = false; + constexpr std::chrono::nanoseconds duration = 1s; + + CountdownTimer countdown(duration); + XrResult pollResult = XR_SUCCESS; + while (!countdown.IsTimeUp() && pollResult == XR_SUCCESS) { + XrEventDataBuffer eventData{XR_TYPE_EVENT_DATA_BUFFER}; + std::fill(eventData.varying, eventData.varying + sizeof(eventData.varying), std::numeric_limits::max()); + + pollResult = xrPollEvent(instance, &eventData); + REQUIRE(XR_SUCCEEDED(pollResult)); + + if (eventData.type == XR_TYPE_EVENT_DATA_USER_PRESENCE_CHANGED_EXT) { + foundUserPresenceEvent = true; + + // We don't require a user to be present for running automated tests, + // so we are not validating the value here... + break; + } + } + + REQUIRE(foundUserPresenceEvent == true); + } +} // namespace Conformance diff --git a/src/conformance/framework/conformance_framework.cpp b/src/conformance/framework/conformance_framework.cpp index d081dc73..6a8a4097 100644 --- a/src/conformance/framework/conformance_framework.cpp +++ b/src/conformance/framework/conformance_framework.cpp @@ -536,6 +536,6 @@ std::string Catch::StringMaker::convert(XrPosef const& value) oss << ", xyz=(" << value.orientation.x; oss << ", " << value.orientation.y; oss << ", " << value.orientation.z; - oss << ")]"; + oss << "))]"; return oss.str(); } diff --git a/src/conformance/framework/graphics_plugin.h b/src/conformance/framework/graphics_plugin.h index bab8fac1..73b13fb6 100644 --- a/src/conformance/framework/graphics_plugin.h +++ b/src/conformance/framework/graphics_plugin.h @@ -105,15 +105,17 @@ namespace Conformance /// A drawable cube, consisting of pose and scale for a nominally 1m x 1m x 1m cube struct Cube { - static inline Cube Make(XrVector3f position, float scale = 0.25f, XrQuaternionf orientation = {0, 0, 0, 1}) + static inline Cube Make(XrVector3f position, float scale = 0.25f, XrQuaternionf orientation = {0, 0, 0, 1}, + XrColor4f tintColor = {0, 0, 0, 0}) { - return Cube{/* pose */ {orientation, position}, /* scale: */ {scale, scale, scale}}; + return Cube{/* pose */ {orientation, position}, /* scale: */ {scale, scale, scale}, tintColor}; } - Cube(XrPosef pose, XrVector3f scale) : params(pose, scale) + Cube(XrPosef pose, XrVector3f scale, XrColor4f tintColor = {0, 0, 0, 0}) : params(pose, scale), tintColor(tintColor) { } DrawableParams params; + XrColor4f tintColor; }; namespace detail @@ -136,9 +138,10 @@ namespace Conformance { MeshHandle handle; DrawableParams params; + XrColor4f tintColor; - MeshDrawable(MeshHandle handle, XrPosef pose = XrPosefCPP{}, XrVector3f scale = {1.0, 1.0, 1.0}) - : handle(handle), params(pose, scale) + MeshDrawable(MeshHandle handle, XrPosef pose = XrPosefCPP{}, XrVector3f scale = {1.0, 1.0, 1.0}, XrColor4f tintColor = {0, 0, 0, 0}) + : handle(handle), params(pose, scale), tintColor(tintColor) { } }; @@ -347,6 +350,7 @@ namespace Conformance } /// Create internal data for a mesh, returning a handle to refer to it. + /// `idx` and `vtx` are copied out of and do not need to outlive this function. /// This handle expires when the internal data is cleared in Shutdown() and ShutdownDevice(). virtual MeshHandle MakeSimpleMesh(span idx, span vtx) = 0; @@ -377,9 +381,10 @@ namespace Conformance } /// Convenience helper function to make a mesh that is "coordinate axes" also called a "gnomon" - MeshHandle MakeGnomonMesh() + MeshHandle MakeGnomonMesh(float armLength = 1.0f, float armThickness = 0.1f) { - return MakeSimpleMesh(Geometry::AxisIndicator::GetInstance().indices, Geometry::AxisIndicator::GetInstance().vertices); + Geometry::AxisIndicator gnomon = Geometry::AxisIndicator(armLength, armThickness); + return MakeSimpleMesh(gnomon.indices, gnomon.vertices); } virtual void RenderView(const XrCompositionLayerProjectionView& layerView, const XrSwapchainImageBaseHeader* colorSwapchainImage, diff --git a/src/conformance/framework/graphics_plugin_d3d11.cpp b/src/conformance/framework/graphics_plugin_d3d11.cpp index 93faf286..e5ee4f5c 100644 --- a/src/conformance/framework/graphics_plugin_d3d11.cpp +++ b/src/conformance/framework/graphics_plugin_d3d11.cpp @@ -775,6 +775,7 @@ namespace Conformance ModelConstantBuffer model; XMStoreFloat4x4(&model.Model, XMMatrixTranspose(XMMatrixScaling(mesh.params.scale.x, mesh.params.scale.y, mesh.params.scale.z) * LoadXrPose(mesh.params.pose))); + model.TintColor = {mesh.tintColor.r, mesh.tintColor.g, mesh.tintColor.b, mesh.tintColor.a}; d3d11DeviceContext->UpdateSubresource(modelCBuffer.Get(), 0, nullptr, &model, 0, 0); // Draw the mesh. @@ -783,7 +784,7 @@ namespace Conformance // Render each cube for (const Cube& cube : params.cubes) { - drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale}); + drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale, cube.tintColor}); } // Render each mesh diff --git a/src/conformance/framework/graphics_plugin_d3d12.cpp b/src/conformance/framework/graphics_plugin_d3d12.cpp index b4e6fd4b..2f648132 100644 --- a/src/conformance/framework/graphics_plugin_d3d12.cpp +++ b/src/conformance/framework/graphics_plugin_d3d12.cpp @@ -1094,6 +1094,7 @@ namespace Conformance ModelConstantBuffer model; calculateModelMat(mesh.params, model.Model); + model.TintColor = {mesh.tintColor.r, mesh.tintColor.g, mesh.tintColor.b, mesh.tintColor.a}; { uint8_t* data; const D3D12_RANGE readRange{0, 0}; @@ -1113,7 +1114,7 @@ namespace Conformance // Render each cube for (const Cube& cube : params.cubes) { - drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale}); + drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale, cube.tintColor}); } // Render each mesh diff --git a/src/conformance/framework/graphics_plugin_opengl.cpp b/src/conformance/framework/graphics_plugin_opengl.cpp index 3a501faf..ba6e8f69 100644 --- a/src/conformance/framework/graphics_plugin_opengl.cpp +++ b/src/conformance/framework/graphics_plugin_opengl.cpp @@ -178,10 +178,11 @@ namespace Conformance out vec3 PSVertexColor; uniform mat4 ModelViewProjection; + uniform vec4 tintColor; void main() { gl_Position = ModelViewProjection * vec4(VertexPos, 1.0); - PSVertexColor = VertexColor; + PSVertexColor = mix(VertexColor, tintColor.rgb, tintColor.a); } )_"; @@ -451,6 +452,7 @@ namespace Conformance GLuint m_swapchainFramebuffer{0}; GLuint m_program{0}; GLint m_modelViewProjectionUniformLocation{0}; + GLint m_tintColorUniformLocation{0}; GLint m_vertexAttribCoords{0}; GLint m_vertexAttribColor{0}; MeshHandle m_cubeMesh{}; @@ -724,6 +726,7 @@ namespace Conformance glDeleteShader(fragmentShader); m_modelViewProjectionUniformLocation = glGetUniformLocation(m_program, "ModelViewProjection"); + m_tintColorUniformLocation = glGetUniformLocation(m_program, "tintColor"); m_vertexAttribCoords = glGetAttribLocation(m_program, "VertexPos"); m_vertexAttribColor = glGetAttribLocation(m_program, "VertexColor"); @@ -1138,6 +1141,7 @@ namespace Conformance XrMatrix4x4f mvp; XrMatrix4x4f_Multiply(&mvp, &vp, &model); glUniformMatrix4fv(m_modelViewProjectionUniformLocation, 1, GL_FALSE, reinterpret_cast(&mvp)); + glUniform4fv(m_tintColorUniformLocation, 1, reinterpret_cast(&mesh.tintColor)); // Draw the mesh. glDrawElements(GL_TRIANGLES, GLsizei(glMesh.m_numIndices), GL_UNSIGNED_SHORT, nullptr); @@ -1145,7 +1149,7 @@ namespace Conformance // Render each cube for (const Cube& cube : params.cubes) { - drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale}); + drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale, cube.tintColor}); } // Render each mesh diff --git a/src/conformance/framework/graphics_plugin_opengles.cpp b/src/conformance/framework/graphics_plugin_opengles.cpp index a12860f9..feca8db3 100644 --- a/src/conformance/framework/graphics_plugin_opengles.cpp +++ b/src/conformance/framework/graphics_plugin_opengles.cpp @@ -75,10 +75,11 @@ namespace Conformance out vec3 PSVertexColor; uniform mat4 ModelViewProjection; + uniform vec4 tintColor; void main() { gl_Position = ModelViewProjection * vec4(VertexPos, 1.0); - PSVertexColor = VertexColor; + PSVertexColor = mix(VertexColor, tintColor.rgb, tintColor.a); } )_"; @@ -323,6 +324,7 @@ namespace Conformance GLuint m_swapchainFramebuffer{0}; GLuint m_program{0}; GLint m_modelViewProjectionUniformLocation{0}; + GLint m_tintColorUniformLocation{0}; GLint m_vertexAttribCoords{0}; GLint m_vertexAttribColor{0}; MeshHandle m_cubeMesh{}; @@ -562,6 +564,7 @@ namespace Conformance GL(glDeleteShader(fragmentShader)); m_modelViewProjectionUniformLocation = glGetUniformLocation(m_program, "ModelViewProjection"); + m_tintColorUniformLocation = glGetUniformLocation(m_program, "tintColor"); m_vertexAttribCoords = glGetAttribLocation(m_program, "VertexPos"); m_vertexAttribColor = glGetAttribLocation(m_program, "VertexColor"); @@ -1260,6 +1263,7 @@ namespace Conformance XrMatrix4x4f mvp; XrMatrix4x4f_Multiply(&mvp, &vp, &model); GL(glUniformMatrix4fv(m_modelViewProjectionUniformLocation, 1, GL_FALSE, reinterpret_cast(&mvp))); + GL(glUniform4fv(m_tintColorUniformLocation, 1, reinterpret_cast(&mesh.tintColor))); // Draw the mesh. GL(glDrawElements(GL_TRIANGLES, glMesh.m_numIndices, GL_UNSIGNED_SHORT, nullptr)); @@ -1267,7 +1271,7 @@ namespace Conformance // Render each cube for (const Cube& cube : params.cubes) { - drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale}); + drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale, cube.tintColor}); } // Render each mesh diff --git a/src/conformance/framework/graphics_plugin_vulkan.cpp b/src/conformance/framework/graphics_plugin_vulkan.cpp index 13bb6345..606fed0e 100644 --- a/src/conformance/framework/graphics_plugin_vulkan.cpp +++ b/src/conformance/framework/graphics_plugin_vulkan.cpp @@ -86,6 +86,7 @@ namespace Conformance layout (std140, push_constant) uniform buf { mat4 mvp; + vec4 tintColor; } ubuf; layout (location = 0) in vec3 Position; @@ -99,8 +100,9 @@ namespace Conformance void main() { - oColor.rgba = Color.rgba; - gl_Position = ubuf.mvp * Position; + oColor.rgb = mix(Color.rgb, ubuf.tintColor.rgb, ubuf.tintColor.a); + oColor.a = 1.0; + gl_Position = ubuf.mvp * vec4(Position, 1); } )_"; @@ -2052,9 +2054,10 @@ namespace Conformance XrMatrix4x4f model; XrMatrix4x4f_CreateTranslationRotationScale(&model, &mesh.params.pose.position, &mesh.params.pose.orientation, &mesh.params.scale); - XrMatrix4x4f mvp; - XrMatrix4x4f_Multiply(&mvp, &vp, &model); - vkCmdPushConstants(m_cmdBuffer.buf, m_pipelineLayout.layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(mvp.m), &mvp.m[0]); + VulkanUniformBuffer ubuf; + ubuf.tintColor = mesh.tintColor; + XrMatrix4x4f_Multiply(&ubuf.mvp, &vp, &model); + vkCmdPushConstants(m_cmdBuffer.buf, m_pipelineLayout.layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(VulkanUniformBuffer), &ubuf); CHECKPOINT(); @@ -2066,7 +2069,7 @@ namespace Conformance // Render each cube for (const Cube& cube : params.cubes) { - drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale}); + drawMesh(MeshDrawable{m_cubeMesh, cube.params.pose, cube.params.scale, cube.tintColor}); } // Render each mesh diff --git a/src/conformance/framework/vulkan_shaders/vert.glsl b/src/conformance/framework/vulkan_shaders/vert.glsl index 54e54272..bb48e8cd 100644 --- a/src/conformance/framework/vulkan_shaders/vert.glsl +++ b/src/conformance/framework/vulkan_shaders/vert.glsl @@ -11,6 +11,7 @@ layout (std140, push_constant) uniform buf { mat4 mvp; + vec4 tintColor; } ubuf; layout (location = 0) in vec3 Position; @@ -24,7 +25,7 @@ out gl_PerVertex void main() { - oColor.rgb = Color.rgb; + oColor.rgb = mix(Color.rgb, ubuf.tintColor.rgb, ubuf.tintColor.a); oColor.a = 1.0; gl_Position = ubuf.mvp * vec4(Position, 1); } diff --git a/src/conformance/framework/vulkan_shaders/vert.spv b/src/conformance/framework/vulkan_shaders/vert.spv index e068a93e..723ad52e 100644 --- a/src/conformance/framework/vulkan_shaders/vert.spv +++ b/src/conformance/framework/vulkan_shaders/vert.spv @@ -1,10 +1,10 @@ -{0x07230203,0x00010000,0x000d0007,0x00000029, +{0x07230203,0x00010000,0x000d000a,0x00000030, 0x00000000,0x00020011,0x00000001,0x0006000b, 0x00000001,0x4c534c47,0x6474732e,0x3035342e, 0x00000000,0x0003000e,0x00000000,0x00000001, 0x0009000f,0x00000000,0x00000004,0x6e69616d, -0x00000000,0x00000009,0x0000000c,0x00000017, -0x00000021,0x00030003,0x00000002,0x00000190, +0x00000000,0x00000009,0x0000000c,0x0000001e, +0x00000028,0x00030003,0x00000002,0x00000190, 0x00090004,0x415f4c47,0x735f4252,0x72617065, 0x5f657461,0x64616873,0x6f5f7265,0x63656a62, 0x00007374,0x00090004,0x415f4c47,0x735f4252, @@ -17,23 +17,23 @@ 0x00040005,0x00000004,0x6e69616d,0x00000000, 0x00040005,0x00000009,0x6c6f436f,0x0000726f, 0x00040005,0x0000000c,0x6f6c6f43,0x00000072, -0x00060005,0x00000015,0x505f6c67,0x65567265, -0x78657472,0x00000000,0x00060006,0x00000015, +0x00060005,0x0000001c,0x505f6c67,0x65567265, +0x78657472,0x00000000,0x00060006,0x0000001c, 0x00000000,0x505f6c67,0x7469736f,0x006e6f69, -0x00030005,0x00000017,0x00000000,0x00030005, -0x0000001b,0x00667562,0x00040006,0x0000001b, -0x00000000,0x0070766d,0x00040005,0x0000001d, -0x66756275,0x00000000,0x00050005,0x00000021, +0x00030005,0x0000001e,0x00000000,0x00030005, +0x00000022,0x00667562,0x00040006,0x00000022, +0x00000000,0x0070766d,0x00040005,0x00000024, +0x66756275,0x00000000,0x00050005,0x00000028, 0x69736f50,0x6e6f6974,0x00000000,0x00040047, 0x00000009,0x0000001e,0x00000000,0x00040047, 0x0000000c,0x0000001e,0x00000001,0x00050048, -0x00000015,0x00000000,0x0000000b,0x00000000, -0x00030047,0x00000015,0x00000002,0x00040048, -0x0000001b,0x00000000,0x00000005,0x00050048, -0x0000001b,0x00000000,0x00000023,0x00000000, -0x00050048,0x0000001b,0x00000000,0x00000007, -0x00000010,0x00030047,0x0000001b,0x00000002, -0x00040047,0x00000021,0x0000001e,0x00000000, +0x0000001c,0x00000000,0x0000000b,0x00000000, +0x00030047,0x0000001c,0x00000002,0x00040048, +0x00000022,0x00000000,0x00000005,0x00050048, +0x00000022,0x00000000,0x00000023,0x00000000, +0x00050048,0x00000022,0x00000000,0x00000007, +0x00000010,0x00030047,0x00000022,0x00000002, +0x00040047,0x00000028,0x0000001e,0x00000000, 0x00020013,0x00000002,0x00030021,0x00000003, 0x00000002,0x00030016,0x00000006,0x00000020, 0x00040017,0x00000007,0x00000006,0x00000004, @@ -42,39 +42,48 @@ 0x00040017,0x0000000a,0x00000006,0x00000003, 0x00040020,0x0000000b,0x00000001,0x0000000a, 0x0004003b,0x0000000b,0x0000000c,0x00000001, -0x0004002b,0x00000006,0x00000010,0x3f800000, -0x00040015,0x00000011,0x00000020,0x00000000, -0x0004002b,0x00000011,0x00000012,0x00000003, -0x00040020,0x00000013,0x00000003,0x00000006, -0x0003001e,0x00000015,0x00000007,0x00040020, -0x00000016,0x00000003,0x00000015,0x0004003b, -0x00000016,0x00000017,0x00000003,0x00040015, -0x00000018,0x00000020,0x00000001,0x0004002b, -0x00000018,0x00000019,0x00000000,0x00040018, -0x0000001a,0x00000007,0x00000004,0x0003001e, -0x0000001b,0x0000001a,0x00040020,0x0000001c, -0x00000009,0x0000001b,0x0004003b,0x0000001c, -0x0000001d,0x00000009,0x00040020,0x0000001e, -0x00000009,0x0000001a,0x0004003b,0x0000000b, -0x00000021,0x00000001,0x00050036,0x00000002, +0x00040015,0x0000000e,0x00000020,0x00000000, +0x0004002b,0x0000000e,0x0000000f,0x00000000, +0x00040020,0x00000010,0x00000003,0x00000006, +0x0004002b,0x0000000e,0x00000013,0x00000001, +0x0004002b,0x0000000e,0x00000016,0x00000002, +0x0004002b,0x00000006,0x00000019,0x3f800000, +0x0004002b,0x0000000e,0x0000001a,0x00000003, +0x0003001e,0x0000001c,0x00000007,0x00040020, +0x0000001d,0x00000003,0x0000001c,0x0004003b, +0x0000001d,0x0000001e,0x00000003,0x00040015, +0x0000001f,0x00000020,0x00000001,0x0004002b, +0x0000001f,0x00000020,0x00000000,0x00040018, +0x00000021,0x00000007,0x00000004,0x0003001e, +0x00000022,0x00000021,0x00040020,0x00000023, +0x00000009,0x00000022,0x0004003b,0x00000023, +0x00000024,0x00000009,0x00040020,0x00000025, +0x00000009,0x00000021,0x0004003b,0x0000000b, +0x00000028,0x00000001,0x00050036,0x00000002, 0x00000004,0x00000000,0x00000003,0x000200f8, 0x00000005,0x0004003d,0x0000000a,0x0000000d, -0x0000000c,0x0004003d,0x00000007,0x0000000e, -0x00000009,0x0009004f,0x00000007,0x0000000f, -0x0000000e,0x0000000d,0x00000004,0x00000005, -0x00000006,0x00000003,0x0003003e,0x00000009, -0x0000000f,0x00050041,0x00000013,0x00000014, -0x00000009,0x00000012,0x0003003e,0x00000014, -0x00000010,0x00050041,0x0000001e,0x0000001f, -0x0000001d,0x00000019,0x0004003d,0x0000001a, -0x00000020,0x0000001f,0x0004003d,0x0000000a, -0x00000022,0x00000021,0x00050051,0x00000006, -0x00000023,0x00000022,0x00000000,0x00050051, -0x00000006,0x00000024,0x00000022,0x00000001, -0x00050051,0x00000006,0x00000025,0x00000022, -0x00000002,0x00070050,0x00000007,0x00000026, -0x00000023,0x00000024,0x00000025,0x00000010, -0x00050091,0x00000007,0x00000027,0x00000020, -0x00000026,0x00050041,0x00000008,0x00000028, -0x00000017,0x00000019,0x0003003e,0x00000028, -0x00000027,0x000100fd,0x00010038} +0x0000000c,0x00050041,0x00000010,0x00000011, +0x00000009,0x0000000f,0x00050051,0x00000006, +0x00000012,0x0000000d,0x00000000,0x0003003e, +0x00000011,0x00000012,0x00050041,0x00000010, +0x00000014,0x00000009,0x00000013,0x00050051, +0x00000006,0x00000015,0x0000000d,0x00000001, +0x0003003e,0x00000014,0x00000015,0x00050041, +0x00000010,0x00000017,0x00000009,0x00000016, +0x00050051,0x00000006,0x00000018,0x0000000d, +0x00000002,0x0003003e,0x00000017,0x00000018, +0x00050041,0x00000010,0x0000001b,0x00000009, +0x0000001a,0x0003003e,0x0000001b,0x00000019, +0x00050041,0x00000025,0x00000026,0x00000024, +0x00000020,0x0004003d,0x00000021,0x00000027, +0x00000026,0x0004003d,0x0000000a,0x00000029, +0x00000028,0x00050051,0x00000006,0x0000002a, +0x00000029,0x00000000,0x00050051,0x00000006, +0x0000002b,0x00000029,0x00000001,0x00050051, +0x00000006,0x0000002c,0x00000029,0x00000002, +0x00070050,0x00000007,0x0000002d,0x0000002a, +0x0000002b,0x0000002c,0x00000019,0x00050091, +0x00000007,0x0000002e,0x00000027,0x0000002d, +0x00050041,0x00000008,0x0000002f,0x0000001e, +0x00000020,0x0003003e,0x0000002f,0x0000002e, +0x000100fd,0x00010038} diff --git a/src/conformance/framework/xml_test_environment.cpp b/src/conformance/framework/xml_test_environment.cpp index aad0cf67..30980685 100644 --- a/src/conformance/framework/xml_test_environment.cpp +++ b/src/conformance/framework/xml_test_environment.cpp @@ -127,6 +127,9 @@ namespace Conformance auto e2 = xml.scopedElement(CTS_XML_NS_PREFIX_QUALIFIER "enabledInstanceExtensions"); for (const auto& name : options.enabledInstanceExtensions) { xml.scopedElement(CTS_XML_NS_PREFIX_QUALIFIER "extension").writeAttribute("name", name); + if (name == XR_EXT_CONFORMANCE_AUTOMATION_EXTENSION_NAME) { + xml.writeComment("Conformance automation enabled - NOT VALID FOR CONFORMANCE SUBMISSION"); + } } } diff --git a/src/conformance/utilities/CMakeLists.txt b/src/conformance/utilities/CMakeLists.txt index 119b37a3..91fe0472 100644 --- a/src/conformance/utilities/CMakeLists.txt +++ b/src/conformance/utilities/CMakeLists.txt @@ -44,6 +44,7 @@ configure_file( add_library( conformance_utilities STATIC + ballistics.cpp bitmask_generator.cpp bitmask_to_string.cpp d3d_common.cpp diff --git a/src/conformance/utilities/Geometry.cpp b/src/conformance/utilities/Geometry.cpp index 178b0749..9cc0474b 100644 --- a/src/conformance/utilities/Geometry.cpp +++ b/src/conformance/utilities/Geometry.cpp @@ -22,14 +22,12 @@ namespace Geometry return input; } - AxisIndicator::AxisIndicator() : count(0) + AxisIndicator::AxisIndicator(float armLength, float armThickness) : count(0) { constexpr int axes = 3; constexpr int verticesPerAxis = 30; constexpr int totalVertices = verticesPerAxis * axes; - constexpr float thickness = 0.1f; - // For each axis, create a copy of the cube mesh missing the -x face. // The +x face will be at 1.0, and the -x faces will be mitered together. // Each axis is colored and rotated differently, but otherwise identical. @@ -44,13 +42,13 @@ namespace Geometry Vertex vertex = c_cubeVertices[index + 6]; // skip -x face vertex.Color = color; - vertex.Position.x *= thickness; - vertex.Position.y *= thickness; - vertex.Position.z *= thickness; + vertex.Position.x *= armThickness; + vertex.Position.y *= armThickness; + vertex.Position.z *= armThickness; if (vertex.Position.x > 0) { // +x vertex, end of the arm, send x to +1 - vertex.Position.x = 1.0; + vertex.Position.x = armLength; } else if (vertex.Position.y > 0 || vertex.Position.z > 0) { // make room for another axis @@ -65,10 +63,4 @@ namespace Geometry vertices[i] = vertex; } } - - const AxisIndicator& AxisIndicator::GetInstance() - { - static const AxisIndicator instance; - return instance; - } } // namespace Geometry diff --git a/src/conformance/utilities/Geometry.h b/src/conformance/utilities/Geometry.h index de687270..1fb3cf23 100644 --- a/src/conformance/utilities/Geometry.h +++ b/src/conformance/utilities/Geometry.h @@ -60,15 +60,11 @@ namespace Geometry struct AxisIndicator { public: - /// Get access to the single instance, constructed in static storage upon first use - static const AxisIndicator& GetInstance(); + explicit AxisIndicator(float armLength, float armThickness); int count; std::array indices; std::array vertices; - - private: - AxisIndicator(); }; } // namespace Geometry diff --git a/src/conformance/utilities/ballistics.cpp b/src/conformance/utilities/ballistics.cpp new file mode 100644 index 00000000..f3bd3e94 --- /dev/null +++ b/src/conformance/utilities/ballistics.cpp @@ -0,0 +1,54 @@ +// Copyright (c) 2017-2024, The Khronos Group Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +#include "ballistics.h" + +#include "common/xr_linear.h" + +#include +#include + +namespace Conformance +{ + void BodyInMotion::doSimulationStep(XrVector3f acceleration, XrTime predictedDisplayTime) + { + if (~this->velocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { + throw std::logic_error("doSimulationStep called without valid linear velocity"); + } + + const XrDuration timeSinceLastTick = predictedDisplayTime - this->updateTime; + if (timeSinceLastTick <= 0) { + throw std::logic_error("Unexpected old frame state predictedDisplayTime or future action state lastChangeTime"); + } + this->updateTime = predictedDisplayTime; + + const float secondSinceLastTick = timeSinceLastTick / (float)1'000'000'000; + + // Apply acceleration to velocity. + XrVector3f deltaAcceleration; + XrVector3f_Scale(&deltaAcceleration, &acceleration, secondSinceLastTick); + XrVector3f_Add(&this->velocity.linearVelocity, &this->velocity.linearVelocity, &deltaAcceleration); + + // Apply velocity to position. + XrVector3f deltaVelocity; + XrVector3f_Scale(&deltaVelocity, &this->velocity.linearVelocity, secondSinceLastTick); + XrVector3f_Add(&this->pose.position, &this->pose.position, &deltaVelocity); + + if (this->velocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT) { + // Convert angular velocity to quaternion with the appropriate amount of rotation for the delta time. + XrQuaternionf angularRotation; + { + const float radiansPerSecond = XrVector3f_Length(&this->velocity.angularVelocity); + XrVector3f angularAxis = this->velocity.angularVelocity; + XrVector3f_Normalize(&angularAxis); + XrQuaternionf_CreateFromAxisAngle(&angularRotation, &angularAxis, radiansPerSecond * secondSinceLastTick); + } + + // Update the orientation given the computed angular rotation. + XrQuaternionf newOrientation; + XrQuaternionf_Multiply(&newOrientation, &this->pose.orientation, &angularRotation); + this->pose.orientation = newOrientation; + } + }; +} // namespace Conformance diff --git a/src/conformance/utilities/ballistics.h b/src/conformance/utilities/ballistics.h new file mode 100644 index 00000000..43e68e32 --- /dev/null +++ b/src/conformance/utilities/ballistics.h @@ -0,0 +1,21 @@ +// Copyright (c) 2017-2024, The Khronos Group Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +namespace Conformance +{ + struct BodyInMotion + { + XrSpaceVelocity velocity; + XrPosef pose; + XrTime updateTime; + XrTime createTime; + + // precondition: velocity.velocityFlags must have VALID linear and angular velocity + void doSimulationStep(XrVector3f acceleration, XrTime predictedDisplayTime); + }; +} // namespace Conformance diff --git a/src/conformance/utilities/d3d_common.h b/src/conformance/utilities/d3d_common.h index 4ea7831c..48c165a6 100644 --- a/src/conformance/utilities/d3d_common.h +++ b/src/conformance/utilities/d3d_common.h @@ -25,6 +25,7 @@ namespace Conformance struct ModelConstantBuffer { DirectX::XMFLOAT4X4 Model; + DirectX::XMFLOAT4 TintColor; }; struct ViewProjectionConstantBuffer { @@ -43,6 +44,7 @@ namespace Conformance }; cbuffer ModelConstantBuffer : register(b0) { float4x4 Model; + float4 TintColor; }; cbuffer ViewProjectionConstantBuffer : register(b1) { float4x4 ViewProjection; @@ -51,7 +53,7 @@ namespace Conformance PSVertex MainVS(Vertex input) { PSVertex output; output.Pos = mul(mul(float4(input.Pos, 1), Model), ViewProjection); - output.Color = input.Color; + output.Color = lerp(input.Color, TintColor.rgb, TintColor.a); return output; } diff --git a/src/conformance/utilities/vulkan_utils.h b/src/conformance/utilities/vulkan_utils.h index 64b6be82..eac68bfe 100644 --- a/src/conformance/utilities/vulkan_utils.h +++ b/src/conformance/utilities/vulkan_utils.h @@ -7,6 +7,7 @@ #ifdef XR_USE_GRAPHICS_API_VULKAN #include "throw_helpers.h" +#include "xr_linear.h" #include "common/xr_dependencies.h" #include "common/vulkan_debug_object_namer.hpp" #include @@ -999,7 +1000,13 @@ namespace Conformance VkDevice m_vkDevice{VK_NULL_HANDLE}; }; - // Simple vertex MVP xform & color fragment shader layout + struct VulkanUniformBuffer + { + XrMatrix4x4f mvp; + XrColor4f tintColor; + }; + + // Simple vertex MVP xform, tint color & color fragment shader layout struct PipelineLayout { VkPipelineLayout layout{VK_NULL_HANDLE}; @@ -1030,7 +1037,7 @@ namespace Conformance VkPushConstantRange pcr = {}; pcr.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pcr.offset = 0; - pcr.size = 4 * 4 * sizeof(float); + pcr.size = sizeof(VulkanUniformBuffer); VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; pipelineLayoutCreateInfo.pushConstantRangeCount = 1; diff --git a/src/loader/manifest_file.cpp b/src/loader/manifest_file.cpp index 4e3e5b49..0caf05c9 100644 --- a/src/loader/manifest_file.cpp +++ b/src/loader/manifest_file.cpp @@ -75,11 +75,12 @@ static inline bool StringEndsWith(const std::string &value, const std::string &e } // If the file found is a manifest file name, add it to the out_files manifest list. -static void AddIfJson(const std::string &full_file, std::vector &manifest_files) { +static bool AddIfJson(const std::string &full_file, std::vector &manifest_files) { if (full_file.empty() || !StringEndsWith(full_file, ".json")) { - return; + return false; } manifest_files.push_back(full_file); + return true; } // Check the current path for any manifest files. If the provided search_path is a directory, look for @@ -381,7 +382,6 @@ static void ReadRuntimeDataFilesInRegistry(const std::string &runtime_registry_l if (ERROR_SUCCESS != open_value) { LoaderLogger::LogWarningMessage("", "ReadRuntimeDataFilesInRegistry - failed to open registry key " + full_registry_location); - return; } @@ -391,7 +391,23 @@ static void ReadRuntimeDataFilesInRegistry(const std::string &runtime_registry_l LoaderLogger::LogWarningMessage( "", "ReadRuntimeDataFilesInRegistry - failed to read registry value " + default_runtime_value_name); } else { - AddFilesInPath(wide_to_utf8(value_w), false, manifest_files); + // Not using AddFilesInPath here (as only api_layer manifest paths allow multiple + // separated paths) + // Small time-of-check vs time-of-use issue here but it mainly only affects the error message. + // It does not introduce a security defect. + std::string activeRuntimePath = wide_to_utf8(value_w); + if (FileSysUtilsIsRegularFile(activeRuntimePath)) { + // If the file exists, try to add it + std::string absolute_path; + FileSysUtilsGetAbsolutePath(activeRuntimePath, absolute_path); + if (!AddIfJson(absolute_path, manifest_files)) { + LoaderLogger::LogErrorMessage( + "", "ReadRuntimeDataFilesInRegistry - registry runtime path is not json " + activeRuntimePath); + } + } else { + LoaderLogger::LogErrorMessage( + "", "ReadRuntimeDataFilesInRegistry - registry runtime path does not exist " + activeRuntimePath); + } } RegCloseKey(hkey); diff --git a/src/scripts/template_gen_dispatch.cpp b/src/scripts/template_gen_dispatch.cpp index c69bf0ea..57163776 100644 --- a/src/scripts/template_gen_dispatch.cpp +++ b/src/scripts/template_gen_dispatch.cpp @@ -8,16 +8,22 @@ #if defined(ANDROID) #include +#define LOG_ERROR(...) __android_log_print(ANDROID_LOG_ERROR, "XrApiLayer_runtime_conformance", __VA_ARGS__) #define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "XrApiLayer_runtime_conformance", __VA_ARGS__) #else #include -#define LOG_FATAL(...) fprintf(stderr, __VA_ARGS__) +#define LOG_ERROR(...) fprintf(stderr, __VA_ARGS__) +#define LOG_FATAL(...) LOG_ERROR(__VA_ARGS__) #endif #include // Unhandled exception at ABI is a catastrophic error in the layer (a bug). #define ABI_CATCH \ + catch (const HandleNotFoundException& e) { \ + LOG_ERROR("ERROR: Conformance Layer: Unknown handle used, created by unrecognized API call? Message = %s\n", e.what()); \ + return XR_ERROR_HANDLE_INVALID; \ + } \ catch (const std::exception& e) { \ LOG_FATAL("FATAL: Conformance Layer Bug: caught exception at ABI level with message = %s\n", e.what()); \ abort(); /* Something went wrong in the layer. */ \