diff options
Diffstat (limited to 'testing/web-platform/tests/webxr/resources')
7 files changed, 871 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webxr/resources/webxr_check.html b/testing/web-platform/tests/webxr/resources/webxr_check.html new file mode 100644 index 0000000000..2d8e5b387d --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_check.html @@ -0,0 +1,17 @@ +<script src=webxr_util.js></script> +<script> +'use strict'; +let definedObjects = []; +let undefinedObjects = []; + +forEachWebxrObject((obj, name) => { + if(obj == undefined) { + undefinedObjects.push(name); + } else { + definedObjects.push(name); + } +}); + +window.parent.postMessage({ undefinedObjects, definedObjects}, '*'); + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/resources/webxr_math_utils.js b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js new file mode 100644 index 0000000000..54a61c1854 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js @@ -0,0 +1,67 @@ +// |matrix| - Float32Array, |input| - point-like dict (must have x, y, z, w) +let transform_point_by_matrix = function(matrix, input) { + return { + x : matrix[0] * input.x + matrix[4] * input.y + matrix[8] * input.z + matrix[12] * input.w, + y : matrix[1] * input.x + matrix[5] * input.y + matrix[9] * input.z + matrix[13] * input.w, + z : matrix[2] * input.x + matrix[6] * input.y + matrix[10] * input.z + matrix[14] * input.w, + w : matrix[3] * input.x + matrix[7] * input.y + matrix[11] * input.z + matrix[15] * input.w, + }; +} + +// Creates a unit-length quaternion. +// |input| - point-like dict (must have x, y, z, w) +let normalize_quaternion = function(input) { + const length_squared = input.x * input.x + input.y * input.y + input.z * input.z + input.w * input.w; + const length = Math.sqrt(length_squared); + + return {x : input.x / length, y : input.y / length, z : input.z / length, w : input.w / length}; +} + +// Returns negated quaternion. +// |input| - point-like dict (must have x, y, z, w) +let flip_quaternion = function(input) { + return {x : -input.x, y : -input.y, z : -input.z, w : -input.w}; +} + +// |input| - point-like dict (must have x, y, z, w) +let conjugate_quaternion = function(input) { + return {x : -input.x, y : -input.y, z : -input.z, w : input.w}; +} + +let multiply_quaternions = function(q1, q2) { + return { + w : q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z, + x : q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + y : q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + z : q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + } +} + +// |point| - point-like dict (must have x, y, z, w) +let normalize_perspective = function(point) { + if(point.w == 0 || point.w == 1) return point; + + return { + x : point.x / point.w, + y : point.y / point.w, + z : point.z / point.w, + w : 1 + }; +} + +// |quaternion| - point-like dict (must have x, y, z, w), +// |input| - point-like dict (must have x, y, z, w) +let transform_point_by_quaternion = function(quaternion, input) { + const q_normalized = normalize_quaternion(quaternion); + const q_conj = conjugate_quaternion(q_normalized); + const p_in = normalize_perspective(input); + + // construct a quaternion out of the point (take xyz & zero the real part). + const p = {x : p_in.x, y : p_in.y, z : p_in.z, w : 0}; + + // transform the input point + const p_mul = multiply_quaternions( q_normalized, multiply_quaternions(p, q_conj) ); + + // add back the w component of the input + return { x : p_mul.x, y : p_mul.y, z : p_mul.z, w : p_in.w }; +} diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js new file mode 100644 index 0000000000..a82f7aaf90 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js @@ -0,0 +1,185 @@ +// Utility assert functions. +// Relies on resources/testharness.js to be included before this file. +// Relies on webxr_test_constants.js to be included before this file. +// Relies on webxr_math_utils.js to be included before this file. + + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// Returns the name of mismatching component between p1 and p2. +const get_mismatched_component = function(p1, p2, epsilon = FLOAT_EPSILON) { + for (const v of ['x', 'y', 'z', 'w']) { + if (Math.abs(p1[v] - p2[v]) > epsilon) { + return v; + } + } + + return null; +} + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_point_approx_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") { + if (p1 == null && p2 == null) { + return; + } + + assert_not_equals(p1, null, prefix + "p1 must be non-null"); + assert_not_equals(p2, null, prefix + "p2 must be non-null"); + + const mismatched_component = get_mismatched_component(p1, p2, epsilon); + + if (mismatched_component !== null) { + let error_message = prefix + ' Point comparison failed.\n'; + error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`; + error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`; + error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`; + assert_approx_equals(p2[mismatched_component], p1[mismatched_component], epsilon, error_message); + } +}; + +const assert_orientation_approx_equals = function(q1, q2, epsilon = FLOAT_EPSILON, prefix = "") { + if (q1 == null && q2 == null) { + return; + } + + assert_not_equals(q1, null, prefix + "q1 must be non-null"); + assert_not_equals(q2, null, prefix + "q2 must be non-null"); + + const q2_flipped = flip_quaternion(q2); + + const mismatched_component = get_mismatched_component(q1, q2, epsilon); + const mismatched_component_flipped = get_mismatched_component(q1, q2_flipped, epsilon); + + if (mismatched_component !== null && mismatched_component_flipped !== null) { + // q1 doesn't match neither q2 nor -q2, so it definitely does not represent the same orientations, + // log an assert failure. + let error_message = prefix + ' Orientation comparison failed.\n'; + error_message += ` p1: {x: ${q1.x}, y: ${q1.y}, z: ${q1.z}, w: ${q1.w}}\n`; + error_message += ` p2: {x: ${q2.x}, y: ${q2.y}, z: ${q2.z}, w: ${q2.w}}\n`; + error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`; + assert_approx_equals(q2[mismatched_component], q1[mismatched_component], epsilon, error_message); + } +} + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_point_significantly_not_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") { + + assert_not_equals(p1, null, prefix + "p1 must be non-null"); + assert_not_equals(p2, null, prefix + "p2 must be non-null"); + + let mismatched_component = get_mismatched_component(p1, p2, epsilon); + + if (mismatched_component === null) { + let error_message = prefix + ' Point comparison failed.\n'; + error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`; + error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`; + error_message += ` Difference in components did not exceeded the given epsilon.\n`; + assert_unreached(error_message); + } +}; + +// |t1|, |t2| - objects containing position and orientation. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_transform_approx_equals = function(t1, t2, epsilon = FLOAT_EPSILON, prefix = "") { + if (t1 == null && t2 == null) { + return; + } + + assert_not_equals(t1, null, prefix + "t1 must be non-null"); + assert_not_equals(t2, null, prefix + "t2 must be non-null"); + + assert_point_approx_equals(t1.position, t2.position, epsilon, prefix + "positions must be equal"); + assert_orientation_approx_equals(t1.orientation, t2.orientation, epsilon, prefix + "orientations must be equal"); +}; + +// |m1|, |m2| - arrays of floating point numbers +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_matrix_approx_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") { + if (m1 == null && m2 == null) { + return; + } + + assert_not_equals(m1, null, prefix + "m1 must be non-null"); + assert_not_equals(m2, null, prefix + "m2 must be non-null"); + + assert_equals(m1.length, 16, prefix + "m1 must have length of 16"); + assert_equals(m2.length, 16, prefix + "m2 must have length of 16"); + + let mismatched_element = -1; + for (let i = 0; i < 16; ++i) { + if (Math.abs(m1[i] - m2[i]) > epsilon) { + mismatched_element = i; + break; + } + } + + if (mismatched_element > -1) { + let error_message = prefix + 'Matrix comparison failed.\n'; + error_message += ' Difference in element ' + mismatched_element + + ' exceeded the given epsilon.\n'; + + error_message += ' Matrix 1: [' + m1.join(',') + ']\n'; + error_message += ' Matrix 2: [' + m2.join(',') + ']\n'; + + assert_approx_equals( + m1[mismatched_element], m2[mismatched_element], epsilon, + error_message); + } +}; + +// |m1|, |m2| - arrays of floating point numbers +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_matrix_significantly_not_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") { + if (m1 == null && m2 == null) { + return; + } + + assert_not_equals(m1, null, prefix + "m1 must be non-null"); + assert_not_equals(m2, null, prefix + "m2 must be non-null"); + + assert_equals(m1.length, 16, prefix + "m1 must have length of 16"); + assert_equals(m2.length, 16, prefix + "m2 must have length of 16"); + + let mismatch = false; + for (let i = 0; i < 16; ++i) { + if (Math.abs(m1[i] - m2[i]) > epsilon) { + mismatch = true; + break; + } + } + + if (!mismatch) { + let m1_str = '['; + let m2_str = '['; + for (let i = 0; i < 16; ++i) { + m1_str += m1[i] + (i < 15 ? ', ' : ''); + m2_str += m2[i] + (i < 15 ? ', ' : ''); + } + m1_str += ']'; + m2_str += ']'; + + let error_message = prefix + 'Matrix comparison failed.\n'; + error_message += + ' No element exceeded the given epsilon ' + epsilon + '.\n'; + + error_message += ' Matrix A: ' + m1_str + '\n'; + error_message += ' Matrix B: ' + m2_str + '\n'; + + assert_unreached(error_message); + } +}; + +// |r1|, |r2| - XRRay objects +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_ray_approx_equals = function(r1, r2, epsilon = FLOAT_EPSILON, prefix = "") { + assert_point_approx_equals(r1.origin, r2.origin, epsilon, prefix + "origin:"); + assert_point_approx_equals(r1.direction, r2.direction, epsilon, prefix + "direction:"); + assert_matrix_approx_equals(r1.matrix, r2.matrix, epsilon, prefix + "matrix:"); +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js new file mode 100644 index 0000000000..1a0d7bddad --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js @@ -0,0 +1,204 @@ +// assert_equals can fail when comparing floats due to precision errors, so +// use assert_approx_equals with this constant instead +const FLOAT_EPSILON = 0.001; + +// Identity matrix +const IDENTITY_MATRIX = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1]; + +const IDENTITY_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +// A valid pose matrix/transform for when we don't care about specific values +// Note that these two should be identical, just different representations +const VALID_POSE_MATRIX = [0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + 1, 1, 1, 1]; + +const VALID_POSE_TRANSFORM = { + position: [1, 1, 1], + orientation: [0.5, 0.5, 0.5, 0.5] +}; + +const VALID_PROJECTION_MATRIX = + [1, 0, 0, 0, 0, 1, 0, 0, 3, 2, -1, -1, 0, 0, -0.2, 0]; + +// This is a decomposed version of the above. +const VALID_FIELD_OF_VIEW = { + upDegrees: 71.565, + downDegrees: -45, + leftDegrees:-63.4349, + rightDegrees: 75.9637 +}; + +// A valid input grip matrix for when we don't care about specific values +const VALID_GRIP = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 4, 3, 2, 1]; + +const VALID_GRIP_TRANSFORM = { + position: [4, 3, 2], + orientation: [0, 0, 0, 1] +}; + +// A valid input pointer offset for when we don't care about specific values +const VALID_POINTER = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 1, 1]; + +const VALID_POINTER_TRANSFORM = { + position: [0, 0, 1], + orientation: [0, 0, 0, 1] +}; + +// A Valid Local to floor matrix/transform for when we don't care about specific +// values. Note that these should be identical, just different representations. +const VALID_FLOOR_ORIGIN_MATRIX = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 1.65, -1, 1]; + +const VALID_FLOOR_ORIGIN = { + position: [-1.0, -1.65, 1.0], + orientation: [0, 0, 0, 1] +}; + +const VALID_BOUNDS = [ + { x: 3.0, z: -2.0 }, + { x: 3.5, z: 0.0 }, + { x: 3.0, z: 2.0 }, + { x: -3.0, z: 2.0 }, + { x: -3.5, z: 0.0 }, + { x: -3.0, z: -2.0 } +]; + +const VALID_RESOLUTION = { + width: 200, + height: 200 +}; + +const LEFT_OFFSET = { + position: [-0.1, 0, 0], + orientation: [0, 0, 0, 1] +}; + +const RIGHT_OFFSET = { + position: [0.1, 0, 0], + orientation: [0, 0, 0, 1] +}; + +const FIRST_PERSON_OFFSET = { + position: [0, 0.1, 0], + orientation: [0, 0, 0, 1] +}; + +const VALID_VIEWS = [{ + eye:"left", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: LEFT_OFFSET, + resolution: VALID_RESOLUTION + }, { + eye:"right", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: RIGHT_OFFSET, + resolution: VALID_RESOLUTION + }, +]; + +const VALID_SECONDARY_VIEWS = [{ + eye: "none", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: FIRST_PERSON_OFFSET, + resolution: VALID_RESOLUTION, + isFirstPersonObserver: true + } +]; + +const NON_IMMERSIVE_VIEWS = [{ + eye: "none", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: IDENTITY_TRANSFORM, + resolution: VALID_RESOLUTION, + } +]; + +const ALL_FEATURES = [ + 'viewer', + 'local', + 'local-floor', + 'bounded-floor', + 'unbounded', + 'hit-test', + 'dom-overlay', + 'light-estimation', + 'anchors', + 'depth-sensing', + 'secondary-views', + 'camera-access', +]; + +const TRACKED_IMMERSIVE_DEVICE = { + supportsImmersive: true, + supportedModes: [ "inline", "immersive-vr"], + views: VALID_VIEWS, + secondaryViews: VALID_SECONDARY_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "world-space" +}; + +const IMMERSIVE_AR_DEVICE = { + supportsImmersive: true, + supportedModes: [ "inline", "immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "additive", + interactionMode: "screen-space" +}; + +const VALID_NON_IMMERSIVE_DEVICE = { + supportsImmersive: false, + supportedModes: ["inline"], + views: NON_IMMERSIVE_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "screen-space" +}; + +const VALID_CONTROLLER = { + handedness: "none", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +const RIGHT_CONTROLLER = { + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +const SCREEN_CONTROLLER = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +// From: https://immersive-web.github.io/webxr/#default-features +const DEFAULT_FEATURES = { + "inline": ["viewer"], + "immersive-vr": ["viewer", "local"], + "immersive-ar": ["viewer", "local"], +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js new file mode 100644 index 0000000000..36890d398d --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js @@ -0,0 +1,78 @@ +'use strict'; + +// This file introduces constants used to mock depth data for depth sensing API. + +const convertDepthBufferToArrayBuffer = function (data, desiredFormat) { + if(desiredFormat == "luminance-alpha") { + const result = new ArrayBuffer(data.length * 2); // each entry has 2 bytes + const view = new Uint16Array(result); + + for(let i = 0; i < data.length; ++i) { + view[i] = data[i]; + } + + return new Uint8Array(result); + } else if(desiredFormat == "float32") { + const result = new ArrayBuffer(data.length * 4); // each entry has 4 bytes + const view = new Float32Array(result); + + for(let i = 0; i < data.length; ++i) { + view[i] = data[i]; + } + + return new Uint8Array(result); + } else { + throw new Error("Unrecognized data format!"); + } +} + +// Let's assume that the depth values are in cm, Xcm = x * 1/100m +const RAW_VALUE_TO_METERS = 1/100; + +const createDepthSensingData = function() { + const depthSensingBufferHeight = 5; + const depthSensingBufferWidth = 7; + const depthSensingBuffer = [ + 1, 1, 1, 1, 1, 1, 1, // first row + 1, 2, 3, 4, 5, 6, 7, + 1, 4, 9, 16, 25, 36, 49, + 1, 8, 27, 64, 125, 216, 343, + 1, 16, 81, 256, 625, 1296, 2401, + ]; // depthSensingBuffer value at column c, row r is Math.pow(c+1, r). + + // Let's assume that the origin of the depth buffer is in the bottom right + // corner, with X's growing to the left and Y's growing upwards. + // This corresponds to the origin at 2401 in the above matrix, with X axis + // growing from 2401 towards 1296, and Y axis growing from 2401 towards 343. + // This corresponds to a rotation around Z axis by 180 degrees, with origin at [1,1]. + const depthSensingBufferFromViewerTransform = { + position: [1, 1, 0], + orientation: [0, 0, 1, 0], + }; + + return { + depthData: convertDepthBufferToArrayBuffer(depthSensingBuffer, "luminance-alpha"), + width: depthSensingBufferWidth, + height: depthSensingBufferHeight, + normDepthBufferFromNormView: depthSensingBufferFromViewerTransform, + rawValueToMeters: RAW_VALUE_TO_METERS, + }; +}; + +const DEPTH_SENSING_DATA = createDepthSensingData(); + +// Returns expected depth value at |column|, |row| coordinates, expressed +// in depth buffer's coordinate system. +const getExpectedValueAt = function(column, row) { + return Math.pow(column+1, row) * RAW_VALUE_TO_METERS; +}; + +const VALID_DEPTH_CONFIG_CPU_USAGE = { + usagePreference: ['cpu-optimized'], + dataFormatPreference: ['luminance-alpha', 'float32'], +}; + +const VALID_DEPTH_CONFIG_GPU_USAGE = { + usagePreference: ['gpu-optimized'], + dataFormatPreference: ['luminance-alpha', 'float32'], +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js new file mode 100644 index 0000000000..7e428e2155 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js @@ -0,0 +1,79 @@ +'use strict'; + +// This file introduces constants used to mock fake world for the purposes of hit test. + +// Generates FakeXRWorldInit dictionary with given dimensions. +// The generated fake world will have floor and front wall treated as planes, +// side walls treated as meshes, and ceiling treated as points. +// width - X axis, in meters +// height - Y axis, in meters +// length - Z axis, in meters +function createFakeWorld( + width, height, length, + front_wall_and_floor_type = "plane", + side_walls_type = "mesh", + ceiling_type = "point") { + // Vertices: + const BOTTOM_LEFT_FRONT = { x: -width/2, y: 0, z: -length/2, w: 1}; + const BOTTOM_RIGHT_FRONT = { x: width/2, y: 0, z: -length/2, w: 1}; + + const TOP_LEFT_FRONT = { x: -width/2, y: height, z: -length/2, w: 1}; + const TOP_RIGHT_FRONT = { x: width/2, y: height, z: -length/2, w: 1}; + + const BOTTOM_LEFT_BACK = { x: -width/2, y: 0, z: length/2, w: 1}; + const BOTTOM_RIGHT_BACK = { x: width/2, y: 0, z: length/2, w: 1}; + + const TOP_LEFT_BACK = { x: -width/2, y: height, z: length/2, w: 1}; + const TOP_RIGHT_BACK = { x: width/2, y: height, z: length/2, w: 1}; + + // Faces: + const FRONT_WALL_AND_FLOOR_FACES = [ + // Front wall: + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT] }, + { vertices: [BOTTOM_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_LEFT_FRONT] }, + // Floor: + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK] }, + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, BOTTOM_RIGHT_BACK] }, + ]; + + const CEILING_FACES = [ + // Ceiling: + { vertices: [TOP_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] }, + { vertices: [TOP_LEFT_FRONT, TOP_LEFT_BACK, TOP_RIGHT_BACK] }, + ]; + + const SIDE_WALLS_FACES = [ + // Left: + { vertices: [BOTTOM_LEFT_FRONT, TOP_LEFT_FRONT, TOP_LEFT_BACK] }, + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, TOP_LEFT_BACK] }, + // Right: + { vertices: [BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] }, + { vertices: [BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK, TOP_RIGHT_BACK] }, + ]; + + // Regions: + const FRONT_WALL_AND_FLOOR_REGION = { + type: front_wall_and_floor_type, + faces: FRONT_WALL_AND_FLOOR_FACES, + }; + + const SIDE_WALLS_REGION = { + type: side_walls_type, + faces: SIDE_WALLS_FACES, + }; + + const CEILING_REGION = { + type: ceiling_type, + faces: CEILING_FACES, + }; + + return { + hitTestRegions : [ + FRONT_WALL_AND_FLOOR_REGION, + SIDE_WALLS_REGION, + CEILING_REGION, + ] + }; +} + +const VALID_FAKE_WORLD = createFakeWorld(5.0, 2.0, 5.0); diff --git a/testing/web-platform/tests/webxr/resources/webxr_util.js b/testing/web-platform/tests/webxr/resources/webxr_util.js new file mode 100644 index 0000000000..625f76450e --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_util.js @@ -0,0 +1,241 @@ +'use strict'; + +// These tests rely on the User Agent providing an implementation of the +// WebXR Testing API (https://github.com/immersive-web/webxr-test-api). +// +// In Chromium-based browsers, this implementation is provided by a JavaScript +// shim in order to reduce the amount of test-only code shipped to users. To +// enable these tests the browser must be run with these options: +// +// --enable-blink-features=MojoJS,MojoJSTest + +// Debugging message helper, by default does nothing. Implementations can +// override this. +var xr_debug = function(name, msg) {}; + +function xr_promise_test(name, func, properties, glContextType, glContextProperties) { + promise_test(async (t) => { + if (glContextType === 'webgl2') { + // Fast fail on platforms not supporting WebGL2. + assert_implements('WebGL2RenderingContext' in window, 'webgl2 not supported.'); + } + // Perform any required test setup: + xr_debug(name, 'setup'); + + assert_implements(navigator.xr, 'missing navigator.xr - ensure test is run in a secure context.'); + + // Only set up once. + if (!navigator.xr.test) { + // Load test-only API helpers. + const script = document.createElement('script'); + script.src = '/resources/test-only-api.js'; + script.async = false; + const p = new Promise((resolve, reject) => { + script.onload = () => { resolve(); }; + script.onerror = e => { reject(e); }; + }) + document.head.appendChild(script); + await p; + + if (isChromiumBased) { + // Chrome setup + await loadChromiumResources(); + } else if (isWebKitBased) { + // WebKit setup + await setupWebKitWebXRTestAPI(); + } + } + + // Either the test api needs to be polyfilled and it's not set up above, or + // something happened to one of the known polyfills and it failed to be + // setup properly. Either way, the fact that xr_promise_test is being used + // means that the tests expect navigator.xr.test to be set. By rejecting now + // we can hopefully provide a clearer indication of what went wrong. + assert_implements(navigator.xr.test, 'missing navigator.xr.test, even after attempted load'); + + let gl = null; + let canvas = null; + if (glContextType) { + canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + gl = canvas.getContext(glContextType, glContextProperties); + } + + // Ensure that any devices are disconnected when done. If this were done in + // a .then() for the success case, a test that expected failure would + // already be marked done at the time that runs and the shutdown would + // interfere with the next test. + t.add_cleanup(async () => { + // Ensure system state is cleaned up. + xr_debug(name, 'cleanup'); + await navigator.xr.test.disconnectAllDevices(); + }); + + xr_debug(name, 'main'); + return func(t, gl); + }, name, properties); +} + +// A utility function for waiting one animation frame before running the callback +// +// This is only needed after calling FakeXRDevice methods outside of an animation frame +// +// This is so that we can paper over the potential race allowed by the "next animation frame" +// concept https://immersive-web.github.io/webxr-test-api/#xrsession-next-animation-frame +function requestSkipAnimationFrame(session, callback) { + session.requestAnimationFrame(() => { + session.requestAnimationFrame(callback); + }); +} + +// A test function which runs through the common steps of requesting a session. +// Calls the passed in test function with the session, the controller for the +// device, and the test object. +function xr_session_promise_test( + name, func, fakeDeviceInit, sessionMode, sessionInit, properties, + glcontextPropertiesParam, gllayerPropertiesParam) { + const glcontextProperties = (glcontextPropertiesParam) ? glcontextPropertiesParam : {}; + const gllayerProperties = (gllayerPropertiesParam) ? gllayerPropertiesParam : {}; + + function runTest(t, glContext) { + let testSession; + let testDeviceController; + let sessionObjects = {gl: glContext}; + + // Ensure that any pending sessions are ended when done. This needs to + // use a cleanup function to ensure proper sequencing. If this were + // done in a .then() for the success case, a test that expected + // failure would already be marked done at the time that runs, and the + // shutdown would interfere with the next test which may have started. + t.add_cleanup(async () => { + // If a session was created, end it. + if (testSession) { + await testSession.end().catch(() => {}); + } + }); + + return navigator.xr.test.simulateDeviceConnection(fakeDeviceInit) + .then((controller) => { + testDeviceController = controller; + return sessionObjects.gl.makeXRCompatible(); + }) + .then(() => new Promise((resolve, reject) => { + // Perform the session request in a user gesture. + xr_debug(name, 'simulateUserActivation'); + navigator.xr.test.simulateUserActivation(() => { + xr_debug(name, 'document.hasFocus()=' + document.hasFocus()); + navigator.xr.requestSession(sessionMode, sessionInit || {}) + .then((session) => { + xr_debug(name, 'session start'); + testSession = session; + session.mode = sessionMode; + session.sessionInit = sessionInit; + let glLayer = new XRWebGLLayer(session, sessionObjects.gl, gllayerProperties); + glLayer.context = sessionObjects.gl; + // Session must have a baseLayer or frame requests + // will be ignored. + session.updateRenderState({ + baseLayer: glLayer + }); + sessionObjects.glLayer = glLayer; + xr_debug(name, 'session.visibilityState=' + session.visibilityState); + try { + resolve(func(session, testDeviceController, t, sessionObjects)); + } catch(err) { + reject("Test function failed with: " + err); + } + }) + .catch((err) => { + xr_debug(name, 'error: ' + err); + reject( + 'Session with params ' + + JSON.stringify(sessionMode) + + ' was rejected on device ' + + JSON.stringify(fakeDeviceInit) + + ' with error: ' + err); + }); + }); + })); + } + + xr_promise_test( + name + ' - webgl', + runTest, + properties, + 'webgl', + {alpha: false, antialias: false, ...glcontextProperties} + ); + xr_promise_test( + name + ' - webgl2', + runTest, + properties, + 'webgl2', + {alpha: false, antialias: false, ...glcontextProperties}); +} + + +// This function wraps the provided function in a +// simulateUserActivation() call, and resolves the promise with the +// result of func(), or an error if one is thrown +function promise_simulate_user_activation(func) { + return new Promise((resolve, reject) => { + navigator.xr.test.simulateUserActivation(() => { + try { let a = func(); resolve(a); } catch(e) { reject(e); } + }); + }); +} + +// This functions calls a callback with each API object as specified +// by https://immersive-web.github.io/webxr/spec/latest/, allowing +// checks to be made on all ojects. +// Arguements: +// callback: A callback function with two arguements, the first +// being the API object, the second being the name of +// that API object. +function forEachWebxrObject(callback) { + callback(window.navigator.xr, 'navigator.xr'); + callback(window.XRSession, 'XRSession'); + callback(window.XRSessionCreationOptions, 'XRSessionCreationOptions'); + callback(window.XRFrameRequestCallback, 'XRFrameRequestCallback'); + callback(window.XRPresentationContext, 'XRPresentationContext'); + callback(window.XRFrame, 'XRFrame'); + callback(window.XRLayer, 'XRLayer'); + callback(window.XRView, 'XRView'); + callback(window.XRViewport, 'XRViewport'); + callback(window.XRViewerPose, 'XRViewerPose'); + callback(window.XRWebGLLayer, 'XRWebGLLayer'); + callback(window.XRWebGLLayerInit, 'XRWebGLLayerInit'); + callback(window.XRCoordinateSystem, 'XRCoordinateSystem'); + callback(window.XRFrameOfReference, 'XRFrameOfReference'); + callback(window.XRStageBounds, 'XRStageBounds'); + callback(window.XRSessionEvent, 'XRSessionEvent'); + callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent'); +} + +// Code for loading test API in Chromium. +async function loadChromiumResources() { + await loadScript('/resources/chromium/webxr-test-math-helper.js'); + await import('/resources/chromium/webxr-test.js'); + await loadScript('/resources/testdriver.js'); + await loadScript('/resources/testdriver-vendor.js'); + + // This infrastructure is also used by Chromium-specific internal tests that + // may need additional resources (e.g. internal API extensions), this allows + // those tests to rely on this infrastructure while ensuring that no tests + // make it into public WPTs that rely on APIs outside of the webxr test API. + if (typeof(additionalChromiumResources) !== 'undefined') { + for (const path of additionalChromiumResources) { + await loadScript(path); + } + } + + xr_debug = navigator.xr.test.Debug; +} + +function setupWebKitWebXRTestAPI() { + // WebKit setup. The internals object is used by the WebKit test runner + // to provide JS access to internal APIs. In this case it's used to + // ensure that XRTest is only exposed to wpt tests. + navigator.xr.test = internals.xrTest; + return Promise.resolve(); +} |