The open source OpenXR runtime
at prediction 553 lines 18 kB view raw
1// Copyright 2021, Collabora, Ltd. 2// Copyright 2024-2025, NVIDIA CORPORATION. 3// SPDX-License-Identifier: BSL-1.0 4/*! 5 * @file 6 * @brief Driver to emulate controllers from hand-tracking input 7 * @author Moshi Turner <moshiturner@protonmail.com> 8 * @author Nick Klingensmith <programmerpichu@gmail.com> 9 * 10 * @ingroup drv_cemu 11 */ 12 13#include "xrt/xrt_defines.h" 14#include "xrt/xrt_device.h" 15 16#include "os/os_time.h" 17 18#include "math/m_api.h" 19#include "math/m_space.h" 20#include "math/m_vec3.h" 21 22#include "util/u_var.h" 23#include "util/u_time.h" 24#include "util/u_misc.h" 25#include "util/u_debug.h" 26#include "util/u_device.h" 27#include "util/u_distortion_mesh.h" 28#include "util/u_config_json.h" 29 30#include "ht_ctrl_emu_interface.h" 31 32#include <assert.h> 33#include <stdio.h> 34 35 36static const float cm2m = 0.01f; 37 38DEBUG_GET_ONCE_LOG_OPTION(cemu_log, "CEMU_LOG", U_LOGGING_TRACE) 39 40#define CEMU_TRACE(d, ...) U_LOG_XDEV_IFL_T(&d->base, d->sys->log_level, __VA_ARGS__) 41#define CEMU_DEBUG(d, ...) U_LOG_XDEV_IFL_D(&d->base, d->sys->log_level, __VA_ARGS__) 42#define CEMU_INFO(d, ...) U_LOG_XDEV_IFL_I(&d->base, d->sys->log_level, __VA_ARGS__) 43#define CEMU_WARN(d, ...) U_LOG_XDEV_IFL_W(&d->base, d->sys->log_level, __VA_ARGS__) 44#define CEMU_ERROR(d, ...) U_LOG_XDEV_IFL_E(&d->base, d->sys->log_level, __VA_ARGS__) 45 46enum cemu_input_index 47{ 48 CEMU_INDEX_HAND_TRACKING, 49 CEMU_INDEX_PINCH_BOOL, 50 CEMU_INDEX_PINCH_FLOAT, 51 CEMU_INDEX_GRIP, 52 CEMU_INDEX_AIM, 53 CEMU_NUM_INPUTS, 54}; 55 56static enum xrt_space_relation_flags valid_flags = (enum xrt_space_relation_flags)( 57 XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT | 58 XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT); 59 60struct cemu_system 61{ 62 // We don't own the head - never free this 63 struct xrt_device *in_head; 64 // We "own" the hand, and it gets replaced by the out_hands. So once they are both freed we need to free the 65 // original hand tracker 66 struct xrt_device *in_hand; 67 68 struct cemu_device *out_hand[2]; 69 70 float grip_offset_from_palm; 71 72 float waggle, curl, twist; 73 74 enum u_logging_level log_level; 75}; 76 77struct cemu_device 78{ 79 struct xrt_device base; 80 struct cemu_system *sys; 81 82 int hand_index; 83 enum xrt_input_name ht_input_name; 84 85 struct xrt_tracking_origin tracking_origin; 86}; 87 88xrt_quat 89wct_to_quat(float waggle, float curl, float twist) 90{ 91 xrt_vec3 waggle_axis = {0, 1, 0}; 92 xrt_quat just_waggle; 93 math_quat_from_angle_vector(waggle, &waggle_axis, &just_waggle); 94 95 xrt_vec3 curl_axis = {1, 0, 0}; 96 xrt_quat just_curl; 97 math_quat_from_angle_vector(curl, &curl_axis, &just_curl); 98 99 xrt_vec3 twist_axis = {0, 0, 1}; 100 xrt_quat just_twist; 101 math_quat_from_angle_vector(twist, &twist_axis, &just_twist); 102 103 xrt_quat out = just_waggle; // Unnecessary but much easier to look at. 104 105 math_quat_rotate(&out, &just_curl, &out); 106 math_quat_rotate(&out, &just_twist, &out); 107 return out; 108} 109 110static inline bool 111find_best_ht_input_names(const struct xrt_device *ht_device, enum xrt_input_name ht_input_names[2]) 112{ 113 assert(ht_device && ht_device->inputs); 114 115 if (!ht_device->supported.hand_tracking) 116 return false; 117 118 constexpr const enum xrt_input_name XRT_NULL_INPUT_NAME = (enum xrt_input_name)0; 119 120 ht_input_names[0] = ht_input_names[1] = XRT_NULL_INPUT_NAME; 121 122 for (uint32_t i = 0; i < ht_device->input_count; ++i) { 123 if (ht_device->inputs[i].name == XRT_INPUT_HT_UNOBSTRUCTED_LEFT) { 124 ht_input_names[0] = XRT_INPUT_HT_UNOBSTRUCTED_LEFT; 125 } 126 127 if (ht_device->inputs[i].name == XRT_INPUT_HT_UNOBSTRUCTED_RIGHT) { 128 ht_input_names[1] = XRT_INPUT_HT_UNOBSTRUCTED_RIGHT; 129 } 130 } 131 132 for (uint32_t i = 0; i < ht_device->input_count; ++i) { 133 if (ht_input_names[0] == XRT_NULL_INPUT_NAME && 134 ht_device->inputs[i].name == XRT_INPUT_HT_CONFORMING_LEFT) { 135 ht_input_names[0] = XRT_INPUT_HT_CONFORMING_LEFT; 136 } 137 138 if (ht_input_names[1] == XRT_NULL_INPUT_NAME && 139 ht_device->inputs[i].name == XRT_INPUT_HT_CONFORMING_RIGHT) { 140 ht_input_names[1] = XRT_INPUT_HT_CONFORMING_RIGHT; 141 } 142 } 143 144 return ht_input_names[0] != XRT_NULL_INPUT_NAME && // 145 ht_input_names[1] != XRT_NULL_INPUT_NAME; 146} 147 148static inline struct cemu_device * 149cemu_device(struct xrt_device *xdev) 150{ 151 return (struct cemu_device *)xdev; 152} 153 154 155static void 156cemu_device_destroy(struct xrt_device *xdev) 157{ 158 struct cemu_device *dev = cemu_device(xdev); 159 struct cemu_system *system = dev->sys; 160 161 // Remove the variable tracking. 162 u_device_free(&system->out_hand[dev->hand_index]->base); 163 164 system->out_hand[dev->hand_index] = NULL; 165 166 if ((system->out_hand[0] == NULL) && (system->out_hand[1] == NULL)) { 167 xrt_device_destroy(&system->in_hand); 168 u_var_remove_root(system); 169 free(system); 170 } 171} 172 173static xrt_result_t 174cemu_device_get_hand_tracking(struct xrt_device *xdev, 175 enum xrt_input_name name, 176 int64_t requested_timestamp_ns, 177 struct xrt_hand_joint_set *out_value, 178 int64_t *out_timestamp_ns) 179{ 180 // Shadows normal hand tracking - does nothing differently 181 182 struct cemu_device *dev = cemu_device(xdev); 183 struct cemu_system *system = dev->sys; 184 185 if (name != dev->ht_input_name) { 186 U_LOG_XDEV_UNSUPPORTED_INPUT(&dev->base, system->log_level, name); 187 return XRT_ERROR_INPUT_UNSUPPORTED; 188 } 189 190 return xrt_device_get_hand_tracking(system->in_hand, dev->ht_input_name, requested_timestamp_ns, out_value, 191 out_timestamp_ns); 192} 193 194static xrt_vec3 195joint_position_global(xrt_hand_joint_set *joint_set, xrt_hand_joint joint) 196{ 197 struct xrt_space_relation out_relation; 198 struct xrt_relation_chain xrc = {}; 199 m_relation_chain_push_relation(&xrc, &joint_set->values.hand_joint_set_default[joint].relation); 200 m_relation_chain_push_relation(&xrc, &joint_set->hand_pose); 201 m_relation_chain_resolve(&xrc, &out_relation); 202 return out_relation.pose.position; 203} 204 205static xrt_pose 206joint_pose_global(xrt_hand_joint_set *joint_set, xrt_hand_joint joint) 207{ 208 struct xrt_space_relation out_relation; 209 struct xrt_relation_chain xrc = {}; 210 m_relation_chain_push_relation(&xrc, &joint_set->values.hand_joint_set_default[joint].relation); 211 m_relation_chain_push_relation(&xrc, &joint_set->hand_pose); 212 m_relation_chain_resolve(&xrc, &out_relation); 213 return out_relation.pose; 214} 215 216static xrt_result_t 217do_grip_pose(struct xrt_hand_joint_set *joint_set, 218 struct xrt_space_relation *out_relation, 219 float grip_offset_from_palm, 220 bool is_right) 221{ 222 223 xrt_pose offset_from_palm; 224 math_pose_identity(&offset_from_palm); 225 offset_from_palm.position.y = -grip_offset_from_palm; 226 xrt_pose palm = joint_pose_global(joint_set, XRT_HAND_JOINT_PALM); 227 228 // Position. 229 struct xrt_relation_chain xrc = {}; 230 m_relation_chain_push_pose(&xrc, &offset_from_palm); 231 m_relation_chain_push_pose(&xrc, &palm); 232 m_relation_chain_resolve(&xrc, out_relation); 233 234 235 // Orientation. 236 xrt_vec3 indx_position = joint_position_global(joint_set, XRT_HAND_JOINT_INDEX_PROXIMAL); 237 xrt_vec3 ring_position = joint_position_global(joint_set, XRT_HAND_JOINT_RING_PROXIMAL); 238 struct xrt_vec3 plus_z = ring_position - indx_position; 239 struct xrt_vec3 plus_x; 240 struct xrt_vec3 to_rotate = {0.0f, is_right ? 1.0f : -1.0f, 0.0f}; 241 242 math_quat_rotate_vec3(&palm.orientation, &to_rotate, &plus_x); 243 244 plus_x = m_vec3_orthonormalize(plus_z, plus_x); 245 246 math_vec3_normalize(&plus_x); 247 math_vec3_normalize(&plus_z); 248 249 math_quat_from_plus_x_z(&plus_x, &plus_z, &out_relation->pose.orientation); 250 251 out_relation->relation_flags = valid_flags; 252 253 return XRT_SUCCESS; 254} 255 256 257 258static xrt_result_t 259get_other_two(struct cemu_device *dev, 260 int64_t head_timestamp_ns, 261 int64_t hand_timestamp_ns, 262 xrt_pose *out_head, 263 xrt_hand_joint_set *out_secondary) 264{ 265 struct cemu_system *sys = dev->sys; 266 struct xrt_space_relation head_rel; 267 xrt_result_t xret = 268 xrt_device_get_tracked_pose(sys->in_head, XRT_INPUT_GENERIC_HEAD_POSE, head_timestamp_ns, &head_rel); 269 U_LOG_CHK_AND_RET(sys->log_level, xret, "xrt_device_get_tracked_pose"); 270 271 *out_head = head_rel.pose; 272 int other; 273 if (dev->hand_index == 0) { 274 other = 1; 275 } else { 276 other = 0; 277 } 278 279 int64_t noop; 280 return xrt_device_get_hand_tracking(sys->in_hand, sys->out_hand[other]->ht_input_name, hand_timestamp_ns, 281 out_secondary, &noop); 282} 283 284// Mostly stolen from 285// https://github.com/maluoi/StereoKit/blob/048b689f71d080a67fde29838c0362a49b88b3d6/StereoKitC/systems/hand/hand_oxr_articulated.cpp#L149 286static xrt_result_t 287do_aim_pose(struct cemu_device *dev, 288 struct xrt_hand_joint_set *joint_set_primary, 289 int64_t head_timestamp_ns, 290 int64_t hand_timestamp_ns, 291 struct xrt_space_relation *out_relation) 292{ 293 struct cemu_system *sys = dev->sys; 294 295 struct xrt_vec3 vec3_up = {0, 1, 0}; 296 struct xrt_pose head; 297 struct xrt_hand_joint_set joint_set_secondary; 298#if 0 299 // "Jakob way" 300 xrt_result_t xret = get_other_two(dev, hand_timestamp_ns, hand_timestamp_ns, &head, &joint_set_secondary); 301#else 302 // "Moshi way" 303 xrt_result_t xret = get_other_two(dev, head_timestamp_ns, hand_timestamp_ns, &head, &joint_set_secondary); 304#endif 305 U_LOG_CHK_AND_RET(sys->log_level, xret, "get_other_two"); 306 307 // Average shoulder width for women:37cm, men:41cm, center of shoulder 308 // joint is around 4cm inwards 309 const float avg_shoulder_width = ((39.0f / 2.0f) - 4.0f) * cm2m; 310 const float head_length = 10 * cm2m; 311 const float neck_length = 7 * cm2m; 312 313 // Chest center is down to the base of the head, and then down the neck. 314 xrt_vec3 down_the_base_of_head; 315 xrt_vec3 base_head_direction = {0, -head_length, 0}; 316 317 math_quat_rotate_vec3(&head.orientation, &base_head_direction, &down_the_base_of_head); 318 319 xrt_vec3 chest_center = head.position + down_the_base_of_head + xrt_vec3{0, -neck_length, 0}; 320 321 xrt_vec3 face_fwd; 322 xrt_vec3 forwards = {0, 0, -1}; 323 324 math_quat_rotate_vec3(&head.orientation, &forwards, &face_fwd); 325 326 face_fwd = m_vec3_mul_scalar(m_vec3_normalize(face_fwd), 2); 327 face_fwd += m_vec3_mul_scalar( 328 m_vec3_normalize(joint_position_global(joint_set_primary, XRT_HAND_JOINT_WRIST) - chest_center), 1); 329 if (joint_set_secondary.is_active) { 330 face_fwd += m_vec3_mul_scalar( 331 m_vec3_normalize(joint_position_global(&joint_set_secondary, XRT_HAND_JOINT_WRIST) - chest_center), 332 1); 333 } 334 face_fwd.y = 0; 335 m_vec3_normalize(face_fwd); 336 337 xrt_vec3 face_right; 338 math_vec3_cross(&face_fwd, &vec3_up, &face_right); 339 math_vec3_normalize(&face_right); 340 face_right *= avg_shoulder_width; 341 342 xrt_vec3 shoulder = chest_center + face_right * (dev->hand_index == 1 ? 1.0f : -1.0f); 343 344 xrt_vec3 ray_joint = joint_position_global(joint_set_primary, XRT_HAND_JOINT_INDEX_PROXIMAL); 345 346 struct xrt_vec3 ray_direction = shoulder - ray_joint; 347 348 struct xrt_vec3 up = {0, 1, 0}; 349 350 struct xrt_vec3 out_x_vector; 351 352 // math_vec3_normalize(&tip_to_palm); 353 math_vec3_normalize(&ray_direction); 354 355 math_vec3_cross(&up, &ray_direction, &out_x_vector); 356 357 out_relation->pose.position = ray_joint; 358 359 math_quat_from_plus_x_z(&out_x_vector, &ray_direction, &out_relation->pose.orientation); 360 361 out_relation->relation_flags = valid_flags; 362 363 return xret; 364} 365 366// Pose for controller emulation 367static xrt_result_t 368cemu_device_get_tracked_pose(struct xrt_device *xdev, 369 enum xrt_input_name name, 370 int64_t at_timestamp_ns, 371 struct xrt_space_relation *out_relation) 372{ 373 struct cemu_device *dev = cemu_device(xdev); 374 struct cemu_system *sys = dev->sys; 375 376 if (name != XRT_INPUT_HAND_CTRL_EMU_AIM_POSE && name != XRT_INPUT_HAND_CTRL_EMU_GRIP_POSE) { 377 U_LOG_XDEV_UNSUPPORTED_INPUT(&dev->base, dev->sys->log_level, name); 378 return XRT_ERROR_INPUT_UNSUPPORTED; 379 } 380 static int64_t hand_timestamp_ns; 381 382 struct xrt_hand_joint_set joint_set; 383 xrt_result_t xret = xrt_device_get_hand_tracking(sys->in_hand, dev->ht_input_name, at_timestamp_ns, &joint_set, 384 &hand_timestamp_ns); 385 U_LOG_CHK_AND_RET(sys->log_level, xret, "xrt_device_get_hand_tracking"); 386 387 if (joint_set.is_active == false) { 388 out_relation->relation_flags = XRT_SPACE_RELATION_BITMASK_NONE; 389 return XRT_SUCCESS; 390 } 391 392 xret = XRT_SUCCESS; 393 switch (name) { 394 case XRT_INPUT_HAND_CTRL_EMU_GRIP_POSE: { 395 xret = do_grip_pose(&joint_set, out_relation, sys->grip_offset_from_palm, dev->hand_index); 396 break; 397 } 398 case XRT_INPUT_HAND_CTRL_EMU_AIM_POSE: { 399 // Assume that now we're doing everything in the timestamp from the hand-tracker, so use 400 // hand_timestamp_ns. This will cause the controller to lag behind but otherwise be correct 401 xret = do_aim_pose(dev, &joint_set, at_timestamp_ns, hand_timestamp_ns, out_relation); 402 break; 403 } 404 default: assert(false); 405 } 406 407 return xret; 408} 409 410//! @todo This is flickery; investigate once we get better hand tracking 411static void 412decide(xrt_vec3 one, xrt_vec3 two, bool *out) 413{ 414 float dist = m_vec3_len_sqrd(one - two); 415 // These used to be 0.02f and 0.04f, but I bumped them way up to compensate for bad tracking. Once our tracking 416 // is better, bump these back down. 417 float activation_dist = 0.02f; 418 float deactivation_dist = 0.04f; 419 const float pinch_activation_dist = 420 (*out ? deactivation_dist * deactivation_dist : activation_dist * activation_dist); 421 422 *out = (dist < pinch_activation_dist); 423} 424 425static xrt_result_t 426cemu_device_update_inputs(struct xrt_device *xdev) 427{ 428 struct cemu_device *dev = cemu_device(xdev); 429 struct cemu_system *sys = dev->sys; 430 431 struct xrt_hand_joint_set joint_set; 432 int64_t noop; 433 434 xrt_result_t xret = xrt_device_get_hand_tracking(dev->sys->in_hand, dev->ht_input_name, os_monotonic_get_ns(), 435 &joint_set, &noop); 436 U_LOG_CHK_AND_RET(sys->log_level, xret, "xrt_device_get_hand_tracking"); 437 438 if (!joint_set.is_active) { 439 xdev->inputs[CEMU_INDEX_PINCH_BOOL].value.boolean = false; 440 xdev->inputs[CEMU_INDEX_PINCH_FLOAT].value.vec1.x = 0.f; 441 return XRT_SUCCESS; 442 } 443 444 decide(joint_set.values.hand_joint_set_default[XRT_HAND_JOINT_INDEX_TIP].relation.pose.position, 445 joint_set.values.hand_joint_set_default[XRT_HAND_JOINT_THUMB_TIP].relation.pose.position, 446 &xdev->inputs[CEMU_INDEX_PINCH_BOOL].value.boolean); 447 448 // For now, all other inputs are off - detecting any gestures more complicated than pinch is too unreliable for 449 // now. 450 if (xdev->inputs[CEMU_INDEX_PINCH_BOOL].value.boolean) { 451 xdev->inputs[CEMU_INDEX_PINCH_FLOAT].value.vec1.x = 1.0f; 452 } else { 453 xdev->inputs[CEMU_INDEX_PINCH_FLOAT].value.vec1.x = 0.0f; 454 } 455 456 return XRT_SUCCESS; 457} 458 459static struct xrt_binding_input_pair simple_inputs[3] = { 460 {XRT_INPUT_SIMPLE_SELECT_CLICK, XRT_INPUT_HAND_CTRL_EMU_PINCH_BOOL}, 461 {XRT_INPUT_SIMPLE_GRIP_POSE, XRT_INPUT_HAND_CTRL_EMU_GRIP_POSE}, 462 {XRT_INPUT_SIMPLE_AIM_POSE, XRT_INPUT_HAND_CTRL_EMU_AIM_POSE}, 463}; 464 465static struct xrt_binding_profile binding_profiles[1] = { 466 { 467 .name = XRT_DEVICE_SIMPLE_CONTROLLER, 468 .inputs = simple_inputs, 469 .input_count = ARRAY_SIZE(simple_inputs), 470 .outputs = nullptr, 471 .output_count = 0, 472 }, 473}; 474 475extern "C" int 476cemu_devices_create(struct xrt_device *head, struct xrt_device *hands, struct xrt_device **out_xdevs) 477{ 478 enum xrt_input_name ht_input_names[2]; 479 if (!find_best_ht_input_names(hands, ht_input_names)) { 480 U_LOG_E("\"hands\" parameter is not a hand-tracking device or does have expected inputs."); 481 return 0; 482 } 483 484 enum u_device_alloc_flags flags = U_DEVICE_ALLOC_NO_FLAGS; 485 486 struct cemu_device *cemud[2]; 487 488 struct cemu_system *system = U_TYPED_CALLOC(struct cemu_system); 489 system->in_hand = hands; 490 system->in_head = head; 491 492 system->log_level = debug_get_log_option_cemu_log(); 493 494 system->grip_offset_from_palm = 0.03f; // 3 centimeters 495 496 for (int i = 0; i < 2; i++) { 497 cemud[i] = U_DEVICE_ALLOCATE(struct cemu_device, flags, CEMU_NUM_INPUTS, 0); 498 499 cemud[i]->sys = system; 500 501 cemud[i]->base.tracking_origin = hands->tracking_origin; 502 503 cemud[i]->base.name = XRT_DEVICE_HAND_CTRL_EMU; 504 cemud[i]->base.supported.hand_tracking = true; 505 cemud[i]->base.supported.orientation_tracking = true; 506 cemud[i]->base.supported.position_tracking = true; 507 cemud[i]->base.binding_profiles = binding_profiles; 508 cemud[i]->base.binding_profile_count = ARRAY_SIZE(binding_profiles); 509 510 cemud[i]->base.inputs[CEMU_INDEX_HAND_TRACKING].name = ht_input_names[i]; 511 cemud[i]->base.inputs[CEMU_INDEX_PINCH_BOOL].name = XRT_INPUT_HAND_CTRL_EMU_PINCH_BOOL; 512 cemud[i]->base.inputs[CEMU_INDEX_PINCH_FLOAT].name = XRT_INPUT_HAND_CTRL_EMU_PINCH_VALUE; 513 cemud[i]->base.inputs[CEMU_INDEX_GRIP].name = XRT_INPUT_HAND_CTRL_EMU_GRIP_POSE; 514 cemud[i]->base.inputs[CEMU_INDEX_AIM].name = XRT_INPUT_HAND_CTRL_EMU_AIM_POSE; 515 516 cemud[i]->base.update_inputs = cemu_device_update_inputs; 517 cemud[i]->base.get_tracked_pose = cemu_device_get_tracked_pose; 518 cemud[i]->base.set_output = u_device_ni_set_output; 519 cemud[i]->base.get_hand_tracking = cemu_device_get_hand_tracking; 520 cemud[i]->base.destroy = cemu_device_destroy; 521 522 cemud[i]->base.device_type = 523 i ? XRT_DEVICE_TYPE_RIGHT_HAND_CONTROLLER : XRT_DEVICE_TYPE_LEFT_HAND_CONTROLLER; 524 525 int n = 526 snprintf(cemud[i]->base.str, XRT_DEVICE_NAME_LEN, "%s %s Hand", i ? "Right" : "Left", hands->str); 527 if (n > XRT_DEVICE_NAME_LEN) { 528 CEMU_DEBUG(cemud[i], "name truncated: %s", cemud[i]->base.str); 529 } 530 531 n = snprintf(cemud[i]->base.serial, XRT_DEVICE_NAME_LEN, "%s (%d)", hands->str, i); 532 if (n > XRT_DEVICE_NAME_LEN) { 533 CEMU_WARN(cemud[i], "serial truncated: %s", cemud[i]->base.str); 534 } 535 536 cemud[i]->ht_input_name = ht_input_names[i]; 537 cemud[i]->hand_index = i; 538 system->out_hand[i] = cemud[i]; 539 540 out_xdevs[i] = &cemud[i]->base; 541 } 542 543 u_var_add_root(system, "Controller emulation!", true); 544 u_var_add_f32(system, &system->grip_offset_from_palm, "Grip pose offset"); 545 546 return 2; 547 548 // We actually don't need these - no failure condition yet. Uncomment whenever you need 'em 549 // cleanup: 550 // cemu_device_destroy(&cemud[0]->base); 551 // cemu_device_destroy(&cemud[1]->base); 552 // return 0; 553}