diff --git a/.github/workflows/Linux-samples-gfx.yml b/.github/workflows/Linux-samples-gfx.yml index 442ea4aecd..8384453adb 100644 --- a/.github/workflows/Linux-samples-gfx.yml +++ b/.github/workflows/Linux-samples-gfx.yml @@ -913,13 +913,31 @@ jobs: # Vulkan through the symbol wrapper pointed at mocks, with # each allocation it makes failed in turn: a chain comes back # with every pass and every deferred-disposal queue, or not - # at all, and nothing is left behind. + # at all, and nothing is left behind. A replay of the last + # frame's final pass reads what that pass read, and a chain + # made to replay keeps one history image more. make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ timeout 60 ./vulkan_chain_alloc_test make clean echo "[pass] vulkan_chain_alloc_test" + - name: Build and run gl3_shader_status_test and gl3_chain_alloc_test (ASan + UBSan) + shell: bash + working-directory: samples/gfx/gl3_shader_status + run: | + set -eu + # gfx/drivers_shader/shader_gl3.c with GL through the glsym + # table pointed at mocks: shader status checks, the chain's + # allocations failed in turn, and a replay of the last frame's + # final pass reading the textures, at the generations, that + # pass read, the history keeping a spare. + make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined + ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ + timeout 60 make check SANITIZER=address,undefined + make clean + echo "[pass] gl3_shader_status_test, gl3_chain_alloc_test" + - name: Build and run vulkan_texture_size_test (ASan) shell: bash working-directory: samples/gfx/vulkan_texture_size @@ -947,6 +965,32 @@ jobs: timeout 60 ./vulkan_texture_size_test echo "[pass] vulkan_texture_size_test" + - name: Build and run vulkan_views_target_test + shell: bash + working-directory: samples/gfx/vulkan_views_target + run: | + set -eu + # gfx/drivers/vulkan.c's retained view targets and headset + # draw, copied out of the driver, against a mocked device: a + # failed allocation is not retried at the same size, a resize + # waits while both retired slots are full, and each headset + # screen is drawn straight into the runtime's image, a repeat + # ending no chain frame. + make clean && timeout 60 make check + echo "[pass] vulkan_views_target_test" + + - name: Build and run gl3_xr_draw_test + shell: bash + working-directory: samples/gfx/gl3_xr_draw + run: | + set -eu + # gfx/drivers/gl3.c's headset draw, copied out of the driver, + # with GL stubbed: each screen straight into the runtime's + # texture, and a screen moved or resized while paused drawn + # again without ending a chain frame. + make clean && timeout 60 make check + echo "[pass] gl3_xr_draw_test" + - name: GLES persistent loan pixel oracle (Mesa EGL) shell: bash run: | diff --git a/.github/workflows/Windows-wine-views.yml b/.github/workflows/Windows-wine-views.yml index 18fb762570..da0315bd68 100644 --- a/.github/workflows/Windows-wine-views.yml +++ b/.github/workflows/Windows-wine-views.yml @@ -28,6 +28,8 @@ on: - 'gfx/video_views_draw.h' - 'gfx/video_driver.c' - 'gfx/video_thread_wrapper.*' + - 'configuration.*' + - 'samples/settings/video_screens/**' - 'samples/cores/video_views/**' - '.github/workflows/Windows-wine-views.yml' pull_request: @@ -45,6 +47,8 @@ on: - 'gfx/video_views_draw.h' - 'gfx/video_driver.c' - 'gfx/video_thread_wrapper.*' + - 'configuration.*' + - 'samples/settings/video_screens/**' - 'samples/cores/video_views/**' - '.github/workflows/Windows-wine-views.yml' workflow_dispatch: @@ -136,10 +140,11 @@ jobs: set -eu export CCACHE_DIR="$HOME/.ccache" ccache -M 250M + # The screens' windows too, which video_screens_test runs. ./configure --host=x86_64-w64-mingw32 \ --disable-qt --disable-vulkan --disable-ffmpeg \ --disable-cheevos --disable-discord --enable-d3d8 \ - --disable-sdl --disable-sdl2 + --disable-sdl --disable-sdl2 --enable-video_windows make -j$(nproc) CC="ccache x86_64-w64-mingw32-gcc" \ CXX="ccache x86_64-w64-mingw32-g++" ccache -s @@ -149,6 +154,13 @@ jobs: done make -C samples/cores/video_views DLL=1 CC=x86_64-w64-mingw32-gcc + - name: Build and run video_screens_test under Wine + shell: bash + run: | + set -eu + WINEDEBUG=-all xvfb-run -a python3 samples/settings/video_screens/build.py \ + --output-dir "$RUNNER_TEMP/video-screens" --runner wine + - name: Views on GDI, D3D8, D3D9, D3D10, D3D11 and D3D12, end to end (Wine, Xvfb, llvmpipe, lavapipe) shell: bash run: | diff --git a/command.h b/command.h index 9d6f7e3efc..726ca96103 100644 --- a/command.h +++ b/command.h @@ -286,6 +286,8 @@ enum event_command CMD_EVENT_LASER_POINTER_TOGGLE, /* Cycle what a core is told the headset displays: immersive, stereoscopic, flat. */ CMD_EVENT_HEADSET_VIEW_TOGGLE, + /* Start or stop moving the headset's screens with its controllers. */ + CMD_EVENT_HEADSET_EDIT_SCREENS, /* AI service. */ CMD_EVENT_AI_SERVICE_TOGGLE, CMD_EVENT_AI_SERVICE_CALL, @@ -1367,6 +1369,7 @@ static const struct cmd_map map[] = { { "HEADSET_RECENTER", RARCH_HEADSET_RECENTER, "Recenter the headset's screens in front of where it looks.", CMD_INFO_NO_TOOL }, { "LASER_POINTER_TOGGLE", RARCH_LASER_POINTER_TOGGLE, "Show or hide the headset's laser pointer.", CMD_INFO_NO_TOOL }, { "HEADSET_VIEW_TOGGLE", RARCH_HEADSET_VIEW_TOGGLE, "Cycle the headset view between immersive, stereoscopic and flat.", CMD_INFO_NO_TOOL }, + { "HEADSET_EDIT_SCREENS", RARCH_HEADSET_EDIT_SCREENS, "Start or stop moving the headset's screens and menu with its controllers.", CMD_INFO_NO_TOOL }, { "FPS_TOGGLE", RARCH_FPS_TOGGLE, "Show or hide the framerate.", CMD_INFO_NO_TOOL }, { "STATISTICS_TOGGLE", RARCH_STATISTICS_TOGGLE, "Show or hide the technical statistics.", CMD_INFO_NO_TOOL }, { "AI_SERVICE", RARCH_AI_SERVICE, "Run the AI service (translate or narrate the screen).", CMD_INFO_NO_TOOL }, diff --git a/config.def.keybinds.h b/config.def.keybinds.h index 98739775d4..8355d38729 100644 --- a/config.def.keybinds.h +++ b/config.def.keybinds.h @@ -469,6 +469,11 @@ static const struct retro_keybind_def retro_keybinds_1[] = { RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE, RETROK_UNKNOWN, true), RARCH_HEADSET_VIEW_TOGGLE, NO_BTN, NO_BTN, 0 }, + { + AXIS_NONE, AXIS_NONE, + RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, RETROK_UNKNOWN, true), + RARCH_HEADSET_EDIT_SCREENS, NO_BTN, NO_BTN, 0 + }, { AXIS_NONE, AXIS_NONE, RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, RETROK_UNKNOWN, true), @@ -966,6 +971,11 @@ static const struct retro_keybind_def retro_keybinds_1[] = { RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE, RETROK_UNKNOWN, true), RARCH_HEADSET_VIEW_TOGGLE, NO_BTN, NO_BTN, 0 }, + { + AXIS_NONE, AXIS_NONE, + RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, RETROK_UNKNOWN, true), + RARCH_HEADSET_EDIT_SCREENS, NO_BTN, NO_BTN, 0 + }, { AXIS_NONE, AXIS_NONE, RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, RETROK_UNKNOWN, true), @@ -1471,6 +1481,11 @@ static const struct retro_keybind_def retro_keybinds_1[] = { RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE, RETROK_v, true), RARCH_HEADSET_VIEW_TOGGLE, NO_BTN, NO_BTN, 0 }, + { + AXIS_NONE, AXIS_NONE, + RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, RETROK_UNKNOWN, true), + RARCH_HEADSET_EDIT_SCREENS, NO_BTN, NO_BTN, 0 + }, { AXIS_NONE, AXIS_NONE, RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, RETROK_F3, true), @@ -1965,6 +1980,11 @@ static const struct retro_keybind_def retro_keybinds_rest[] = { RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE, RETROK_UNKNOWN, true), RARCH_HEADSET_VIEW_TOGGLE, NO_BTN, NO_BTN, 0 }, + { + AXIS_NONE, AXIS_NONE, + RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, RETROK_UNKNOWN, true), + RARCH_HEADSET_EDIT_SCREENS, NO_BTN, NO_BTN, 0 + }, { AXIS_NONE, AXIS_NONE, RETRO_KEYBIND_PACK(MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, RETROK_UNKNOWN, true), diff --git a/configuration.c b/configuration.c index ca44b57d5d..86043dc9f5 100644 --- a/configuration.c +++ b/configuration.c @@ -39,6 +39,7 @@ #include "file_path_special.h" #include "command.h" #include "configuration.h" +#include "gfx/video_views.h" #include "gfx/video_windows.h" #include "content.h" #include "config.def.h" @@ -455,6 +456,7 @@ const struct input_bind_map input_config_bind_map[RARCH_BIND_LIST_END_NULL] = { DECLARE_META_BIND(2, headset_recenter, RARCH_HEADSET_RECENTER, MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_RECENTER), DECLARE_META_BIND(2, laser_pointer_toggle, RARCH_LASER_POINTER_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_LASER_POINTER_TOGGLE), DECLARE_META_BIND(2, headset_view_toggle, RARCH_HEADSET_VIEW_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE), + DECLARE_META_BIND(2, headset_edit_screens, RARCH_HEADSET_EDIT_SCREENS, MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS), DECLARE_META_BIND(2, fps_toggle, RARCH_FPS_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE), DECLARE_META_BIND(2, toggle_statistics, RARCH_STATISTICS_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_STATISTICS_TOGGLE), DECLARE_META_BIND(2, ai_service, RARCH_AI_SERVICE, MENU_ENUM_LABEL_VALUE_INPUT_META_AI_SERVICE), @@ -5889,6 +5891,127 @@ static struct config_int_setting *populate_settings_int( strlcpy(playlist_path, new_file, sizeof(playlist_path)); \ } \ +void config_reset_video_screen_xr(settings_t *settings, + unsigned first, unsigned count) +{ + video_screen_xr_t screen; + unsigned i; + + if (!settings || first >= RETRO_VIDEO_VIEWS_MAX) + return; + if (count > RETRO_VIDEO_VIEWS_MAX - first) + count = RETRO_VIDEO_VIEWS_MAX - first; + + video_views_xr_reset(&screen, 1); + for (i = first; i < first + count; i++) + { + settings->floats.video_screen_xr_x[i] = screen.x; + settings->floats.video_screen_xr_y[i] = screen.y; + settings->floats.video_screen_xr_z[i] = screen.z; + settings->floats.video_screen_xr_yaw[i] = screen.yaw; + settings->floats.video_screen_xr_pitch[i] = screen.pitch; + settings->floats.video_screen_xr_roll[i] = screen.roll; + settings->floats.video_screen_xr_scale[i] = screen.scale; + settings->bools.video_screen_xr_visible[i] = screen.visible; + } +} + +void config_reset_video_menu_xr(settings_t *settings) +{ + video_screen_xr_t menu; + if (!settings) + return; + video_views_xr_reset(&menu, 1); + settings->floats.video_openxr_menu_xr_x = menu.x; + settings->floats.video_openxr_menu_xr_y = menu.y; + settings->floats.video_openxr_menu_xr_z = menu.z; + settings->floats.video_openxr_menu_xr_yaw = menu.yaw; + settings->floats.video_openxr_menu_xr_pitch = menu.pitch; + settings->floats.video_openxr_menu_xr_roll = menu.roll; + settings->floats.video_openxr_menu_xr_scale = menu.scale; + settings->bools.video_openxr_menu_placed = false; +} + +void config_get_video_screen_xr(unsigned screen, video_screen_xr_t *out) +{ + settings_t *settings = config_get_ptr(); + video_views_xr_reset(out, 1); + if (!settings || screen >= RETRO_VIDEO_VIEWS_MAX) + return; + out->x = settings->floats.video_screen_xr_x[screen]; + out->y = settings->floats.video_screen_xr_y[screen]; + out->z = settings->floats.video_screen_xr_z[screen]; + out->yaw = settings->floats.video_screen_xr_yaw[screen]; + out->pitch = settings->floats.video_screen_xr_pitch[screen]; + out->roll = settings->floats.video_screen_xr_roll[screen]; + out->scale = settings->floats.video_screen_xr_scale[screen]; + out->visible = settings->bools.video_screen_xr_visible[screen]; +} + +bool config_get_video_menu_xr(video_screen_xr_t *out) +{ + settings_t *settings = config_get_ptr(); + video_views_xr_reset(out, 1); + if (!settings) + return false; + out->x = settings->floats.video_openxr_menu_xr_x; + out->y = settings->floats.video_openxr_menu_xr_y; + out->z = settings->floats.video_openxr_menu_xr_z; + out->yaw = settings->floats.video_openxr_menu_xr_yaw; + out->pitch = settings->floats.video_openxr_menu_xr_pitch; + out->roll = settings->floats.video_openxr_menu_xr_roll; + out->scale = settings->floats.video_openxr_menu_xr_scale; + return settings->bools.video_openxr_menu_placed; +} + +void config_set_video_screen_xr(unsigned screen, + const video_screen_xr_t *values) +{ + video_screen_xr_t s; + settings_t *settings = config_get_ptr(); + if (!settings || screen >= RETRO_VIDEO_VIEWS_MAX) + return; + if (!values) + config_reset_video_screen_xr(settings, screen, 1); + else + { + s = *values; + video_views_xr_normalize(&s); + settings->floats.video_screen_xr_x[screen] = s.x; + settings->floats.video_screen_xr_y[screen] = s.y; + settings->floats.video_screen_xr_z[screen] = s.z; + settings->floats.video_screen_xr_yaw[screen] = s.yaw; + settings->floats.video_screen_xr_pitch[screen] = s.pitch; + settings->floats.video_screen_xr_roll[screen] = s.roll; + settings->floats.video_screen_xr_scale[screen] = s.scale; + } + settings->flags |= SETTINGS_FLG_MODIFIED; +} + +void config_set_video_menu_xr(const video_screen_xr_t *values) +{ + video_screen_xr_t s; + settings_t *settings = config_get_ptr(); + if (!settings) + return; + if (!values) + config_reset_video_menu_xr(settings); + else + { + s = *values; + video_views_xr_normalize(&s); + settings->floats.video_openxr_menu_xr_x = s.x; + settings->floats.video_openxr_menu_xr_y = s.y; + settings->floats.video_openxr_menu_xr_z = s.z; + settings->floats.video_openxr_menu_xr_yaw = s.yaw; + settings->floats.video_openxr_menu_xr_pitch = s.pitch; + settings->floats.video_openxr_menu_xr_roll = s.roll; + settings->floats.video_openxr_menu_xr_scale = s.scale; + settings->bools.video_openxr_menu_placed = true; + } + settings->flags |= SETTINGS_FLG_MODIFIED; +} + /** * config_set_defaults: * @@ -6243,6 +6366,9 @@ void config_set_defaults(settings_t *target) settings->uints.input_touchpad_right[i] = RETRO_DEVICE_ID_JOYPAD_START; } + config_reset_video_screen_xr(settings, 0, RETRO_VIDEO_VIEWS_MAX); + config_reset_video_menu_xr(settings); + #ifdef HAVE_VIDEO_WINDOWS for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) { @@ -7310,6 +7436,63 @@ static bool config_load_file(const char *path, settings_t *settings) config_load_video_windows(settings, conf); #endif + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + video_screen_xr_t screen; + char prefix[64]; + size_t len = snprintf(prefix, sizeof(prefix), "video_screen%u_xr_", i + 1); + + video_views_xr_reset(&screen, 1); + strlcpy_lit(prefix + len, "x", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.x); + strlcpy_lit(prefix + len, "y", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.y); + strlcpy_lit(prefix + len, "z", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.z); + strlcpy_lit(prefix + len, "yaw", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.yaw); + strlcpy_lit(prefix + len, "pitch", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.pitch); + strlcpy_lit(prefix + len, "roll", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.roll); + strlcpy_lit(prefix + len, "scale", sizeof(prefix) - len); + config_get_float(conf, prefix, &screen.scale); + strlcpy_lit(prefix + len, "visible", sizeof(prefix) - len); + config_get_bool(conf, prefix, &screen.visible); + video_views_xr_normalize(&screen); + settings->floats.video_screen_xr_x[i] = screen.x; + settings->floats.video_screen_xr_y[i] = screen.y; + settings->floats.video_screen_xr_z[i] = screen.z; + settings->floats.video_screen_xr_yaw[i] = screen.yaw; + settings->floats.video_screen_xr_pitch[i] = screen.pitch; + settings->floats.video_screen_xr_roll[i] = screen.roll; + settings->floats.video_screen_xr_scale[i] = screen.scale; + settings->bools.video_screen_xr_visible[i] = screen.visible; + } + + { + video_screen_xr_t menu; + bool placed = false; + video_views_xr_reset(&menu, 1); + config_get_float(conf, "video_openxr_menu_xr_x", &menu.x); + config_get_float(conf, "video_openxr_menu_xr_y", &menu.y); + config_get_float(conf, "video_openxr_menu_xr_z", &menu.z); + config_get_float(conf, "video_openxr_menu_xr_yaw", &menu.yaw); + config_get_float(conf, "video_openxr_menu_xr_pitch", &menu.pitch); + config_get_float(conf, "video_openxr_menu_xr_roll", &menu.roll); + config_get_float(conf, "video_openxr_menu_xr_scale", &menu.scale); + config_get_bool(conf, "video_openxr_menu_placed", &placed); + video_views_xr_normalize(&menu); + settings->floats.video_openxr_menu_xr_x = menu.x; + settings->floats.video_openxr_menu_xr_y = menu.y; + settings->floats.video_openxr_menu_xr_z = menu.z; + settings->floats.video_openxr_menu_xr_yaw = menu.yaw; + settings->floats.video_openxr_menu_xr_pitch = menu.pitch; + settings->floats.video_openxr_menu_xr_roll = menu.roll; + settings->floats.video_openxr_menu_xr_scale = menu.scale; + settings->bools.video_openxr_menu_placed = placed; + } + { char prefix[64]; size_t _len = strlcpy_lit(prefix, "input_player", sizeof(prefix)); @@ -9816,6 +9999,111 @@ bool config_save_file(const char *path) } #endif + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + char cfg[64]; + size_t len = snprintf(cfg, sizeof(cfg), "video_screen%u_xr_", i + 1); + + strlcpy_lit(cfg + len, "x", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_x[i] != defaults->floats.video_screen_xr_x[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_x[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "y", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_y[i] != defaults->floats.video_screen_xr_y[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_y[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "z", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_z[i] != defaults->floats.video_screen_xr_z[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_z[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "yaw", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_yaw[i] != defaults->floats.video_screen_xr_yaw[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_yaw[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "pitch", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_pitch[i] != defaults->floats.video_screen_xr_pitch[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_pitch[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "roll", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_roll[i] != defaults->floats.video_screen_xr_roll[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_roll[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "scale", sizeof(cfg) - len); + if ( !minimal + || settings->floats.video_screen_xr_scale[i] != defaults->floats.video_screen_xr_scale[i]) + config_set_float(conf, cfg, settings->floats.video_screen_xr_scale[i]); + else + config_unset(conf, cfg); + + strlcpy_lit(cfg + len, "visible", sizeof(cfg) - len); + if ( !minimal + || settings->bools.video_screen_xr_visible[i] != defaults->bools.video_screen_xr_visible[i]) + config_set_string(conf, cfg, + settings->bools.video_screen_xr_visible[i] ? "true" : "false"); + else + config_unset(conf, cfg); + } + + if ( !minimal + || settings->floats.video_openxr_menu_xr_x != defaults->floats.video_openxr_menu_xr_x) + config_set_float(conf, "video_openxr_menu_xr_x", settings->floats.video_openxr_menu_xr_x); + else + config_unset(conf, "video_openxr_menu_xr_x"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_y != defaults->floats.video_openxr_menu_xr_y) + config_set_float(conf, "video_openxr_menu_xr_y", settings->floats.video_openxr_menu_xr_y); + else + config_unset(conf, "video_openxr_menu_xr_y"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_z != defaults->floats.video_openxr_menu_xr_z) + config_set_float(conf, "video_openxr_menu_xr_z", settings->floats.video_openxr_menu_xr_z); + else + config_unset(conf, "video_openxr_menu_xr_z"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_yaw != defaults->floats.video_openxr_menu_xr_yaw) + config_set_float(conf, "video_openxr_menu_xr_yaw", settings->floats.video_openxr_menu_xr_yaw); + else + config_unset(conf, "video_openxr_menu_xr_yaw"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_pitch != defaults->floats.video_openxr_menu_xr_pitch) + config_set_float(conf, "video_openxr_menu_xr_pitch", settings->floats.video_openxr_menu_xr_pitch); + else + config_unset(conf, "video_openxr_menu_xr_pitch"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_roll != defaults->floats.video_openxr_menu_xr_roll) + config_set_float(conf, "video_openxr_menu_xr_roll", settings->floats.video_openxr_menu_xr_roll); + else + config_unset(conf, "video_openxr_menu_xr_roll"); + if ( !minimal + || settings->floats.video_openxr_menu_xr_scale != defaults->floats.video_openxr_menu_xr_scale) + config_set_float(conf, "video_openxr_menu_xr_scale", settings->floats.video_openxr_menu_xr_scale); + else + config_unset(conf, "video_openxr_menu_xr_scale"); + if ( !minimal + || settings->bools.video_openxr_menu_placed != defaults->bools.video_openxr_menu_placed) + config_set_string(conf, "video_openxr_menu_placed", + settings->bools.video_openxr_menu_placed ? "true" : "false"); + else + config_unset(conf, "video_openxr_menu_placed"); + for (i = 0; i < MAX_USERS; i++) { size_t _len; @@ -10480,6 +10768,79 @@ int8_t config_save_overrides(enum override_type type, } #endif + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + char cfg[64]; + size_t len = snprintf(cfg, sizeof(cfg), "video_screen%u_xr_", i + 1); + + if (settings->floats.video_screen_xr_x[i] != overrides->floats.video_screen_xr_x[i]) + { + strlcpy_lit(cfg + len, "x", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_x[i]); + } + + if (settings->floats.video_screen_xr_y[i] != overrides->floats.video_screen_xr_y[i]) + { + strlcpy_lit(cfg + len, "y", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_y[i]); + } + + if (settings->floats.video_screen_xr_z[i] != overrides->floats.video_screen_xr_z[i]) + { + strlcpy_lit(cfg + len, "z", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_z[i]); + } + + if (settings->floats.video_screen_xr_yaw[i] != overrides->floats.video_screen_xr_yaw[i]) + { + strlcpy_lit(cfg + len, "yaw", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_yaw[i]); + } + + if (settings->floats.video_screen_xr_pitch[i] != overrides->floats.video_screen_xr_pitch[i]) + { + strlcpy_lit(cfg + len, "pitch", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_pitch[i]); + } + + if (settings->floats.video_screen_xr_roll[i] != overrides->floats.video_screen_xr_roll[i]) + { + strlcpy_lit(cfg + len, "roll", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_roll[i]); + } + + if (settings->floats.video_screen_xr_scale[i] != overrides->floats.video_screen_xr_scale[i]) + { + strlcpy_lit(cfg + len, "scale", sizeof(cfg) - len); + config_set_float(conf, cfg, overrides->floats.video_screen_xr_scale[i]); + } + + if (settings->bools.video_screen_xr_visible[i] != overrides->bools.video_screen_xr_visible[i]) + { + strlcpy_lit(cfg + len, "visible", sizeof(cfg) - len); + config_set_string(conf, cfg, + overrides->bools.video_screen_xr_visible[i] ? "true" : "false"); + } + } + + if (settings->floats.video_openxr_menu_xr_x != overrides->floats.video_openxr_menu_xr_x) + config_set_float(conf, "video_openxr_menu_xr_x", overrides->floats.video_openxr_menu_xr_x); + if (settings->floats.video_openxr_menu_xr_y != overrides->floats.video_openxr_menu_xr_y) + config_set_float(conf, "video_openxr_menu_xr_y", overrides->floats.video_openxr_menu_xr_y); + if (settings->floats.video_openxr_menu_xr_z != overrides->floats.video_openxr_menu_xr_z) + config_set_float(conf, "video_openxr_menu_xr_z", overrides->floats.video_openxr_menu_xr_z); + if (settings->floats.video_openxr_menu_xr_yaw != overrides->floats.video_openxr_menu_xr_yaw) + config_set_float(conf, "video_openxr_menu_xr_yaw", overrides->floats.video_openxr_menu_xr_yaw); + if (settings->floats.video_openxr_menu_xr_pitch != overrides->floats.video_openxr_menu_xr_pitch) + config_set_float(conf, "video_openxr_menu_xr_pitch", overrides->floats.video_openxr_menu_xr_pitch); + if (settings->floats.video_openxr_menu_xr_roll != overrides->floats.video_openxr_menu_xr_roll) + config_set_float(conf, "video_openxr_menu_xr_roll", overrides->floats.video_openxr_menu_xr_roll); + if (settings->floats.video_openxr_menu_xr_scale != overrides->floats.video_openxr_menu_xr_scale) + config_set_float(conf, "video_openxr_menu_xr_scale", overrides->floats.video_openxr_menu_xr_scale); + if (settings->bools.video_openxr_menu_placed != overrides->bools.video_openxr_menu_placed) + config_set_string(conf, "video_openxr_menu_placed", + overrides->bools.video_openxr_menu_placed ? "true" : "false"); + for (i = 0; i < MAX_USERS; i++) { size_t _len; diff --git a/configuration.h b/configuration.h index c7413292da..de4000a31c 100644 --- a/configuration.h +++ b/configuration.h @@ -316,6 +316,10 @@ typedef struct settings unsigned video_openxr_refresh_rate; unsigned video_openxr_controllers; unsigned video_openxr_laser; + unsigned video_openxr_laser_beam; + unsigned video_openxr_edit_snap_angles; + unsigned video_openxr_edit_hinge_gap; + unsigned video_openxr_edit_smoothing; unsigned video_openxr_placement; unsigned video_fse_negotiation; unsigned video_hw_decode; @@ -582,6 +586,21 @@ typedef struct settings struct { float placeholder; + float video_screen_xr_x[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_y[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_z[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_yaw[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_pitch[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_roll[RETRO_VIDEO_VIEWS_MAX]; + float video_screen_xr_scale[RETRO_VIDEO_VIEWS_MAX]; + float video_openxr_menu_xr_x; + float video_openxr_menu_xr_y; + float video_openxr_menu_xr_z; + float video_openxr_menu_xr_yaw; + float video_openxr_menu_xr_pitch; + float video_openxr_menu_xr_roll; + float video_openxr_menu_xr_scale; + float video_aspect_ratio; float video_vp_bias_x; float video_vp_bias_y; @@ -677,6 +696,8 @@ typedef struct settings struct { bool placeholder; + bool video_screen_xr_visible[RETRO_VIDEO_VIEWS_MAX]; + bool video_openxr_menu_placed; #ifdef HAVE_VIDEO_WINDOWS bool video_windows_enable; bool video_screen_window_fullscreen[RETRO_VIDEO_VIEWS_MAX]; @@ -698,7 +719,10 @@ typedef struct settings bool video_smooth; bool video_stereo_swap_eyes; bool video_openxr_enable; + bool video_openxr_menu_follow; bool video_openxr_perf_boost; + bool video_openxr_edit_pause; + bool video_openxr_edit_snap; bool video_ctx_scaling; bool video_force_aspect; bool video_frame_delay_auto; @@ -1728,6 +1752,23 @@ bool config_overlay_enable_default(void); bool config_metal_arg_buffers_default(void); #endif +void config_reset_video_screen_xr(settings_t *settings, unsigned first, unsigned count); + +/* The headset's menu back in front of screen 1. */ +void config_reset_video_menu_xr(settings_t *settings); + +struct video_screen_xr; +/* Edit Screens' reads and writes of a screen's place, or the menu's, + * as the Stereo 3D & Screens rows keep them. A write normalises, marks + * the settings modified and leaves Visible alone; NULL resets a screen + * as Reset Screen does, or puts the menu back in front of screen 1. The + * menu's read is false while it follows screen 1. */ +void config_get_video_screen_xr(unsigned screen, struct video_screen_xr *out); +bool config_get_video_menu_xr(struct video_screen_xr *out); +void config_set_video_screen_xr(unsigned screen, + const struct video_screen_xr *values); +void config_set_video_menu_xr(const struct video_screen_xr *values); + void config_set_defaults(settings_t *target); void config_load(void); diff --git a/docs/openxr.md b/docs/openxr.md index 682382ea7e..44f15b3b60 100644 --- a/docs/openxr.md +++ b/docs/openxr.md @@ -19,15 +19,285 @@ drivers, and on the Meta Quest with `vulkan` and `gl`. At run time an OpenXR runtime has to be active (SteamVR or Monado on Linux). Then turn on **Settings > Video > Stereo 3D & Screens > Headset -Output (OpenXR)**. The other rows there are Headset Refresh Rate, Screen -Distance, Screen Width, Screen Placement (Fixed in Room, Follow Head, -Lazy Follow), Headset Controllers and Laser Pointer. The Recenter -Headset Screens and Laser Pointer (Toggle) hotkeys are unbound by +Output (OpenXR)**. Under it come Edit Screens, Snap to Screens, Snap to +Angles, Hinge Gap, Edit Smoothing and While Editing Screens, described +below, then Headset Refresh Rate, Headset Controllers, Laser Pointer, +Laser Beam, Screen Placement (Fixed in Room, Follow Head, Lazy Follow), +Screen Distance and Screen Width. The Recenter Headset Screens, Laser +Pointer (Toggle) and Edit Headset Screens hotkeys are unbound by default. +**Laser Beam** draws a thin line from each controller to its laser's +dot, turned to face you. It shows only where the dot can: outside edit +mode, Laser Pointer Off, or its hotkey, hides both; while editing, both +hands always have a laser, and the hotkey only switches Laser Pointer +for afterwards. Edit Mode Only (the default) shows it while editing +screens, Always also for the menu and light guns, Off never. Pointing +at nothing, the line is 1.5 m long and fades out. + Without a runtime or a headset, RetroArch says so once and shows its window as usual. +## Adjusting floating screens + +In **Settings > Video > Stereo 3D & Screens**, use **Screen** to choose +which floating screen to adjust. The number is the core's logical screen +ID plus one, from 1 to 8. Only screens in the current frame's map are +listed; a tracked projection's eyes are excluded. Without a map, +including with no content loaded, Screen 1 is listed: a core without a +view map shows its whole frame as Screen 1, which these rows then move +like any other. Changing the order of views, or temporarily removing a +screen, preserves that screen's saved adjustments. + +- **Horizontal Offset**, **Vertical Offset** and **Depth Offset** move + the selected screen relative to its normal placement, from -10 to +10 + metres. Positive values move it right, up and towards you respectively, + in the shared placement anchor's coordinates. +- **Yaw**, **Pitch** and **Roll** rotate it from -180 to +180 degrees. + **Scale** changes its width and height together, from 0.1 to 10 times + its normal size. +- **Visible** hides or shows it. The menu remains available even when + every floating screen is hidden or moved out of sight. +- **Reset Screen** restores the selected screen's zero offsets, zero + rotations, unit scale and visibility. **Reset All Screens** does this + for all eight screen IDs, including those absent from the current map. + Reset All Screens remains available when there is no floating screen + to select. + +These adjustments apply to floating screens, including the two eyes of +a stereoscopic screen together. They do not change a tracked core's eye +poses or fields of view. Changes also take effect while content is paused. +The shared Screen Distance, Screen Width, Screen Placement and layout +settings still determine the base placement. **Recenter Headset Screens** +updates the shared anchor for all screens while preserving their relative +offsets, rotations, scales and visibility; it does not reset them. + +The menu opens at its own spot, straight ahead of the anchor. With +**Menu in Front of Screen 1** on, it floats 10 cm in front of Screen 1 +and faces the way it does, so moving Screen 1 moves the menu with it. It +looks as large as it does at its own spot, and comes no nearer to you +than 40 cm. While Screen 1 is hidden, is a tracked core's projection or +faces away from you, the menu stays at its own spot. A menu placed in +edit mode (below) stays where it was put until it is reset there, or by +Reset All Screens. + +Screen adjustments are configuration settings. Save the configuration to +keep them as defaults, or use **Quick Menu > Overrides** to save core, +content-directory or game overrides. Loading an override applies its +screen values; unloading it restores the inherited values. The selector +itself is only a menu choice and is not saved. Configuration keys use +`video_screenN_xr_x`, `_xr_y`, `_xr_z`, `_xr_yaw`, `_xr_pitch`, `_xr_roll`, +`_xr_scale` and `_xr_visible`, with `N` from 1 to 8. Finite values outside +the supported ranges are clamped; non-finite values use the defaults. + +With the legacy **gl** driver on desktop or Quest, opening the menu shows +the whole window on an unadjusted floating screen. Per-screen adjustments +resume when the menu closes. This keeps the menu and resets reachable +without applying the primary content screen's hidden state or transform +to the menu. The desktop **glcore** and **vulkan** paths use a separate +menu screen. + +## Editing screens in the headset + +**Edit Screens** moves the floating screens and the menu with the +controllers instead of the menu's rows. Start it from **Settings > Video > +Stereo 3D & Screens > Edit Screens** (shown with Headset Output on; the +menu closes into it), the **Edit Headset Screens** hotkey, the headset's +own **Edit Screens** action, or the `HEADSET_EDIT_SCREENS` network +command. It needs content running. The same hotkey, action or command +stops it, as do opening the menu, loading or closing content, and the +end of the headset session. The hotkey and the action are unbound by +default; bind the action in the runtime's controller settings, in both +RetroArch's game sets and its Edit Screens set. If the runtime refuses +the Edit Screens set's bindings for a hand's controller, Edit Screens +says it is unavailable. It also stops and says so when the runtime +binds neither that set's Grab nor its RetroArch Menu, as a controller +binding saved in the runtime before RetroArch had them can. + +While editing, every item has a faint outline just outside it, a light +line between two dark ones so it shows against any border. It is bright +on what a hand holds, and on what a laser points at unless an item is +chosen (below). Both hands have a laser whatever **Laser Pointer** is +set to, with a beam unless **Laser Beam** is Off. The menu also shows a +handle bar under it in the same light and dark, faint, and bright when +the menu's outline is. While a held screen is hinged (see **Snap to +Screens**), its edge and the edge it hinges on lose their outline, and a +6 mm green line runs along the hinge where the two overlap, midway +between the edges and drawn a centimetre nearer the eye so it shows +over both screens, while a let-go would settle the screen into the +hinge and for 0.15 s after. +Two screens no hand holds that rest exactly where a hinge holds one of +them, so that taking it up would leave it there, show the same: any +screen let go where a hinge holds it, or set out so. Screens only near +a hinge show nothing until hinged once. A screen hinged at rest on two +edges at a corner keeps one of those two rims. +The headset's controllers only edit; +keyboards and other gamepads still reach the content. The desktop +window shows a notice when editing starts, with the controls, for +about 8 seconds, and a short one when it stops. **Select +Next** and **Select Previous** choose an item, below. + +| Controller | Grab | Tilt | Distance and size | Turn and tilt (other hand) | Reset | Menu | Select Next / Previous | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Steam Frame | grip | trigger | stick | stick | A; left hand: D-pad down | right stick click | left D-pad right / left | +| Valve Index | grip | trigger | stick | stick | A | left trackpad press | right B / left B | +| Oculus Touch | grip | trigger | stick | stick | X / A | menu button | B / Y | +| HTC Vive | grip | trigger | trackpad | trackpad | trackpad click | left menu button | right menu button / - | +| Simple controller | select | - | - | - | - | left menu button | - | + +- A grip on an item grabs it: it keeps its place relative to the hand + and follows it. Each hand holds one item; the other hand on the same + item does nothing. +- Items keep the pitch and roll they had when grabbed, and their angle + to the way the hand points across the floor: they move with the hand + and turn left and right as it does, but raising, lowering or + twisting the hand does not tip, roll or swivel them. Pointing nearly + straight up or down, a twist of the hand turns them instead, and + tipping it 5 degrees past straight up or down turns them round to + face the other way. Hold the trigger while grabbing to let them ride + the hand, pitch and roll too; let go of the trigger to keep them. +- The other hand's stick, while that hand holds nothing, turns and + tilts the held item about its middle, whether or not the trigger is + held, faster the further the stick is pushed, with no stop. Right + turns it about the vertical, keeping its pitch and roll, + anticlockwise as seen from above, so a screen facing you turns + towards your right; left the other way. Up tips its top back about + its own left-right line, as when pushing a monitor's top back; down + the other way. Within about 25 degrees of left or right the stick + only turns, and within about 25 degrees of up or down it only tilts; + pushed clearly diagonally, it does both at once. As the stick turns + and tilts it, the item clicks into steps of Snap to Angles, counted + from facing you and from upright: it holds within about 2 degrees of + each and the holding hand buzzes once; pushing on leaves the step. + Carrying it or tilting it with the trigger never clicks. Snapping + still catches it parallel or square, and wins over a step. +- On the holding hand's stick, up pushes the item away along the laser + and down pulls it closer; right makes it larger and left smaller, + faster the further the stick is pushed. +- Offsets stop at +-10 m and scale at 0.1 to 10, as in the menu: the + item waits at the limit and moves on from there once the hand comes + back. +- Reset on a screen does what Reset Screen does; on the menu it makes + the menu follow Screen 1 again. +- The menu is grabbed by its handle bar, even where a screen is nearer, + and within about 3 cm of the bar where the laser meets no other item; + elsewhere on the menu the grip takes the screen behind. Once a laser + is on the bar, the bar stays its target while the laser stays within + those 3 cm, even over a screen; a laser that comes into the 3 cm from + a screen keeps the screen. Grabbing the menu places it where it + shows, even without moving it, so it no longer follows Screen 1. The + legacy **gl** driver draws the menu in its window, so only screens + can be grabbed there. +- Select Next and Select Previous choose an item: they step from + Laser through each screen that can be moved, in number order (a 3D + screen is one), then the menu while it is shown, and back to Laser. + Laser is the usual way: grip and Reset act on what a laser points at. + With an item chosen, its outline is bright and pointing lights + nothing else; either hand's grip takes it from wherever the + hand is, as if on a stick, and Reset resets it. While one hand holds + it, the other points, grabs and resets as usual. The choice stays + after letting go, until stepped back to Laser or edit mode ends; a + chosen screen that is hidden or goes away goes back to Laser. This + takes a screen hidden behind another. +- A faint tag just above the hand that last pressed Select Next or + Select Previous (the left hand at first) names the chosen item: + Laser, Screen 1, Screen 2 and so on, or Menu. +- Seen from behind, a screen or the menu shows a plain dark back. + Edit mode grabs and outlines it from there as from the front. +- A head-tracked core's projection cannot be grabbed, nor can the whole + frame a views core falls back to when its screens cannot be placed. In + Follow Head a grabbed screen keeps its place in view; Lazy Follow does + not glide while anything is held. Recenter lets go of what is held + first. A hand that loses tracking leaves its item still. + +**While Editing Screens** chooses whether the content keeps running (the +default) or pauses; leaving edit mode resumes only content it paused. + +**Snap to Screens** (on by default) lines a held screen up with the +other screens within about 30 cm of it that no hand holds. Within 5 +degrees of facing the same way as one, or of a quarter turn from it +about its vertical, the held screen turns exactly so; of two, it turns +to the one whose edge is nearer. Then, within 3 cm, it moves into the +other's plane, and its left, right, top, bottom or centre lines up with +the other's, level; edge to edge, or at a corner for a screen at a +quarter turn, it keeps the gap described under Hinge Gap. Or, at any +angle between them, an edge of the held screen hinges on an edge of the +other where the two overlap along it, top, bottom or side, within 5 +degrees of parallel and 3 cm of its plane, up to 10 cm out from the +edge: it turns exactly parallel to that edge and moves into the other's +plane, keeping its gap from that edge, centres or ends level along it, +keeping its angle, like the two halves of a DS. The green line +shows wherever a let-go would settle the screen into that hinge, and +0.15 s longer once it leaves, so a shaking hand never flickers it; it +stays when the screen is let go there. A screen taken up where it rests +hinged starts hinged, with no buzz, until it is carried out of the +hinge it rests in, another hinge takes hold or the other hand takes +the screen it hinges on. At a fold or a square corner with a gap +only the screen carried in does: the other one's edge is off its +plane, so it is taken up free. Screens that would overlap do not +hinge. While a screen is hinged, the other hand's stick swings it +about the shared edge, the tilt for a top or bottom edge and the +turn for a side one, and it stays where the swing leaves it. The +swing lets go just past square, but that axis keeps pivoting it on the +same edge until the edge is carried out of the hinge's reach or the +other hand takes the screen it hinges on; the stick's other axis turns +or tilts it as before and can pull it off. Past +each 3 cm range and the 1 cm click the pull fades out over as far again, +a hinge fades out from 10 to 13 cm, and from 30 to 60 cm away every pull +fades, so the screen catches up with the hand instead of jumping. A +screen just taken does not move: snapping fades in as it is carried, +whole once it is 5 cm or 10 degrees from where it was taken, but a +hinge it rests in, where holding it moves nothing, holds at once. The +holding hand gets a short buzz as a snap takes hold, and none again from +that snap until the screen has left its pull. A hinge taking hold gets a +longer, stronger buzz instead, and short buzzes due while it plays are +skipped; it comes again only once the hinge has let go, 0.15 s after +the screen leaves where it would settle, or for another edge, so a +swing past square and back, or a wobble where the hinge takes hold, +does not repeat it. The menu neither snaps nor is +snapped to. Let go, it stays where, turned and as large as it was a +moment (0.08 s) before the grip opened, so the hand's twitch as it +opens does not move it. With Edit Smoothing on, which damps that +twitch, it stays where, turned and as large as it shows, unless it +settles into a hinge. That is where it is saved. + +**Snap to Angles** (15 degrees by default) is the step the other hand's +stick turns and tilts a held item in: 15, 30, 45 or 90 degrees. A +hinged screen's steps count from flat with the other screen, both ways. +Off turns and tilts it smoothly, with no steps and no buzz. + +**Hinge Gap** (2 cm by default) is the gap Snap to Screens leaves +between screens that meet edge to edge, side by side, at a corner or +hinged at an angle: 0, 1, 2, 3, 5 or 10 cm. A screen within 1 cm of it +clicks to it exactly, with a short buzz; let go within 3 cm of it, or +within 6 cm of level along the edge, a hinged screen settles there +exactly; anywhere else from touching to 10 cm the gap stays where the +hand puts it, still hinged, and from 10 to 13 cm the hinge lets go. At +0 the edges touch. It does nothing while Snap to Screens is off; +screens already placed do not move. Pushed past touching, a hinged +screen stops there, so two screens can be pushed together with no gap +and let go touching, with one short buzz per push (below a 3 cm gap, +the click's). From 3 cm past (3.5 cm at a 1 cm gap) the hinge lets go +and the screen slides on into the other, free from 5.5 cm (6 cm); +pulled back, it hinges again 5 mm inside that 3 cm. With Snap to +Screens off nothing stops them, and screens overlap freely. + +**Edit Smoothing** (Medium by default) steadies the controllers' aim +while editing, so a screen held far out on the laser does not shake +with the hand: a slow or still hand is smoothed a lot and a fast one +little, so quick moves keep up. Low, Medium and High smooth more in +turn; Off uses the aim as tracked. Everything edit mode does with the +lasers follows the smoothed aim: the dots and beams, what lights, what +a grip takes, the stick and what is held; a screen is saved where it +was shown. The lasers are never smoothed outside edit mode. + +Edits change the same settings as the menu's rows and are saved the +same way: on quit while **Save Configuration on Quit** is on (the +default), or by saving the configuration or an override. +The menu's own place is saved as `video_openxr_menu_xr_x`, `_xr_y`, +`_xr_z`, `_xr_yaw`, `_xr_pitch`, `_xr_roll`, `_xr_scale` and +`video_openxr_menu_placed`; while that is false the menu follows Screen 1. + ## What a core sees A core describes its frame with `RETRO_ENVIRONMENT_SET_VIDEO_VIEWS`: a @@ -123,3 +393,6 @@ host. Model it on the three above: headset in headless gamescope, through a test API layer (`samples/openxr/test_layer`) that records every frame's layers and images. It needs a GPU, so it is not run in CI. +- The `edit-*` cases drive edit mode through the test layer's scripted + aims (`aim ... []`) and `edit/*` actions on Vulkan, glcore and + gl. diff --git a/gfx/common/gl_openxr.c b/gfx/common/gl_openxr.c index 680d6881e1..0d3ac318db 100644 --- a/gfx/common/gl_openxr.c +++ b/gfx/common/gl_openxr.c @@ -402,17 +402,26 @@ struct gl_openxr_dot static bool gl_openxr_cursor_fill(void *user, uint32_t index, const char **step, int *code) { - uint8_t px[VIDEO_XR_CURSOR_DIM * VIDEO_XR_CURSOR_DIM * 4]; GLenum err; struct gl_openxr_dot *dot = (struct gl_openxr_dot*)user; gl_openxr_t *xr = dot->xr; - video_xr_cursor_pixels(px); + unsigned w = openxr_frame_cursor_w(&xr->frame); + uint8_t *px = (uint8_t*)malloc( + (size_t)VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4); + if (!px) + { + *step = "malloc"; + *code = 0; + return false; + } + openxr_frame_cursor_pixels(px, w, true); while (xr->gl.GetError() != GL_NO_ERROR) { } xr->gl.BindTexture(GL_TEXTURE_2D, dot->images[index][0]); xr->gl.PixelStorei(GL_UNPACK_ALIGNMENT, 4); - xr->gl.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIDEO_XR_CURSOR_DIM, - VIDEO_XR_CURSOR_DIM, GL_RGBA, GL_UNSIGNED_BYTE, px); + xr->gl.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, VIDEO_XR_CURSOR_H, + GL_RGBA, GL_UNSIGNED_BYTE, px); xr->gl.BindTexture(GL_TEXTURE_2D, 0); + free(px); if ((err = xr->gl.GetError()) == GL_NO_ERROR) return true; *step = "glTexSubImage2D"; @@ -432,11 +441,20 @@ static void gl_openxr_cursor_create(gl_openxr_t *xr) li.sc = &xr->sc; li.tex = dot.images; xr->frame.cursor = openxr_swapchains_create_filled(&xr->sc, formats, 2, - VIDEO_SCALE_PACK(VIDEO_XR_CURSOR_DIM, VIDEO_XR_CURSOR_DIM), + VIDEO_SCALE_PACK(openxr_frame_cursor_w(&xr->frame), + VIDEO_XR_CURSOR_H), openxr_binding_gl_images, &li, gl_openxr_cursor_fill, &dot, &step, &code); - if (xr->frame.cursor) - RARCH_LOG("[OpenXR] Laser cursor ready.\n"); + if (xr->frame.cursor && xr->frame.rt->max_width) + RARCH_LOG("[OpenXR] Laser cursor ready: %u wide, with %s (the " + "headset's widest %u).\n", openxr_frame_cursor_w(&xr->frame), + openxr_frame_cursor_parts(&xr->frame), + xr->frame.rt->max_width); + else if (xr->frame.cursor) + RARCH_LOG("[OpenXR] Laser cursor ready: %u wide, with %s (the " + "headset does not say its widest).\n", + openxr_frame_cursor_w(&xr->frame), + openxr_frame_cursor_parts(&xr->frame)); else RARCH_WARN("[OpenXR] No laser cursor (%s, %d).\n", step, code); } diff --git a/gfx/common/openxr_frame.c b/gfx/common/openxr_frame.c index 49cb13a645..b8d89d3451 100644 --- a/gfx/common/openxr_frame.c +++ b/gfx/common/openxr_frame.c @@ -13,6 +13,7 @@ * If not, see . */ +#include #include #include #include @@ -21,19 +22,27 @@ #include #include #include +#include +#include #include "openxr_frame.h" #include "../video_defines.h" +#include "../font_driver.h" +#include "../../msg_hash.h" #include "../../runloop.h" #include "../../verbosity.h" /* The seqlock's copy holds the largest thing it publishes. */ +#define OPENXR_SEQ_WORDS \ + ((OPENXR_WORDS(openxr_grabs_t) > OPENXR_WORDS(video_xr_quad_set_t)) \ + ? OPENXR_WORDS(openxr_grabs_t) : OPENXR_WORDS(video_xr_quad_set_t)) typedef char openxr_seq_fits[ - ( OPENXR_WORDS(openxr_tracked_t) <= OPENXR_WORDS(video_xr_quad_set_t) - && OPENXR_WORDS(openxr_screens_t) <= OPENXR_WORDS(video_xr_quad_set_t) - && OPENXR_WORDS(openxr_projection_t) <= OPENXR_WORDS(video_xr_quad_set_t)) + ( OPENXR_WORDS(openxr_tracked_t) <= OPENXR_SEQ_WORDS + && OPENXR_WORDS(openxr_screens_t) <= OPENXR_SEQ_WORDS + && OPENXR_WORDS(openxr_projection_t) <= OPENXR_SEQ_WORDS + && OPENXR_WORDS(openxr_aims_t) <= OPENXR_SEQ_WORDS) ? 1 : -1]; /* A latched seqlock: two copies of the words, one writer, any number @@ -54,7 +63,7 @@ static void openxr_seq_store(retro_atomic_int_t *words, static void openxr_seq_publish(retro_atomic_int_t *seq, retro_atomic_int_t *words, const void *src, size_t len) { - int tmp[OPENXR_WORDS(video_xr_quad_set_t)]; + int tmp[OPENXR_SEQ_WORDS]; size_t n = (len + sizeof(int) - 1) / sizeof(int); int s = retro_atomic_load_relaxed_int(seq); @@ -71,7 +80,7 @@ static void openxr_seq_publish(retro_atomic_int_t *seq, static void openxr_seq_read(retro_atomic_int_t *seq, retro_atomic_int_t *words, void *dst, size_t len) { - int tmp[OPENXR_WORDS(video_xr_quad_set_t)]; + int tmp[OPENXR_SEQ_WORDS]; size_t n = (len + sizeof(int) - 1) / sizeof(int); for (;;) { @@ -169,37 +178,150 @@ static const XrSwapchain *openxr_frame_chains(openxr_slot_t *slot, return (*layers && slot->old[0]) ? slot->old : NULL; } -/* The published quads whose slot shows an image. No slot changes - * under it: slots change under the graphics lock, or on this thread - * where the host has none. */ -static unsigned openxr_frame_layers(openxr_frame_t *f, +/* Every quad of slot's item at pose and g's size, both eyes of a stereo + * screen; a fixed slot is no item. */ +static void openxr_frame_hold_slot(video_xr_quad_set_t *set, unsigned slot, + const video_xr_pose_t *pose, const video_xr_grab_t *g) +{ + unsigned i; + if (set->slots[slot].fixed) + return; + for (i = 0; i < set->num_quads; i++) + if (set->quads[i].slot == slot) + { + set->quads[i].pose = *pose; + set->quads[i].width = g->width; + set->quads[i].height = g->height; + } +} + +/* Edit mode, in the screens' frame: a held item goes where the hand's + * aim at time puts it, kept while untracked, snapped and hinged as the + * main thread does; a let-go one stays until quads planned at it show + * it. */ +static void openxr_frame_hold(openxr_frame_t *f, XrTime time, + video_xr_quad_set_t *set) +{ + unsigned h, s; + video_xr_pose_t inv; + unsigned holding = 0; + XrSpace space = f->screens.view + ? f->session->view_space : f->session->local_space; + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + { + const video_xr_grab_t *g = &f->grabs.let_go[s]; + if ( g->state == VIDEO_XR_GRAB_RELEASED + && (int)(set->slots[s].edit_seq - g->release_seq) < 0) + openxr_frame_hold_slot(set, s, &g->last, g); + } + for (h = 0; h < 2; h++) + if ( f->grabs.hand[h].state == VIDEO_XR_GRAB_HELD + && f->grabs.hand[h].slot < VIDEO_XR_MAX_SLOTS) + holding |= 1u << f->grabs.hand[h].slot; + video_xr_pose_invert(&f->screens.pose, &inv); + f->hinged = 0; + for (h = 0; h < 2; h++) + { + video_xr_pose_t aim; + const video_xr_grab_t *g = &f->grabs.hand[h]; + openxr_held_t *held = &f->held[h]; + if (held->state != g->state || held->slot != g->slot) + { + held->pose = g->last; + held->slot = g->slot; + held->state = g->state; + held->hinged = false; + } + if (g->state != VIDEO_XR_GRAB_HELD || g->slot >= VIDEO_XR_MAX_SLOTS) + continue; + if (f->aim && time && f->aim(f->extra_user, h, space, time, &aim)) + { + const video_xr_quad_set_t *targets = f->grabs.snap ? set : NULL; + video_xr_compose(&inv, &aim, &aim); + video_xr_grab_hinge(g, &aim, targets, holding, + &f->grabs.start[h], &held->pose, NULL); + } + openxr_frame_hold_slot(set, g->slot, &held->pose, g); + /* The hinge the main thread marks, nothing of the frame's own: + * a wobble between its polls never flips the line. */ + held->hinged = video_xr_grab_marked(g, &held->pose, set, holding, + &held->hinge); + if (held->hinged) + { + video_xr_pose_t at; + video_xr_vec3_t dir, flat; + video_xr_hinge_t *k = &f->hinges[h]; + *k = held->hinge; + video_xr_pose_identity(&at); + at.position = k->point; + video_xr_compose(&f->screens.pose, &at, &at); + k->point = at.position; + dir = k->dir; + flat = k->flat; + video_xr_rotate(&f->screens.pose.orientation, &dir, &k->dir); + video_xr_rotate(&f->screens.pose.orientation, &flat, &k->flat); + f->hinged |= 1u << h; + } + } +} + +static bool openxr_frame_head(openxr_frame_t *f, XrTime time, + video_xr_pose_t *head); + +/* This frame's quads, holds applied, where the screens are, into + * f->shown; layers of those whose slot shows an image. Runtimes draw + * only a quad's front, so with the head behind one its back, the laser + * image's grey square, takes its layer, turned to the head. No slot + * changes under it: slots change under the graphics lock, or on this + * thread where the host has none. */ +static unsigned openxr_frame_layers(openxr_frame_t *f, XrTime time, XrCompositionLayerQuad *layers, const XrCompositionLayerBaseHeader **ptrs) { unsigned i; - unsigned n = 0; - video_xr_quad_set_t quads; + video_xr_pose_t head; + unsigned n = 0; + bool seen = false; + video_xr_quad_set_t *quads = &f->shown; openxr_seq_read(&f->quads_seq, f->quads_words, - &quads, sizeof(quads)); - for (i = 0; i < quads.num_quads; i++) + quads, sizeof(*quads)); + openxr_frame_hold(f, time, quads); + f->backs = 0; + f->menu_layer = -1; + /* Follow Head's space is the head's own. Backs are for edit mode, + * where screens are turned around; outside it nothing changes. */ + if (f->cursor && f->grabs.editing) + { + if (f->screens.view) + { + video_xr_pose_identity(&head); + seen = true; + } + else + seen = openxr_frame_head(f, time, &head); + } + for (i = 0; i < quads->num_quads; i++) { - video_xr_pose_t pose; unsigned dims, count; const XrSwapchain *chains; - const video_xr_quad_t *q = &quads.quads[i]; - openxr_slot_t *slot = &f->sc->slots[q->slot]; - XrCompositionLayerQuad *l = &layers[n]; + video_xr_pose_t pose; + bool back; + video_xr_quad_t *q = &quads->quads[i]; + openxr_slot_t *slot = &f->sc->slots[q->slot]; + XrCompositionLayerQuad *l = &layers[n]; + video_xr_compose(&f->screens.pose, &q->pose, &q->pose); if ( !(chains = openxr_frame_chains(slot, &dims, &count)) || q->layer >= count) continue; - video_xr_compose(&f->screens.pose, &q->pose, &pose); + back = seen && video_xr_quad_behind(q, &head.position); + pose = q->pose; l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; l->next = NULL; l->subImage.imageRect.offset.x = 0; l->subImage.imageRect.offset.y = 0; l->subImage.imageArrayIndex = 0; /* The UI is drawn over transparent black: premultiplied. */ - l->layerFlags = (q->kind == VIDEO_XR_QUAD_MENU) + l->layerFlags = (q->kind == VIDEO_XR_QUAD_MENU && !back) ? XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT : 0; l->space = f->screens.view ? f->session->view_space : f->session->local_space; @@ -207,9 +329,24 @@ static unsigned openxr_frame_layers(openxr_frame_t *f, ? XR_EYE_VISIBILITY_LEFT : ((q->eye == VIDEO_XR_EYE_RIGHT) ? XR_EYE_VISIBILITY_RIGHT : XR_EYE_VISIBILITY_BOTH); - l->subImage.swapchain = chains[q->layer]; - l->subImage.imageRect.extent.width = (int32_t)VIDEO_SCALE_W(dims); - l->subImage.imageRect.extent.height = (int32_t)VIDEO_SCALE_H(dims); + if (back) + { + video_xr_turn_back(&pose); + f->backs |= 1u << i; + l->subImage.swapchain = f->cursor; + l->subImage.imageRect.offset.x = + VIDEO_XR_BACK_X + VIDEO_XR_BACK_MARGIN; + l->subImage.imageRect.offset.y = + VIDEO_XR_BACK_Y + VIDEO_XR_BACK_MARGIN; + l->subImage.imageRect.extent.width = VIDEO_XR_BACK_DIM; + l->subImage.imageRect.extent.height = VIDEO_XR_BACK_DIM; + } + else + { + l->subImage.swapchain = chains[q->layer]; + l->subImage.imageRect.extent.width = (int32_t)VIDEO_SCALE_W(dims); + l->subImage.imageRect.extent.height = (int32_t)VIDEO_SCALE_H(dims); + } l->pose.orientation.x = pose.orientation.x; l->pose.orientation.y = pose.orientation.y; l->pose.orientation.z = pose.orientation.z; @@ -219,6 +356,8 @@ static unsigned openxr_frame_layers(openxr_frame_t *f, l->pose.position.z = pose.position.z; l->size.width = q->width; l->size.height = q->height; + if (q->kind == VIDEO_XR_QUAD_MENU) + f->menu_layer = (int)n; ptrs[n++] = (const XrCompositionLayerBaseHeader*)l; } return n; @@ -353,8 +492,19 @@ static void openxr_frame_screens_anchor(openxr_frame_t *f, static void openxr_frame_place_screens(openxr_frame_t *f, XrTime time) { video_xr_pose_t at; + bool held; int recenters = retro_atomic_load_acquire_int(&f->recenters); int placement = retro_atomic_load_acquire_int(&f->placement); + int grabs = retro_atomic_load_acquire_int(&f->grabs_seq); + /* Outside edit mode nothing is published: no copy. */ + if (grabs != f->grabs_read) + { + openxr_seq_read(&f->grabs_seq, f->grabs_words, &f->grabs, + sizeof(f->grabs)); + f->grabs_read = grabs; + } + held = f->grabs.hand[0].state == VIDEO_XR_GRAB_HELD + || f->grabs.hand[1].state == VIDEO_XR_GRAB_HELD; openxr_frame_screens_anchor(f, &at); f->screens.view = (placement == VIDEO_OPENXR_PLACEMENT_FOLLOW); if (placement != VIDEO_OPENXR_PLACEMENT_LAZY) @@ -386,7 +536,9 @@ static void openxr_frame_place_screens(openxr_frame_t *f, XrTime time) f->lazy_recenters = recenters; } } - else if ( openxr_frame_head(f, time, &head) + /* Edit mode holds the glide while anything is held. */ + else if ( !held + && openxr_frame_head(f, time, &head) && video_xr_anchor_from_head(&head, &target)) f->lazy_gliding = video_xr_lazy_follow(&f->lazy, &target, (f->lazy_time > 0 && time > f->lazy_time) @@ -582,19 +734,15 @@ static bool openxr_frame_wait_begin(openxr_frame_t *f, /* At most the runtime's layer count: the last screens go first, so the * menu and the host's layers over it stay. The quads are ptrs[first] - * up to ptrs[end]; returns the count kept. */ + * up to ptrs[end], as openxr_frame_layers made them; returns the count + * kept. */ static uint32_t openxr_frame_fit(const openxr_frame_t *f, const XrCompositionLayerBaseHeader **ptrs, uint32_t count, uint32_t first, uint32_t end) { - XrSwapchain menu = f->sc->slots[VIDEO_XR_MENU_SLOT].swapchains[0]; - XrSwapchain menu_old = f->sc->slots[VIDEO_XR_MENU_SLOT].old[0]; while (count > f->rt->max_layers && end > first) { - const XrCompositionLayerQuad *q = - (const XrCompositionLayerQuad*)ptrs[--end]; - if ( q->subImage.swapchain == menu - || q->subImage.swapchain == menu_old) + if ((int)(--end - first) == f->menu_layer) continue; memmove(&ptrs[end], &ptrs[end + 1], (count - end - 1) * sizeof(*ptrs)); @@ -630,13 +778,33 @@ static void openxr_frame_submit(openxr_frame_t *f, ptrs[end_info.layerCount++] = (const XrCompositionLayerBaseHeader*)&proj; first = end_info.layerCount; - end = first + openxr_frame_layers(f, layers, - ptrs + first); + end = first + openxr_frame_layers(f, b->display_time, + layers, ptrs + first); end_info.layerCount = end; if (f->extra_layers) - end_info.layerCount += f->extra_layers(f->extra_user, - b->display_time, ptrs + end_info.layerCount, - OPENXR_MAX_EXTRA_LAYERS); + { + unsigned i, n; + unsigned under = 0; + unsigned cap = (f->rt->max_layers > end) + ? (unsigned)(f->rt->max_layers - end) : 0; + if (cap < OPENXR_KEPT_EXTRA_LAYERS) + cap = OPENXR_KEPT_EXTRA_LAYERS; + if (cap > OPENXR_MAX_EXTRA_LAYERS) + cap = OPENXR_MAX_EXTRA_LAYERS; + n = f->extra_layers(f->extra_user, b->display_time, &f->shown, + f->screens.view + ? f->session->view_space : f->session->local_space, + ptrs + end, cap, &under); + end_info.layerCount += n; + /* The first under go before the quads. */ + for (i = 0; i < under && i < n; i++, first++, end++) + { + const XrCompositionLayerBaseHeader *l = ptrs[end]; + memmove(&ptrs[first + 1], &ptrs[first], + (end - first) * sizeof(*ptrs)); + ptrs[first] = l; + } + } end_info.layerCount = openxr_frame_fit(f, ptrs, end_info.layerCount, first, end); } @@ -839,6 +1007,12 @@ void openxr_frame_deinit(openxr_frame_t *f) f->idle_ready = false; } +/* A new session's frame loop holds nothing, whatever the input does: + * its own copy is cleared and what is published now is passed over, + * the main thread staying the one writer of the grabs. */ +static const openxr_grabs_t openxr_frame_no_grabs; +static const openxr_aims_t openxr_frame_no_aims; + void openxr_frame_session_created(openxr_frame_t *f) { video_xr_pose_identity(&f->tracked.anchor); @@ -849,6 +1023,10 @@ void openxr_frame_session_created(openxr_frame_t *f) openxr_frame_publish_tracked(f); openxr_seq_publish(&f->screens_seq, f->screens_words, &f->screens, sizeof(f->screens)); + f->grabs = openxr_frame_no_grabs; + f->grabs_read = retro_atomic_load_acquire_int(&f->grabs_seq); + openxr_seq_publish(&f->aims_seq, f->aims_words, + &openxr_frame_no_aims, sizeof(openxr_frame_no_aims)); retro_atomic_store_release_int(&f->recenter, 0); openxr_frame_list_rates(f); } @@ -881,6 +1059,7 @@ void openxr_frame_input_attach(openxr_frame_t *f) h.hand_data_source = f->rt->hand_data_source; f->extra_layers = in->frame_layers; f->locate_hands = in->locate_hands; + f->aim = in->aim; f->extra_user = in->user; f->input_attached = true; in->session_created(in->user, &h); @@ -894,6 +1073,7 @@ void openxr_frame_input_detach(openxr_frame_t *f) f->input_attached = false; f->extra_layers = NULL; f->locate_hands = NULL; + f->aim = NULL; f->extra_user = NULL; if (in->session_destroying) in->session_destroying(in->user); @@ -915,6 +1095,24 @@ XrSwapchain openxr_frame_cursor(const openxr_frame_t *f) return f->cursor; } +unsigned openxr_frame_cursor_w(const openxr_frame_t *f) +{ + /* Not said: 4096, as openxr_runtime's max_dim. */ + unsigned max = f->rt->max_width ? f->rt->max_width : 4096; + if (max >= VIDEO_XR_CURSOR_W) + return VIDEO_XR_CURSOR_W; + return (max >= VIDEO_XR_CURSOR_TAGGED_W) + ? VIDEO_XR_CURSOR_TAGGED_W : VIDEO_XR_CURSOR_UNTAGGED_W; +} + +const char *openxr_frame_cursor_parts(const openxr_frame_t *f) +{ + unsigned w = openxr_frame_cursor_w(f); + if (w == VIDEO_XR_CURSOR_W) + return "cut outlines and tags"; + return (w == VIDEO_XR_CURSOR_TAGGED_W) ? "tags" : "no tags"; +} + bool openxr_frame_start(openxr_frame_t *f) { if (f->inline_host || f->thread) @@ -1195,6 +1393,16 @@ void openxr_frame_publish(openxr_frame_t *f, const video_xr_quad_set_t *set) openxr_seq_publish(&f->quads_seq, f->quads_words, set, sizeof(*set)); } +void openxr_frame_publish_grabs(openxr_frame_t *f, const openxr_grabs_t *g) +{ + openxr_seq_publish(&f->grabs_seq, f->grabs_words, g, sizeof(*g)); +} + +void openxr_frame_publish_aims(openxr_frame_t *f, const openxr_aims_t *a) +{ + openxr_seq_publish(&f->aims_seq, f->aims_words, a, sizeof(*a)); +} + void openxr_frame_publish_projection(openxr_frame_t *f, const openxr_projection_t *p) { @@ -1238,6 +1446,20 @@ void openxr_frame_get_shown_quads(openxr_frame_t *f, *space = at.view ? f->session->view_space : f->session->local_space; } +void openxr_frame_get_screens(openxr_frame_t *f, video_xr_pose_t *pose, + XrSpace *space) +{ + openxr_screens_t at; + openxr_seq_read(&f->screens_seq, f->screens_words, &at, sizeof(at)); + *pose = at.pose; + *space = at.view ? f->session->view_space : f->session->local_space; +} + +void openxr_frame_get_aims(openxr_frame_t *f, openxr_aims_t *out) +{ + openxr_seq_read(&f->aims_seq, f->aims_words, out, sizeof(*out)); +} + void openxr_frame_request_recenter(openxr_frame_t *f) { retro_atomic_store_release_int(&f->recenter, 1); @@ -1252,3 +1474,143 @@ XrDuration openxr_frame_period(openxr_frame_t *f) { return (XrDuration)retro_atomic_load_acquire_int(&f->period_ns); } + +/* A glyph's coverage from the font's atlas into a tag cell's cov at x, + * y, kept VIDEO_XR_TAG_INSET inside the cell. */ +static void openxr_frame_tag_glyph(const struct font_atlas *atlas, + const struct font_glyph *g, int x, int y, uint8_t *cov) +{ + unsigned gx, gy; + unsigned w = VIDEO_SCALE_W(g->dims); + unsigned h = VIDEO_SCALE_H(g->dims); + unsigned ax = VIDEO_SCALE_W(g->atlas_pos); + unsigned ay = VIDEO_SCALE_H(g->atlas_pos); + if (!atlas || !atlas->buffer || atlas->format != FONT_ATLAS_FORMAT_A8) + return; + for (gy = 0; gy < h && ay + gy < atlas->height; gy++) + for (gx = 0; gx < w && ax + gx < atlas->width; gx++) + { + uint8_t a; + int cx = x + (int)gx; + int cy = y + (int)gy; + if ( cx < VIDEO_XR_TAG_INSET || cy < VIDEO_XR_TAG_INSET + || cx >= VIDEO_XR_TAG_COLS - VIDEO_XR_TAG_INSET + || cy >= VIDEO_XR_TAG_ROWS - VIDEO_XR_TAG_INSET) + continue; + a = atlas->buffer[(ay + gy) * atlas->width + ax + gx]; + if (a > cov[cy * VIDEO_XR_TAG_COLS + cx]) + cov[cy * VIDEO_XR_TAG_COLS + cx] = a; + } +} + +/* A line of font's, size high, in whole rows above and below its + * baseline: the font's own, or, where it gives none, a 1.25 em line + * as a common font's. */ +static void openxr_frame_tag_metrics(const font_renderer_driver_t *drv, + void *font, unsigned size, int *asc, int *desc) +{ + struct font_line_metrics *m = NULL; + *asc = (int)size; + *desc = (int)size / 4; + if (drv->get_line_metrics) + drv->get_line_metrics(font, &m); + if (m) + { + *asc = (int)ceil(m->ascender); + *desc = (int)ceil(m->descender); + } +} + +/* text in one line, centred in a tag cell's coverage cov, the line + * (asc rows over its baseline, desc under) in the cell's text rows; a + * line too tall loses the top of its ascender, never a descender. */ +static void openxr_frame_tag_line(const font_renderer_driver_t *drv, + void *font, int asc, int desc, const char *text, uint8_t *cov) +{ + const char *msg = text; + size_t msg_len = strlen(text); + void *font_data = font; + const struct font_glyph *(*get_glyph)(void*, uint32_t) = + drv->get_glyph; + const struct font_glyph *glyph_q = drv->get_glyph(font, '?'); + int line_x = 0; + int spare = VIDEO_XR_TAG_TEXT_ROWS - asc - desc; + int base = VIDEO_XR_TAG_ROWS + - VIDEO_XR_TAG_INSET - desc - (spare > 0 ? spare / 2 : 0); +#define FONT_LAYOUT_ALIGNED 1 +#define FONT_LAYOUT_LINE(line, width, count, bytes) \ + (void)(line); \ + line_x = ((int)VIDEO_XR_TAG_COLS - (int)(width)) / 2; +#define FONT_LAYOUT_GLYPH(glyph, pen_x, pen_y) \ + openxr_frame_tag_glyph(drv->get_atlas(font), (glyph), \ + line_x + (pen_x) + (glyph)->draw_offset_x, \ + base - (pen_y) + (glyph)->draw_offset_y, cov); +#include "../font_layout.h" +} + +/* Tag tag's words: the laser's, a screen's by number, the menu's. */ +static void openxr_frame_tag_text(unsigned tag, char *s, size_t len) +{ + if (!tag) + strlcpy(s, msg_hash_to_str(MSG_OPENXR_EDIT_TAG_LASER), len); + else if (tag == VIDEO_XR_MENU_SLOT + 1) + strlcpy(s, msg_hash_to_str(MSG_OPENXR_EDIT_TAG_MENU), len); + else + snprintf(s, len, msg_hash_to_str(MSG_OPENXR_EDIT_TAG_SCREEN), tag); +} + +/* Edit mode's tags into px, in the default font. */ +static void openxr_frame_cursor_tags(uint8_t *px, bool bottom_up) +{ + unsigned i; + int asc, desc; + uint8_t *cov = NULL; + void *font = NULL; + const font_renderer_driver_t *drv = NULL; + unsigned size = VIDEO_XR_TAG_FONT; + if (!font_renderer_create_default(&drv, &font, NULL, size, + FONT_ATLAS_FORMAT_A8)) + return; + openxr_frame_tag_metrics(drv, font, size, &asc, &desc); + /* A font whose line is taller than the rows is drawn smaller, so no + * descender is cut. */ + if (asc + desc > VIDEO_XR_TAG_TEXT_ROWS) + { + if (drv->free) + drv->free(font); + font = NULL; + size = VIDEO_XR_TAG_FONT * VIDEO_XR_TAG_TEXT_ROWS / (asc + desc); + if (!size || !font_renderer_create_default(&drv, &font, NULL, size, + FONT_ATLAS_FORMAT_A8)) + return; + openxr_frame_tag_metrics(drv, font, size, &asc, &desc); + } + if ((cov = (uint8_t*)malloc(VIDEO_XR_TAG_COLS * VIDEO_XR_TAG_ROWS))) + { + for (i = 0; i < VIDEO_XR_TAGS; i++) + { + char text[64]; + openxr_frame_tag_text(i, text, sizeof(text)); + memset(cov, 0, VIDEO_XR_TAG_COLS * VIDEO_XR_TAG_ROWS); + openxr_frame_tag_line(drv, font, asc, desc, text, cov); + video_xr_tag_pixels(px, i, cov, bottom_up); + } + free(cov); + } + if (drv->free) + drv->free(font); +} + +void openxr_frame_cursor_pixels(uint8_t *px, unsigned w, bool bottom_up) +{ + unsigned i; + video_xr_cursor_pixels(px); + if (w == VIDEO_XR_CURSOR_W) + video_xr_cut_pixels(px, bottom_up); + if (w >= VIDEO_XR_CURSOR_TAGGED_W) + openxr_frame_cursor_tags(px, bottom_up); + if (w < VIDEO_XR_CURSOR_W) + for (i = 1; i < VIDEO_XR_CURSOR_H; i++) + memmove(px + (size_t)i * w * 4, + px + (size_t)i * VIDEO_XR_CURSOR_W * 4, (size_t)w * 4); +} diff --git a/gfx/common/openxr_frame.h b/gfx/common/openxr_frame.h index f683f3ccf1..082d7f009a 100644 --- a/gfx/common/openxr_frame.h +++ b/gfx/common/openxr_frame.h @@ -36,7 +36,59 @@ RETRO_BEGIN_DECLS -#define OPENXR_MAX_EXTRA_LAYERS 4 +/* Layers the input may add to the screens: the laser's dots, kept + * where the screens must make room, then edit mode's outlines and the + * menu's handle, then its hinges' lines, each hand's, then up to + * OPENXR_REST_HINGES between screens at rest, then its tag, then the + * beams, only in room they leave. */ +#define OPENXR_KEPT_EXTRA_LAYERS 2 +/* The most edge to edge pairs eight equal screens make in a plane. */ +#define OPENXR_REST_HINGES 10 +#define OPENXR_HINGE_LAYERS (2 + OPENXR_REST_HINGES) +#define OPENXR_BEAM_LAYERS 2 +#define OPENXR_TAG_LAYERS 1 +#define OPENXR_MAX_EXTRA_LAYERS \ + (OPENXR_KEPT_EXTRA_LAYERS + OPENXR_HINGE_LAYERS + OPENXR_BEAM_LAYERS \ + + OPENXR_TAG_LAYERS + VIDEO_XR_MAX_SLOTS + 1) + +/* Edit mode's hands, published by the main thread for the frame loop. */ +typedef struct openxr_grabs +{ + video_xr_grab_t hand[2]; + video_xr_pose_t start[2]; /* items as taken; snaps fade in */ + /* Each slot's last let-go, until quads planned at it show it. */ + video_xr_grab_t let_go[VIDEO_XR_MAX_SLOTS]; + unsigned chosen; /* its slot plus one, 0 the laser's */ + unsigned stuck; /* hands with the menu's handle stuck, bits */ + unsigned tag; /* the hand that chose last (0, the left) */ + unsigned num_rests; /* screens no hand holds resting in a hinge */ + video_xr_rest_t rests[OPENXR_REST_HINGES]; + bool snap; /* Snap to Screens */ + bool editing; /* Edit Screens is on */ +} openxr_grabs_t; + +/* Edit Smoothing's aims, the frame loop's for the main thread: each + * hand's, in the frame's space, as the frame last built from it took + * it, and that frame's display time; bit h of valid while hand h's is + * tracked. */ +typedef struct openxr_aims +{ + video_xr_pose_t pose[2]; + XrTime time[2]; + unsigned valid; +} openxr_aims_t; + +/* Where a hand's held item was last shown, for the hold (slot and + * state) it was shown for, and the hinge the main thread marks, if + * hinged, in the screens' frame. */ +typedef struct openxr_held +{ + video_xr_pose_t pose; + video_xr_hinge_t hinge; + unsigned slot; + unsigned state; + bool hinged; +} openxr_held_t; /* What the XR thread publishes for the drawing thread. */ typedef struct openxr_tracked @@ -92,9 +144,17 @@ typedef struct openxr_frame const openxr_runtime_t *rt; openxr_session_t *session; openxr_swapchains_t *sc; - /* Layers after the screens (the laser, its cursor); may be NULL. */ + /* Layers after the screens (the laser, its cursor), the first *under + * of them under the screens instead, shown holding this frame's quads + * where it shows them, in space; may be NULL. */ unsigned (*extra_layers)(void *user, XrTime display_time, - const XrCompositionLayerBaseHeader **layers, unsigned cap); + const video_xr_quad_set_t *shown, XrSpace space, + const XrCompositionLayerBaseHeader **layers, unsigned cap, + unsigned *under); + /* The input's: a hand's aim pose at a frame's time in space, for what + * it holds; may be NULL. */ + bool (*aim)(void *user, unsigned hand, XrSpace space, XrTime time, + video_xr_pose_t *pose); void *extra_user; /* The hands; may be NULL. */ bool (*locate_hands)(void *user, XrSpace base, XrTime time, @@ -122,6 +182,26 @@ typedef struct openxr_frame openxr_screens_t screens; /* the frame loop's thread */ retro_atomic_int_t screens_seq; retro_atomic_int_t screens_words[2 * OPENXR_WORDS(openxr_screens_t)]; + /* Edit mode's hands as the main thread publishes them, the same way, + * and the frame loop's copy, read again only once the stamp it was + * read at moves. The main thread is the one writer. */ + retro_atomic_int_t grabs_seq; + retro_atomic_int_t grabs_words[2 * OPENXR_WORDS(openxr_grabs_t)]; + openxr_grabs_t grabs; /* the frame loop's */ + int grabs_read; /* the frame loop's */ + openxr_held_t held[2]; /* the frame loop's */ + /* Each held hinge where this frame draws, bit h for hand h's. */ + video_xr_hinge_t hinges[2]; /* the frame loop's */ + unsigned hinged; /* the frame loop's */ + /* Edit Smoothing's aims as the frame loop publishes them. */ + retro_atomic_int_t aims_seq; + retro_atomic_int_t aims_words[2 * OPENXR_WORDS(openxr_aims_t)]; + /* This frame's quads where it shows them, for the input's layers, + * and those it shows by their backs, a bit each. */ + video_xr_quad_set_t shown; /* the frame loop's */ + uint32_t backs; /* the frame loop's */ + /* The menu's index among this frame's quad layers, or -1. */ + int menu_layer; /* the frame loop's */ /* The drawing thread's quads for the XR thread and the pointer, the * same way. */ retro_atomic_int_t quads_seq; @@ -208,21 +288,31 @@ typedef struct openxr_input_hooks void (*session_created)(void *user, const openxr_input_handles_t *h); /* The frame loop has stopped; the session is destroyed next. */ void (*session_destroying)(void *user); - /* The frame loop's thread, each headset frame: up to cap layers over - * the screens. Returns how many were written. */ + /* The frame loop's thread, each headset frame: up to cap layers with + * the screens, which shown holds where this frame shows them, in + * space; the first OPENXR_KEPT_EXTRA_LAYERS for the dots, the rest + * for what only fits in room the screens leave. Returns how many were + * written: the first *under go under the screens, the rest over. */ unsigned (*frame_layers)(void *user, XrTime display_time, - const XrCompositionLayerBaseHeader **layers, unsigned cap); + const video_xr_quad_set_t *shown, XrSpace space, + const XrCompositionLayerBaseHeader **layers, unsigned cap, + unsigned *under); /* The two hands' poses and joints in base at time. * False when none could be located. */ bool (*locate_hands)(void *user, XrSpace base, XrTime time, struct retro_vr_controller_poses *out); + /* The frame loop's thread: a hand's aim pose at a frame's time in + * space; false while it is not tracked. */ + bool (*aim)(void *user, unsigned hand, XrSpace space, XrTime time, + video_xr_pose_t *pose); void *user; } openxr_input_hooks_t; bool openxr_frame_load(openxr_frame_t *f, const openxr_runtime_t *rt, openxr_session_t *session, openxr_swapchains_t *sc); void openxr_frame_deinit(openxr_frame_t *f); -/* A new session: the anchor straight ahead, the rates it offers. */ +/* A new session: the anchor straight ahead, nothing held, the rates it + * offers. */ void openxr_frame_session_created(openxr_frame_t *f); /* The desktop host: the XR thread. */ bool openxr_frame_start(openxr_frame_t *f); @@ -262,6 +352,13 @@ void openxr_frame_request_rate(openxr_frame_t *f, float hz); /* The drawing thread's quads, placed from an identity anchor: each * headset frame puts them where the screens are. */ void openxr_frame_publish(openxr_frame_t *f, const video_xr_quad_set_t *set); +/* Main thread: edit mode's hands, for each frame to place what they + * hold at its own aim. */ +void openxr_frame_publish_grabs(openxr_frame_t *f, const openxr_grabs_t *g); +/* The frame loop's thread: the hands' smoothed aims. */ +void openxr_frame_publish_aims(openxr_frame_t *f, const openxr_aims_t *a); +/* The main thread: the aims last published. */ +void openxr_frame_get_aims(openxr_frame_t *f, openxr_aims_t *out); /* The projection the XR thread submits from now on, behind the * screens; left out until its slot releases an image, and once the * track space it was drawn in is replaced without a carry. Published @@ -279,6 +376,10 @@ void openxr_frame_set_placement(openxr_frame_t *f, unsigned placement); * showed them, and the space they are in: what the laser points at. */ void openxr_frame_get_shown_quads(openxr_frame_t *f, video_xr_quad_set_t *out, XrSpace *space); +/* Any thread: where the last frame showed the screens (the anchor, + * VIEW's identity, or Lazy Follow's), and their space. */ +void openxr_frame_get_screens(openxr_frame_t *f, video_xr_pose_t *pose, + XrSpace *space); void openxr_frame_request_recenter(openxr_frame_t *f); /* Any thread: how often the anchor has moved, and the display period * the frames publish, 0 until measured. */ @@ -292,6 +393,20 @@ void openxr_frame_set_input_hooks(const openxr_input_hooks_t *hooks); void openxr_frame_input_attach(openxr_frame_t *f); void openxr_frame_input_detach(openxr_frame_t *f); XrSwapchain openxr_frame_cursor(const openxr_frame_t *f); +/* The width hosts make the laser's image: VIDEO_XR_CURSOR_W; where the + * runtime's swapchains are narrower (4096 if it does not say), + * VIDEO_XR_CURSOR_TAGGED_W, without the cut outlines, or + * VIDEO_XR_CURSOR_UNTAGGED_W, without the tags too. */ +unsigned openxr_frame_cursor_w(const openxr_frame_t *f); +/* What the image at that width has past the outlines, for the log. */ +const char *openxr_frame_cursor_parts(const openxr_frame_t *f); +/* The laser's image (video_xr_cursor_pixels, video_xr_cut_pixels) w + * wide, as openxr_frame_cursor_w says, in px's VIDEO_XR_CURSOR_W by + * VIDEO_XR_CURSOR_H texels; from VIDEO_XR_CURSOR_TAGGED_W wide with + * edit mode's tags written in the default font and the interface + * language as it is now; bottom_up for an image whose rows run bottom + * up, as GL's. Without a font, the tags stay clear. */ +void openxr_frame_cursor_pixels(uint8_t *px, unsigned w, bool bottom_up); RETRO_END_DECLS diff --git a/gfx/common/openxr_runtime.c b/gfx/common/openxr_runtime.c index 5238ab0288..b8f720298c 100644 --- a/gfx/common/openxr_runtime.c +++ b/gfx/common/openxr_runtime.c @@ -250,6 +250,7 @@ enum openxr_runtime_result openxr_runtime_find_system(openxr_runtime_t *rt) } rt->max_dim = 4096; + rt->max_width = 0; rt->max_layers = XR_MIN_COMPOSITION_LAYERS_SUPPORTED; props.type = XR_TYPE_SYSTEM_PROPERTIES; props.next = NULL; @@ -261,6 +262,7 @@ enum openxr_runtime_result openxr_runtime_find_system(openxr_runtime_t *rt) { uint32_t mw = props.graphicsProperties.maxSwapchainImageWidth; uint32_t mh = props.graphicsProperties.maxSwapchainImageHeight; + rt->max_width = mw; if (mw && mw < rt->max_dim) rt->max_dim = mw; if (mh && mh < rt->max_dim) diff --git a/gfx/common/openxr_runtime.h b/gfx/common/openxr_runtime.h index 42b012fdea..bb5da5698e 100644 --- a/gfx/common/openxr_runtime.h +++ b/gfx/common/openxr_runtime.h @@ -53,6 +53,7 @@ typedef struct openxr_runtime XrSystemId system; XrEnvironmentBlendMode blend_mode; unsigned max_dim; /* the largest swapchain side */ + unsigned max_width; /* the widest swapchain, 0 if not said */ uint32_t rec_width; /* one eye's recommended image width */ uint32_t rec_height; /* and its height */ uint32_t max_layers; /* the most layers a frame may end with */ diff --git a/gfx/common/vulkan_openxr.c b/gfx/common/vulkan_openxr.c index c47b0da4f9..5cb8a528d3 100644 --- a/gfx/common/vulkan_openxr.c +++ b/gfx/common/vulkan_openxr.c @@ -584,8 +584,8 @@ static bool vulkan_openxr_cursor_fill(void *user, uint32_t index, VkCommandBuffer cmd = VK_NULL_HANDLE; VkFence fence = VK_NULL_HANDLE; void *map = NULL; - VkDeviceSize size = VIDEO_XR_CURSOR_DIM - * VIDEO_XR_CURSOR_DIM * 4; + VkDeviceSize size = (VkDeviceSize)VIDEO_XR_CURSOR_W + * VIDEO_XR_CURSOR_H * 4; VkMemoryPropertyFlags host = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; VkBufferCreateInfo bi; @@ -632,7 +632,8 @@ static bool vulkan_openxr_cursor_fill(void *user, uint32_t index, failed = "staging memory"; goto end; } - video_xr_cursor_pixels((uint8_t*)map); + openxr_frame_cursor_pixels((uint8_t*)map, + openxr_frame_cursor_w(&dot->xr->frame), false); vkUnmapMemory(device, memory); pi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -690,8 +691,9 @@ static bool vulkan_openxr_cursor_fill(void *user, uint32_t index, region.imageOffset.z = 0; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; - region.imageExtent.width = VIDEO_XR_CURSOR_DIM; - region.imageExtent.height = VIDEO_XR_CURSOR_DIM; + region.imageExtent.width = + openxr_frame_cursor_w(&dot->xr->frame); + region.imageExtent.height = VIDEO_XR_CURSOR_H; region.imageExtent.depth = 1; vkCmdCopyBufferToImage(cmd, buffer, dot->images[index][0], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); @@ -760,11 +762,20 @@ static void vulkan_openxr_cursor_create(vulkan_openxr_t *xr, li.xr = xr; li.images = dot.images; xr->frame.cursor = openxr_swapchains_create_filled(&xr->sc, formats, 4, - VIDEO_SCALE_PACK(VIDEO_XR_CURSOR_DIM, VIDEO_XR_CURSOR_DIM), + VIDEO_SCALE_PACK(openxr_frame_cursor_w(&xr->frame), + VIDEO_XR_CURSOR_H), vulkan_openxr_list_images, &li, vulkan_openxr_cursor_fill, &dot, &step, &code); - if (xr->frame.cursor) - RARCH_LOG("[OpenXR] Laser cursor ready.\n"); + if (xr->frame.cursor && xr->frame.rt->max_width) + RARCH_LOG("[OpenXR] Laser cursor ready: %u wide, with %s (the " + "headset's widest %u).\n", openxr_frame_cursor_w(&xr->frame), + openxr_frame_cursor_parts(&xr->frame), + xr->frame.rt->max_width); + else if (xr->frame.cursor) + RARCH_LOG("[OpenXR] Laser cursor ready: %u wide, with %s (the " + "headset does not say its widest).\n", + openxr_frame_cursor_w(&xr->frame), + openxr_frame_cursor_parts(&xr->frame)); else RARCH_WARN("[OpenXR] No laser cursor (%s, %d).\n", step, code); } diff --git a/gfx/drivers/gl2.c b/gfx/drivers/gl2.c index 568b0359e5..fbeaf5ed95 100644 --- a/gfx/drivers/gl2.c +++ b/gfx/drivers/gl2.c @@ -5153,8 +5153,8 @@ typedef struct gl2_desktop_xr video_xr_quad_set_t set; /* Where each slot's layers lie in the window, from its top left. */ video_views_rect_t rects[VIDEO_XR_MAX_SLOTS][2]; - /* The slots the headset has an image for. */ - bool published[VIDEO_XR_MAX_SLOTS]; + /* The size of each slot's image the headset has, 0 for none. */ + unsigned published[VIDEO_XR_MAX_SLOTS]; GLuint fbo; /* The core's last frame size: a repeat brings none. */ unsigned frame_dims; @@ -5228,6 +5228,7 @@ static void gl2_desktop_xr_plan(gl2_t *gl, const video_frame_info_t *video_info, const video_viewport_t *vp, bool map, bool eyes) { + unsigned s; video_xr_params_t p; gl2_desktop_xr_t *xr = gl->desktop_xr; bool menu = false; @@ -5241,6 +5242,10 @@ static void gl2_desktop_xr_plan(gl2_t *gl, p.map = map ? &video_info->views : NULL; p.frame_dims = gl->video_dims; video_xr_pose_identity(&p.anchor); + /* A window shown whole despite a map is not screen 1. */ + p.screens = (map || !video_info->views.num_views) + ? video_info->headset_screens : NULL; + p.edit_seq = video_info->headset_edit_seq; p.distance = video_info->headset_distance; p.width = video_info->headset_width; p.px_per_rad = gl_openxr_pixels_per_radian(xr->host); @@ -5254,6 +5259,9 @@ static void gl2_desktop_xr_plan(gl2_t *gl, video_xr_place_window(&p, menu, map && gl2_desktop_xr_tracked(video_info), vp, xr->frame_dims, &xr->set, xr->rects); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + xr->set.slots[s].dims = video_xr_keep_dims(xr->set.slots[s].dims, + xr->published[s], video_info->headset_editing); } /* The plan's rectangles out of the window's back buffer, which is @@ -5280,7 +5288,7 @@ static void gl2_desktop_xr_draw(gl2_t *gl, bool eyes) else gl_openxr_slot_forget(xr->host, s); } - xr->published[s] = false; + xr->published[s] = 0; continue; } if ( gl_openxr_slot_refused(xr->host, s) == plan->dims @@ -5304,7 +5312,7 @@ static void gl2_desktop_xr_draw(gl2_t *gl, bool eyes) GL_COLOR_BUFFER_BIT, GL_LINEAR); } gl_openxr_slot_publish(xr->host, s, &xr->set, NULL); - xr->published[s] = true; + xr->published[s] = plan->dims; } } @@ -5330,7 +5338,7 @@ static bool gl2_desktop_xr_ready(gl2_t *gl) { if (xr->published[s]) gl_openxr_slot_forget(xr->host, s); - xr->published[s] = false; + xr->published[s] = 0; } if (xr->proj_shown) gl_openxr_slot_forget(xr->host, OPENXR_PROJECTION_SLOT); @@ -7370,7 +7378,8 @@ static bool gl2_frame(void *data, const void *frame, ? 0 : video_info->headset_interval, video_info->headset_request_hz); gl_android_openxr_set_views(&video_info->views, dims, &xr_vp, gl->rotation / 90, video_info->screen_layout, - video_info->stereo_swap_eyes); + video_info->stereo_swap_eyes, video_info->headset_screens, + video_info->headset_edit_seq, video_info->headset_editing); if (gl_android_openxr_active()) { bool new_frame = frame && !video_info->frame_repeat; diff --git a/gfx/drivers/gl3.c b/gfx/drivers/gl3.c index f65ee85e8f..8cff070b37 100644 --- a/gfx/drivers/gl3.c +++ b/gfx/drivers/gl3.c @@ -3165,7 +3165,7 @@ static void gl3_views_build_chains(gl3_t *gl) set->preset[i] = gl3_filter_chain_create_from_preset( gl->views.preset, gl->video_info.smooth ? GLSLANG_FILTER_CHAIN_LINEAR - : GLSLANG_FILTER_CHAIN_NEAREST, true); + : GLSLANG_FILTER_CHAIN_NEAREST, true, false); if (!set->preset[i]) RARCH_ERR("[GLCore] Failed to create view %u's preset: \"%s\".\n", i, gl->views.preset); @@ -3184,7 +3184,7 @@ static void gl3_views_build_chains(gl3_t *gl) *chain = gl3_filter_chain_create_from_preset(gl->views.preset, gl->video_info.smooth ? GLSLANG_FILTER_CHAIN_LINEAR - : GLSLANG_FILTER_CHAIN_NEAREST, false); + : GLSLANG_FILTER_CHAIN_NEAREST, false, true); if (!*chain) RARCH_ERR("[OpenXR] Failed to create the headset's preset: \"%s\".\n", gl->views.preset); @@ -3211,7 +3211,7 @@ static bool gl3_init_filter_chain_preset(gl3_t *gl, const char *shader_path) gl->video_info.smooth ? GLSLANG_FILTER_CHAIN_LINEAR : GLSLANG_FILTER_CHAIN_NEAREST, - (gl->flags & GL3_FLAG_HW_RENDER_ENABLE) != 0); + (gl->flags & GL3_FLAG_HW_RENDER_ENABLE) != 0, false); if (!gl->filter_chain) { @@ -6520,6 +6520,7 @@ static void gl3_xr_plan(gl3_t *gl, const video_frame_info_t *video_info, bool views, bool eyes, unsigned frame_dims, unsigned src_dims, unsigned ui_dims) { + unsigned s; video_xr_params_t p; gl_openxr_t *xr = gl->xr.host; @@ -6530,6 +6531,13 @@ static void gl3_xr_plan(gl3_t *gl, const video_frame_info_t *video_info, / (float)VIDEO_SCALE_H(frame_dims); p.frame_dims = src_dims; video_xr_pose_identity(&p.anchor); + /* A frame shown whole despite a map is not screen 1. */ + p.screens = (views || !video_info->views.num_views) + ? video_info->headset_screens : NULL; + p.edit_seq = video_info->headset_edit_seq; + p.menu = video_info->headset_menu_placed + ? &video_info->headset_menu : NULL; + p.menu_follow = video_info->headset_menu_follow; p.distance = video_info->headset_distance; p.width = video_info->headset_width; p.px_per_rad = gl_openxr_pixels_per_radian(xr); @@ -6542,6 +6550,11 @@ static void gl3_xr_plan(gl3_t *gl, const video_frame_info_t *video_info, if (views && eyes) p.skip_screens = gl3_xr_eye_screens(video_info); video_xr_place(&p, &gl->xr.set); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + gl->xr.set.slots[s].dims = video_xr_keep_dims( + gl->xr.set.slots[s].dims, + gl->xr.shown[s].published ? gl->xr.shown[s].info.dims : 0, + video_info->headset_editing); } /* Whether slot s's plan this frame is what it last published. */ @@ -6592,10 +6605,13 @@ static void gl3_xr_forget(gl3_t *gl, unsigned s, bool yield) } /* Each layer of a screen slot drawn whole into tex by its view's - * headset chain or the whole frame's: its passes at the image's size. */ + * headset chain or the whole frame's: its passes at the image's size. A + * repeat - a screen moved, resized or not yet shown while no new frame + * came - replays the last frame's final pass, so feedback and history + * stay where that frame left them. */ static void gl3_xr_draw_screen(gl3_t *gl, unsigned s, const uint32_t *tex, gl3_filter_chain_t **chains, gl3_filter_chain_t *frame_chain, - const struct gl3_filter_chain_texture *frame_tex) + const struct gl3_filter_chain_texture *frame_tex, bool new_frame) { unsigned l; struct gl3_viewport vp; @@ -6608,6 +6624,8 @@ static void gl3_xr_draw_screen(gl3_t *gl, unsigned s, const uint32_t *tex, struct gl3_filter_chain_texture in; int v = plan->view[l]; gl3_filter_chain_t *chain = (v >= 0) ? chains[v] : frame_chain; + bool replay = chain && !new_frame + && gl3_filter_chain_replay_ready(chain); if (v >= 0) { @@ -6619,7 +6637,7 @@ static void gl3_xr_draw_screen(gl3_t *gl, unsigned s, const uint32_t *tex, } else in = *frame_tex; - if (chain) + if (chain && !replay) { gl3_filter_chain_set_input_texture(chain, &in); gl3_filter_chain_build_offscreen_passes(chain, &vp); @@ -6631,10 +6649,13 @@ static void gl3_xr_draw_screen(gl3_t *gl, unsigned s, const uint32_t *tex, glViewport(0, 0, VIDEO_SCALE_W(plan->dims), VIDEO_SCALE_H(plan->dims)); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); - if (chain) + if (replay) + gl3_filter_chain_build_viewport_pass_replay(chain, &vp, gl->xr.mvp); + else if (chain) { gl3_filter_chain_build_viewport_pass(chain, &vp, gl->xr.mvp); - gl3_filter_chain_end_frame(chain); + if (new_frame) + gl3_filter_chain_end_frame(chain); } } } @@ -6794,7 +6815,8 @@ static void gl3_xr_draw(gl3_t *gl, const video_frame_info_t *video_info, if (s == VIDEO_XR_MENU_SLOT) gl3_xr_copy_ui(gl, s, tex[0], ui_dims); else - gl3_xr_draw_screen(gl, s, tex, chains, frame_chain, frame_tex); + gl3_xr_draw_screen(gl, s, tex, chains, frame_chain, frame_tex, + new_frame); gl_openxr_slot_publish(xr, s, &gl->xr.set, NULL); gl3_xr_keep(gl, s); } @@ -6947,6 +6969,7 @@ static bool gl3_frame(void *data, const void *frame, bool views_direct = false; bool views_in_place = false; bool views_ui = false; + bool views_ui_menu = false; unsigned ui_layer_dims = 0; /* A software frame a preset may draw as views, written once they are * known: straight into each view's texture if they need copies */ @@ -7626,13 +7649,19 @@ static bool gl3_frame(void *data, const void *frame, /* A failed size stays latched until the layer changes size. */ if (frame && !ui_layer_dims) gl3_vv_target_drop(gl, &gl->views.ui_target, &gl->views.ui); + views_ui_menu = (gl->flags & GL3_FLAG_MENU_TEXTURE_ENABLE) ? true : false; +#ifdef HAVE_GL3_XR + /* Edit mode moves the menu's quad: it stays up, empty. */ + if (xr_draw && video_info->headset_editing) + views_ui_menu = true; +#endif #ifdef HAVE_GFX_WIDGETS views_ui = VIDEO_VIEWS_UI_WANTED(ui_layer_dims, - (gl->flags & GL3_FLAG_MENU_TEXTURE_ENABLE), msg, statistics_show, + views_ui_menu, msg, statistics_show, widgets_active && gfx_widgets_visible(video_info)) #else views_ui = VIDEO_VIEWS_UI_WANTED(ui_layer_dims, - (gl->flags & GL3_FLAG_MENU_TEXTURE_ENABLE), msg, statistics_show, false) + views_ui_menu, msg, statistics_show, false) #endif && gl3_vv_target_ensure(gl, &gl->views.ui_target, &gl->views.ui, ui_layer_dims); diff --git a/gfx/drivers/vulkan.c b/gfx/drivers/vulkan.c index 99aee945af..5e9a30f49c 100644 --- a/gfx/drivers/vulkan.c +++ b/gfx/drivers/vulkan.c @@ -6613,6 +6613,7 @@ static void vulkan_filter_chain_info_init(vk_t *vk, info->swapchain.render_pass = vk->render_pass; info->swapchain.num_indices = vk->context->num_swapchain_images; info->frame_rect = false; + info->replay = false; } static bool vulkan_chain_emits_hdr10(vk_t *vk, vulkan_filter_chain_t *chain) @@ -11262,6 +11263,7 @@ static void vulkan_xr_plan(vk_t *vk, const video_frame_info_t *video_info, bool views, bool eyes, unsigned frame_dims, unsigned src_dims, unsigned ui_dims) { + unsigned s; video_xr_params_t p; vulkan_openxr_t *xr = vk->context->xr; @@ -11272,6 +11274,13 @@ static void vulkan_xr_plan(vk_t *vk, const video_frame_info_t *video_info, / (float)VIDEO_SCALE_H(frame_dims); p.frame_dims = src_dims; video_xr_pose_identity(&p.anchor); + /* A frame shown whole despite a map is not screen 1. */ + p.screens = (views || !video_info->views.num_views) + ? video_info->headset_screens : NULL; + p.edit_seq = video_info->headset_edit_seq; + p.menu = video_info->headset_menu_placed + ? &video_info->headset_menu : NULL; + p.menu_follow = video_info->headset_menu_follow; p.distance = video_info->headset_distance; p.width = video_info->headset_width; p.px_per_rad = vulkan_openxr_pixels_per_radian(xr); @@ -11284,6 +11293,11 @@ static void vulkan_xr_plan(vk_t *vk, const video_frame_info_t *video_info, if (views && eyes) p.skip_screens = vulkan_xr_eye_screens(video_info); video_xr_place(&p, &vk->xr.set); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + vk->xr.set.slots[s].dims = video_xr_keep_dims( + vk->xr.set.slots[s].dims, + vk->xr.slots[s].num_images ? vk->xr.slots[s].dims : 0, + video_info->headset_editing); } /* The headset's chain for a view, or for the whole frame when view is @@ -11317,10 +11331,14 @@ static vulkan_filter_chain_t *vulkan_xr_chain(vk_t *vk, return *chain; } -/* Each layer of slot s's acquired image, drawn whole by its view's - * headset chain or the whole frame's: its passes at the image's size. */ +/* Slot s's layers, each through its view's headset chain or the whole + * frame's, straight into the runtime's acquired images. A repeat - a + * screen resized, made again or not yet shown while no new frame came - + * replays the last frame's final pass at the image's size, so feedback + * and history stay where that frame left them. */ static void vulkan_xr_draw_screen(vk_t *vk, unsigned s, - vulkan_filter_chain_t **xr_chains, vulkan_filter_chain_t *xr_frame) + vulkan_filter_chain_t **xr_chains, vulkan_filter_chain_t *xr_frame, + bool new_frame) { unsigned l; VkViewport vp; @@ -11356,7 +11374,11 @@ static void vulkan_xr_draw_screen(vk_t *vk, unsigned s, vulkan_filter_chain_t *chain = (plan->view[l] >= 0) ? xr_chains[plan->view[l]] : xr_frame; - if (chain) + bool replay = false; + + if (chain && !new_frame) + replay = vulkan_filter_chain_replay_ready(chain); + if (chain && !replay) vulkan_filter_chain_build_offscreen_passes(chain, vk->cmd, &vp); rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rp.pNext = NULL; @@ -11371,11 +11393,14 @@ static void vulkan_xr_draw_screen(vk_t *vk, unsigned s, vkCmdBeginRenderPass(vk->cmd, &rp, VK_SUBPASS_CONTENTS_INLINE); vk->tracker.dirty |= VULKAN_DIRTY_DYNAMIC_BIT; vk->tracker.pipeline = VK_NULL_HANDLE; - if (chain) + if (replay) + vulkan_filter_chain_build_viewport_pass_replay(chain, vk->cmd, + &vp, vk->xr.mvp.data); + else if (chain) vulkan_filter_chain_build_viewport_pass(chain, vk->cmd, &vp, vk->xr.mvp.data); vkCmdEndRenderPass(vk->cmd); - if (chain) + if (chain && new_frame) vulkan_filter_chain_end_frame(chain, vk->cmd); } @@ -11610,7 +11635,8 @@ static void vulkan_xr_draw(vk_t *vk, const video_frame_info_t *video_info, unsigned frame_dims, unsigned src_dims, unsigned ui_dims, bool window_drawn) { - unsigned s; + unsigned s, q; + unsigned omit = 0; vulkan_openxr_t *xr = vk->context->xr; if (window_drawn) @@ -11632,10 +11658,18 @@ static void vulkan_xr_draw(vk_t *vk, const video_frame_info_t *video_info, vulkan_openxr_slot_forget(xr, s); vk->xr.slots[s].released = false; } - if ( !vk->xr.set.slots[s].dims - || !vulkan_xr_slot_ensure(vk, s, vk->xr.set.slots[s].dims, - vk->xr.set.slots[s].layers)) + if (!vk->xr.set.slots[s].dims) continue; + if (!vulkan_xr_slot_ensure(vk, s, vk->xr.set.slots[s].dims, + vk->xr.set.slots[s].layers)) + { + /* A slot the runtime left with no swapchain has nothing to + * show; one still holding its last size keeps the quad. */ + if (!vk->xr.slots[s].num_images) + omit |= 1u << s; + continue; + } + /* A swapchain made again for a new size is not released. */ if ( s != VIDEO_XR_MENU_SLOT && !new_frame && vk->xr.slots[s].released) continue; @@ -11648,15 +11682,21 @@ static void vulkan_xr_draw(vk_t *vk, const video_frame_info_t *video_info, #ifdef HAVE_THREADS slock_unlock(vk->context->queue_lock); #endif + /* The quad stays: the headset shows the slot's last image, or + * the swapchains a resize keeps until these have one. */ if (!ok) continue; /* The menu slot has no view: -1 would draw the whole frame. */ if (s == VIDEO_XR_MENU_SLOT) vulkan_xr_copy_ui(vk, s, ui_dims); else - vulkan_xr_draw_screen(vk, s, xr_chains, xr_frame); + vulkan_xr_draw_screen(vk, s, xr_chains, xr_frame, new_frame); vk->xr.slots[s].drawn = true; } + for (s = 0, q = 0; q < vk->xr.set.num_quads; q++) + if (!(omit & (1u << vk->xr.set.quads[q].slot))) + vk->xr.set.quads[s++] = vk->xr.set.quads[q]; + vk->xr.set.num_quads = s; /* A repeat draws the eyes only for a projection not shown yet. */ if (eyes && (new_frame || !vk->xr.proj_shown)) vulkan_xr_draw_projection(vk, video_info); @@ -11958,6 +11998,7 @@ static bool vulkan_frame(void *data, const void *frame, bool views_blend = false; bool views_per_eye = false; bool views_ui = false; + bool views_ui_menu = false; unsigned ui_layer_dims = 0; #ifdef HAVE_OPENXR vulkan_filter_chain_t *xr_chains[RETRO_VIDEO_VIEWS_MAX]; @@ -12903,13 +12944,19 @@ static bool vulkan_frame(void *data, const void *frame, /* The UI layer is drawn and composited only when there is UI, by * the same test the HDR composite makes. Overlays are hidden per * eye, and drawn around the layer when it is for a headset. */ + views_ui_menu = (vk->flags & VK_FLAG_MENU_ENABLE) ? true : false; +#ifdef HAVE_OPENXR + /* Edit mode moves the menu's quad: it stays up, empty. */ + if (xr_draw && video_info->headset_editing) + views_ui_menu = true; +#endif #ifdef HAVE_GFX_WIDGETS views_ui = VIDEO_VIEWS_UI_WANTED(ui_layer_dims, - (vk->flags & VK_FLAG_MENU_ENABLE), msg, statistics_show, + views_ui_menu, msg, statistics_show, widgets_active && gfx_widgets_visible(video_info)) #else views_ui = VIDEO_VIEWS_UI_WANTED(ui_layer_dims, - (vk->flags & VK_FLAG_MENU_ENABLE), msg, statistics_show, false) + views_ui_menu, msg, statistics_show, false) #endif && vk->pipelines.alpha_premult && vulkan_vv_target_ensure(vk, &vk->views.ui_target, &vk->views.ui, diff --git a/gfx/drivers_context/gl_android_openxr.h b/gfx/drivers_context/gl_android_openxr.h index cda37c16e6..24af4acd0f 100644 --- a/gfx/drivers_context/gl_android_openxr.h +++ b/gfx/drivers_context/gl_android_openxr.h @@ -27,9 +27,11 @@ static INLINE void gl_android_openxr_set_pacing(unsigned interval, float hz) { (void)interval; (void)hz; } static INLINE void gl_android_openxr_set_views(const video_views_map_t *map, unsigned frame_dims, const video_viewport_t *vp, unsigned rotation, - unsigned screen_layout, bool swap_eyes) + unsigned screen_layout, bool swap_eyes, + const video_screen_xr_t *screens, unsigned edit_seq, bool editing) { (void)map; (void)frame_dims; (void)vp; (void)rotation; - (void)screen_layout; (void)swap_eyes; } + (void)screen_layout; (void)swap_eyes; (void)screens; + (void)edit_seq; (void)editing; } static INLINE float gl_android_openxr_refresh(float *rates, unsigned cap, unsigned *count) { (void)rates; (void)cap; *count = 0; return 0.0f; } static INLINE void gl_android_openxr_new_frame(void) { } @@ -76,11 +78,13 @@ void gl_android_openxr_set_placement(unsigned placement, float distance, void gl_android_openxr_set_pacing(unsigned interval, float hz); /* gl2, once a frame: the frame's views and the size they are in, where * the window draws the frame (from its top left) and its quarter turns, - * and how screens are laid out and eyes swapped. While the menu is + * and how screens are laid out and eyes swapped. Screen adjustments, + * edit mode and its count are copied for this frame. While the menu is * closed, an untracked map's screens are cut from the window. */ void gl_android_openxr_set_views(const video_views_map_t *map, unsigned frame_dims, const video_viewport_t *vp, unsigned rotation, - unsigned screen_layout, bool swap_eyes); + unsigned screen_layout, bool swap_eyes, + const video_screen_xr_t *screens, unsigned edit_seq, bool editing); /* The headset's measured rate, 0 until known, and up to cap of the * rates it can be asked for. */ float gl_android_openxr_refresh(float *rates, unsigned cap, diff --git a/gfx/drivers_context/gl_android_openxr_ctx.c b/gfx/drivers_context/gl_android_openxr_ctx.c index 5e535e797b..33f9e59107 100644 --- a/gfx/drivers_context/gl_android_openxr_ctx.c +++ b/gfx/drivers_context/gl_android_openxr_ctx.c @@ -114,7 +114,9 @@ typedef struct gl_android_openxr * has the frame: gl2 sets them in its frame, before the swap that * draws with them, both on the video thread. */ video_views_map_t views; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; video_viewport_t views_vp; + unsigned edit_seq; unsigned views_dims; unsigned rotation; unsigned screen_layout; @@ -122,6 +124,7 @@ typedef struct gl_android_openxr /* An sRGB swapchain filled with GL_FRAMEBUFFER_SRGB_EXT off around * the copy (EXT_sRGB_write_control). */ bool srgb_copy; + bool editing; EGLDisplay egl_display; EGLConfig egl_config; @@ -138,6 +141,7 @@ static gl_android_openxr_t gl_xr; static bool gl_xr_egl_init(gl_android_openxr_t *xr) { + const char *ext; EGLint num_configs = 0; EGLint major = 0, minor = 0; @@ -172,7 +176,7 @@ static bool gl_xr_egl_init(gl_android_openxr_t *xr) return false; } - const char *ext = eglQueryString(xr->egl_display, EGL_EXTENSIONS); + ext = eglQueryString(xr->egl_display, EGL_EXTENSIONS); if (ext && strstr(ext, "EGL_KHR_surfaceless_context")) RARCH_DBG("[XR] EGL_KHR_surfaceless_context supported\n"); @@ -371,17 +375,31 @@ static void gl_android_openxr_host_register(gl_android_openxr_t *xr) /* ===================== the session ===================== */ +struct gl_xr_dot +{ + uint32_t tex[OPENXR_MAX_IMAGES][2]; + unsigned w; +}; + /* The dot's image, with the video context current. */ static bool gl_xr_cursor_fill(void *user, uint32_t index, const char **step, int *code) { - static uint8_t px[VIDEO_XR_CURSOR_DIM * VIDEO_XR_CURSOR_DIM * 4]; - uint32_t (*tex)[2] = (uint32_t (*)[2])user; - video_xr_cursor_pixels(px); - glBindTexture(GL_TEXTURE_2D, (GLuint)tex[index][0]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIDEO_XR_CURSOR_DIM, - VIDEO_XR_CURSOR_DIM, GL_RGBA, GL_UNSIGNED_BYTE, px); + struct gl_xr_dot *dot = (struct gl_xr_dot*)user; + uint8_t *px = (uint8_t*)malloc( + (size_t)VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4); + if (!px) + { + *step = "malloc"; + *code = 0; + return false; + } + openxr_frame_cursor_pixels(px, dot->w, true); + glBindTexture(GL_TEXTURE_2D, (GLuint)dot->tex[index][0]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, dot->w, VIDEO_XR_CURSOR_H, + GL_RGBA, GL_UNSIGNED_BYTE, px); glBindTexture(GL_TEXTURE_2D, 0); + free(px); /* The compositor reads the image on its own context. */ glFlush(); return true; @@ -392,19 +410,27 @@ static void gl_xr_cursor_create(gl_android_openxr_t *xr) { static const int64_t formats[2] = { OPENXR_GLES_SRGB8_ALPHA8, OPENXR_GLES_RGBA8 }; - uint32_t tex[OPENXR_MAX_IMAGES][2]; + struct gl_xr_dot dot; openxr_gles_images_t li; const char *step = NULL; int code = 0; + dot.w = openxr_frame_cursor_w(&xr->frame); li.sc = &xr->sc; - li.tex = tex; + li.tex = dot.tex; xr->frame.cursor = openxr_swapchains_create_filled(&xr->sc, formats, 2, - VIDEO_SCALE_PACK(VIDEO_XR_CURSOR_DIM, VIDEO_XR_CURSOR_DIM), - openxr_binding_gles_images, &li, gl_xr_cursor_fill, tex, + VIDEO_SCALE_PACK(dot.w, VIDEO_XR_CURSOR_H), + openxr_binding_gles_images, &li, gl_xr_cursor_fill, &dot, &step, &code); - if (xr->frame.cursor) - RARCH_LOG("[XR] Laser cursor ready.\n"); + if (xr->frame.cursor && xr->frame.rt->max_width) + RARCH_LOG("[XR] Laser cursor ready: %u wide, with %s (the " + "headset's widest %u).\n", dot.w, + openxr_frame_cursor_parts(&xr->frame), + xr->frame.rt->max_width); + else if (xr->frame.cursor) + RARCH_LOG("[XR] Laser cursor ready: %u wide, with %s (the " + "headset does not say its widest).\n", dot.w, + openxr_frame_cursor_parts(&xr->frame)); else RARCH_WARN("[XR] No laser cursor (%s, %d).\n", step, code); } @@ -601,6 +627,8 @@ static void gl_xr_draw(gl_android_openxr_t *xr) p.map = &xr->views; p.frame_dims = xr->dims; video_xr_pose_identity(&p.anchor); + p.screens = xr->screens; + p.edit_seq = xr->edit_seq; p.distance = xr->distance; p.width = xr->width; p.px_per_rad = openxr_frame_pixels_per_radian(&xr->frame); @@ -613,6 +641,9 @@ static void gl_xr_draw(gl_android_openxr_t *xr) /* With no width, an empty set: the eyes are all there is. */ video_xr_place_window(&p, menu, stereo, &xr->views_vp, xr->views_dims, &xr->set, rects); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + xr->set.slots[s].dims = video_xr_keep_dims(xr->set.slots[s].dims, + xr->num_images[s] ? xr->slot_dims[s] : 0, xr->editing); /* The blits are scissored: whatever gl2 drew last must not clip them. */ glDisable(GL_SCISSOR_TEST); /* The copy keeps the stored values; the compositor decodes them. @@ -691,10 +722,17 @@ void gl_android_openxr_set_placement(unsigned placement, float distance, void gl_android_openxr_set_views(const video_views_map_t *map, unsigned frame_dims, const video_viewport_t *vp, unsigned rotation, - unsigned screen_layout, bool swap_eyes) + unsigned screen_layout, bool swap_eyes, + const video_screen_xr_t *screens, unsigned edit_seq, bool editing) { gl_android_openxr_t *xr = &gl_xr; xr->views = *map; + if (screens) + memcpy(xr->screens, screens, sizeof(xr->screens)); + else + video_views_xr_reset(xr->screens, RETRO_VIDEO_VIEWS_MAX); + xr->edit_seq = edit_seq; + xr->editing = editing; xr->views_dims = frame_dims; xr->views_vp = *vp; xr->rotation = rotation; diff --git a/gfx/drivers_shader/shader_gl3.c b/gfx/drivers_shader/shader_gl3.c index 914b725d67..776036abfc 100644 --- a/gfx/drivers_shader/shader_gl3.c +++ b/gfx/drivers_shader/shader_gl3.c @@ -3037,9 +3037,18 @@ struct gl3_filter_chain struct gl3_framebuffer *copy_framebuffer; gl3_common_resources common; gl3_filter_chain_texture input_texture; + /* The count the last frame's final pass was drawn with. */ + uint64_t drawn_frame_count; + /* num_original_history framebuffers; one more than the shaders read + * for a replaying chain, so the frame's input goes where the last + * frame did not. */ struct gl3_framebuffer **original_history; size_t num_original_history; bool require_clear; + /* A frame's final pass was drawn since the passes were built. */ + bool drawn; + /* See gl3_filter_chain_create_from_preset. */ + bool replay; bool alias_initialized; /* Every pass reads a frame where it lies: none is copied out. */ bool frame_in_place; @@ -3165,7 +3174,7 @@ static void gl3_chain_update_history_info(struct gl3_filter_chain *chain) { unsigned i; - for (i = 0; i < chain->num_original_history; i++) + for (i = 0; i < chain->common.num_original_history; i++) { gl3_texture_t *source = (gl3_texture_t*) &chain->common.original_history[i]; @@ -3331,12 +3340,32 @@ static void gl3_chain_build_viewport_pass(struct gl3_filter_chain *chain, const /* For feedback FBOs, swap current and previous. */ if (!again) + { for (i = 0; i < chain->num_passes; i++) { struct gl3_framebuffer *fb = gl3_pass_get_feedback_framebuffer(chain->passes[i]); if (fb) gl3_pass_end_frame(chain->passes[i]); } + chain->drawn_frame_count = + chain->passes[chain->num_passes - 1]->frame_count; + chain->drawn = true; + } +} + +/* The last frame's final pass in a later frame, at vp: what it read + * then - the passes' outputs and feedback, the history it saw - and its + * count. Nothing else runs; feedback does not swap, history stays. */ +static void gl3_chain_replay_viewport_pass(struct gl3_filter_chain *chain, + const gl3_viewport vp, const float *mvp) +{ + uint64_t count; + struct gl3_pass *pass = chain->passes[chain->num_passes - 1]; + + count = pass->frame_count; + pass->frame_count = chain->drawn_frame_count; + gl3_chain_build_viewport_pass(chain, vp, mvp, true); + pass->frame_count = count; } static bool gl3_chain_init_history(struct gl3_filter_chain *chain) @@ -3371,14 +3400,17 @@ static bool gl3_chain_init_history(struct gl3_filter_chain *chain) /* We don't need to store array element #0, * since it's aliased with the actual original. */ required_images--; + if (!gl3_texture_array_resize(&chain->common.original_history, + &chain->common.num_original_history, required_images)) + return false; + /* And one more to replay: the last frame's history stays. */ + if (chain->replay) + required_images++; chain->original_history = (struct gl3_framebuffer**) calloc(required_images, sizeof(*chain->original_history)); if (!chain->original_history) return false; chain->num_original_history = required_images; - if (!gl3_texture_array_resize(&chain->common.original_history, - &chain->common.num_original_history, required_images)) - return false; for (i = 0; i < required_images; i++) { @@ -3387,7 +3419,8 @@ static bool gl3_chain_init_history(struct gl3_filter_chain *chain) return false; } - RARCH_LOG("[GLCore] Using history of %u frames.\n", (unsigned)(required_images)); + RARCH_LOG("[GLCore] Using history of %u frames.\n", + (unsigned)chain->common.num_original_history); /* On first frame, we need to clear the textures to * a known state, but we need @@ -3699,6 +3732,8 @@ static bool gl3_chain_init(struct gl3_filter_chain *chain) if (!gl3_texture_array_resize(&chain->common.pass_outputs, &chain->common.num_pass_outputs, chain->num_passes)) return false; + /* A rebuild remakes what a replay reads. */ + chain->drawn = false; gl3_chain_init_frame_in_place(chain); return true; } @@ -4010,6 +4045,8 @@ static bool gl3_chain_finalize(struct gl3_filter_chain *chain) if (!gl3_texture_array_resize(&chain->common.pass_outputs, &chain->common.num_pass_outputs, chain->num_passes)) return false; + /* A rebuild remakes what a replay reads. */ + chain->drawn = false; gl3_chain_init_frame_in_place(chain); return true; } @@ -4372,7 +4409,8 @@ gl3_filter_chain_t *gl3_filter_chain_create_shrink(void) } gl3_filter_chain_t *gl3_filter_chain_create_from_preset( - const char *path, glslang_filter_chain_filter filter, bool frame_rect) + const char *path, glslang_filter_chain_filter filter, bool frame_rect, + bool replay) { size_t j; unsigned i; @@ -4412,6 +4450,7 @@ gl3_filter_chain_t *gl3_filter_chain_create_from_preset( } chain->common.sim_source_dims = shader->sim_source_dims; chain->common.frame_rect = frame_rect; + chain->replay = replay; chain->last_pass = last_pass; if ( shader->luts @@ -5041,6 +5080,18 @@ void gl3_filter_chain_build_viewport_pass_again( gl3_chain_build_viewport_pass(chain, *vp, mvp, true); } +bool gl3_filter_chain_replay_ready(const gl3_filter_chain_t *chain) +{ + return chain->drawn; +} + +void gl3_filter_chain_build_viewport_pass_replay( + gl3_filter_chain_t *chain, + const gl3_viewport *vp, const float *mvp) +{ + gl3_chain_replay_viewport_pass(chain, *vp, mvp); +} + void gl3_filter_chain_end_frame(gl3_filter_chain_t *chain) { gl3_chain_end_frame(chain); diff --git a/gfx/drivers_shader/shader_gl3.h b/gfx/drivers_shader/shader_gl3.h index 2a1a599564..087b74d666 100644 --- a/gfx/drivers_shader/shader_gl3.h +++ b/gfx/drivers_shader/shader_gl3.h @@ -206,11 +206,13 @@ gl3_filter_chain_t *gl3_filter_chain_create_shrink(void); /* @frame_rect: frames may come inside a larger texture (padded_dims); * the passes are built to read them there where the preset allows, - * rather than have each copied out. */ + * rather than have each copied out. @replay: the chain may draw its last + * frame's final pass again in a later frame (build_viewport_pass_replay), + * and its history keeps one more texture for that. */ gl3_filter_chain_t *gl3_filter_chain_create_from_preset( const char *path, enum glslang_filter_chain_filter filter, - bool frame_rect); + bool frame_rect, bool replay); /** * Deferred (per-frame) filter chain creation API. @@ -259,6 +261,17 @@ bool gl3_filter_chain_compile_pass( **/ bool gl3_filter_chain_finalize(gl3_filter_chain_t *chain); +/* Whether a frame's final pass was drawn since the passes were built, + * for build_viewport_pass_replay to draw again. */ +bool gl3_filter_chain_replay_ready(const gl3_filter_chain_t *chain); +/* The last frame's final pass, in a later frame and at vp, from what it + * read then, in place of the offscreen and viewport passes: no other + * pass runs, the feedback buffers do not swap and the history stays. + * For a frame shown again, resized or redrawn. */ +void gl3_filter_chain_build_viewport_pass_replay( + gl3_filter_chain_t *chain, + const struct gl3_viewport *vp, + const float *mvp); void gl3_filter_chain_end_frame(gl3_filter_chain_t *chain); GLuint gl3_cross_compile_program( diff --git a/gfx/drivers_shader/shader_vulkan.c b/gfx/drivers_shader/shader_vulkan.c index ee2c1827ad..67273fd7cf 100644 --- a/gfx/drivers_shader/shader_vulkan.c +++ b/gfx/drivers_shader/shader_vulkan.c @@ -982,10 +982,18 @@ struct vulkan_filter_chain vulkan_filter_chain_swapchain_info swapchain_info; unsigned current_sync_index; + /* The count the last frame's final pass was drawn with. */ + uint64_t drawn_frame_count; + /* num_history slots; one more than the shaders read for a replaying + * chain, so the frame's input goes where the last frame did not. */ struct slang_framebuffer **original_history; size_t num_history; unsigned history_ring_index; bool require_clear; + /* A frame's final pass was drawn since the passes were built. */ + bool drawn; + /* See vulkan_filter_chain_create_info. */ + bool replay; bool alias_initialized; bool emits_hdr_colorspace; bool emits_hdr16_output; @@ -1620,6 +1628,7 @@ static struct vulkan_filter_chain *slang_chain_new( chain->max_input_size_dims = info->max_input_dims; chain->deferred_source_dims = chain->max_input_size_dims; chain->common.frame_rect = info->frame_rect; + chain->replay = info->replay; if ( !slang_chain_set_swapchain_info(chain, info->swapchain) || !slang_chain_set_num_passes(chain, info->num_passes)) { @@ -1813,7 +1822,7 @@ static void slang_chain_update_history_info(struct vulkan_filter_chain *chain) unsigned i; unsigned hist_size = (unsigned)chain->num_history; - for (i = 0; i < hist_size; i++) + for (i = 0; i < chain->common.num_original_history; i++) { /* Map logical index i (0 = most recent) to the ring buffer slot. */ unsigned ring_slot = (chain->history_ring_index + i) % hist_size; @@ -2036,8 +2045,46 @@ static void slang_chain_build_viewport_pass(struct vulkan_filter_chain *chain, /* For feedback FBOs, swap current and previous. */ if (!again) + { for (i = 0; i < chain->pass_count; i++) slang_pass_end_frame(chain->passes[i]); + chain->drawn_frame_count = + chain->passes[chain->pass_count - 1]->frame_count; + chain->drawn = true; + } +} + +/* The last frame's final pass in a later frame, at vp: what it read + * then - the passes' outputs and feedback, the history it saw - and its + * count. Nothing else runs; feedback does not swap, history stays. */ +static void slang_chain_replay_viewport_pass( + struct vulkan_filter_chain *chain, VkCommandBuffer cmd, + const VkViewport vp, const float *mvp) +{ + Texture source; + Texture original; + uint64_t count; + struct slang_pass *pass = chain->passes[chain->pass_count - 1]; + struct deferred_disposes *disposer = &chain->deferred_calls[chain->current_sync_index]; + + original.texture = chain->input_texture; + original.filter = chain->passes[0]->pass_info.source_filter; + original.mip_filter = chain->passes[0]->pass_info.mip_filter; + original.address = chain->passes[0]->pass_info.address; + if (chain->pass_count == 1) + { + source = original; + source.filter = pass->pass_info.source_filter; + source.mip_filter = pass->pass_info.mip_filter; + source.address = pass->pass_info.address; + } + else + source = chain->common.pass_outputs[chain->pass_count - 2]; + count = pass->frame_count; + pass->frame_count = chain->drawn_frame_count; + slang_pass_build_commands(pass, disposer, cmd, &original, &source, + &vp, mvp, false); + pass->frame_count = count; } static bool slang_chain_init_history(struct vulkan_filter_chain *chain) @@ -2063,7 +2110,8 @@ static bool slang_chain_init_history(struct vulkan_filter_chain *chain) * swapchain. The num_history test is load-bearing, not redundant: * without it a fresh chain with required_images == 1 would take this * return and skip the common.original_history reset below. */ - if (chain->num_history && chain->num_history + 1 == required_images) + if ( chain->num_history + && chain->num_history + (chain->replay ? 0 : 1) == required_images) return true; for (i = 0; i < chain->num_history; i++) @@ -2086,12 +2134,15 @@ static bool slang_chain_init_history(struct vulkan_filter_chain *chain) /* We don't need to store array element #0, * since it's aliased with the actual original. */ required_images--; - if (!(chain->original_history = (struct slang_framebuffer**) - calloc(required_images, sizeof(*chain->original_history)))) - return false; if (!texture_array_resize(&chain->common.original_history, &chain->common.num_original_history, required_images)) return false; + /* And one more to replay: the last frame's history stays. */ + if (chain->replay) + required_images++; + if (!(chain->original_history = (struct slang_framebuffer**) + calloc(required_images, sizeof(*chain->original_history)))) + return false; for (i = 0; i < required_images; i++) { @@ -2106,7 +2157,8 @@ static bool slang_chain_init_history(struct vulkan_filter_chain *chain) } #ifdef VULKAN_DEBUG - RARCH_LOG("[Vulkan] Using history of %u frames.\n", (unsigned)(required_images)); + RARCH_LOG("[Vulkan] Using history of %u frames.\n", + (unsigned)chain->common.num_original_history); #endif /* On first frame, we need to clear the textures to @@ -2436,6 +2488,8 @@ static bool slang_chain_init(struct vulkan_filter_chain *chain) RARCH_DBG("[Vulkan] Chain feedback ready.\n"); texture_array_resize(&chain->common.pass_outputs, &chain->common.num_pass_outputs, chain->pass_count); + /* A rebuild remakes what a replay reads. */ + chain->drawn = false; slang_chain_init_frame_in_place(chain); return true; } @@ -2776,6 +2830,8 @@ static bool slang_chain_finalize(struct vulkan_filter_chain *chain) return false; texture_array_resize(&chain->common.pass_outputs, &chain->common.num_pass_outputs, chain->pass_count); + /* A rebuild remakes what a replay reads. */ + chain->drawn = false; slang_chain_init_frame_in_place(chain); return true; } @@ -5756,6 +5812,18 @@ void vulkan_filter_chain_build_viewport_pass_again( slang_chain_build_viewport_pass(chain, cmd, *vp, mvp, true); } +bool vulkan_filter_chain_replay_ready(const vulkan_filter_chain_t *chain) +{ + return chain->drawn; +} + +void vulkan_filter_chain_build_viewport_pass_replay( + vulkan_filter_chain_t *chain, + VkCommandBuffer cmd, const VkViewport *vp, const float *mvp) +{ + slang_chain_replay_viewport_pass(chain, cmd, *vp, mvp); +} + void vulkan_filter_chain_end_frame( vulkan_filter_chain_t *chain, VkCommandBuffer cmd) diff --git a/gfx/drivers_shader/shader_vulkan.h b/gfx/drivers_shader/shader_vulkan.h index 8d2412c947..93c7b17ea9 100644 --- a/gfx/drivers_shader/shader_vulkan.h +++ b/gfx/drivers_shader/shader_vulkan.h @@ -112,6 +112,10 @@ struct vulkan_filter_chain_create_info /* Frames may come inside a larger image (padded_dims, origin): the * passes are built to read them there where the preset allows. */ bool frame_rect; + /* The chain may draw its last frame's final pass again in a later + * frame (build_viewport_pass_replay): its history keeps one more + * image for that. */ + bool replay; #ifdef VULKAN_HDR_SWAPCHAIN bool hdr_enabled; #endif /* VULKAN_HDR_SWAPCHAIN */ @@ -232,6 +236,16 @@ void vulkan_filter_chain_build_viewport_pass(vulkan_filter_chain_t *chain, void vulkan_filter_chain_build_viewport_pass_again( vulkan_filter_chain_t *chain, VkCommandBuffer cmd, const VkViewport *vp, const float *mvp); +/* Whether a frame's final pass was drawn since the passes were built, + * for build_viewport_pass_replay to draw again. */ +bool vulkan_filter_chain_replay_ready(const vulkan_filter_chain_t *chain); +/* The last frame's final pass, in a later frame and at vp, from what it + * read then, in place of the offscreen and viewport passes: no other + * pass runs, the feedback buffers do not swap and the history stays. + * For a frame shown again, resized or redrawn. */ +void vulkan_filter_chain_build_viewport_pass_replay( + vulkan_filter_chain_t *chain, + VkCommandBuffer cmd, const VkViewport *vp, const float *mvp); void vulkan_filter_chain_end_frame(vulkan_filter_chain_t *chain, VkCommandBuffer cmd); diff --git a/gfx/video_driver.c b/gfx/video_driver.c index b8537df5d7..23e49a52f2 100644 --- a/gfx/video_driver.c +++ b/gfx/video_driver.c @@ -4165,6 +4165,12 @@ unsigned video_driver_headset_view_toggle(void) return VIDEO_HEADSET_VIEW_LAST; } +void video_driver_headset_edit(bool editing, unsigned seq) +{ + video_driver_st.headset_editing = editing; + video_driver_st.headset_edit_seq = seq; +} + void video_driver_headset_exit_request(void) { retro_atomic_store_release_int(&video_driver_st.headset_exit, 1); @@ -6839,6 +6845,39 @@ void video_driver_ff_frameskip_decide(retro_time_t now) video_st->ff_last_time = now; } +static void video_driver_headset_screens(const settings_t *settings, + video_screen_xr_t *screens) +{ + unsigned i; + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + video_screen_xr_t *screen = &screens[i]; + screen->x = settings->floats.video_screen_xr_x[i]; + screen->y = settings->floats.video_screen_xr_y[i]; + screen->z = settings->floats.video_screen_xr_z[i]; + screen->yaw = settings->floats.video_screen_xr_yaw[i]; + screen->pitch = settings->floats.video_screen_xr_pitch[i]; + screen->roll = settings->floats.video_screen_xr_roll[i]; + screen->scale = settings->floats.video_screen_xr_scale[i]; + screen->visible = settings->bools.video_screen_xr_visible[i]; + video_views_xr_normalize(screen); + } +} + +static void video_driver_headset_menu(const settings_t *settings, + video_screen_xr_t *menu) +{ + menu->x = settings->floats.video_openxr_menu_xr_x; + menu->y = settings->floats.video_openxr_menu_xr_y; + menu->z = settings->floats.video_openxr_menu_xr_z; + menu->yaw = settings->floats.video_openxr_menu_xr_yaw; + menu->pitch = settings->floats.video_openxr_menu_xr_pitch; + menu->roll = settings->floats.video_openxr_menu_xr_roll; + menu->scale = settings->floats.video_openxr_menu_xr_scale; + menu->visible = true; + video_views_xr_normalize(menu); +} + void video_driver_build_info(video_frame_info_t *video_info) { video_viewport_settings_t *custom_vp = NULL; @@ -6895,10 +6934,16 @@ void video_driver_build_info(video_frame_info_t *video_info) video_info->bfi_dark_frames = settings->uints.video_bfi_dark_frames; video_info->shader_subframes = video_st->headset_interval ? 1 : settings->uints.video_shader_subframes; + video_driver_headset_screens(settings, video_info->headset_screens); + video_driver_headset_menu(settings, &video_info->headset_menu); + video_info->headset_menu_placed = settings->bools.video_openxr_menu_placed; + video_info->headset_menu_follow = settings->bools.video_openxr_menu_follow; video_info->headset_distance = settings->floats.video_openxr_distance; video_info->headset_width = settings->floats.video_openxr_width; video_info->headset_placement = settings->uints.video_openxr_placement; video_info->headset_recenter = video_st->headset_recenter; + video_info->headset_edit_seq = video_st->headset_edit_seq; + video_info->headset_editing = video_st->headset_editing; video_info->headset_interval = video_st->headset_interval; video_info->headset_request_hz = video_st->headset_request_hz; video_info->screen_layout = settings->uints.video_screen_layout; diff --git a/gfx/video_driver.h b/gfx/video_driver.h index 36d84736ab..4bd9e1846a 100644 --- a/gfx/video_driver.h +++ b/gfx/video_driver.h @@ -786,6 +786,10 @@ typedef struct video_frame_info * none, or no views are presented. Owned by video_driver_state_t, * or under the threaded wrapper by the frame's slot. */ const video_windows_frame_t *windows; + /* Each screen's placement from its menu rows, normalized. */ + video_screen_xr_t headset_screens[RETRO_VIDEO_VIEWS_MAX]; + /* The menu's own place, used once headset_menu_placed. */ + video_screen_xr_t headset_menu; /* Headset output: the screens' distance and width in metres, where * they are kept (enum video_openxr_placement), and a count of * recenter requests the driver compares with the last it saw. */ @@ -793,6 +797,9 @@ typedef struct video_frame_info float headset_width; unsigned headset_placement; unsigned headset_recenter; + /* Edit mode's count of let-go grabs: a quad set planned with it + * shows them where the hands left them. */ + unsigned headset_edit_seq; /* Each core frame shows for this many headset frames and waits for * them in place of the window's vsync; 0 while the window paces. */ unsigned headset_interval; @@ -800,8 +807,13 @@ typedef struct video_frame_info float headset_request_hz; unsigned screen_layout; bool stereo_swap_eyes; + bool headset_menu_placed; + /* An unplaced menu opens in front of screen 1, not at its spot. */ + bool headset_menu_follow; /* The last frame sent again (paused, menu), not a new core frame. */ bool frame_repeat; + /* Edit mode moves the headset's items: the menu's quad stays up. */ + bool headset_editing; /* The tracked eyes' views, which the headset shows as its * projection; -1 while it shows none. */ int headset_eye[2]; @@ -2070,6 +2082,8 @@ typedef struct /* enum video_headset_view: the Headset View hotkey's choice, reset * when content closes. */ unsigned headset_view; + /* Edit mode's let-go grabs, counted, main thread. */ + unsigned headset_edit_seq; /* The headset's runtime asked RetroArch to exit (OpenXR EXITING). */ retro_atomic_int_t headset_exit; /* Headset pacing, main thread, never saved: the headset's measured @@ -2088,6 +2102,7 @@ typedef struct double headset_notice_fps; unsigned headset_notice_hz; bool headset_vsync; + bool headset_editing; /* Set while video_driver_cached_frame() sends the last frame again. */ bool frame_repeat; #ifdef HAVE_VIDEO_WINDOWS @@ -2508,6 +2523,9 @@ void video_driver_headset_recenter(void); * video_headset_view and return it; VIDEO_HEADSET_VIEW_LAST, unchanged, * without a presenting headset. */ unsigned video_driver_headset_view_toggle(void); +/* Main thread: the headset's edit mode, on or off, and its count of + * let-go grabs, for the next frame snapshot. */ +void video_driver_headset_edit(bool editing, unsigned seq); /* Any thread: the headset's runtime asked RetroArch to exit. */ void video_driver_headset_exit_request(void); diff --git a/gfx/video_views.c b/gfx/video_views.c index d1fe98da25..4ae0a11c63 100644 --- a/gfx/video_views.c +++ b/gfx/video_views.c @@ -15,11 +15,52 @@ #include #include +#include #include #include "video_views.h" #include "video_defines.h" +void video_views_xr_reset(video_screen_xr_t *screens, unsigned count) +{ + unsigned i; + if (!screens) + return; + if (count > RETRO_VIDEO_VIEWS_MAX) + count = RETRO_VIDEO_VIEWS_MAX; + for (i = 0; i < count; i++) + { + memset(&screens[i], 0, sizeof(screens[i])); + screens[i].scale = 1.0f; + screens[i].visible = true; + } +} + +static float video_views_xr_value(float value, float low, float high, + float fallback) +{ + if (!(value >= -FLT_MAX && value <= FLT_MAX)) + return fallback; + if (value < low) + return low; + if (value > high) + return high; + return value; +} + +void video_views_xr_normalize(video_screen_xr_t *screen) +{ + if (!screen) + return; + screen->x = video_views_xr_value(screen->x, -10.0f, 10.0f, 0.0f); + screen->y = video_views_xr_value(screen->y, -10.0f, 10.0f, 0.0f); + screen->z = video_views_xr_value(screen->z, -10.0f, 10.0f, 0.0f); + screen->yaw = video_views_xr_value(screen->yaw, -180.0f, 180.0f, 0.0f); + screen->pitch = video_views_xr_value(screen->pitch, -180.0f, 180.0f, 0.0f); + screen->roll = video_views_xr_value(screen->roll, -180.0f, 180.0f, 0.0f); + screen->scale = video_views_xr_value(screen->scale, 0.1f, 10.0f, 1.0f); +} + bool video_views_validate(const struct retro_video_view *views, unsigned num_views, video_views_map_t *out) { diff --git a/gfx/video_views.h b/gfx/video_views.h index 3052d46477..1b1c2e0b61 100644 --- a/gfx/video_views.h +++ b/gfx/video_views.h @@ -45,7 +45,8 @@ enum video_screen_layout VIDEO_SCREEN_LAYOUT_LAST }; -/* Headset input settings (video_openxr_controllers, video_openxr_laser). */ +/* Headset input settings (video_openxr_controllers, video_openxr_laser, + * video_openxr_laser_beam). */ enum video_openxr_controllers { VIDEO_OPENXR_CONTROLLERS_COMBINED = 0, @@ -73,6 +74,50 @@ enum video_headset_view VIDEO_HEADSET_VIEW_LAST }; +enum video_openxr_laser_beam +{ + VIDEO_OPENXR_LASER_BEAM_OFF = 0, + VIDEO_OPENXR_LASER_BEAM_EDIT, + VIDEO_OPENXR_LASER_BEAM_ALWAYS, + VIDEO_OPENXR_LASER_BEAM_LAST +}; + +/* Snap to Angles: the step edit mode turns and tilts a held item in + * (video_openxr_edit_snap_angles). */ +enum video_openxr_edit_snap_angles +{ + VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF = 0, + VIDEO_OPENXR_EDIT_SNAP_ANGLES_15, + VIDEO_OPENXR_EDIT_SNAP_ANGLES_30, + VIDEO_OPENXR_EDIT_SNAP_ANGLES_45, + VIDEO_OPENXR_EDIT_SNAP_ANGLES_90, + VIDEO_OPENXR_EDIT_SNAP_ANGLES_LAST +}; + +/* Hinge Gap: the gap edit mode leaves between screens whose edges + * meet (video_openxr_edit_hinge_gap). */ +enum video_openxr_edit_hinge_gap +{ + VIDEO_OPENXR_EDIT_HINGE_GAP_0 = 0, + VIDEO_OPENXR_EDIT_HINGE_GAP_1, + VIDEO_OPENXR_EDIT_HINGE_GAP_2, + VIDEO_OPENXR_EDIT_HINGE_GAP_3, + VIDEO_OPENXR_EDIT_HINGE_GAP_5, + VIDEO_OPENXR_EDIT_HINGE_GAP_10, + VIDEO_OPENXR_EDIT_HINGE_GAP_LAST +}; + +/* Edit Smoothing: how much edit mode steadies the controllers' aim + * (video_openxr_edit_smoothing). */ +enum video_openxr_edit_smoothing +{ + VIDEO_OPENXR_EDIT_SMOOTHING_OFF = 0, + VIDEO_OPENXR_EDIT_SMOOTHING_LOW, + VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, + VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, + VIDEO_OPENXR_EDIT_SMOOTHING_LAST +}; + /* video_openxr_placement: where the headset keeps the screens. */ enum video_openxr_placement { @@ -108,6 +153,21 @@ typedef struct video_views_map unsigned num_screens; } video_views_map_t; +typedef struct video_screen_xr +{ + float x; + float y; + float z; + float yaw; + float pitch; + float roll; + float scale; + bool visible; +} video_screen_xr_t; + +void video_views_xr_reset(video_screen_xr_t *screens, unsigned count); +void video_views_xr_normalize(video_screen_xr_t *screen); + bool video_views_validate(const struct retro_video_view *views, unsigned num_views, video_views_map_t *out); diff --git a/gfx/video_xr.c b/gfx/video_xr.c index 5f7d39f6ea..e44f8ba930 100644 --- a/gfx/video_xr.c +++ b/gfx/video_xr.c @@ -87,6 +87,21 @@ void video_xr_compose(const video_xr_pose_t *a, const video_xr_pose_t *b, out->orientation = q; } +void video_xr_pose_invert(const video_xr_pose_t *a, video_xr_pose_t *out) +{ + video_xr_quat_t q; + video_xr_vec3_t p; + q.x = -a->orientation.x; + q.y = -a->orientation.y; + q.z = -a->orientation.z; + q.w = a->orientation.w; + p.x = -a->position.x; + p.y = -a->position.y; + p.z = -a->position.z; + video_xr_rotate(&q, &p, &out->position); + out->orientation = q; +} + bool video_xr_lazy_follow(video_xr_pose_t *anchor, const video_xr_pose_t *target, double dt, bool gliding) { @@ -169,20 +184,123 @@ bool video_xr_shrinks(unsigned image_dims, unsigned source_dims, return VIDEO_SCALE_W(image_dims) < sw || VIDEO_SCALE_H(image_dims) < sh; } -/* A point in the anchor's frame, as a pose in LOCAL space facing the - * way the anchor does. */ -static void video_xr_put(const video_xr_params_t *p, - double x, double y, double z, video_xr_pose_t *out) +unsigned video_xr_keep_dims(unsigned planned, unsigned current, + bool editing) { - video_xr_vec3_t local, world; - local.x = (float)x; - local.y = (float)y; - local.z = (float)z; - video_xr_rotate(&p->anchor.orientation, &local, &world); - out->orientation = p->anchor.orientation; - out->position.x = p->anchor.position.x + world.x; - out->position.y = p->anchor.position.y + world.y; - out->position.z = p->anchor.position.z + world.z; + unsigned pw = VIDEO_SCALE_W(planned); + unsigned ph = VIDEO_SCALE_H(planned); + unsigned cw = VIDEO_SCALE_W(current); + unsigned ch = VIDEO_SCALE_H(current); + if ( editing + && 5 * pw >= 4 * cw && 4 * pw <= 5 * cw + && 5 * ph >= 4 * ch && 4 * ph <= 5 * ch) + return current; + return planned; +} + +static void video_xr_screen_rotate(const video_screen_xr_t *screen, + video_xr_pose_t *pose) +{ + video_xr_pose_t local; + double yaw = screen->yaw * VIDEO_XR_PI / 360.0; + double pitch = screen->pitch * VIDEO_XR_PI / 360.0; + double roll = screen->roll * VIDEO_XR_PI / 360.0; + double cy = cos(yaw); + double sy = sin(yaw); + double cp = cos(pitch); + double sp = sin(pitch); + double cr = cos(roll); + double sr = sin(roll); + + video_xr_pose_identity(&local); + local.orientation.x = (float)(cy * sp * cr + sy * cp * sr); + local.orientation.y = (float)(sy * cp * cr - cy * sp * sr); + local.orientation.z = (float)(cy * cp * sr - sy * sp * cr); + local.orientation.w = (float)(cy * cp * cr + sy * sp * sr); + video_xr_compose(pose, &local, pose); +} + +/* Within this of straight up or down a turn has no roll of its own. */ +#define VIDEO_XR_POLE 0.99999 + +/* The sine of q's pitch. */ +static double video_xr_pitch_sin(const video_xr_quat_t *q) +{ + double sp = -2.0 * ((double)q->y * q->z - (double)q->w * q->x); + if (sp > 1.0) + return 1.0; + if (sp < -1.0) + return -1.0; + return sp; +} + +void video_xr_euler(const video_xr_quat_t *q, float *yaw, float *pitch, + float *roll) +{ + double x = q->x; + double y = q->y; + double z = q->z; + double w = q->w; + double sp = video_xr_pitch_sin(q); + *pitch = (float)(asin(sp) * 180.0 / VIDEO_XR_PI); + if (fabs(sp) > VIDEO_XR_POLE) + { + *yaw = (float)(atan2(-2.0 * (x * z - w * y), + 1.0 - 2.0 * (y * y + z * z)) * 180.0 / VIDEO_XR_PI); + *roll = 0.0f; + return; + } + *yaw = (float)(atan2(2.0 * (x * z + w * y), + 1.0 - 2.0 * (x * x + y * y)) * 180.0 / VIDEO_XR_PI); + *roll = (float)(atan2(2.0 * (x * y + w * z), + 1.0 - 2.0 * (x * x + z * z)) * 180.0 / VIDEO_XR_PI); +} + +void video_xr_item_pose(const video_xr_vec3_t *base, + const video_screen_xr_t *screen, video_xr_pose_t *out) +{ + video_xr_pose_identity(out); + out->position = *base; + if (!screen) + return; + out->position.x += screen->x; + out->position.y += screen->y; + out->position.z += screen->z; + if (screen->yaw != 0.0f || screen->pitch != 0.0f || screen->roll != 0.0f) + video_xr_screen_rotate(screen, out); +} + +void video_xr_item_settings(const video_xr_vec3_t *base, + const video_xr_pose_t *pose, video_screen_xr_t *out) +{ + out->x = pose->position.x - base->x; + out->y = pose->position.y - base->y; + out->z = pose->position.z - base->z; + video_xr_euler(&pose->orientation, &out->yaw, &out->pitch, &out->roll); +} + +/* An item from its layout spot, adjusted by screen (NULL: as laid out): + * its pose from p's anchor, its size scaled, and the distance its image + * is sized for, which only an offset changes. */ +static void video_xr_adjust(const video_xr_params_t *p, + const video_xr_vec3_t *base, const video_screen_xr_t *screen, + video_xr_pose_t *pose, double *w, double *h, float *distance) +{ + video_xr_pose_t local; + video_xr_item_pose(base, screen, &local); + if (screen) + { + *w *= screen->scale; + *h *= screen->scale; + if (screen->x != 0.0f || screen->y != 0.0f || screen->z != 0.0f) + { + double d = sqrt((double)local.position.x * local.position.x + + (double)local.position.y * local.position.y + + (double)local.position.z * local.position.z); + *distance = (float)(d < 0.05 ? 0.05 : d); + } + } + video_xr_compose(&p->anchor, &local, pose); } static void video_xr_add(video_xr_quad_set_t *out, unsigned kind, @@ -207,18 +325,15 @@ static void video_xr_add(video_xr_quad_set_t *out, unsigned kind, * smoothly; a preset's passes get at least the source's pixels. Without * a density, the source's size either way. */ static unsigned video_xr_screen_dims(const video_xr_params_t *p, - float width, float height, unsigned native_dims) + float width, float height, float distance, unsigned native_dims) { if (p->stock && p->px_per_rad > 0.0f) native_dims = 0; - return video_xr_image_dims(width, height, p->distance, p->px_per_rad, + return video_xr_image_dims(width, height, distance, p->px_per_rad, native_dims, p->max_dim); } -/* Screens sized by display width against screen 0's; screen 0 straight - * ahead, the rest below it or to its right. */ -static void video_xr_place_screens(const video_xr_params_t *p, - video_xr_quad_set_t *out) +void video_xr_layout(const video_xr_params_t *p, video_xr_layout_t *out) { unsigned s; double k; @@ -230,6 +345,24 @@ static void video_xr_place_screens(const video_xr_params_t *p, double gap = VIDEO_XR_GAP * p->width; double edge = 0.0; + memset(out, 0, sizeof(*out)); + out->menu.z = -(p->distance - VIDEO_XR_MENU_LIFT); + if (!map || !map->num_views) + { + float aspect = p->frame_aspect; + if (aspect <= 0.0f && VIDEO_SCALE_H(p->frame_dims)) + aspect = (float)VIDEO_SCALE_W(p->frame_dims) + / (float)VIDEO_SCALE_H(p->frame_dims); + if (aspect <= 0.0f) + aspect = 4.0f / 3.0f; + out->pos[0].z = -p->distance; + out->width[0] = p->width; + out->height[0] = p->width / aspect; + out->native[0] = p->frame_dims; + out->count = 1; + return; + } + for (s = 0; s < map->num_screens; s++) { const struct retro_video_view *v = &map->views[ @@ -248,17 +381,10 @@ static void video_xr_place_screens(const video_xr_params_t *p, for (s = 0; s < map->num_screens; s++) { - video_xr_pose_t pose; - int left = video_views_find(map, s, - RETRO_VIDEO_VIEW_EYE_LEFT); - int right = video_views_find(map, s, - RETRO_VIDEO_VIEW_EYE_RIGHT); - double w = disp_w[s] * k; - double h = disp_h[s] * k; - double x = 0.0; - double y = 0.0; - video_xr_slot_t *slot = &out->slots[s]; - + double w = disp_w[s] * k; + double h = disp_h[s] * k; + double x = 0.0; + double y = 0.0; if (s == 0) edge = horizontal ? w / 2.0 : -h / 2.0; else if (horizontal) @@ -271,13 +397,52 @@ static void video_xr_place_screens(const video_xr_params_t *p, y = edge - gap - h / 2.0; edge = y - h / 2.0; } + out->pos[s].x = (float)x; + out->pos[s].y = (float)y; + out->pos[s].z = -p->distance; + out->width[s] = (float)w; + out->height[s] = (float)h; + out->native[s] = VIDEO_SCALE_PACK((unsigned)(disp_w[s] + 0.5), + (unsigned)(disp_h[s] + 0.5)); + } + out->count = map->num_screens; +} + +/* Screens from their layout spots, each moved by its adjustments. */ +static void video_xr_place_screens(const video_xr_params_t *p, + const video_xr_layout_t *lay, video_xr_quad_set_t *out) +{ + unsigned s; + const video_views_map_t *map = p->map; + + for (s = 0; s < lay->count; s++) + { + video_xr_pose_t pose; + video_screen_xr_t screen; + float distance = p->distance; + int left = video_views_find(map, s, + RETRO_VIDEO_VIEW_EYE_LEFT); + int right = video_views_find(map, s, + RETRO_VIDEO_VIEW_EYE_RIGHT); + double w = lay->width[s]; + double h = lay->height[s]; + video_xr_slot_t *slot = &out->slots[s]; + + slot->base = lay->pos[s]; if (p->skip_screens & (1u << s)) continue; - video_xr_put(p, x, y, -p->distance, &pose); + if (p->screens) + { + screen = p->screens[s]; + video_views_xr_normalize(&screen); + if (!screen.visible) + continue; + } + video_xr_adjust(p, &lay->pos[s], p->screens ? &screen : NULL, + &pose, &w, &h, &distance); - slot->dims = video_xr_screen_dims(p, (float)w, (float)h, - VIDEO_SCALE_PACK((unsigned)(disp_w[s] + 0.5), - (unsigned)(disp_h[s] + 0.5))); + slot->dims = video_xr_screen_dims(p, (float)w, (float)h, distance, + lay->native[s]); slot->view[0] = left; if (left != right) { @@ -319,63 +484,172 @@ static unsigned video_xr_menu_dims(const video_xr_params_t *p, VIDEO_SCALE_PACK(w, h), p->max_dim); } +/* At its own spot; or with menu_follow VIDEO_XR_MENU_LIFT in front of + * screen 0, facing as it does, while screen 0 floats and shows. Following, it + * comes no nearer than VIDEO_XR_MENU_NEAREST, and w and h scale with + * its distance so it looks as large as at its spot; facing away from + * the anchor, where no laser reaches it, it stays at its spot. */ +static void video_xr_menu_spot(const video_xr_params_t *p, + const video_xr_layout_t *lay, video_xr_pose_t *pose, + double *w, double *h) +{ + video_xr_pose_t local, lift; + video_screen_xr_t screen; + bool follow = p->menu_follow && lay->count && !(p->skip_screens & 1u); + if (follow && p->screens) + { + screen = p->screens[0]; + video_views_xr_normalize(&screen); + follow = screen.visible; + } + if (!follow) + { + video_xr_item_pose(&lay->menu, NULL, &local); + video_xr_compose(&p->anchor, &local, pose); + return; + } + video_xr_item_pose(&lay->pos[0], p->screens ? &screen : NULL, &local); + video_xr_pose_identity(&lift); + lift.position.z = VIDEO_XR_MENU_LIFT; + video_xr_compose(&local, &lift, &local); + if (p->screens && (screen.x != 0.0f || screen.y != 0.0f + || screen.z != 0.0f || screen.yaw != 0.0f + || screen.pitch != 0.0f || screen.roll != 0.0f)) + { + video_xr_vec3_t face; + double spot = p->distance - VIDEO_XR_MENU_LIFT; + double d = sqrt((double)local.position.x * local.position.x + + (double)local.position.y * local.position.y + + (double)local.position.z * local.position.z); + if (d < VIDEO_XR_MENU_NEAREST) + { + /* Out along the line from the anchor; at it, straight ahead. */ + if (d < 1e-6) + { + local.position.x = 0.0f; + local.position.y = 0.0f; + local.position.z = -1.0f; + d = 1.0; + } + local.position.x = (float)(local.position.x + * VIDEO_XR_MENU_NEAREST / d); + local.position.y = (float)(local.position.y + * VIDEO_XR_MENU_NEAREST / d); + local.position.z = (float)(local.position.z + * VIDEO_XR_MENU_NEAREST / d); + d = VIDEO_XR_MENU_NEAREST; + } + face.x = 0.0f; + face.y = 0.0f; + face.z = 1.0f; + video_xr_rotate(&local.orientation, &face, &face); + if ( (double)face.x * local.position.x + + (double)face.y * local.position.y + + (double)face.z * local.position.z >= 0.0) + { + video_xr_item_pose(&lay->menu, NULL, &local); + video_xr_compose(&p->anchor, &local, pose); + return; + } + if (spot > 0.0) + { + *w *= d / spot; + *h *= d / spot; + } + } + video_xr_compose(&p->anchor, &local, pose); +} + void video_xr_place(const video_xr_params_t *p, video_xr_quad_set_t *out) { unsigned s; video_xr_pose_t pose; + video_xr_layout_t lay; memset(out, 0, sizeof(*out)); for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) { - out->slots[s].view[0] = -1; - out->slots[s].view[1] = -1; + out->slots[s].view[0] = -1; + out->slots[s].view[1] = -1; + out->slots[s].edit_seq = p->edit_seq; } if (p->width <= 0.0f || p->distance <= 0.0f) return; + video_xr_layout(p, &lay); if (p->map && p->map->num_views) - video_xr_place_screens(p, out); + video_xr_place_screens(p, &lay, out); else { - float h; - float aspect = p->frame_aspect; - if (aspect <= 0.0f && VIDEO_SCALE_H(p->frame_dims)) - aspect = (float)VIDEO_SCALE_W(p->frame_dims) - / (float)VIDEO_SCALE_H(p->frame_dims); - if (aspect <= 0.0f) - aspect = 4.0f / 3.0f; - h = p->width / aspect; - video_xr_put(p, 0.0, 0.0, -p->distance, &pose); - out->slots[0].dims = video_xr_screen_dims(p, p->width, h, - p->frame_dims); - out->slots[0].layers = 1; - video_xr_add(out, p->window ? VIDEO_XR_QUAD_WINDOW - : VIDEO_XR_QUAD_FRAME, VIDEO_XR_EYE_BOTH, 0, 0, 0, - &pose, p->width, h); + video_screen_xr_t screen; + double w = lay.width[0]; + double h = lay.height[0]; + float distance = p->distance; + out->slots[0].base = lay.pos[0]; + /* A whole frame is screen 0; without the screens' settings, slot 0 + * is no item. */ + if (p->screens) + { + screen = p->screens[0]; + video_views_xr_normalize(&screen); + } + else + out->slots[0].fixed = true; + if (!p->screens || screen.visible) + { + video_xr_adjust(p, &lay.pos[0], p->screens ? &screen : NULL, + &pose, &w, &h, &distance); + out->slots[0].dims = video_xr_screen_dims(p, (float)w, (float)h, + distance, p->frame_dims); + out->slots[0].layers = 1; + video_xr_add(out, p->window ? VIDEO_XR_QUAD_WINDOW + : VIDEO_XR_QUAD_FRAME, VIDEO_XR_EYE_BOTH, 0, 0, 0, + &pose, (float)w, (float)h); + } } if (VIDEO_SCALE_W(p->ui_dims) && VIDEO_SCALE_H(p->ui_dims)) { + unsigned dims; + double w, h; float aspect = (float)VIDEO_SCALE_W(p->ui_dims) / (float)VIDEO_SCALE_H(p->ui_dims); - float d = p->distance - VIDEO_XR_MENU_LIFT; - float w = p->width; - float h = w / aspect; - if (h > p->width * VIDEO_XR_MENU_MAX_H) + float mw = p->width; + float mh = mw / aspect; + if (mh > p->width * VIDEO_XR_MENU_MAX_H) { - h = p->width * VIDEO_XR_MENU_MAX_H; - w = h * aspect; + mh = p->width * VIDEO_XR_MENU_MAX_H; + mw = mh * aspect; } - video_xr_put(p, 0.0, 0.0, -d, &pose); - out->slots[VIDEO_XR_MENU_SLOT].dims = video_xr_menu_dims(p, w, h, d); + w = mw; + h = mh; + if (p->menu) + { + video_screen_xr_t menu = *p->menu; + float d = p->distance - VIDEO_XR_MENU_LIFT; + video_views_xr_normalize(&menu); + video_xr_adjust(p, &lay.menu, &menu, &pose, &w, &h, &d); + dims = video_xr_menu_dims(p, (float)w, (float)h, d); + } + else + { + /* Its angular size, so its image, is the one at its spot. */ + dims = video_xr_menu_dims(p, mw, mh, + p->distance - VIDEO_XR_MENU_LIFT); + video_xr_menu_spot(p, &lay, &pose, &w, &h); + } + out->slots[VIDEO_XR_MENU_SLOT].base = lay.menu; + out->slots[VIDEO_XR_MENU_SLOT].dims = dims; out->slots[VIDEO_XR_MENU_SLOT].layers = 1; video_xr_add(out, VIDEO_XR_QUAD_MENU, VIDEO_XR_EYE_BOTH, 0, - VIDEO_XR_MENU_SLOT, 0, &pose, w, h); + VIDEO_XR_MENU_SLOT, 0, &pose, (float)w, (float)h); } } -bool video_xr_ray_hit(const video_xr_quad_t *q, const video_xr_vec3_t *origin, - const video_xr_vec3_t *dir, float *u, float *v, float *t) +/* video_xr_ray_hit, or with both either face. */ +static bool video_xr_ray_hit_sides(const video_xr_quad_t *q, + const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, bool both, + float *u, float *v, float *t) { double denom, dist, hu, hv; video_xr_quat_t inv; @@ -386,8 +660,7 @@ bool video_xr_ray_hit(const video_xr_quad_t *q, const video_xr_vec3_t *origin, z.z = 1.0f; video_xr_rotate(&q->pose.orientation, &z, &n); denom = n.x * dir->x + n.y * dir->y + n.z * dir->z; - /* Only the front face. */ - if (denom > -1e-6) + if (both ? fabs(denom) < 1e-6 : denom > -1e-6) return false; dist = (n.x * (q->pose.position.x - origin->x) + n.y * (q->pose.position.y - origin->y) @@ -414,6 +687,35 @@ bool video_xr_ray_hit(const video_xr_quad_t *q, const video_xr_vec3_t *origin, return true; } +bool video_xr_ray_hit(const video_xr_quad_t *q, const video_xr_vec3_t *origin, + const video_xr_vec3_t *dir, float *u, float *v, float *t) +{ + return video_xr_ray_hit_sides(q, origin, dir, false, u, v, t); +} + +bool video_xr_quad_behind(const video_xr_quad_t *q, + const video_xr_vec3_t *eye) +{ + video_xr_vec3_t z, n; + z.x = 0.0f; + z.y = 0.0f; + z.z = 1.0f; + video_xr_rotate(&q->pose.orientation, &z, &n); + return n.x * (eye->x - q->pose.position.x) + + n.y * (eye->y - q->pose.position.y) + + n.z * (eye->z - q->pose.position.z) < 0.0f; +} + +void video_xr_turn_back(video_xr_pose_t *pose) +{ + /* orientation times a half turn about Y, (0, 1, 0, 0). */ + video_xr_quat_t o = pose->orientation; + pose->orientation.x = -o.z; + pose->orientation.y = o.w; + pose->orientation.z = o.x; + pose->orientation.w = -o.y; +} + static int16_t video_xr_norm(unsigned p, unsigned size) { if (p == 0 || size < 2) @@ -533,7 +835,10 @@ void video_xr_place_window(const video_xr_params_t *p, bool menu, /* The menu is drawn over the frame whole, and a tracked map's eyes * are the projection's: the window as it is. */ if (menu || tracked) - q.map = NULL; + { + q.map = NULL; + q.screens = NULL; + } video_xr_place(&q, out); for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) { @@ -555,6 +860,8 @@ void video_xr_place_window(const video_xr_params_t *p, bool menu, bool video_xr_quad_live(const video_xr_quad_t *q, unsigned laser, bool menu_open) { + if (laser == VIDEO_XR_LIVE_EDIT) + return true; if (laser == VIDEO_OPENXR_LASER_OFF) return false; switch (q->kind) @@ -575,21 +882,87 @@ bool video_xr_quad_live(const video_xr_quad_t *q, unsigned laser, return laser == VIDEO_OPENXR_LASER_ALWAYS; } -int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, - bool menu_open, const video_xr_vec3_t *origin, - const video_xr_vec3_t *dir, float *u, float *v, float *t) +/* How far below the menu's middle its handle's top edge is. */ +static float video_xr_handle_top(const video_xr_quad_t *menu) +{ + video_xr_quad_t outline; + video_xr_outline(menu, &outline); + return outline.height * 0.5f + VIDEO_XR_HANDLE_GAP; +} + +void video_xr_menu_handle(const video_xr_quad_t *menu, video_xr_quad_t *out) +{ + video_xr_vec3_t down, world; + *out = *menu; + out->width = menu->width * VIDEO_XR_HANDLE_WIDTH; + out->height = VIDEO_XR_HANDLE_HEIGHT; + down.x = 0.0f; + down.y = -video_xr_handle_top(menu) - VIDEO_XR_HANDLE_HEIGHT * 0.5f; + down.z = 0.0f; + video_xr_rotate(&menu->pose.orientation, &down, &world); + out->pose.position.x += world.x; + out->pose.position.y += world.y; + out->pose.position.z += world.z; +} + +/* Where a ray meets the menu q's handle, or comes within the margin of + * it, as a point in q's own frame; *bar whether on the bar itself. */ +static bool video_xr_handle_hit(const video_xr_quad_t *q, + const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, bool both, + float *u, float *v, float *t, bool *bar) +{ + float hu, hv; + video_xr_quad_t handle; + if (q->width <= 0.0f || q->height <= 0.0f) + return false; + video_xr_menu_handle(q, &handle); + handle.width += 2.0f * VIDEO_XR_HANDLE_MARGIN; + handle.height += 2.0f * VIDEO_XR_HANDLE_MARGIN; + if (!video_xr_ray_hit_sides(&handle, origin, dir, both, &hu, &hv, t)) + return false; + *bar = fabs(hu - 0.5f) * handle.width + <= handle.width * 0.5f - VIDEO_XR_HANDLE_MARGIN + && fabs(hv - 0.5f) * handle.height + <= handle.height * 0.5f - VIDEO_XR_HANDLE_MARGIN; + *u = 0.5f + (hu - 0.5f) * handle.width / q->width; + *v = 0.5f + (video_xr_handle_top(q) - VIDEO_XR_HANDLE_MARGIN + + hv * handle.height) / q->height; + return true; +} + +static int video_xr_pick_with(const video_xr_quad_set_t *set, + unsigned laser, bool menu_open, bool stuck, + const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, + float *u, float *v, float *t, bool *stick) { unsigned i; - int best = -1; - float bu = 0.0f; - float bv = 0.0f; - float bt = 0.0f; + int best = -1; + int handle = -1; + bool bar = false; + bool both = laser == VIDEO_XR_LIVE_EDIT; + float bu = 0.0f; + float bv = 0.0f; + float bt = 0.0f; for (i = 0; i < set->num_quads; i++) { float qu, qv, qt; - if ( !video_xr_quad_live(&set->quads[i], laser, menu_open) - || !video_xr_ray_hit(&set->quads[i], origin, dir, - &qu, &qv, &qt)) + const video_xr_quad_t *q = &set->quads[i]; + if ( !video_xr_quad_live(q, laser, menu_open) + || (laser == VIDEO_XR_LIVE_EDIT && set->slots[q->slot].fixed)) + continue; + if (laser == VIDEO_XR_LIVE_EDIT && q->kind == VIDEO_XR_QUAD_MENU) + { + if (video_xr_handle_hit(q, origin, dir, both, &qu, &qv, &qt, + &bar)) + { + handle = (int)i; + *u = qu; + *v = qv; + *t = qt; + } + continue; + } + if (!video_xr_ray_hit_sides(q, origin, dir, both, &qu, &qv, &qt)) continue; if (best < 0 || qt < bt) { @@ -599,6 +972,12 @@ int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, bt = qt; } } + /* The bar wins over anything the ray meets, its margin over + * nothing, or over anything while stuck. */ + if (stick) + *stick = handle >= 0 && (bar || stuck); + if (handle >= 0 && (bar || stuck || best < 0)) + return handle; if (best >= 0) { *u = bu; @@ -608,6 +987,22 @@ int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, return best; } +int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, + bool menu_open, const video_xr_vec3_t *origin, + const video_xr_vec3_t *dir, float *u, float *v, float *t) +{ + return video_xr_pick_with(set, laser, menu_open, false, origin, dir, + u, v, t, NULL); +} + +int video_xr_pick_edit(const video_xr_quad_set_t *set, bool stuck, + const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, + float *u, float *v, float *t, bool *stick) +{ + return video_xr_pick_with(set, VIDEO_XR_LIVE_EDIT, false, stuck, + origin, dir, u, v, t, stick); +} + float video_xr_cursor(const video_xr_quad_t *q, float u, float v, float dist, video_xr_pose_t *pose) { @@ -624,28 +1019,2783 @@ float video_xr_cursor(const video_xr_quad_t *q, float u, float v, return (size < VIDEO_XR_CURSOR_MIN) ? VIDEO_XR_CURSOR_MIN : size; } -void video_xr_cursor_pixels(uint8_t *px) +/* The rotation taking X, Y and Z to the orthonormal x, y and z. */ +static void video_xr_quat_axes(const video_xr_vec3_t *x, + const video_xr_vec3_t *y, const video_xr_vec3_t *z, + video_xr_quat_t *q) +{ + double s; + double tr = (double)x->x + y->y + z->z; + if (tr > 0.0) + { + s = sqrt(tr + 1.0) * 2.0; + q->w = (float)(0.25 * s); + q->x = (float)((y->z - z->y) / s); + q->y = (float)((z->x - x->z) / s); + q->z = (float)((x->y - y->x) / s); + } + else if (x->x > y->y && x->x > z->z) + { + s = sqrt(1.0 + x->x - y->y - z->z) * 2.0; + q->w = (float)((y->z - z->y) / s); + q->x = (float)(0.25 * s); + q->y = (float)((y->x + x->y) / s); + q->z = (float)((z->x + x->z) / s); + } + else if (y->y > z->z) + { + s = sqrt(1.0 + y->y - x->x - z->z) * 2.0; + q->w = (float)((z->x - x->z) / s); + q->x = (float)((y->x + x->y) / s); + q->y = (float)(0.25 * s); + q->z = (float)((z->y + y->z) / s); + } + else + { + s = sqrt(1.0 + z->z - x->x - y->y) * 2.0; + q->w = (float)((x->y - y->x) / s); + q->x = (float)((z->x + x->z) / s); + q->y = (float)((z->y + y->z) / s); + q->z = (float)(0.25 * s); + } +} + +void video_xr_beam(const video_xr_vec3_t *o, const video_xr_vec3_t *d, + float len, const video_xr_vec3_t *eye, video_xr_pose_t *pose) +{ + video_xr_vec3_t y, z; + float k, n; + pose->position.x = o->x + d->x * len * 0.5f; + pose->position.y = o->y + d->y * len * 0.5f; + pose->position.z = o->z + d->z * len * 0.5f; + z.x = eye->x - pose->position.x; + z.y = eye->y - pose->position.y; + z.z = eye->z - pose->position.z; + k = z.x * d->x + z.y * d->y + z.z * d->z; + z.x -= d->x * k; + z.y -= d->y * k; + z.z -= d->z * k; + n = (float)sqrt(z.x * z.x + z.y * z.y + z.z * z.z); + if (n < 0.0001f) + { + /* Seen end on: any face square to the ray. */ + z.x = 0.0f; + z.y = (fabs(d->y) < 0.9) ? 1.0f : 0.0f; + z.z = 1.0f - z.y; + k = z.y * d->y + z.z * d->z; + z.x -= d->x * k; + z.y -= d->y * k; + z.z -= d->z * k; + n = (float)sqrt(z.x * z.x + z.y * z.y + z.z * z.z); + } + z.x /= n; + z.y /= n; + z.z /= n; + y.x = z.y * d->z - z.z * d->y; + y.y = z.z * d->x - z.x * d->z; + y.z = z.x * d->y - z.y * d->x; + video_xr_quat_axes(d, &y, &z, &pose->orientation); +} + +void video_xr_tag(const video_xr_vec3_t *hand, const video_xr_vec3_t *eye, + video_xr_pose_t *pose) +{ + float n; + video_xr_vec3_t x, y, z; + pose->position.x = hand->x; + pose->position.y = hand->y + VIDEO_XR_TAG_LIFT; + pose->position.z = hand->z; + z.x = eye->x - pose->position.x; + z.y = eye->y - pose->position.y; + z.z = eye->z - pose->position.z; + n = (float)sqrt(z.x * z.x + z.y * z.y + z.z * z.z); + if (n < 0.0001f) + { + pose->orientation.x = 0.0f; + pose->orientation.y = 0.0f; + pose->orientation.z = 0.0f; + pose->orientation.w = 1.0f; + return; + } + z.x /= n; + z.y /= n; + z.z /= n; + /* Level: square to the vertical and to the way to the eye. */ + x.x = z.z; + x.y = 0.0f; + x.z = -z.x; + n = (float)sqrt(x.x * x.x + x.z * x.z); + if (n < 0.0001f) + { + x.x = 1.0f; + x.z = 0.0f; + } + else + { + x.x /= n; + x.z /= n; + } + y.x = z.y * x.z - z.z * x.y; + y.y = z.z * x.x - z.x * x.z; + y.z = z.x * x.y - z.y * x.x; + video_xr_quat_axes(&x, &y, &z, &pose->orientation); +} + +void video_xr_outline(const video_xr_quad_t *q, video_xr_quad_t *out) +{ + const float inner = 1.0f - 2.0f * (VIDEO_XR_OUTLINE_EDGE + 0.5f) + / VIDEO_XR_OUTLINE_DIM; + *out = *q; + out->width = (q->width + 2.0f * VIDEO_XR_OUTLINE_GAP) / inner; + out->height = (q->height + 2.0f * VIDEO_XR_OUTLINE_GAP) / inner; +} + +void video_xr_outline_crop(video_xr_quad_t *o, unsigned hide, bool back, + unsigned *x, unsigned *w) +{ + unsigned lo, hi; + float texel; + video_xr_vec3_t shift, world; + *x = 0; + *w = VIDEO_XR_OUTLINE_DIM; + if (!hide) + return; + /* Turned to the head, its x runs the item's other way. */ + if (back) + hide = (hide & 3u) | ((hide & 4u) << 1) | ((hide & 8u) >> 1); + if (hide & 3u) + { + video_xr_pose_t turn; + float t; + video_xr_pose_identity(&turn); + turn.orientation.z = 0.70710678f; + turn.orientation.w = 0.70710678f; + video_xr_compose(&o->pose, &turn, &o->pose); + t = o->width; + o->width = o->height; + o->height = t; + hi = hide & 1u; + lo = (hide >> 1) & 1u; + } + else + { + hi = (hide >> 2) & 1u; + lo = (hide >> 3) & 1u; + } + *x = lo * VIDEO_XR_OUTLINE_CROP; + *w = VIDEO_XR_OUTLINE_DIM - (lo + hi) * VIDEO_XR_OUTLINE_CROP; + texel = o->width / VIDEO_XR_OUTLINE_DIM; + shift.x = 0.5f * texel * VIDEO_XR_OUTLINE_CROP + * ((float)lo - (float)hi); + shift.y = 0.0f; + shift.z = 0.0f; + video_xr_rotate(&o->pose.orientation, &shift, &world); + o->pose.position.x += world.x; + o->pose.position.y += world.y; + o->pose.position.z += world.z; + o->width = texel * (float)*w; +} + +/* The hide bits of each VIDEO_XR_CUT_X cut. */ +static const uint8_t video_xr_cuts[VIDEO_XR_OUTLINE_CUTS] = { + 1u, 2u, 4u, 8u, 3u, 12u }; + +unsigned video_xr_outline_cell(unsigned hide, bool back, unsigned lit) +{ + unsigned c; + if (back) + hide = (hide & 3u) | ((hide & 4u) << 1) | ((hide & 8u) >> 1); + hide &= (hide & 3u) ? 3u : 12u; + for (c = 0; c < VIDEO_XR_OUTLINE_CUTS; c++) + if (video_xr_cuts[c] == hide) + return VIDEO_XR_CUT_X(c, lit); + return VIDEO_XR_OUTLINE_X(lit); +} + +/* A pull's share: whole within r, none from 2r, falling between. */ +static double video_xr_snap_share(double d, double r) +{ + d = fabs(d); + if (d <= r) + return 1.0; + if (d >= 2.0 * r) + return 0.0; + return (2.0 * r - d) / r; +} + +/* How far a snap moves what is d off its line: all the way within r, + * then less by as much as d grows, nothing from 2r, so the item + * catches up with the hand at twice its speed. */ +static double video_xr_snap_pull(double d, double r) +{ + double a = fabs(d); + if (a <= r) + return d; + if (a >= 2.0 * r) + return 0.0; + return (d < 0.0) ? a - 2.0 * r : 2.0 * r - a; +} + +static double video_xr_quat_dot(const video_xr_quat_t *a, + const video_xr_quat_t *b) +{ + return (double)a->x * b->x + (double)a->y * b->y + + (double)a->z * b->z + (double)a->w * b->w; +} + +/* Degrees between two turns. */ +static double video_xr_quat_angle(const video_xr_quat_t *a, + const video_xr_quat_t *b) +{ + double d = fabs(video_xr_quat_dot(a, b)); + return (d >= 1.0) ? 0.0 : 2.0 * acos(d) * 180.0 / VIDEO_XR_PI; +} + +/* q turned k of the way to to, the short way; exactly to from k 1. */ +static void video_xr_quat_toward(video_xr_quat_t *q, + const video_xr_quat_t *to, double k) +{ + double wq, wt, n; + double d = video_xr_quat_dot(q, to); + double s = (d < 0.0) ? -1.0 : 1.0; + if (k >= 1.0) + { + *q = *to; + return; + } + if (k <= 0.0) + return; + d = fabs(d); + if (d > 0.9999) + { + wq = 1.0 - k; + wt = k; + } + else + { + double a = acos(d); + wq = sin((1.0 - k) * a) / sin(a); + wt = sin(k * a) / sin(a); + } + wt *= s; + q->x = (float)(wq * q->x + wt * to->x); + q->y = (float)(wq * q->y + wt * to->y); + q->z = (float)(wq * q->z + wt * to->z); + q->w = (float)(wq * q->w + wt * to->w); + n = sqrt(video_xr_quat_dot(q, q)); + if (n > 0.0) + { + q->x = (float)(q->x / n); + q->y = (float)(q->y / n); + q->z = (float)(q->z / n); + q->w = (float)(q->w / n); + } +} + +float video_xr_snap_fade(const video_xr_pose_t *start, + const video_xr_pose_t *pose) +{ + double dx = (double)pose->position.x - start->position.x; + double dy = (double)pose->position.y - start->position.y; + double dz = (double)pose->position.z - start->position.z; + double k = sqrt(dx * dx + dy * dy + dz * dz) + / VIDEO_XR_SNAP_FADE_DIST; + double turn = video_xr_quat_angle(&start->orientation, + &pose->orientation) / VIDEO_XR_SNAP_FADE_ANGLE; + if (turn > k) + k = turn; + return (k < 1.0) ? (float)k : 1.0f; +} + +/* Edit Smoothing's levels, Low to High: the turn's least cutoff in Hz + * and its rise per radian a second, then the position's and its rise + * per metre a second. */ +static const float video_xr_smooth_levels[3][4] = { + { 2.0f, 0.6f, 4.0f, 1.0f }, + { 1.0f, 0.4f, 2.0f, 0.7f }, + { 0.5f, 0.3f, 1.0f, 0.5f } +}; + +/* A smoother's share of the way for a step of dt at a cutoff of hz. */ +static double video_xr_smooth_alpha(double dt, double hz) +{ + double tau = 1.0 / (2.0 * VIDEO_XR_PI * hz); + return 1.0 / (1.0 + tau / dt); +} + +static bool video_xr_smooth_jumped(const video_xr_pose_t *a, + const video_xr_pose_t *b) +{ + double dx = (double)b->position.x - a->position.x; + double dy = (double)b->position.y - a->position.y; + double dz = (double)b->position.z - a->position.z; + return sqrt(dx * dx + dy * dy + dz * dz) > VIDEO_XR_SMOOTH_JUMP_M + || video_xr_quat_angle(&a->orientation, &b->orientation) + > VIDEO_XR_SMOOTH_JUMP_DEG; +} + +void video_xr_smooth_step(video_xr_smooth_t *s, unsigned level, + const video_xr_pose_t *raw, double dt, video_xr_pose_t *out) +{ + const float *k; + double dx, dy, dz, move, turn, d, a; + if ( !level || level >= VIDEO_OPENXR_EDIT_SMOOTHING_LAST + || !s->on || dt <= 0.0 || video_xr_smooth_jumped(&s->raw, raw)) + { + s->raw = *raw; + s->pose = *raw; + s->speed = 0.0f; + s->turn = 0.0f; + s->on = level && level < VIDEO_OPENXR_EDIT_SMOOTHING_LAST; + *out = *raw; + return; + } + k = video_xr_smooth_levels[level - 1]; + /* The speeds from the last smoothed pose, as the filter's own. */ + dx = (double)raw->position.x - s->pose.position.x; + dy = (double)raw->position.y - s->pose.position.y; + dz = (double)raw->position.z - s->pose.position.z; + move = sqrt(dx * dx + dy * dy + dz * dz) / dt; + turn = video_xr_quat_angle(&s->pose.orientation, &raw->orientation) + * VIDEO_XR_PI / 180.0 / dt; + d = video_xr_smooth_alpha(dt, VIDEO_XR_SMOOTH_DCUTOFF); + s->speed += (float)(d * (move - s->speed)); + s->turn += (float)(d * (turn - s->turn)); + a = video_xr_smooth_alpha(dt, k[2] + k[3] * s->speed); + s->pose.position.x += (float)(a * dx); + s->pose.position.y += (float)(a * dy); + s->pose.position.z += (float)(a * dz); + a = video_xr_smooth_alpha(dt, k[0] + k[1] * s->turn); + video_xr_quat_toward(&s->pose.orientation, &raw->orientation, a); + s->raw = *raw; + *out = s->pose; +} + +/* A screen a held one may snap to: the turn it is nearest, parallel or + * square, how many degrees off that it is, how near its edges are, and + * the share of its pulls: the gap's, times the grab's fade-in. */ +typedef struct video_xr_snap_target +{ + const video_xr_quad_t *q; + video_xr_quat_t goal; + double angle; + double dist; + double share; + bool square; +} video_xr_snap_target_t; + +/* The best line yet on one of the held screen's axes: how far off it + * is, the share of the pull it gets, and how far that pull moves it. */ +typedef struct video_xr_snap_line +{ + double d; + double k; + double pull; + bool any; +} video_xr_snap_line_t; + +/* Nearer by less than this, in metres or degrees, is rounding: a tie. */ +#define VIDEO_XR_SNAP_TIE 1e-5 +/* Past square by less than this, in degrees, is square: a step parks + * a swing exactly there, where rounding would straddle the limit. */ +#define VIDEO_XR_SNAP_FOLD_TIE 0.01 + +/* -1 when a is below b by more than rounding, 1 above, else 0. */ +static int video_xr_snap_cmp(double a, double b) +{ + if (a < b - VIDEO_XR_SNAP_TIE) + return -1; + return (a > b + VIDEO_XR_SNAP_TIE) ? 1 : 0; +} + +/* The line kept is the one left nearest after its pull (a whole pull + * leaves exactly 0), then the stronger pull, then the nearest, then + * the first. */ +static void video_xr_snap_offer_pull(video_xr_snap_line_t *l, double d, + double k, double pull) +{ + if (k <= 0.0 || fabs(d) >= 2.0 * VIDEO_XR_SNAP_DIST) + return; + if (l->any) + { + double e = fabs(d) - fabs(pull); + double f = fabs(l->d) - fabs(l->pull); + int c = (e < f) ? -1 : (e > f) ? 1 : 0; + if (!c) + c = (k > l->k) ? -1 : (k < l->k) ? 1 : 0; + if (!c) + c = video_xr_snap_cmp(fabs(d), fabs(l->d)); + if (c >= 0) + return; + } + l->d = d; + l->k = k; + l->pull = pull; + l->any = true; +} + +static void video_xr_snap_offer(video_xr_snap_line_t *l, double d, + double k) +{ + video_xr_snap_offer_pull(l, d, k, + video_xr_snap_pull(d, VIDEO_XR_SNAP_DIST) * k); +} + +/* A line at the set gap: whole within VIDEO_XR_SNAP_CLICK, fading out + * over as far again, none past. */ +static void video_xr_snap_offer_click(video_xr_snap_line_t *l, double d, + double k) +{ + if (fabs(d) < 2.0 * VIDEO_XR_SNAP_CLICK) + video_xr_snap_offer_pull(l, d, k, + video_xr_snap_pull(d, VIDEO_XR_SNAP_CLICK) * k); +} + +/* How much of the way to t's goal its pull turns the screen. */ +static double video_xr_snap_turn(const video_xr_snap_target_t *t) +{ + return (t->angle > 0.0) ? t->share + * video_xr_snap_pull(t->angle, VIDEO_XR_SNAP_ANGLE) / t->angle + : t->share; +} + +static double video_xr_snap_nearer(double a, double b) +{ + return (fabs(a) <= fabs(b)) ? a : b; +} + +/* Goal i of a screen snapping to q: 0 parallel, q's own turn; 1 and 2 + * square, a quarter turn either way about its up. */ +static void video_xr_snap_goal(const video_xr_quad_t *q, unsigned i, + video_xr_quat_t *goal) +{ + video_xr_pose_t turn, g; + if (!i) + { + *goal = q->pose.orientation; + return; + } + video_xr_pose_identity(&turn); + turn.orientation.y = (i == 1) ? 0.70710678f : -0.70710678f; + turn.orientation.w = 0.70710678f; + video_xr_compose(&q->pose, &turn, &g); + *goal = g.orientation; +} + +/* q as a target for a screen at pose, a and b its half width and + * height; false when too far off in turn or place to pull at all. */ +static bool video_xr_snap_target(const video_xr_quad_t *q, + const video_xr_pose_t *pose, double a, double b, + video_xr_snap_target_t *t) +{ + unsigned i; + double gx, gy, gz; + video_xr_pose_t inv, rel; + t->q = q; + video_xr_snap_goal(q, 0, &t->goal); + t->angle = video_xr_quat_angle(&pose->orientation, &t->goal); + t->square = false; + for (i = 1; i < 3; i++) + { + double e; + video_xr_quat_t goal; + video_xr_snap_goal(q, i, &goal); + e = video_xr_quat_angle(&pose->orientation, &goal); + if (e < t->angle) + { + t->goal = goal; + t->angle = e; + t->square = true; + } + } + if (t->angle >= 2.0 * VIDEO_XR_SNAP_ANGLE) + return false; + /* The gap as if lined up: square, the held screen's width runs along + * the target's facing. */ + video_xr_pose_invert(&q->pose, &inv); + video_xr_compose(&inv, pose, &rel); + gx = fabs(rel.position.x) - q->width * 0.5 - (t->square ? 0.0 : a); + gy = fabs(rel.position.y) - q->height * 0.5 - b; + gz = fabs(rel.position.z) - (t->square ? a : 0.0); + if (gx < 0.0) + gx = 0.0; + if (gy < 0.0) + gy = 0.0; + if (gz < 0.0) + gz = 0.0; + t->dist = sqrt(gx * gx + gy * gy + gz * gz); + t->share = video_xr_snap_share(t->dist, VIDEO_XR_SNAP_NEAR); + return t->share > 0.0; +} + +/* Edge e of a screen a by b metres from its middle to its sides, in its + * own frame: 0 top, 1 bottom, 2 right, 3 left. Writes its middle, the + * way out of the screen across it and half its length; returns the + * axis it runs along, 0 X or 1 Y. */ +static unsigned video_xr_snap_edge(unsigned e, double a, double b, + video_xr_vec3_t *mid, video_xr_vec3_t *out, double *half) +{ + float s = (e & 1u) ? -1.0f : 1.0f; + mid->x = 0.0f; + mid->y = 0.0f; + mid->z = 0.0f; + *out = *mid; + if (e < 2) + { + mid->y = (float)(s * b); + out->y = s; + *half = a; + return 0; + } + mid->x = (float)(s * a); + out->x = s; + *half = b; + return 1; +} + +/* One held edge against one target edge, the target at rel in the held + * screen's frame: the held axis and degrees that turn them parallel, + * where the hinge would hold the held edge and the moves, along the + * held axes, that put it there. */ +typedef struct video_xr_snap_pair +{ + video_xr_vec3_t axis; + double angle; + double across; /* past the target's edge, in its plane */ + double plane; /* out of that plane */ + double hold; /* how whole the hinge holds it there */ + double gap; /* metres from its place at the set gap */ + double overlap; /* of the edges, along the line */ + double fold; /* from flat, 180 folded shut */ + double off[3]; /* to there: the gap if it clicks, else touching */ + double pull[3]; /* the hinge's whole pulls toward there */ + double to_plane[3]; /* off, into the plane alone */ + double plane_pull[3]; /* pull, into the plane alone */ + double level[3]; /* along the edge (run), levelling centres or ends */ + double away[3]; /* out across the target's edge */ + double stop; /* how far the stop alone pushes it back out */ + unsigned run; +} video_xr_snap_pair_t; + +/* How far past touching the stop's whole range starts, gap the set + * one: where a click pulling from below touching lets go of it, so + * the stop holds touching over VIDEO_XR_SNAP_STOP at any gap. */ +static double video_xr_snap_stop_from(double gap) +{ + return (gap > 0.0 && gap < 2.0 * VIDEO_XR_SNAP_CLICK) + ? VIDEO_XR_SNAP_CLICK - 0.5 * gap : 0.0; +} + +/* The stop's share for an edge x metres past touching: whole within + * VIDEO_XR_SNAP_STOP past where it starts, to rounding, none from + * VIDEO_XR_SNAP_STOP_FADE past there. */ +static double video_xr_snap_stop_share(double x, double gap) +{ + double s = video_xr_snap_stop_from(gap); + double w = s + VIDEO_XR_SNAP_STOP + VIDEO_XR_SNAP_TIE; + double f = s + VIDEO_XR_SNAP_STOP_FADE; + if (x <= w) + return 1.0; + if (x >= f) + return 0.0; + return (f - x) / (f - w); +} + +/* Whether the stop holds that edge whole, VIDEO_XR_SNAP_STOP_ARM short + * once a push has let its hinge go (through). */ +static bool video_xr_snap_stop_held(double x, double gap, bool through) +{ + return x <= video_xr_snap_stop_from(gap) + VIDEO_XR_SNAP_STOP + + VIDEO_XR_SNAP_TIE - (through ? VIDEO_XR_SNAP_STOP_ARM : 0.0); +} + +/* How far the stop moves that edge back to touching: all the way + * while whole, then less, so nothing hops. Through, past its short + * hold it lets the edge in by twice the snap's tie and + * VIDEO_XR_SNAP_STOP_SLIP of the hand's way, never less than the fade + * does: out of the hinge's tie from just where it stops holding, so a + * hinge takes hold only where it holds, but by too little to see. */ +#define VIDEO_XR_SNAP_STOP_SLIP 0.02 +static double video_xr_snap_stop(double x, double gap, bool through) +{ + double w = video_xr_snap_stop_from(gap) + VIDEO_XR_SNAP_STOP + + VIDEO_XR_SNAP_TIE; + double s = (x <= w) ? x : w * video_xr_snap_stop_share(x, gap); + if (through && !video_xr_snap_stop_held(x, gap, true)) + { + double t = x - 2.0 * VIDEO_XR_SNAP_TIE - VIDEO_XR_SNAP_STOP_SLIP + * (x - (w - VIDEO_XR_SNAP_STOP_ARM)); + if (t < s) + s = t; + } + return s; +} + +/* How whole a hinge holds an edge g metres out past the other's, gap + * the set one: as its stop past touching, whole from there to + * VIDEO_XR_SNAP_FREE, fading out over VIDEO_XR_SNAP_DIST past that, + * and within VIDEO_XR_SNAP_CLICK of gap, fading out over as far + * again. */ +static double video_xr_snap_gap_share(double g, double gap) +{ + double click = video_xr_snap_share(g - gap, VIDEO_XR_SNAP_CLICK); + double x = (g > VIDEO_XR_SNAP_FREE) ? g - VIDEO_XR_SNAP_FREE : 0.0; + double held = (g < 0.0) ? video_xr_snap_stop_share(-g, gap) + : (x <= VIDEO_XR_SNAP_TIE) ? 1.0 + : video_xr_snap_share(VIDEO_XR_SNAP_DIST + x, VIDEO_XR_SNAP_DIST); + return (click > held) ? click : held; +} + +/* v moved along d until its run part is 0, d[run] above 0. */ +static void video_xr_snap_slide(double *v, const double *d, unsigned run) +{ + double k = v[run] / d[run]; + v[0] -= k * d[0]; + v[1] -= k * d[1]; + v[2] -= k * d[2]; +} + +static double video_xr_snap_dot(const double *a, const double *b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +/* Degrees from g to f about d, all three at right angles to d. */ +static double video_xr_snap_about(const double *g, const double *f, + const double *d) +{ + double c[3]; + c[0] = g[1] * f[2] - g[2] * f[1]; + c[1] = g[2] * f[0] - g[0] * f[2]; + c[2] = g[0] * f[1] - g[1] * f[0]; + return atan2(video_xr_snap_dot(c, d), video_xr_snap_dot(g, f)) + * 180.0 / VIDEO_XR_PI; +} + +/* v less its part along d. */ +static void video_xr_snap_across(double *v, const double *d) +{ + double k = video_xr_snap_dot(v, d); + v[0] -= k * d[0]; + v[1] -= k * d[1]; + v[2] -= k * d[2]; +} + +/* Held edge he (a, b the held screen's half size) against target edge te + * (A, B the target's). */ +static void video_xr_snap_pair(const video_xr_pose_t *rel, double A, + double B, double a, double b, unsigned he, unsigned te, double gap, + bool through, video_xr_snap_pair_t *p) +{ + unsigned i; + double lt, lh, s, n, k, e, q; + double c[3], d[3], u[3], m[3], f[3], t[3]; + video_xr_vec3_t mid, out, v; + unsigned run = video_xr_snap_edge(te, A, B, &mid, &out, <); + video_xr_rotate(&rel->orientation, &mid, &v); + c[0] = (double)rel->position.x + v.x; + c[1] = (double)rel->position.y + v.y; + c[2] = (double)rel->position.z + v.z; + video_xr_rotate(&rel->orientation, &out, &v); + u[0] = v.x; + u[1] = v.y; + u[2] = v.z; + mid.x = 0.0f; + mid.y = 0.0f; + mid.z = 1.0f; + video_xr_rotate(&rel->orientation, &mid, &v); + t[0] = v.x; + t[1] = v.y; + t[2] = v.z; + mid.x = run ? 0.0f : 1.0f; + mid.y = run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&rel->orientation, &mid, &v); + d[0] = v.x; + d[1] = v.y; + d[2] = v.z; + run = video_xr_snap_edge(he, a, b, &mid, &out, &lh); + p->run = run; + /* The target's line the held edge's way. */ + s = (d[run] < 0.0) ? -1.0 : 1.0; + for (i = 0; i < 3; i++) + d[i] *= s; + /* The held edge's way crossed with it. */ + m[0] = run ? d[2] : 0.0; + m[1] = run ? 0.0 : -d[2]; + m[2] = run ? -d[0] : d[1]; + n = sqrt(video_xr_snap_dot(m, m)); + p->angle = atan2(n, d[run]) * 180.0 / VIDEO_XR_PI; + p->axis.x = (float)(n > 0.0 ? m[0] / n : 1.0); + p->axis.y = (float)(n > 0.0 ? m[1] / n : 0.0); + p->axis.z = (float)(n > 0.0 ? m[2] / n : 0.0); + /* The line where it passes the held edge's middle. */ + s = (d[run] > 0.0) ? -c[run] / d[run] : 0.0; + m[0] = mid.x; + m[1] = mid.y; + m[2] = 0.0; + for (i = 0; i < 3; i++) + p->off[i] = m[i] - c[i] - s * d[i]; + p->level[0] = c[run]; + p->level[1] = c[run] + lt - lh; + p->level[2] = c[run] - lt + lh; + /* Past the target's edge and out of its plane; the gap clicks in + * VIDEO_XR_SNAP_CLICK, the plane pulls in VIDEO_XR_SNAP_DIST. Both + * ways are square to the line, so sliding along it to the held + * edge's middle leaves them be. */ + p->across = video_xr_snap_dot(p->off, u); + p->plane = video_xr_snap_dot(p->off, t); + k = gap - p->across; + if (fabs(k) >= 2.0 * VIDEO_XR_SNAP_CLICK) + k = 0.0; + e = video_xr_snap_pull(k, VIDEO_XR_SNAP_CLICK); + /* Past touching, both push it back out: the farther wins, so the + * click's fade hands over to the stop smoothly. */ + p->stop = (p->across < 0.0) + ? video_xr_snap_stop(-p->across, gap, through) : 0.0; + if (p->across < 0.0 && p->stop > e) + { + k = -p->across; + e = p->stop; + } + q = video_xr_snap_pull(-p->plane, VIDEO_XR_SNAP_DIST); + for (i = 0; i < 3; i++) + { + p->to_plane[i] = -p->plane * t[i]; + p->plane_pull[i] = q * t[i]; + p->off[i] = k * u[i]; + p->pull[i] = e * u[i]; + p->away[i] = u[i]; + } + if (d[run] > 0.0) + { + video_xr_snap_slide(p->to_plane, d, run); + video_xr_snap_slide(p->plane_pull, d, run); + video_xr_snap_slide(p->off, d, run); + video_xr_snap_slide(p->pull, d, run); + video_xr_snap_slide(p->away, d, run); + } + for (i = 0; i < 3; i++) + { + p->off[i] += p->to_plane[i]; + p->pull[i] += p->plane_pull[i]; + } + p->gap = sqrt(p->plane * p->plane + + (p->across - gap) * (p->across - gap)); + p->hold = video_xr_snap_share(p->plane, VIDEO_XR_SNAP_DIST) + * video_xr_snap_gap_share(p->across, gap); + p->overlap = lt * d[run] + lh - fabs(c[run]); + /* Into the held screen from its edge, against flat: out of the + * target across its edge. */ + f[0] = -out.x; + f[1] = -out.y; + f[2] = 0.0; + video_xr_snap_across(f, d); + video_xr_snap_across(u, d); + p->fold = video_xr_snap_about(u, f, d); +} + +/* Whether the held edge, half long, lies in the target's plane and + * parallel to its edge to within rounding at both its ends. */ +static bool video_xr_snap_on_line(const video_xr_snap_pair_t *p, + double half) +{ + double tip = half * sin(p->angle * VIDEO_XR_PI / 180.0); + return fabs(p->plane) + tip <= VIDEO_XR_SNAP_TIE; +} + +/* Whether a held edge half long lies where its hinge engages: in the + * target's plane on its line, free or at the set gap (the click's fade + * leaves it between), overlapping the target's by VIDEO_XR_SNAP_DIST, + * flat to square. */ +static bool video_xr_snap_engaged(const video_xr_snap_pair_t *p, + double half, double gap) +{ + return p->angle < 2.0 * VIDEO_XR_SNAP_ANGLE + && video_xr_snap_on_line(p, half) + && p->hold >= 1.0 + && ( fabs(p->across - gap) <= VIDEO_XR_SNAP_TIE + || ( p->across >= -VIDEO_XR_SNAP_TIE + && p->across <= VIDEO_XR_SNAP_FREE + VIDEO_XR_SNAP_TIE)) + && p->overlap >= VIDEO_XR_SNAP_DIST + && fabs(p->fold) <= 90.0 + VIDEO_XR_SNAP_FOLD_TIE; +} + +/* The edges' overlap as a share: whole from VIDEO_XR_SNAP_DIST, none + * without, so a hinge slides off an end smoothly. */ +static double video_xr_snap_overlap(const video_xr_snap_pair_t *p) +{ + double x = 2.0 * VIDEO_XR_SNAP_DIST - p->overlap; + return video_xr_snap_share((x > 0.0) ? x : 0.0, VIDEO_XR_SNAP_DIST); +} + +/* A hinge's share of its pulls: its place's, the overlap's, the angle's + * between the screens (whole to square, none folded shut) and the + * grab's fade. */ +static double video_xr_snap_hinge_share(const video_xr_snap_pair_t *p, + double fade) +{ + return fade * p->hold * video_xr_snap_overlap(p) + * video_xr_snap_share(p->fold, 90.0); +} + +/* The held axis, other than the edge's, the way out of the target + * across its edge runs most along. */ +static unsigned video_xr_snap_away_axis(const video_xr_snap_pair_t *p) +{ + unsigned i; + unsigned a = p->run ? 0 : 1; + for (i = 0; i < 3; i++) + if (i != p->run && fabs(p->away[i]) > fabs(p->away[a])) + a = i; + return a; +} + +/* Across the hinge's edge, the screen's own lines blended into the + * hinge's (share k, holding w of the pull), never past it, so nothing + * hops; a blend short of the pull ranks no better than a whole line. */ +static void video_xr_snap_hinge_join(video_xr_snap_line_t *line, + const video_xr_snap_line_t *own, const video_xr_snap_pair_t *p, + double k, double w, double stop, bool loose) +{ + unsigned i; + for (i = 0; i < 3; i++) + { + double d, pull, kk; + if (i == p->run) + continue; + /* Whole, its own lines win a tie: the turn and the hinge chose + * its screen, the nearer, in whichever order the quads come. */ + if (!loose) + { + if (own[i].any) + { + video_xr_snap_line_t other = line[i]; + line[i] = own[i]; + if (other.any) + video_xr_snap_offer_pull(&line[i], other.d, other.k, + other.pull); + } + continue; + } + d = p->off[i]; + kk = k; + pull = p->pull[i] * k; + if (own[i].any) + { + double to = p->to_plane[i]; + double carry = w * ((fabs(own[i].pull) < fabs(to)) + ? fabs(own[i].pull) : fabs(to)); + if (carry > fabs(p->plane_pull[i] * k)) + { + pull = (p->pull[i] - p->plane_pull[i]) * k + + ((to < 0.0) ? -carry : carry); + if (own[i].k > kk) + kk = own[i].k; + } + pull += (1.0 - w) * own[i].pull; + d = w * d + (1.0 - w) * own[i].d; + if (fabs(pull) > fabs(d)) + d = pull; + } + video_xr_snap_offer_pull(&line[i], d, kk, pull); + } + /* Whole, its stop joins its own lines along the axis the way out + * runs, as strong as the hinge, where it pushes farther than they + * do: so the click hands over to it smoothly at any strength. */ + if (!loose && p->stop > 0.0) + { + unsigned a = video_xr_snap_away_axis(p); + double pull = p->stop * p->away[a] * stop; + if (!own[a].any || fabs(pull) > fabs(own[a].pull)) + video_xr_snap_offer_pull(&line[a], -p->across * p->away[a], + stop, pull); + } +} + +/* How near the held screen's edges (inv, its pose inverted) come to + * q's, one measure for every target, so one stacked behind never + * outranks a hinged one; dist past that if no edge is near parallel. */ +static double video_xr_snap_edge_dist(const video_xr_quad_t *q, + const video_xr_pose_t *inv, double a, double b, double gap, + double dist) +{ + unsigned he, te; + video_xr_pose_t rel; + video_xr_snap_pair_t p; + double reach = gap + VIDEO_XR_SNAP_FREE + VIDEO_XR_SNAP_DIST; + dist += reach; + video_xr_compose(inv, &q->pose, &rel); + for (he = 0; he < 4; he++) + { + for (te = 0; te < 4; te++) + { + double d, f, w; + video_xr_snap_pair(&rel, q->width * 0.5, q->height * 0.5, a, b, + he, te, gap, false, &p); + if (p.angle >= 2.0 * VIDEO_XR_SNAP_ANGLE) + continue; + f = fabs(p.fold); + w = (f <= 90.0 + VIDEO_XR_SNAP_ANGLE) ? 1.0 + : (f >= 180.0) ? 0.0 + : (180.0 - f) / (90.0 - VIDEO_XR_SNAP_ANGLE); + d = p.gap + reach * (1.0 - video_xr_snap_overlap(&p) * w); + if (d < dist) + dist = d; + } + } + return dist; +} + +float video_xr_hinge_angle(const video_xr_hinge_t *h, + const video_xr_quat_t *q) +{ + double f[3], g[3], d[3]; + video_xr_vec3_t mid, out, v; + double half; + video_xr_snap_edge(h->edge, 1.0, 1.0, &mid, &out, &half); + out.x = -out.x; + out.y = -out.y; + video_xr_rotate(q, &out, &v); + f[0] = v.x; + f[1] = v.y; + f[2] = v.z; + g[0] = h->flat.x; + g[1] = h->flat.y; + g[2] = h->flat.z; + d[0] = h->dir.x; + d[1] = h->dir.y; + d[2] = h->dir.z; + video_xr_snap_across(f, d); + video_xr_snap_across(g, d); + return (float)video_xr_snap_about(g, f, d); +} + +bool video_xr_hinge_line(const video_xr_hinge_t *h, float width, + float height, const video_xr_quad_t *target, video_xr_vec3_t *a, + video_xr_vec3_t *b) +{ + unsigned run; + double lh, lt, s, k, lo, hi, g; + video_xr_vec3_t mid, out, e; + video_xr_snap_edge(h->edge, width * 0.5, height * 0.5, &mid, &out, &lh); + run = video_xr_snap_edge(h->on, target->width * 0.5, + target->height * 0.5, &mid, &out, <); + video_xr_rotate(&target->pose.orientation, &mid, &e); + /* Along the line from the held edge's middle: the target edge's + * middle, and half its length seen along the line. */ + s = ((double)target->pose.position.x + e.x - h->point.x) * h->dir.x + + ((double)target->pose.position.y + e.y - h->point.y) * h->dir.y + + ((double)target->pose.position.z + e.z - h->point.z) * h->dir.z; + mid.x = run ? 0.0f : 1.0f; + mid.y = run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&target->pose.orientation, &mid, &e); + k = lt * fabs((double)e.x * h->dir.x + (double)e.y * h->dir.y + + (double)e.z * h->dir.z); + lo = (s - k > -lh) ? s - k : -lh; + hi = (s + k < lh) ? s + k : lh; + if (hi - lo <= VIDEO_XR_SNAP_TIE) + return false; + g = h->gap * 0.5; + a->x = (float)(h->point.x + h->dir.x * lo - h->flat.x * g); + a->y = (float)(h->point.y + h->dir.y * lo - h->flat.y * g); + a->z = (float)(h->point.z + h->dir.z * lo - h->flat.z * g); + b->x = (float)(h->point.x + h->dir.x * hi - h->flat.x * g); + b->y = (float)(h->point.y + h->dir.y * hi - h->flat.y * g); + b->z = (float)(h->point.z + h->dir.z * hi - h->flat.z * g); + return true; +} + +void video_xr_hinge_place(video_xr_hinge_t *h, const video_xr_pose_t *pose, + float width, float height, const video_xr_quad_t *target, bool over) +{ + unsigned i, run; + double half, n, x; + double m[3], d[3], e[3], f[3], g[3]; + video_xr_vec3_t mid, out, v; + run = video_xr_snap_edge(h->edge, width * 0.5, height * 0.5, &mid, + &out, &half); + video_xr_rotate(&pose->orientation, &mid, &v); + m[0] = (double)pose->position.x + v.x; + m[1] = (double)pose->position.y + v.y; + m[2] = (double)pose->position.z + v.z; + mid.x = run ? 0.0f : 1.0f; + mid.y = run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&pose->orientation, &mid, &v); + d[0] = v.x; + d[1] = v.y; + d[2] = v.z; + run = video_xr_snap_edge(h->on, target->width * 0.5, + target->height * 0.5, &mid, &out, &half); + video_xr_rotate(&target->pose.orientation, &mid, &v); + g[0] = m[0] - target->pose.position.x - v.x; + g[1] = m[1] - target->pose.position.y - v.y; + g[2] = m[2] - target->pose.position.z - v.z; + video_xr_rotate(&target->pose.orientation, &out, &v); + f[0] = v.x; + f[1] = v.y; + f[2] = v.z; + mid.x = run ? 0.0f : 1.0f; + mid.y = run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&target->pose.orientation, &mid, &v); + e[0] = v.x; + e[1] = v.y; + e[2] = v.z; + /* The line's way midway between the edges'. */ + x = (video_xr_snap_dot(d, e) < 0.0) ? -1.0 : 1.0; + for (i = 0; i < 3; i++) + { + e[i] *= x; + d[i] += e[i]; + } + n = sqrt(video_xr_snap_dot(d, d)); + for (i = 0; i < 3; i++) + d[i] /= n; + /* From the target's edge to the held edge's middle, square to both + * edges, so the line's ends stay where the held edge's are. */ + video_xr_snap_across(g, e); + video_xr_snap_across(g, d); + x = video_xr_snap_dot(g, f); + if (x < 0.0) + { + for (i = 0; i < 3; i++) + { + g[i] -= x * f[i]; + if (!over) + m[i] -= x * f[i]; + } + } + n = sqrt(video_xr_snap_dot(g, g)); + h->point.x = (float)m[0]; + h->point.y = (float)m[1]; + h->point.z = (float)m[2]; + h->dir.x = (float)d[0]; + h->dir.y = (float)d[1]; + h->dir.z = (float)d[2]; + h->flat.x = (float)((n > VIDEO_XR_SNAP_TIE) ? g[0] / n : f[0]); + h->flat.y = (float)((n > VIDEO_XR_SNAP_TIE) ? g[1] / n : f[1]); + h->flat.z = (float)((n > VIDEO_XR_SNAP_TIE) ? g[2] / n : f[2]); + h->gap = (float)n; +} + +/* video_xr_snap_hinge, but the screens in whole (slots, as bits) pull + * whole whatever fade: a hinge's target as a grab taken in it, or + * taking a screen where it rests, holds it. */ +static unsigned video_xr_snap_hinge_at(const video_xr_quad_set_t *set, + unsigned slot, unsigned skip, unsigned whole, float width, + float height, float gap, float fade, bool through, + video_xr_pose_t *pose, video_xr_hinge_t *hinge) +{ + unsigned i, he, te; + double e; + double turn = 0.0; + video_xr_snap_target_t t[RETRO_VIDEO_VIEWS_MAX]; + const video_xr_quad_t *h[RETRO_VIDEO_VIEWS_MAX]; + video_xr_snap_line_t line[3], own[3]; + video_xr_snap_pair_t p; + video_xr_pose_t inv, rel, turned; + video_xr_vec3_t move, world; + double pull[3]; + unsigned n = 0; + unsigned nh = 0; + unsigned done = 0; + unsigned snapped = 0; + int ref = -1; + int hq = -1; + unsigned hhe = 0; + unsigned hte = 0; + double hk = 0.0; + double ha = 0.0; + double hg = 0.0; + double hw = 0.0; + double hn = 0.0; + double hs = 0.0; + double hin = 0.0; + double hk_line = 0.0; + double hk_stop = 0.0; + double hf = 0.0; + bool joined = false; + bool pressed = false; + double a = width * 0.5; + double b = height * 0.5; + + if (!set || slot >= VIDEO_XR_MENU_SLOT || (fade <= 0.0f && !whole)) + return 0; + video_xr_pose_invert(pose, &inv); + for (i = 0; i < set->num_quads && nh < RETRO_VIDEO_VIEWS_MAX; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if ( q->slot >= VIDEO_XR_MENU_SLOT || q->slot == slot + || q->kind == VIDEO_XR_QUAD_MENU + || (((skip | done) >> q->slot) & 1u) + || set->slots[q->slot].fixed) + continue; + done |= 1u << q->slot; + h[nh++] = q; + if (video_xr_snap_target(q, pose, a, b, &t[n])) + { + t[n].share *= ((whole >> q->slot) & 1u) ? 1.0 : fade; + t[n].dist = video_xr_snap_edge_dist(q, &inv, a, b, gap, + t[n].dist); + n++; + } + } + if (!nh) + return 0; + + /* The turn: while the rest fade in, to a screen pulling whole, not a + * nearer one still fading; of two in its whole snap, to the nearer, + * so it turns to the screen it hinges to; else to the target whose + * pull leaves it nearest parallel or square, then the stronger pull, + * the smaller turn, the nearer, the first. */ + for (i = 0; i < n; i++) + { + double k = video_xr_snap_turn(&t[i]); + int c = -1; + int lead = (ref >= 0 && fade < 1.0f) + ? (int)((whole >> t[ref].q->slot) & 1u) + - (int)((whole >> t[i].q->slot) & 1u) : 0; + if (ref >= 0) + { + bool both = t[i].angle <= VIDEO_XR_SNAP_ANGLE + && t[ref].angle <= VIDEO_XR_SNAP_ANGLE; + c = lead; + if (!c && both) + c = video_xr_snap_cmp(t[i].dist, t[ref].dist); + if (!c) + c = video_xr_snap_cmp(t[i].angle * (1.0 - k), + t[ref].angle * (1.0 - turn)); + if (!c) + c = (k > turn) ? -1 : (k < turn) ? 1 : 0; + if (!c && !both) + c = (t[i].angle < t[ref].angle) ? -1 + : (t[i].angle > t[ref].angle) ? 1 : 0; + if (!c && t[i].dist < t[ref].dist) + c = -1; + } + if (c < 0) + { + ref = (int)i; + turn = k; + } + } + + /* A hinge: one of the held screen's edges turned on parallel to a + * target's, at any angle between the screens, by the same rule, the + * smaller turn only past the whole snap, then the nearer; to a screen + * the turn does not go to, only as far as that turn leaves it free. */ + for (i = 0; i < nh; i++) + { + double f = ((whole >> h[i]->slot) & 1u) ? 1.0 : fade; + video_xr_compose(&inv, &h[i]->pose, &rel); + for (he = 0; he < 4; he++) + { + for (te = 0; te < 4; te++) + { + int c; + double k; + video_xr_snap_pair(&rel, h[i]->width * 0.5, + h[i]->height * 0.5, a, b, he, te, gap, false, &p); + if (p.angle >= 2.0 * VIDEO_XR_SNAP_ANGLE) + continue; + k = video_xr_snap_hinge_share(&p, f); + if (k <= 0.0) + continue; + if (ref >= 0 && t[ref].q != h[i]) + k *= 1.0 - turn; + if (p.angle > 0.0) + k *= video_xr_snap_pull(p.angle, VIDEO_XR_SNAP_ANGLE) + / p.angle; + c = (hq < 0) ? -1 : video_xr_snap_cmp(p.angle * (1.0 - k), + ha * (1.0 - hk)); + if (!c) + c = (k > hk) ? -1 : (k < hk) ? 1 : 0; + if ( !c && (p.angle > VIDEO_XR_SNAP_ANGLE + || ha > VIDEO_XR_SNAP_ANGLE)) + c = video_xr_snap_cmp(p.angle, ha); + if (!c) + c = video_xr_snap_cmp(p.gap, hg); + if (c < 0) + { + hq = (int)i; + hhe = he; + hte = te; + hk = k; + ha = p.angle; + hg = p.gap; + hn = p.hold; + hw = p.hold * video_xr_snap_overlap(&p); + hs = video_xr_snap_hinge_share(&p, f); + hf = f; + hin = p.angle; + } + } + } + } + + if (ref >= 0) + { + video_xr_vec3_t at, from, to; + double half; + e = t[ref].angle; + /* Hinged to the same screen, it turns about its own edge, so a + * swing out of flat or square keeps the edge's gap. */ + video_xr_snap_edge(hhe, a, b, &at, &from, &half); + half = (hq >= 0 && h[hq] == t[ref].q) ? hn * hw : 0.0; + at.x = (float)(at.x * half); + at.y = (float)(at.y * half); + video_xr_rotate(&pose->orientation, &at, &from); + video_xr_quat_toward(&pose->orientation, &t[ref].goal, turn); + video_xr_rotate(&pose->orientation, &at, &to); + pose->position.x += from.x - to.x; + pose->position.y += from.y - to.y; + pose->position.z += from.z - to.z; + if (e <= VIDEO_XR_SNAP_ANGLE && t[ref].share >= 1.0) + snapped |= VIDEO_XR_SNAP_TURN; + if (turn > 0.0) + snapped |= VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_TURN); + } + video_xr_pose_invert(pose, &turned); + if (hq >= 0) + { + /* The rest of the way parallel, as the turn left it. */ + video_xr_compose(&turned, &h[hq]->pose, &rel); + video_xr_snap_pair(&rel, h[hq]->width * 0.5, h[hq]->height * 0.5, + a, b, hhe, hte, gap, false, &p); + ha = p.angle; + hk = (ha < 2.0 * VIDEO_XR_SNAP_ANGLE) ? hs : 0.0; + if (ref >= 0 && t[ref].q != h[hq]) + hk *= 1.0 - turn; + if (ha > 0.0) + hk *= video_xr_snap_pull(ha, VIDEO_XR_SNAP_ANGLE) / ha; + if (ha * hk > VIDEO_XR_SNAP_TIE) + { + video_xr_pose_t held, by; + video_xr_vec3_t at, out, from, to; + double half; + double s = sin(ha * hk * VIDEO_XR_PI / 360.0); + video_xr_pose_identity(&held); + video_xr_pose_identity(&by); + held.orientation = pose->orientation; + by.orientation.x = (float)(p.axis.x * s); + by.orientation.y = (float)(p.axis.y * s); + by.orientation.z = (float)(p.axis.z * s); + by.orientation.w = (float)cos(ha * hk * VIDEO_XR_PI / 360.0); + /* About its edge's middle, so its place along the line + * keeps through any roll. */ + video_xr_snap_edge(hhe, a, b, &at, &out, &half); + video_xr_rotate(&pose->orientation, &at, &from); + video_xr_compose(&held, &by, &held); + pose->orientation = held.orientation; + video_xr_rotate(&pose->orientation, &at, &to); + pose->position.x += from.x - to.x; + pose->position.y += from.y - to.y; + pose->position.z += from.z - to.z; + } + } + + /* Lines along the held screen's own axes, as now turned; they fade + * with each target's turn. The hinge's screen's lines across its + * edge are kept apart, to join the hinge's own. */ + memset(line, 0, sizeof(line)); + memset(own, 0, sizeof(own)); + video_xr_pose_invert(pose, &inv); + hk_line = hs * video_xr_snap_share(hin, VIDEO_XR_SNAP_ANGLE); + if (hq >= 0) + { + double fold; + video_xr_compose(&inv, &h[hq]->pose, &rel); + video_xr_snap_pair(&rel, h[hq]->width * 0.5, h[hq]->height * 0.5, + a, b, hhe, hte, gap, through, &p); + /* As strong as the hinge where the hand holds it, the share + * that chose it and set the turn's pivot, so the stop lets go + * with it; folded as the turn put it, square to rounding while + * that is whole. */ + fold = fabs(p.fold) - VIDEO_XR_SNAP_FOLD_TIE; + hk_stop = hf * hw + * video_xr_snap_share((fold > 0.0) ? fold : 0.0, 90.0); + } + for (i = 0; i < n; i++) + { + unsigned j; + double x, y, z; + video_xr_snap_line_t *l[3]; + double A = t[i].q->width * 0.5; + double B = t[i].q->height * 0.5; + double k = t[i].share + * video_xr_snap_share(t[i].angle, VIDEO_XR_SNAP_ANGLE); + for (j = 0; j < 3; j++) + l[j] = (hq >= 0 && t[i].q == h[hq] && j != p.run) + ? &own[j] : &line[j]; + video_xr_compose(&inv, &t[i].q->pose, &rel); + x = rel.position.x; + y = rel.position.y; + z = rel.position.z; + if (!t[i].square) + { + video_xr_snap_offer(l[0], x - A + a, k); + video_xr_snap_offer(l[0], x + A - a, k); + video_xr_snap_offer(l[0], x, k); + video_xr_snap_offer_click(l[0], x + A + a + gap, k); + video_xr_snap_offer_click(l[0], x - A - a - gap, k); + video_xr_snap_offer(l[1], y + B - b, k); + video_xr_snap_offer(l[1], y - B + b, k); + video_xr_snap_offer(l[1], y, k); + video_xr_snap_offer_click(l[1], y + B + b + gap, k); + video_xr_snap_offer_click(l[1], y - B - b - gap, k); + video_xr_snap_offer(l[2], z, k); + } + else + { + /* A vertical edge into its plane, its own plane the gap out + * past one of its vertical edges: a corner, held while the gap + * is, clicking to the set one. */ + double g1 = z - A; + double g2 = -z - A; + double dx = video_xr_snap_nearer(x - a, x + a); + double dz = (g1 >= g2) ? g1 - gap : gap - g2; + video_xr_snap_offer(l[0], dx, + k * video_xr_snap_gap_share((g1 >= g2) ? g1 : g2, gap)); + video_xr_snap_offer_click(l[2], dz, + k * video_xr_snap_share(dx, VIDEO_XR_SNAP_DIST)); + video_xr_snap_offer(l[1], y + B - b, k); + video_xr_snap_offer(l[1], y - B + b, k); + video_xr_snap_offer(l[1], y, k); + } + if (l[0] == &own[0] || l[1] == &own[1]) + { + video_xr_snap_hinge_join(line, own, &p, hk_line, hw, hk_stop, + t[i].angle > VIDEO_XR_SNAP_ANGLE); + joined = true; + } + } + /* The hinge: its edge onto the line, joined across it with its + * screen's own lines there, each by how hard it pulls, so neither + * hops to the other; along it centres or ends level. */ + if (hq >= 0) + { + if (!joined) + video_xr_snap_hinge_join(line, own, &p, hk_line, hw, hk_stop, + true); + video_xr_snap_offer(&line[p.run], p.level[0], hk_line); + video_xr_snap_offer(&line[p.run], p.level[1], hk_line); + video_xr_snap_offer(&line[p.run], p.level[2], hk_line); + } + if ( hq >= 0 && hk_line > 0.0 + && fabs(p.across - gap) < 2.0 * VIDEO_XR_SNAP_CLICK) + snapped |= VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP); + /* Near touching the stop counts as pulling, on its axis and on the + * gap's bit, so it ticks once per push. */ + if (hq >= 0 && hk_stop > 0.0 && p.across < VIDEO_XR_SNAP_STOP_ARM) + { + snapped |= VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP + | (VIDEO_XR_SNAP_X << video_xr_snap_away_axis(&p))); + pressed = p.across < 0.0; + } + for (i = 0; i < 3; i++) + { + pull[i] = 0.0; + if (!line[i].any) + continue; + snapped |= VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_X << i); + pull[i] = line[i].pull; + if ( line[i].k >= 1.0 + && fabs(line[i].d) - fabs(line[i].pull) <= VIDEO_XR_SNAP_TIE) + snapped |= VIDEO_XR_SNAP_X << i; + } + move.x = (float)pull[0]; + move.y = (float)pull[1]; + move.z = (float)pull[2]; + video_xr_rotate(&pose->orientation, &move, &world); + pose->position.x += world.x; + pose->position.y += world.y; + pose->position.z += world.z; + + /* Engaged, by whatever pull put it there, its fade whole. */ + if (hq >= 0 && hf >= 1.0) + { + video_xr_pose_invert(pose, &inv); + video_xr_compose(&inv, &h[hq]->pose, &rel); + video_xr_snap_pair(&rel, h[hq]->width * 0.5, h[hq]->height * 0.5, + a, b, hhe, hte, gap, false, &p); + if (video_xr_snap_engaged(&p, hhe < 2 ? a : b, gap)) + { + snapped |= VIDEO_XR_SNAP_HINGE; + if ( fabs(p.across - gap) <= VIDEO_XR_SNAP_TIE + || (pressed && fabs(p.across) <= VIDEO_XR_SNAP_TIE)) + snapped |= VIDEO_XR_SNAP_GAP; + if (hinge) + { + video_xr_vec3_t mid, out, x; + double half, tx, ty; + video_xr_snap_edge(hte, 1.0, 1.0, &mid, &out, &half); + video_xr_rotate(&h[hq]->pose.orientation, &out, &hinge->flat); + p.run = video_xr_snap_edge(hhe, a, b, &mid, &out, &half); + video_xr_rotate(&pose->orientation, &mid, &x); + hinge->point.x = pose->position.x + x.x; + hinge->point.y = pose->position.y + x.y; + hinge->point.z = pose->position.z + x.z; + mid.x = p.run ? 0.0f : 1.0f; + mid.y = p.run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&pose->orientation, &mid, &hinge->dir); + hinge->edge = hhe; + hinge->slot = h[hq]->slot; + hinge->on = hte; + hinge->gap = (float)p.across; + hinge->angle = video_xr_hinge_angle(hinge, &pose->orientation); + /* The tilt turns about the screen's own X, the turn about + * the vertical: whichever lies nearer the line. */ + mid.x = 1.0f; + mid.y = 0.0f; + video_xr_rotate(&pose->orientation, &mid, &x); + tx = fabs((double)x.x * hinge->dir.x + (double)x.y * hinge->dir.y + + (double)x.z * hinge->dir.z); + ty = fabs(hinge->dir.y); + hinge->axis = (tx >= ty) ? VIDEO_XR_STEP_TILT + : VIDEO_XR_STEP_TURN; + } + } + } + return snapped; +} + +unsigned video_xr_snap_hinge(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, float fade, + video_xr_pose_t *pose, video_xr_hinge_t *hinge) +{ + return video_xr_snap_hinge_at(set, slot, skip, 0, width, height, gap, + fade, false, pose, hinge); +} + +unsigned video_xr_snap(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float fade, + video_xr_pose_t *pose) +{ + return video_xr_snap_hinge(set, slot, skip, width, height, 0.0f, fade, + pose, NULL); +} + +/* The screens of set a hinge can take, but those in skip (slots, as + * bits), as video_xr_snap_hinge's targets: a stereo pair once, at most + * RETRO_VIDEO_VIEWS_MAX into s. Returns how many. */ +static unsigned video_xr_rest_screens(const video_xr_quad_set_t *set, + unsigned skip, const video_xr_quad_t **s) +{ + unsigned i; + unsigned n = 0; + unsigned done = 0; + for (i = 0; i < set->num_quads && n < RETRO_VIDEO_VIEWS_MAX; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if ( q->slot >= VIDEO_XR_MENU_SLOT + || q->kind == VIDEO_XR_QUAD_MENU + || (((skip | done) >> q->slot) & 1u) + || set->slots[q->slot].fixed) + continue; + done |= 1u << q->slot; + s[n++] = q; + } + return n; +} + +/* Whether a screen a by b metres from its middle to its sides, inv its + * pose's inverse, rests with its edge he where a hinge whole at gap + * metres engages it on q's edge te; the pair into p. Unmoved, its edge + * is the hinge's own, so whole is the engaged test's hold. */ +static bool video_xr_rest_pair(const video_xr_quad_t *q, + const video_xr_pose_t *inv, double a, double b, unsigned he, + unsigned te, double gap, video_xr_snap_pair_t *p) +{ + video_xr_pose_t rel; + video_xr_compose(inv, &q->pose, &rel); + video_xr_snap_pair(&rel, q->width * 0.5, q->height * 0.5, a, b, he, te, + gap, false, p); + return video_xr_snap_engaged(p, (he < 2) ? a : b, gap); +} + +/* Whether screen c, taken where it rests with nothing else pulling yet + * (a grab's fade of none), stays unmoved held whole by the hinge of + * its edge edge on target's edge on, the screens in skip held; so a + * hinge shown at rest stays hinged when its screen is taken. */ +static bool video_xr_rest_holds(const video_xr_quad_set_t *set, + const video_xr_quad_t *c, const video_xr_pose_t *hand, unsigned skip, + unsigned target, unsigned edge, unsigned on, float gap) +{ + video_xr_hinge_t k; + video_xr_pose_t moved = *hand; + const video_xr_quat_t *q = &c->pose.orientation; + const video_xr_quat_t *m = &moved.orientation; + /* The turn by its dot over both lengths: video_xr_quat_angle reads a + * float turn, unit only to rounding, hundredths of a degree off + * itself. */ + return (video_xr_snap_hinge_at(set, c->slot, skip, 1u << target, + c->width, c->height, gap, 0.0f, false, &moved, &k) + & VIDEO_XR_SNAP_HINGE) + && k.slot == target && k.edge == edge && k.on == on + && fabs((double)moved.position.x - c->pose.position.x) + <= VIDEO_XR_SNAP_TIE + && fabs((double)moved.position.y - c->pose.position.y) + <= VIDEO_XR_SNAP_TIE + && fabs((double)moved.position.z - c->pose.position.z) + <= VIDEO_XR_SNAP_TIE + && fabs(video_xr_quat_dot(m, q)) + >= sqrt(video_xr_quat_dot(m, m) * video_xr_quat_dot(q, q)) + * cos(VIDEO_XR_SNAP_FOLD_TIE * VIDEO_XR_PI / 360.0); +} + +/* hand turned on about the middle of c's edge he until the snap's turn + * to target, parallel or square about its up as video_xr_snap_target, + * shows c from it: c turned y degrees off short of twice + * VIDEO_XR_SNAP_ANGLE shows from x, where x less the turn's pull + * (2r - x) is y. */ +static void video_xr_rest_unturn(const video_xr_quad_t *c, + const video_xr_quad_t *target, unsigned he, video_xr_pose_t *hand) +{ + unsigned i; + double half, x, k; + video_xr_pose_t goal, at, by; + video_xr_vec3_t mid, out, e, f; + double r[3] = { 0.0, 0.0, 0.0 }; + double y = 2.0 * VIDEO_XR_SNAP_ANGLE; + double n = 0.0; + double s = 1.0; + const video_xr_quat_t *qc = &c->pose.orientation; + video_xr_pose_identity(&goal); + for (i = 0; i < 3; i++) + { + /* The turn off each goal as an axis and angle: acos would lose a + * turn of hundredths of a degree to float rounding. */ + double v[3], m, a, d; + const video_xr_quat_t *g = &goal.orientation; + video_xr_snap_goal(target, i, &goal.orientation); + v[0] = (double)g->w * qc->x - (double)g->x * qc->w + - (double)g->y * qc->z + (double)g->z * qc->y; + v[1] = (double)g->w * qc->y + (double)g->x * qc->z + - (double)g->y * qc->w - (double)g->z * qc->x; + v[2] = (double)g->w * qc->z - (double)g->x * qc->y + + (double)g->y * qc->x - (double)g->z * qc->w; + d = video_xr_quat_dot(g, qc); + m = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + a = 2.0 * atan2(m, fabs(d)) * 180.0 / VIDEO_XR_PI; + if (a < y) + { + at = goal; + y = a; + n = m; + s = (d < 0.0) ? -1.0 : 1.0; + r[0] = v[0]; + r[1] = v[1]; + r[2] = v[2]; + } + } + video_xr_snap_edge(he, c->width * 0.5, c->height * 0.5, &mid, &out, + &half); + /* A turn to the goal that moves c less than video_xr_rest_exact sees + * is rounding, which a flat rest on a turned target carries. */ + if ( n <= 0.0 + || (y <= VIDEO_XR_SNAP_FOLD_TIE + && y * VIDEO_XR_PI / 180.0 + * sqrt((double)mid.x * mid.x + (double)mid.y * mid.y) + <= VIDEO_XR_SNAP_TIE)) + return; + x = (y + 2.0 * VIDEO_XR_SNAP_ANGLE) * 0.5; + k = s * sin(x * VIDEO_XR_PI / 360.0) / n; + video_xr_pose_identity(&by); + by.orientation.x = (float)(r[0] * k); + by.orientation.y = (float)(r[1] * k); + by.orientation.z = (float)(r[2] * k); + by.orientation.w = (float)cos(x * VIDEO_XR_PI / 360.0); + video_xr_compose(&at, &by, &at); + hand->orientation = at.orientation; + video_xr_rotate(qc, &mid, &e); + video_xr_rotate(&hand->orientation, &mid, &f); + hand->position.x += e.x - f.x; + hand->position.y += e.y - f.y; + hand->position.z += e.z - f.z; +} + +/* Where a hand taking screen c, resting in the hinge of its edge he on + * target's edge te, holds it (video_xr_rest_take). */ +static void video_xr_rest_hand(const video_xr_quad_t *c, + const video_xr_quad_t *target, unsigned he, unsigned te, double gap, + video_xr_pose_t *hand) +{ + video_xr_pose_t inv, rel; + video_xr_snap_pair_t p; + video_xr_vec3_t m, w; + double s = video_xr_snap_stop_from(gap) + VIDEO_XR_SNAP_STOP_ARM; + *hand = c->pose; + video_xr_rest_unturn(c, target, he, hand); + video_xr_pose_invert(&c->pose, &inv); + video_xr_compose(&inv, &target->pose, &rel); + video_xr_snap_pair(&rel, target->width * 0.5, target->height * 0.5, + c->width * 0.5, c->height * 0.5, he, te, gap, false, &p); + if (fabs(p.across) > VIDEO_XR_SNAP_TIE) + return; + m.x = (float)(-s * p.away[0]); + m.y = (float)(-s * p.away[1]); + m.z = (float)(-s * p.away[2]); + video_xr_rotate(&c->pose.orientation, &m, &w); + hand->position.x += w.x; + hand->position.y += w.y; + hand->position.z += w.z; +} + +/* Whether pair p, gap metres the set gap, lies where a let-go leaves + * it: not apart within VIDEO_XR_HINGE_SETTLE of the gap but off it. */ +static bool video_xr_rest_settled(const video_xr_snap_pair_t *p, + double gap) +{ + double d = fabs(p->across - gap); + return p->across <= VIDEO_XR_SNAP_TIE || d <= VIDEO_XR_SNAP_TIE + || d > VIDEO_XR_HINGE_SETTLE; +} + +/* Whether screen c, carried, rests exactly in the hinge of its edge he + * on target's edge te: engaged as the poses are, where a let-go leaves + * it, and held there unmoved from where a hand taking it holds it. The + * one test for both a hinge shown at rest and one taken up. */ +static bool video_xr_rest_exact(const video_xr_quad_set_t *set, + const video_xr_quad_t *c, const video_xr_quad_t *target, + unsigned skip, unsigned he, unsigned te, float gap) +{ + video_xr_pose_t inv, hand; + video_xr_snap_pair_t p; + video_xr_pose_invert(&c->pose, &inv); + if ( !video_xr_rest_pair(target, &inv, c->width * 0.5, + c->height * 0.5, he, te, gap, &p) + || !video_xr_rest_settled(&p, gap)) + return false; + video_xr_rest_hand(c, target, he, te, gap, &hand); + return video_xr_rest_holds(set, c, &hand, skip, target->slot, he, te, + gap); +} + +/* The first edge of screen c resting exactly hinged on an edge of + * target, c carried: into r; false with none. */ +static bool video_xr_rest_on(const video_xr_quad_set_t *set, + const video_xr_quad_t *c, const video_xr_quad_t *target, + unsigned skip, float gap, video_xr_rest_t *r) +{ + unsigned he, te; + for (he = 0; he < 4; he++) + for (te = 0; te < 4; te++) + if (video_xr_rest_exact(set, c, target, skip, he, te, gap)) + { + r->slot = (unsigned char)c->slot; + r->edge = (unsigned char)he; + r->target = (unsigned char)target->slot; + r->on = (unsigned char)te; + return true; + } + return false; +} + +void video_xr_rest_take(const video_xr_quad_set_t *set, unsigned slot, + float width, float height, float gap, const video_xr_hinge_t *h, + video_xr_pose_t *pose) +{ + unsigned i; + video_xr_quad_t c; + if (!set) + return; + memset(&c, 0, sizeof(c)); + c.pose = *pose; + c.width = width; + c.height = height; + c.slot = slot; + for (i = 0; i < set->num_quads; i++) + if ( set->quads[i].slot == h->slot + && set->quads[i].kind != VIDEO_XR_QUAD_MENU) + { + video_xr_rest_hand(&c, &set->quads[i], h->edge, h->on, gap, pose); + return; + } +} + +unsigned video_xr_rest_hinges(const video_xr_quad_set_t *set, + unsigned skip, float gap, video_xr_rest_t *rest, unsigned max) +{ + unsigned i, j; + const video_xr_quad_t *s[RETRO_VIDEO_VIEWS_MAX]; + unsigned n = 0; + unsigned ns = set ? video_xr_rest_screens(set, skip, s) : 0; + for (i = 0; i < ns; i++) + for (j = i + 1; j < ns && n < max; j++) + if ( video_xr_rest_on(set, s[i], s[j], skip, gap, &rest[n]) + || video_xr_rest_on(set, s[j], s[i], skip, gap, &rest[n])) + n++; + return n; +} + +bool video_xr_rest_hinge(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_pose_t *pose, video_xr_hinge_t *hinge) +{ + unsigned i, n; + video_xr_quad_t c; + video_xr_rest_t r; + video_xr_pose_t moved; + const video_xr_quad_t *s[RETRO_VIDEO_VIEWS_MAX]; + unsigned on = 0; + if (!set || slot >= VIDEO_XR_MENU_SLOT) + return false; + memset(&c, 0, sizeof(c)); + c.pose = *pose; + c.width = width; + c.height = height; + c.slot = slot; + n = video_xr_rest_screens(set, skip | (1u << slot), s); + for (i = 0; i < n; i++) + if (video_xr_rest_on(set, &c, s[i], skip, gap, &r)) + on |= 1u << s[i]->slot; + if (!on) + return false; + /* Of those, the one the snap ranks best; it may rank another edge of + * that screen, which must rest exactly too. */ + moved = *pose; + if (!(video_xr_snap_hinge(set, slot, ~on, width, height, gap, 1.0f, + &moved, hinge) & VIDEO_XR_SNAP_HINGE)) + return false; + for (i = 0; i < n; i++) + if (s[i]->slot == hinge->slot) + break; + return i < n && video_xr_rest_exact(set, &c, s[i], skip, hinge->edge, + hinge->on, gap); +} + +/* Whether a screen turned q has its edge along its axis run (0 x, 1 y) + * within VIDEO_XR_SNAP_ANGLE degrees of h's line, where a grab's hinge + * holds it whole. */ +static bool video_xr_hinge_along(const video_xr_hinge_t *h, + const video_xr_quat_t *q, unsigned run) +{ + video_xr_vec3_t mid, e; + mid.x = run ? 0.0f : 1.0f; + mid.y = run ? 1.0f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(q, &mid, &e); + return fabs((double)e.x * h->dir.x + (double)e.y * h->dir.y + + (double)e.z * h->dir.z) + >= cos(VIDEO_XR_SNAP_ANGLE * VIDEO_XR_PI / 180.0); +} + +bool video_xr_hinge_settle(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_hinge_t *h, const video_xr_pose_t *hand, + video_xr_pose_t *pose) +{ + unsigned i; + video_xr_quad_t c; + video_xr_hinge_t k; + video_xr_snap_pair_t p; + video_xr_pose_t inv; + video_xr_vec3_t m, w; + const video_xr_quad_t *target = NULL; + double across = 0.0; + double along = 0.0; + double nearest = 2.0 * VIDEO_XR_SNAP_DIST; + if (!set || h->slot >= VIDEO_XR_MENU_SLOT || ((skip >> h->slot) & 1u)) + return false; + for (i = 0; i < set->num_quads && !target; i++) + if ( set->quads[i].slot == h->slot + && set->quads[i].kind != VIDEO_XR_QUAD_MENU) + target = &set->quads[i]; + if (!target) + return false; + memset(&c, 0, sizeof(c)); + c.pose = *hand; + c.width = width; + c.height = height; + c.slot = slot; + if ( !(video_xr_snap_hinge_at(set, slot, skip, 1u << h->slot, width, + height, gap, 0.0f, false, &c.pose, &k) + & VIDEO_XR_SNAP_HINGE) + || k.slot != h->slot || k.edge != h->edge || k.on != h->on) + return false; + video_xr_pose_invert(&c.pose, &inv); + video_xr_rest_pair(target, &inv, width * 0.5, height * 0.5, h->edge, + h->on, gap, &p); + /* The snap turns it parallel from farther than the hinge engages. */ + if (!video_xr_hinge_along(&k, &hand->orientation, p.run)) + return false; + /* Pushed or touching, the stop has put it where it stays. */ + if ( p.across > VIDEO_XR_SNAP_TIE + && fabs(p.across - gap) <= VIDEO_XR_HINGE_SETTLE) + across = gap - p.across; + for (i = 0; i < 3; i++) + if (fabs(p.level[i]) < nearest) + { + nearest = fabs(p.level[i]); + along = p.level[i]; + } + m.x = (float)(across * p.away[0] + (p.run ? 0.0 : along)); + m.y = (float)(across * p.away[1] + (p.run ? along : 0.0)); + m.z = (float)(across * p.away[2]); + video_xr_rotate(&c.pose.orientation, &m, &w); + c.pose.position.x += w.x; + c.pose.position.y += w.y; + c.pose.position.z += w.z; + *pose = c.pose; + return video_xr_rest_exact(set, &c, target, skip, h->edge, h->on, gap); +} + +bool video_xr_grab_settle(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_hinge_t *h, video_xr_pose_t *pose) +{ + video_xr_pose_t hand; + if (!targets) + return false; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &hand, NULL, NULL); + return video_xr_hinge_settle(targets, g->slot, skip, g->width, + g->height, g->gap * 0.01f, h, &hand, pose); +} + +/* Past this pitch of the ray, in degrees, its heading blends evenly + * into the one the hand's up gives, which straight up or down is the + * only one: so it never jumps there, but a twist there turns it. */ +#define VIDEO_XR_HEADING_BLEND 80.0 + +/* The heading of aim's ray, as a turn about the vertical. */ +static void video_xr_heading(const video_xr_quat_t *aim, + video_xr_pose_t *out) +{ + double x, z, t, half; + video_xr_vec3_t v, ray, up; + v.x = 0.0f; + v.y = 0.0f; + v.z = -1.0f; + video_xr_rotate(aim, &v, &ray); + v.y = 1.0f; + v.z = 0.0f; + video_xr_rotate(aim, &v, &up); + x = ray.x; + z = ray.z; + t = fabs(ray.y); + t = (asin(t < 1.0 ? t : 1.0) * 180.0 / VIDEO_XR_PI + - VIDEO_XR_HEADING_BLEND) / (90.0 - VIDEO_XR_HEADING_BLEND); + if (t > 0.0) + { + /* Tipped up, the hand's up points back from the heading, tipped + * down along it; this far tipped, it is never upright. */ + double ux = -ray.y * up.x; + double uz = -ray.y * up.z; + double n = sqrt(x * x + z * z); + double m = sqrt(ux * ux + uz * uz); + if (t > 1.0) + t = 1.0; + x = (n > 0.0 ? (1.0 - t) * x / n : 0.0) + t * ux / m; + z = (n > 0.0 ? (1.0 - t) * z / n : 0.0) + t * uz / m; + } + half = atan2(-x, -z) / 2.0; + video_xr_pose_identity(out); + out->orientation.y = (float)sin(half); + out->orientation.w = (float)cos(half); +} + +void video_xr_grab_hold(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quat_t *q) +{ + video_xr_pose_t head, turn; + video_xr_heading(&aim->orientation, &head); + video_xr_pose_invert(&head, &head); + video_xr_pose_identity(&turn); + turn.orientation = *q; + video_xr_compose(&head, &turn, &turn); + g->turn = turn.orientation; +} + +/* Where g puts its item for an aim pose, with no snap or clamp. */ +static void video_xr_grab_aimed(const video_xr_grab_t *g, + const video_xr_pose_t *aim, video_xr_pose_t *pose) +{ + video_xr_compose(aim, &g->rel, pose); + if (!g->tilt) + { + video_xr_pose_t head, turn; + video_xr_heading(&aim->orientation, &head); + video_xr_pose_identity(&turn); + turn.orientation = g->turn; + video_xr_compose(&head, &turn, &turn); + pose->orientation = turn.orientation; + } +} + +/* video_xr_grab_pose, and the hinge the snap holds unless NULL. */ +static bool video_xr_grab_snapped(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start, video_xr_pose_t *pose, + video_screen_xr_t *out, unsigned *snapped, video_xr_hinge_t *hinge) +{ + video_screen_xr_t s, c; + bool clamped; + unsigned snaps = 0; + video_xr_grab_aimed(g, aim, pose); + if (targets) + { + video_xr_hinge_t kept; + unsigned whole = video_xr_grab_kept(g, aim, targets, start, &kept) + ? 1u << kept.slot : 0u; + snaps = video_xr_snap_hinge_at(targets, g->slot, skip, whole, + g->width, g->height, g->gap * 0.01f, + video_xr_snap_fade(start, pose), g->through != 0, pose, hinge); + } + if (snapped) + *snapped = snaps; + video_views_xr_reset(&s, 1); + video_xr_item_settings(&g->base, pose, &s); + c = s; + video_views_xr_normalize(&c); + clamped = c.x != s.x || c.y != s.y || c.z != s.z; + if (clamped) + { + pose->position.x = g->base.x + c.x; + pose->position.y = g->base.y + c.y; + pose->position.z = g->base.z + c.z; + } + if (out) + { + out->x = c.x; + out->y = c.y; + out->z = c.z; + out->yaw = c.yaw; + out->pitch = c.pitch; + out->roll = c.roll; + } + return clamped; +} + +bool video_xr_grab_pose(const video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, video_xr_pose_t *pose, + video_screen_xr_t *out, unsigned *snapped) +{ + return video_xr_grab_snapped(g, aim, targets, skip, start, pose, out, + snapped, NULL); +} + +/* A stick axis past the deadzone, as a fraction of the travel left. */ +static double video_xr_deflection(float v) +{ + double a = fabs(v); + if (a <= VIDEO_XR_EDIT_DEADZONE) + return 0.0; + if (a > 1.0) + a = 1.0; + a = (a - VIDEO_XR_EDIT_DEADZONE) / (1.0 - VIDEO_XR_EDIT_DEADZONE); + return (v < 0.0f) ? -a : a; +} + +void video_xr_grab_stick(video_xr_grab_t *g, float x, float y, double dt, + float *scale) +{ + double push = video_xr_deflection(y) * VIDEO_XR_EDIT_PUSH_RATE * dt; + double grow = video_xr_deflection(x) * VIDEO_XR_EDIT_SCALE_RATE * dt; + if (push != 0.0) + { + /* The aim's ray is its -Z. */ + double z = g->rel.position.z - push; + if (push < 0.0 && z > -VIDEO_XR_EDIT_REACH) + z = (g->rel.position.z > -VIDEO_XR_EDIT_REACH) + ? g->rel.position.z : -VIDEO_XR_EDIT_REACH; + g->rel.position.z = (float)z; + } + if (grow != 0.0) + { + video_screen_xr_t s; + video_views_xr_reset(&s, 1); + s.scale = (float)(*scale * pow(2.0, grow)); + video_views_xr_normalize(&s); + *scale = s.scale; + } +} + +/* g's item turned about the vertical through its middle and tipped + * about its own X, half and tip being half the angles, as a quaternion + * takes them. */ +static void video_xr_grab_rotate(video_xr_grab_t *g, + const video_xr_pose_t *aim, double half, double tip) +{ + double n, s; + video_xr_pose_t turn, item; + video_xr_vec3_t axis; + video_xr_quat_t *q; + if (half == 0.0 && tip == 0.0) + return; + axis.x = 0.0f; + axis.y = 1.0f; + axis.z = 0.0f; + video_xr_pose_identity(&item); + if (g->tilt) + { + video_xr_pose_t inv; + /* About the anchor's up in the aim's frame, before g->rel. */ + video_xr_pose_invert(aim, &inv); + video_xr_rotate(&inv.orientation, &axis, &axis); + item.orientation = g->rel.orientation; + q = &g->rel.orientation; + } + else + { + item.orientation = g->turn; + q = &g->turn; + } + s = sin(half); + video_xr_pose_identity(&turn); + turn.orientation.x = (float)(axis.x * s); + turn.orientation.y = (float)(axis.y * s); + turn.orientation.z = (float)(axis.z * s); + turn.orientation.w = (float)cos(half); + video_xr_compose(&turn, &item, &item); + /* Composed after it: about its own X, carried or tilting. */ + video_xr_pose_identity(&turn); + turn.orientation.x = (float)sin(tip); + turn.orientation.w = (float)cos(tip); + video_xr_compose(&item, &turn, &item); + n = sqrt((double)item.orientation.x * item.orientation.x + + (double)item.orientation.y * item.orientation.y + + (double)item.orientation.z * item.orientation.z + + (double)item.orientation.w * item.orientation.w); + if (n > 0.0) + { + q->x = (float)(item.orientation.x / n); + q->y = (float)(item.orientation.y / n); + q->z = (float)(item.orientation.z / n); + q->w = (float)(item.orientation.w / n); + } +} + +/* video_xr_grab_rotate, but lid's axis (lid unless NULL) about the + * middle of g's item's edge lid->edge. */ +static void video_xr_grab_rotate_lid(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *lid, + double half, double tip) +{ + double l; + video_xr_pose_t pose; + video_xr_vec3_t mid, out, e, f; + if (!lid) + { + video_xr_grab_rotate(g, aim, half, tip); + return; + } + if (lid->axis == VIDEO_XR_STEP_TURN) + { + video_xr_grab_rotate(g, aim, 0.0, tip); + tip = 0.0; + } + else + { + video_xr_grab_rotate(g, aim, half, 0.0); + half = 0.0; + } + if (half == 0.0 && tip == 0.0) + return; + video_xr_snap_edge(lid->edge, g->width * 0.5, g->height * 0.5, &mid, + &out, &l); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + video_xr_rotate(&pose.orientation, &mid, &e); + video_xr_grab_rotate(g, aim, half, tip); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + video_xr_rotate(&pose.orientation, &mid, &f); + pose.position.x += e.x - f.x; + pose.position.y += e.y - f.y; + pose.position.z += e.z - f.z; + video_xr_grab_put(g, aim, &pose); +} + +unsigned video_xr_grab_turn_lid(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *lid, float x, + float y, double dt) +{ + double half, tip; + double lock = tan(VIDEO_XR_EDIT_TURN_LOCK * VIDEO_XR_PI / 180.0); + /* A push a little off one axis is meant along it: a turn takes no + * tilt, a tilt no turn. */ + if (fabs(y) < fabs(x) * lock) + y = 0.0f; + else if (fabs(x) < fabs(y) * lock) + x = 0.0f; + half = video_xr_deflection(x) * VIDEO_XR_EDIT_TURN_RATE + * dt * VIDEO_XR_PI / 360.0; + /* Up tips its top back: about its own -X. */ + tip = -video_xr_deflection(y) * VIDEO_XR_EDIT_TURN_RATE + * dt * VIDEO_XR_PI / 360.0; + video_xr_grab_rotate_lid(g, aim, lid, half, tip); + return (half != 0.0 ? VIDEO_XR_STEP_TURN : 0u) + | (tip != 0.0 ? VIDEO_XR_STEP_TILT : 0u); +} + +unsigned video_xr_grab_turn(video_xr_grab_t *g, const video_xr_pose_t *aim, + float x, float y, double dt) +{ + return video_xr_grab_turn_lid(g, aim, NULL, x, y, dt); +} + +/* Degrees within half a turn either way. */ +static double video_xr_wrap(double deg) +{ + deg = fmod(deg, 360.0); + if (deg > 180.0) + deg -= 360.0; + else if (deg <= -180.0) + deg += 360.0; + return deg; +} + +/* An item's turn from facing head across the floor, in degrees; false + * with head right above or below its middle. */ +static bool video_xr_facing(const video_xr_pose_t *item, + const video_xr_vec3_t *head, double *deg) +{ + video_xr_pose_t back, h; + double dx = (double)head->x - item->position.x; + double dz = (double)head->z - item->position.z; + if (dx * dx + dz * dz < 1e-8) + return false; + /* It faces its +Z, the heading reads a ray's -Z: so the heading of + * it turned half round, which lying flat blends as a ray's does. */ + video_xr_pose_identity(&back); + back.orientation.y = 1.0f; + back.orientation.w = 0.0f; + video_xr_compose(item, &back, &back); + video_xr_heading(&back.orientation, &h); + *deg = video_xr_wrap((2.0 * atan2(h.orientation.y, h.orientation.w) + - atan2(-dx, -dz)) * 180.0 / VIDEO_XR_PI); + return true; +} + +/* An item's tilt from upright about its own X, in degrees; false on its + * side, with no upright about that line. */ +static bool video_xr_upright(const video_xr_quat_t *q, double *deg) +{ + video_xr_vec3_t v, front, up; + v.x = 0.0f; + v.y = 0.0f; + v.z = 1.0f; + video_xr_rotate(q, &v, &front); + v.y = 1.0f; + v.z = 0.0f; + video_xr_rotate(q, &v, &up); + if ((double)front.y * front.y + (double)up.y * up.y < 1e-8) + return false; + *deg = atan2(-(double)front.y, (double)up.y) * 180.0 / VIDEO_XR_PI; + return true; +} + +/* The step, every size degrees (0: none), within + * VIDEO_XR_EDIT_TURN_HOLD degrees of deg, if one is. */ +static bool video_xr_step_near(double deg, double size, double *step) +{ + double s; + if (size <= 0.0) + return false; + s = floor(deg / size + 0.5) * size; + if (fabs(deg - s) > VIDEO_XR_EDIT_TURN_HOLD) + return false; + *step = s; + return true; +} + +unsigned video_xr_grab_step(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_vec3_t *head, float size, unsigned axes, float *rest, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, const video_xr_hinge_t *lid) +{ + double deg, step; + video_xr_pose_t pose; + unsigned held = 0; + /* Where the stick alone has taken it. */ + video_xr_grab_rotate_lid(g, aim, lid, + (axes & VIDEO_XR_STEP_TURN) ? rest[0] * VIDEO_XR_PI / 360.0 : 0.0, + (axes & VIDEO_XR_STEP_TILT) ? rest[1] * VIDEO_XR_PI / 360.0 : 0.0); + if (axes & VIDEO_XR_STEP_TURN) + rest[0] = 0.0f; + if (axes & VIDEO_XR_STEP_TILT) + rest[1] = 0.0f; + if (!head) + axes &= ~VIDEO_XR_STEP_TURN; + if (!axes) + return 0; + if (targets) + { + unsigned snapped = 0; + video_xr_grab_pose(g, aim, targets, skip, start, &pose, NULL, + &snapped); + if (snapped & VIDEO_XR_SNAP_ALL) + return 0; + } + /* The tilt first: a turn about the vertical keeps it. */ + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + if ( (axes & VIDEO_XR_STEP_TILT) + && video_xr_upright(&pose.orientation, °) + && video_xr_step_near(deg, size, &step)) + { + video_xr_grab_rotate_lid(g, aim, lid, 0.0, + (step - deg) * VIDEO_XR_PI / 360.0); + rest[1] = (float)(deg - step); + held |= VIDEO_XR_STEP_TILT; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + } + if ( (axes & VIDEO_XR_STEP_TURN) + && video_xr_facing(&pose, head, °) + && video_xr_step_near(deg, size, &step)) + { + video_xr_grab_rotate_lid(g, aim, lid, + (step - deg) * VIDEO_XR_PI / 360.0, 0.0); + rest[0] = (float)(deg - step); + held |= VIDEO_XR_STEP_TURN; + } + return held; +} + +unsigned video_xr_grab_off_step(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_vec3_t *head, float size, + const float *rest) +{ + double deg, step; + video_xr_pose_t pose; + unsigned off = 0; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + /* rest adds to the angles exactly: it turns about the vertical and + * tips about the item's own X. */ + if ( video_xr_upright(&pose.orientation, °) + && !video_xr_step_near(deg + rest[1], size, &step)) + off |= VIDEO_XR_STEP_TILT; + if ( head + && video_xr_facing(&pose, head, °) + && !video_xr_step_near(deg + rest[0], size, &step)) + off |= VIDEO_XR_STEP_TURN; + return off; +} + +/* g's item where it is with no snap, in pose, and in m the middle of + * its edge h->edge from h's line, straight across it. Returns the + * edge's way, as video_xr_snap_edge. */ +static unsigned video_xr_hinge_across(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, + video_xr_pose_t *pose, double *m) +{ + double half; + double d[3]; + video_xr_vec3_t mid, out, v; + unsigned run = video_xr_snap_edge(h->edge, g->width * 0.5, + g->height * 0.5, &mid, &out, &half); + video_xr_grab_pose(g, aim, NULL, 0, NULL, pose, NULL, NULL); + video_xr_rotate(&pose->orientation, &mid, &v); + m[0] = (double)pose->position.x + v.x - h->point.x; + m[1] = (double)pose->position.y + v.y - h->point.y; + m[2] = (double)pose->position.z + v.z - h->point.z; + d[0] = h->dir.x; + d[1] = h->dir.y; + d[2] = h->dir.z; + video_xr_snap_across(m, d); + return run; +} + +/* h's plane's facing: square to its line and to flat. */ +static void video_xr_hinge_normal(const video_xr_hinge_t *h, double *n) +{ + n[0] = (double)h->dir.y * h->flat.z - (double)h->dir.z * h->flat.y; + n[1] = (double)h->dir.z * h->flat.x - (double)h->dir.x * h->flat.z; + n[2] = (double)h->dir.x * h->flat.y - (double)h->dir.y * h->flat.x; +} + +/* Whether g's item, where it is with no snap, has its edge h->edge + * within VIDEO_XR_SNAP_DIST metres of h's plane, VIDEO_XR_SNAP_ANGLE + * degrees of its line's way and at a gap the hinge holds whole. */ +static bool video_xr_hinge_whole(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h) +{ + double m[3], n[3], f[3]; + video_xr_pose_t pose; + unsigned run = video_xr_hinge_across(g, aim, h, &pose, m); + video_xr_hinge_normal(h, n); + f[0] = h->flat.x; + f[1] = h->flat.y; + f[2] = h->flat.z; + return fabs(video_xr_snap_dot(m, n)) <= VIDEO_XR_SNAP_DIST + && video_xr_snap_gap_share(h->gap + video_xr_snap_dot(m, f), + g->gap * 0.01f) >= 1.0 + && video_xr_hinge_along(h, &pose.orientation, run); +} + +/* g's item where it is with no snap, its edge h->edge paired with h's + * target's in targets. False if targets has no such target. */ +static bool video_xr_grab_hinge_pair(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h, video_xr_snap_pair_t *p) +{ + unsigned i; + video_xr_pose_t pose, inv, rel; + if (!targets) + return false; + for (i = 0; i < targets->num_quads; i++) + { + const video_xr_quad_t *q = &targets->quads[i]; + if (q->slot != h->slot || q->kind == VIDEO_XR_QUAD_MENU) + continue; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + video_xr_pose_invert(&pose, &inv); + video_xr_compose(&inv, &q->pose, &rel); + video_xr_snap_pair(&rel, q->width * 0.5, q->height * 0.5, + g->width * 0.5, g->height * 0.5, h->edge, h->on, + g->gap * 0.01f, false, p); + return true; + } + return false; +} + +bool video_xr_grab_near_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h, float reach) +{ + video_xr_snap_pair_t p; + double gap = g->gap * 0.01f; + if (!video_xr_grab_hinge_pair(g, aim, targets, h, &p)) + return false; + return fabs(p.plane) <= reach + && ((p.across < 0.0) + ? video_xr_snap_stop_held(-p.across, gap, g->through != 0) + : video_xr_snap_gap_share(p.across, gap) > 0.0) + && p.overlap > 0.0; +} + +bool video_xr_grab_through(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h) +{ + video_xr_snap_pair_t p; + return video_xr_grab_hinge_pair(g, aim, targets, h, &p) + && p.across < 0.0 + && !video_xr_snap_stop_held(-p.across, g->gap * 0.01f, + g->through != 0); +} + +bool video_xr_grab_kept(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_pose_t *start, video_xr_hinge_t *kept) +{ + video_xr_pose_t pose; + if (!g->rest_slot || !targets) + return false; + memset(kept, 0, sizeof(*kept)); + kept->slot = g->rest_slot - 1u; + kept->on = g->rest_on; + kept->edge = g->rest_edge; + if (video_xr_grab_near_hinge(g, aim, targets, kept, + VIDEO_XR_HINGE_REACH)) + return true; + video_xr_grab_aimed(g, aim, &pose); + return video_xr_snap_fade(start, &pose) < 1.0f; +} + +bool video_xr_grab_marked(const video_xr_grab_t *g, + const video_xr_pose_t *pose, const video_xr_quad_set_t *set, + unsigned holding, video_xr_hinge_t *h) +{ + unsigned i; + unsigned ti = set->num_quads; + unsigned hi = set->num_quads; + if (!g->mark_slot) + return false; + memset(h, 0, sizeof(*h)); + h->slot = g->mark_slot - 1u; + h->on = g->mark_on; + h->edge = g->mark_edge; + /* The other hand can take its target later in the poll that + * publishes it. */ + if (holding & (1u << h->slot)) + return false; + /* On the edges as they are; crossed, on the one drawn later, as the + * quads draw in order. */ + for (i = 0; i < set->num_quads; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if (q->kind == VIDEO_XR_QUAD_MENU) + continue; + if (ti == set->num_quads && q->slot == h->slot) + ti = i; + if (hi == set->num_quads && q->slot == g->slot) + hi = i; + } + /* Its target not in this frame's quads: no line, no rim cut. */ + if (ti == set->num_quads) + return false; + video_xr_hinge_place(h, pose, g->width, g->height, &set->quads[ti], + hi > ti); + return true; +} + +unsigned video_xr_grab_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start, video_xr_pose_t *pose, + video_xr_hinge_t *hinge) +{ + unsigned snaps = 0; + video_xr_hinge_t own; + if (!hinge) + hinge = &own; + /* Not a hinge only other pulls put it on: a swing about a line its + * own edge is far from would carry it round. */ + if ( video_xr_grab_snapped(g, aim, targets, skip, start, pose, NULL, + &snaps, hinge) + || ( (snaps & VIDEO_XR_SNAP_HINGE) + && !video_xr_hinge_whole(g, aim, hinge))) + snaps &= ~VIDEO_XR_SNAP_HINGE; + return snaps; +} + +void video_xr_grab_put(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_pose_t *pose) +{ + video_xr_pose_t inv, rel; + video_xr_pose_invert(aim, &inv); + video_xr_compose(&inv, pose, &rel); + g->rel.position = rel.position; + if (g->tilt) + g->rel.orientation = rel.orientation; + else + video_xr_grab_hold(g, aim, &pose->orientation); +} + +void video_xr_grab_swing(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h, double deg) +{ + double s, half; + video_xr_pose_t pose, by, at; + video_xr_vec3_t mid, out, e; + if (deg == 0.0) + return; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + video_xr_snap_edge(h->edge, g->width * 0.5, g->height * 0.5, &mid, + &out, &half); + video_xr_rotate(&pose.orientation, &mid, &e); + e.x += pose.position.x; + e.y += pose.position.y; + e.z += pose.position.z; + s = sin(deg * VIDEO_XR_PI / 360.0); + video_xr_pose_identity(&by); + by.orientation.x = (float)(h->dir.x * s); + by.orientation.y = (float)(h->dir.y * s); + by.orientation.z = (float)(h->dir.z * s); + by.orientation.w = (float)cos(deg * VIDEO_XR_PI / 360.0); + /* About the line through its edge's middle: there, turned, and + * back, so the gap keeps. */ + video_xr_pose_identity(&at); + at.position.x = -e.x; + at.position.y = -e.y; + at.position.z = -e.z; + video_xr_compose(&at, &pose, &pose); + video_xr_compose(&by, &pose, &pose); + pose.position.x += e.x; + pose.position.y += e.y; + pose.position.z += e.z; + video_xr_grab_put(g, aim, &pose); +} + +void video_xr_grab_put_hinge(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h, const video_xr_pose_t *pose, bool turned) +{ + if (turned) + { + double half; + video_xr_pose_t under, at; + video_xr_vec3_t mid, out, e, f; + video_xr_snap_edge(h->edge, g->width * 0.5, g->height * 0.5, &mid, + &out, &half); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &under, NULL, NULL); + video_xr_rotate(&under.orientation, &mid, &e); + video_xr_rotate(&pose->orientation, &mid, &f); + at = under; + at.orientation = pose->orientation; + at.position.x += e.x - f.x; + at.position.y += e.y - f.y; + at.position.z += e.z - f.z; + video_xr_grab_put(g, aim, &at); + } + video_xr_grab_put_line(g, aim, h); +} + +void video_xr_grab_put_line(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h) +{ + double m[3], n[3], f[3], o, k; + video_xr_pose_t pose; + double set = g->gap * 0.01; + video_xr_hinge_across(g, aim, h, &pose, m); + video_xr_hinge_normal(h, n); + f[0] = h->flat.x; + f[1] = h->flat.y; + f[2] = h->flat.z; + o = video_xr_snap_dot(m, n); + /* Clicked, to the set gap it shows already: a drift while unhinged, + * as past the hinge's limit and back, starts again from there. */ + k = set - h->gap - video_xr_snap_dot(m, f); + if ( fabs(h->gap - set) > VIDEO_XR_SNAP_TIE + || fabs(k) > VIDEO_XR_SNAP_CLICK) + k = 0.0; + pose.position.x += (float)(k * f[0] - o * n[0]); + pose.position.y += (float)(k * f[1] - o * n[1]); + pose.position.z += (float)(k * f[2] - o * n[2]); + video_xr_grab_put(g, aim, &pose); +} + +unsigned video_xr_grab_turn_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float x, + float y, double dt) +{ + double deg, s; + unsigned acted; + video_xr_vec3_t v, w; + double lock = tan(VIDEO_XR_EDIT_TURN_LOCK * VIDEO_XR_PI / 180.0); + if (!h) + return video_xr_grab_turn(g, aim, x, y, dt); + if (fabs(y) < fabs(x) * lock) + y = 0.0f; + else if (fabs(x) < fabs(y) * lock) + x = 0.0f; + /* The turn's own way, about the vertical, or the tilt's, about the + * item's own X with up tipping its top back: along the line. */ + if (h->axis == VIDEO_XR_STEP_TILT) + { + video_xr_pose_t pose; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + v.x = 1.0f; + v.y = 0.0f; + v.z = 0.0f; + video_xr_rotate(&pose.orientation, &v, &w); + deg = -video_xr_deflection(y); + y = 0.0f; + } + else + { + w.x = 0.0f; + w.y = 1.0f; + w.z = 0.0f; + deg = video_xr_deflection(x); + x = 0.0f; + } + s = (double)w.x * h->dir.x + (double)w.y * h->dir.y + + (double)w.z * h->dir.z; + deg *= VIDEO_XR_EDIT_TURN_RATE * dt * ((s < 0.0) ? -1.0 : 1.0); + acted = video_xr_grab_turn(g, aim, x, y, dt); + video_xr_grab_swing(g, aim, h, deg); + return acted | ((deg != 0.0) ? h->axis : 0u); +} + +/* Which of rest the hinge's axis keeps. */ +static unsigned video_xr_hinge_rest(const video_xr_hinge_t *h) +{ + return (h->axis == VIDEO_XR_STEP_TURN) ? 0 : 1; +} + +unsigned video_xr_grab_step_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + unsigned axes, float *rest, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start) +{ + double deg, step; + video_xr_pose_t pose; + unsigned k = video_xr_hinge_rest(h); + unsigned other = h->axis ^ (VIDEO_XR_STEP_TURN | VIDEO_XR_STEP_TILT); + /* Where the stick alone has taken it. */ + if (axes & other) + { + video_xr_grab_rotate(g, aim, + (other & VIDEO_XR_STEP_TURN) ? rest[0] * VIDEO_XR_PI / 360.0 + : 0.0, + (other & VIDEO_XR_STEP_TILT) ? rest[1] * VIDEO_XR_PI / 360.0 + : 0.0); + rest[k ^ 1] = 0.0f; + } + if (!(axes & h->axis)) + return 0; + video_xr_grab_swing(g, aim, h, rest[k]); + rest[k] = 0.0f; + if ( targets + && (video_xr_grab_hinge(g, aim, targets, skip, start, &pose, NULL) + & VIDEO_XR_SNAP_TURN)) + return 0; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + deg = video_xr_hinge_angle(h, &pose.orientation); + if (!video_xr_step_near(deg, size, &step)) + return 0; + video_xr_grab_swing(g, aim, h, step - deg); + rest[k] = (float)(deg - step); + return h->axis; +} + +unsigned video_xr_grab_off_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + const float *rest) +{ + double step; + video_xr_pose_t pose; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + return video_xr_step_near(video_xr_hinge_angle(h, &pose.orientation) + + rest[video_xr_hinge_rest(h)], size, &step) ? 0u : h->axis; +} + +/* A grey premultiplied texel of the laser's image. */ +static void video_xr_cursor_texel(uint8_t *px, unsigned x, unsigned y, + uint8_t level, uint8_t alpha) +{ + uint8_t *p = px + ((size_t)y * VIDEO_XR_CURSOR_W + x) * 4; + p[0] = level; + p[1] = level; + p[2] = level; + p[3] = alpha; +} + +/* How many texels x, y is in from the nearest edge of a w by h part. */ +static unsigned video_xr_rim_depth(unsigned x, unsigned y, unsigned w, + unsigned h) +{ + unsigned d = x; + if (y < d) + d = y; + if (w - 1 - x < d) + d = w - 1 - x; + if (h - 1 - y < d) + d = h - 1 - y; + return d; +} + +/* An outline's square at x0, at alpha, without the rims of the edges in + * cut (bit e for edge e as video_xr_outline_crop takes them, rows top + * down) VIDEO_XR_OUTLINE_CROP texels deep. */ +static void video_xr_outline_pixels(uint8_t *px, unsigned x0, + uint8_t alpha, unsigned cut) { unsigned x, y; + const unsigned end = VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP; + for (y = 0; y < VIDEO_XR_OUTLINE_DIM; y++) + for (x = 0; x < VIDEO_XR_OUTLINE_DIM; x++) + { + unsigned band = video_xr_rim_depth(x, y, VIDEO_XR_OUTLINE_DIM, + VIDEO_XR_OUTLINE_DIM) / VIDEO_XR_OUTLINE_BAND; + if ( band < 3 + && !((cut & 1u) && y < VIDEO_XR_OUTLINE_CROP) + && !((cut & 2u) && y >= end) + && !((cut & 4u) && x >= end) + && !((cut & 8u) && x < VIDEO_XR_OUTLINE_CROP)) + video_xr_cursor_texel(px, x0 + x, y, (band == 1) ? alpha : 0, + alpha); + } +} + +void video_xr_cursor_pixels(uint8_t *px) +{ + unsigned x, y, k; const float c = (VIDEO_XR_CURSOR_DIM - 1) * 0.5f; const float r = VIDEO_XR_CURSOR_DIM * 0.5f; + memset(px, 0, (size_t)VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4); for (y = 0; y < VIDEO_XR_CURSOR_DIM; y++) { for (x = 0; x < VIDEO_XR_CURSOR_DIM; x++) { - float dx = (float)x - c; - float dy = (float)y - c; - float d = (float)sqrt(dx * dx + dy * dy) / r; - float a = (d < 1.0f - 1.0f / r) ? 1.0f + float dx = (float)x - c; + float dy = (float)y - c; + float d = (float)sqrt(dx * dx + dy * dy) / r; + float a = (d < 1.0f - 1.0f / r) ? 1.0f : ((d < 1.0f) ? (1.0f - d) * r : 0.0f); - float l = (d < 0.6f) ? 1.0f : 0.15f; - uint8_t *p = px + ((size_t)y * VIDEO_XR_CURSOR_DIM + x) * 4; - p[0] = (uint8_t)(l * a * 255.0f + 0.5f); - p[1] = p[0]; - p[2] = p[0]; - p[3] = (uint8_t)(a * 255.0f + 0.5f); + float l = (d < 0.6f) ? 1.0f : 0.15f; + video_xr_cursor_texel(px, x, y + VIDEO_XR_CURSOR_Y, + (uint8_t)(l * a * 255.0f + 0.5f), + (uint8_t)(a * 255.0f + 0.5f)); } } + for (k = 0; k < 2; k++) + { + uint8_t alpha = k ? 255 : VIDEO_XR_OUTLINE_FAINT; + video_xr_outline_pixels(px, VIDEO_XR_OUTLINE_X(k), alpha, 0); + for (y = 0; y < VIDEO_XR_HANDLE_ROWS; y++) + for (x = 0; x < VIDEO_XR_HANDLE_COLS; x++) + video_xr_cursor_texel(px, VIDEO_XR_HANDLE_X(k) + x, + VIDEO_XR_HANDLE_Y + y, + (video_xr_rim_depth(x, y, VIDEO_XR_HANDLE_COLS, + VIDEO_XR_HANDLE_ROWS) < VIDEO_XR_OUTLINE_BAND) + ? 0 : alpha, alpha); + } + for (k = 0; k < 2; k++) + for (x = 0; x < VIDEO_XR_BEAM_LEN; x++) + { + float in = ((float)x + 0.5f) / VIDEO_XR_BEAM_FADE_IN; + float out = ((float)(VIDEO_XR_BEAM_LEN - x) - 0.5f) + / VIDEO_XR_BEAM_FADE_OUT; + uint8_t level; + if (in > 1.0f) + in = 1.0f; + if (!k || out > 1.0f) + out = 1.0f; + level = (uint8_t)(in * out * VIDEO_XR_BEAM_LEVEL + 0.5f); + for (y = 0; y < VIDEO_XR_BEAM_ROWS; y++) + video_xr_cursor_texel(px, VIDEO_XR_BEAM_X(k) + x, + VIDEO_XR_BEAM_Y + y, level, level); + } + for (y = 0; y < VIDEO_XR_BACK_CELL; y++) + for (x = 0; x < VIDEO_XR_BACK_CELL; x++) + video_xr_cursor_texel(px, VIDEO_XR_BACK_X + x, VIDEO_XR_BACK_Y + y, + VIDEO_XR_BACK_LEVEL, 255); + for (y = 0; y < VIDEO_XR_HINGE_ROWS; y++) + for (x = 0; x < VIDEO_XR_HINGE_COLS; x++) + { + uint8_t *p = px + ((size_t)(VIDEO_XR_HINGE_Y + y) + * VIDEO_XR_CURSOR_W + VIDEO_XR_HINGE_X + x) * 4; + p[0] = (uint8_t)((VIDEO_XR_HINGE_RB * VIDEO_XR_HINGE_LEVEL + 127) + / 255); + p[1] = (uint8_t)((VIDEO_XR_HINGE_G * VIDEO_XR_HINGE_LEVEL + 127) + / 255); + p[2] = p[0]; + p[3] = VIDEO_XR_HINGE_LEVEL; + } +} + +void video_xr_cut_pixels(uint8_t *px, bool bottom_up) +{ + unsigned k, c; + for (k = 0; k < 2; k++) + for (c = 0; c < VIDEO_XR_OUTLINE_CUTS; c++) + { + unsigned cut = video_xr_cuts[c]; + if (bottom_up) + cut = (cut & 12u) | ((cut & 1u) << 1) | ((cut >> 1) & 1u); + video_xr_outline_pixels(px, VIDEO_XR_CUT_X(c, k), + k ? 255 : VIDEO_XR_OUTLINE_FAINT, cut); + } +} + +void video_xr_tag_pixels(uint8_t *px, unsigned tag, const uint8_t *cov, + bool bottom_up) +{ + unsigned x, y; + for (y = 0; y < VIDEO_XR_TAG_ROWS; y++) + for (x = 0; x < VIDEO_XR_TAG_COLS; x++) + { + unsigned i, j, a; + unsigned c = cov[y * VIDEO_XR_TAG_COLS + x]; + unsigned halo = 0; + unsigned row = bottom_up ? VIDEO_XR_TAG_ROWS - 1 - y : y; + for (j = (y > VIDEO_XR_TAG_HALO) ? y - VIDEO_XR_TAG_HALO : 0; + j <= y + VIDEO_XR_TAG_HALO && j < VIDEO_XR_TAG_ROWS; j++) + for (i = (x > VIDEO_XR_TAG_HALO) ? x - VIDEO_XR_TAG_HALO : 0; + i <= x + VIDEO_XR_TAG_HALO && i < VIDEO_XR_TAG_COLS; + i++) + if (cov[j * VIDEO_XR_TAG_COLS + i] > halo) + halo = cov[j * VIDEO_XR_TAG_COLS + i]; + /* Light text over its dark halo, premultiplied. */ + a = c + halo * (255 - c) / 255; + video_xr_cursor_texel(px, VIDEO_XR_TAG_X(tag) + x, + VIDEO_XR_TAG_Y + row, + (uint8_t)(c * VIDEO_XR_TAG_FAINT / 255), + (uint8_t)(a * VIDEO_XR_TAG_FAINT / 255)); + } } /* Two rates this close in skew are a tie. */ diff --git a/gfx/video_xr.h b/gfx/video_xr.h index 08c7609a59..ab15171c80 100644 --- a/gfx/video_xr.h +++ b/gfx/video_xr.h @@ -40,6 +40,8 @@ RETRO_BEGIN_DECLS #define VIDEO_XR_GAP 0.02f /* How far the UI floats in front of screen 0, in metres. */ #define VIDEO_XR_MENU_LIFT 0.10f +/* Following screen 0, the UI comes no nearer the anchor than this. */ +#define VIDEO_XR_MENU_NEAREST 0.40f /* The UI's quad fits in screen 0's width by this much of it. */ #define VIDEO_XR_MENU_MAX_H 0.75f /* At most this many UI pixels per headset pixel across its quad. */ @@ -99,6 +101,11 @@ typedef struct video_xr_slot unsigned dims; /* image size, VIDEO_SCALE_PACK; 0 unused */ unsigned layers; /* 2 for a stereo screen */ int view[2]; /* map view drawn per layer; -1 frame or UI */ + video_xr_vec3_t base; /* its item's layout spot, the anchor's frame */ + unsigned edit_seq; /* edit mode's count when it was planned */ + /* A frame shown whole without the screens' settings (a views core's + * fallback): their edits would not move it, so it is no item. */ + bool fixed; } video_xr_slot_t; typedef struct video_xr_quad_set @@ -113,6 +120,11 @@ typedef struct video_xr_params /* The frame's views; NULL or empty for one quad of the whole * frame, shown at frame_aspect (0: frame_dims' own). */ const video_views_map_t *map; + /* Floating-screen adjustments by screen ID; NULL uses defaults. */ + const video_screen_xr_t *screens; + /* The menu's own place from its spot; NULL: at its spot, or with + * menu_follow in front of screen 0. */ + const video_screen_xr_t *menu; float frame_aspect; unsigned frame_dims; /* Screen 0 is centred straight ahead of this position and yaw. */ @@ -126,11 +138,15 @@ typedef struct video_xr_params unsigned rotation; /* The UI layer's size, or 0 for no menu quad. */ unsigned ui_dims; + /* Edit mode's count of let-go grabs, carried by every slot. */ + unsigned edit_seq; /* Screens the headset's projection shows, a bit each: no quad, and * the others keep their places. */ unsigned skip_screens; bool swap_eyes; + /* An unplaced menu opens in front of screen 0 and moves with it. */ + bool menu_follow; /* The whole frame is the window, with the menu drawn in it. */ bool window; /* The screens are drawn by the stock chain, which shrinks smoothly: @@ -138,6 +154,451 @@ typedef struct video_xr_params bool stock; } video_xr_params_t; +/* Where video_xr_place starts each floating item, in the anchor's + * frame, before its adjustments: screen s of the map (a whole frame is + * screen 0) centred at pos[s], width[s] by height[s] metres at scale 1, + * from a source native[s] in size (VIDEO_SCALE_PACK); the menu's own + * spot at menu. count screens. */ +typedef struct video_xr_layout +{ + video_xr_vec3_t pos[RETRO_VIDEO_VIEWS_MAX]; + video_xr_vec3_t menu; + float width[RETRO_VIDEO_VIEWS_MAX]; + float height[RETRO_VIDEO_VIEWS_MAX]; + unsigned native[RETRO_VIDEO_VIEWS_MAX]; + unsigned count; +} video_xr_layout_t; + +void video_xr_layout(const video_xr_params_t *p, video_xr_layout_t *out); + +/* An item's pose in the anchor's frame: at base moved by screen's + * offsets, turned by its yaw, pitch and roll; NULL leaves it at base. */ +void video_xr_item_pose(const video_xr_vec3_t *base, + const video_screen_xr_t *screen, video_xr_pose_t *out); + +/* The offsets and turn that put an item from base at pose, both in the + * anchor's frame; out's scale and visible are left as they are. */ +void video_xr_item_settings(const video_xr_vec3_t *base, + const video_xr_pose_t *pose, video_screen_xr_t *out); + +/* Edit mode's stick: its deadzone as a fraction of its travel; at full + * travel past it, the push along the aim's ray in metres a second, the + * resize in doublings a second, and the free hand's turn and tilt in + * degrees a second, that stick within VIDEO_XR_EDIT_TURN_LOCK degrees + * of left-right only turning and of up-down only tilting; as it turns + * and tilts, Snap to Angles' steps each holding within + * VIDEO_XR_EDIT_TURN_HOLD degrees of it. A pulled item comes no nearer + * the hand than VIDEO_XR_EDIT_REACH metres. */ +#define VIDEO_XR_EDIT_DEADZONE 0.2f +#define VIDEO_XR_EDIT_PUSH_RATE 1.5f +#define VIDEO_XR_EDIT_SCALE_RATE 1.0f +#define VIDEO_XR_EDIT_TURN_RATE 90.0f +#define VIDEO_XR_EDIT_TURN_LOCK 25.0f +#define VIDEO_XR_EDIT_TURN_HOLD 2.0f +#define VIDEO_XR_EDIT_REACH 0.1f + +enum video_xr_grab_state +{ + VIDEO_XR_GRAB_NONE = 0, + VIDEO_XR_GRAB_HELD, + /* Let go: the item stays where the hand left it until a quad set + * planned at release_seq or later shows it. */ + VIDEO_XR_GRAB_RELEASED +}; + +/* A hand's hold on a floating item, in the anchor's frame. */ +typedef struct video_xr_grab +{ + video_xr_pose_t rel; /* the item in the aim pose's frame */ + video_xr_pose_t last; /* where the hand last put it */ + video_xr_quat_t turn; /* its turn from the aim's heading, kept + * while it does not tilt */ + video_xr_vec3_t base; /* its layout spot */ + float width; /* metres, at its scale now */ + float height; + unsigned slot; /* its quads' slot */ + unsigned release_seq; + unsigned state; /* enum video_xr_grab_state */ + bool tilt; /* its pitch and roll follow the hand too */ + unsigned char gap; /* Hinge Gap, whole centimetres */ + /* The hinge it rested in as it was taken, whole from the grab while + * video_xr_grab_kept: its target's slot plus one (0, none), the + * target's edge and its own. */ + unsigned char rest_slot; + unsigned char rest_on; + unsigned char rest_edge; + /* A push past the stop's hold let its hinge go + * (video_xr_grab_through): the stop holds VIDEO_XR_SNAP_STOP_ARM + * short until a hinge takes hold again. */ + unsigned char through; + /* The hinge the main thread marks, where a let-go would settle it, + * kept a moment after the hand leaves: its target's slot plus one + * (0, none), the target's edge and its own. */ + unsigned char mark_slot; + unsigned char mark_on; + unsigned char mark_edge; +} video_xr_grab_t; + +/* Where g puts its item for an aim pose: the aim composed with g->rel, + * its turn g->turn from the aim's heading unless g->tilt, snapped to + * the screens of targets but those in skip, fading in from start, the + * item's pose as it was grabbed (video_xr_snap, video_xr_snap_fade; + * NULL targets: none, and start unread), the target of the hinge it was + * taken in pulling whole while video_xr_grab_kept, its offsets from + * g->base clamped to the settings' range. Writes the pose and, unless + * NULL, the offsets and turn and the snaps engaged or pulling + * (video_xr_snap). Returns whether a clamp held it back, never for a + * snap alone; the caller then holds g->rel again from pose, so the item + * resumes from the limit. */ +bool video_xr_grab_pose(const video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, video_xr_pose_t *pose, + video_screen_xr_t *out, unsigned *snapped); + +/* g carries its item, turned q at aim, from here on: it keeps its yaw + * from the aim's heading, the way the ray points across the floor, and + * its pitch and roll. Near straight up or down, the heading blends into + * the one the hand's up gives, so a twist there turns it. */ +void video_xr_grab_hold(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quat_t *q); + +/* The stick for dt seconds: past the deadzone y (up, +1) pushes g's + * item along the aim's ray and x (right, +1) enlarges it, each by how + * far past it is; *scale stays within the setting's range. */ +void video_xr_grab_stick(video_xr_grab_t *g, float x, float y, double dt, + float *scale); + +/* The free hand's stick for dt seconds, g's item held by aim: past the + * deadzone x (right, +1) turns it about the vertical through its + * middle, anticlockwise seen from above, and y (up, +1) tips its top + * back about its own left-right line through its middle, each by how + * far past it is; near one axis, only that one acts + * (VIDEO_XR_EDIT_TURN_LOCK). Returns the axes that acted, as + * VIDEO_XR_STEP_TURN and VIDEO_XR_STEP_TILT. */ +#define VIDEO_XR_STEP_TURN 1u +#define VIDEO_XR_STEP_TILT 2u +unsigned video_xr_grab_turn(video_xr_grab_t *g, const video_xr_pose_t *aim, + float x, float y, double dt); + +struct video_xr_hinge; + +/* Holds g's item at a step, every size degrees (0: none), on the axes + * video_xr_grab_turn moved: the turn counts from facing head across the + * floor (NULL: no turn steps), the tilt from upright. rest is how far + * the stick has taken each past where it shows, so pushing on leaves + * the step; an axis not in axes keeps its rest. A snap to targets that + * would engage wins. With lid, a hinge that has let the item go (NULL: + * none), lid's axis holds about the item's edge lid->edge, as a lid + * turns, not its middle. Returns the axes held. */ +unsigned video_xr_grab_step(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_vec3_t *head, float size, unsigned axes, float *rest, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, const struct video_xr_hinge *lid); + +/* video_xr_grab_turn, but with lid (unless NULL) its axis turns about the + * item's edge lid->edge, as video_xr_grab_step holds it. */ +unsigned video_xr_grab_turn_lid(video_xr_grab_t *g, + const video_xr_pose_t *aim, const struct video_xr_hinge *lid, + float x, float y, double dt); + +/* The axes on which g's item, with rest as video_xr_grab_step keeps it, + * is outside every step's hold. NULL head: the turn is not known. */ +unsigned video_xr_grab_off_step(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_vec3_t *head, float size, + const float *rest); + +/* Snap to Screens: a held screen turns parallel or square to one within + * VIDEO_XR_SNAP_ANGLE degrees and lines up within VIDEO_XR_SNAP_DIST + * metres, or hinges on an edge. Each pull is whole in range and fades + * out over as far again; all fade past VIDEO_XR_SNAP_NEAR apart. */ +#define VIDEO_XR_SNAP_DIST 0.03f +#define VIDEO_XR_SNAP_ANGLE 5.0f +#define VIDEO_XR_SNAP_NEAR 0.30f +/* Meeting edges click to the set gap within this of it (m). */ +#define VIDEO_XR_SNAP_CLICK 0.01f +/* Else the gap is where the hand leaves it, touching to this (m). */ +#define VIDEO_XR_SNAP_FREE 0.10f +/* A hinged edge pushed past touching stops there, whole this far past + * where the click lets go (m); past that the hinge ends. */ +#define VIDEO_XR_SNAP_STOP 0.03f +/* The stop fades to none this far past where the click lets go (m). */ +#define VIDEO_XR_SNAP_STOP_FADE 0.055f +/* A push that ended the hinge holds this short of where it takes hold + * again; the stop ticks again once the edge is this far apart (m). */ +#define VIDEO_XR_SNAP_STOP_ARM 0.005f +/* The snaps engaged, each whole: video_xr_snap's low bits. */ +#define VIDEO_XR_SNAP_TURN 1u +#define VIDEO_XR_SNAP_X 2u +#define VIDEO_XR_SNAP_Y 4u +#define VIDEO_XR_SNAP_Z 8u +#define VIDEO_XR_SNAP_ALL 15u +/* Each snap pulling at all, whole or fading (or a hinge's stop near + * touching): its bit shifted up by this, also in what video_xr_snap + * returns. */ +#define VIDEO_XR_SNAP_PULL_SHIFT 4 +#define VIDEO_XR_SNAP_PULLING(bits) ((bits) << VIDEO_XR_SNAP_PULL_SHIFT) + +/* Snaps a screen in slot, width x height metres, held at pose, to the + * other screens of set no hand holds (skip: slots, as bits), every + * pull scaled by fade (0 none, 1 whole; a snap is engaged only whole), + * edges that meet touching. Never the menu, held or as a target; a + * stereo pair is one; a fixed slot none. Returns the snaps engaged and + * VIDEO_XR_SNAP_PULLING of those pulling at all. Keeps nothing between + * calls. */ +unsigned video_xr_snap(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float fade, + video_xr_pose_t *pose); + +/* A hinge engaged: an edge of the held screen exactly in a target's + * plane, parallel to its edge at a gap the hinge holds, both screens + * either side of that line at any angle; also in what video_xr_snap + * returns. */ +#define VIDEO_XR_SNAP_HINGE 256u +/* That hinge's edge exactly the set gap from the target's, clicked to + * it, or held touching by its stop; with its pulling bit, also in what + * video_xr_snap returns. */ +#define VIDEO_XR_SNAP_GAP 512u + +/* The hinge video_xr_snap_hinge engaged, in the anchor's frame: the held + * edge's middle on the hinge's line, the line's way, the way from it + * that is flat with the target, the held screen's edge (0 top, + * 1 bottom, 2 right, 3 left), the screens' angle from flat in degrees + * about dir, the gap in metres from the target's edge to the line, in + * the target's plane, the free hand's stick axis whose turn lies + * nearer the line (VIDEO_XR_STEP_TILT or VIDEO_XR_STEP_TURN), and the + * target's slot and the edge of it the hinge is on. */ +typedef struct video_xr_hinge +{ + video_xr_vec3_t point; + video_xr_vec3_t dir; + video_xr_vec3_t flat; + float angle; + float gap; + unsigned edge; + unsigned axis; + unsigned slot; + unsigned on; +} video_xr_hinge_t; + +/* video_xr_snap, edges that meet gap metres apart, and the hinge it + * engaged unless hinge is NULL; hinge is written only with + * VIDEO_XR_SNAP_HINGE returned. */ +unsigned video_xr_snap_hinge(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, float fade, + video_xr_pose_t *pose, video_xr_hinge_t *hinge); + +/* A held screen turned q, its edge h->edge along h's line: its angle + * from flat about h->dir, in degrees. */ +float video_xr_hinge_angle(const video_xr_hinge_t *h, + const video_xr_quat_t *q); + +/* Where hinge h shows its line, its held screen width x height metres, + * on target: from a to b along h->dir where the held edge and target's + * edge h->on overlap, midway across the gap; its layer is drawn as the + * beam is, VIDEO_XR_HINGE_WIDTH wide, each end VIDEO_XR_HINGE_LIFT + * nearer the eye. False where they do not overlap. */ +#define VIDEO_XR_HINGE_WIDTH 0.006f +#define VIDEO_XR_HINGE_LIFT 0.01f +bool video_xr_hinge_line(const video_xr_hinge_t *h, float width, + float height, const video_xr_quad_t *target, video_xr_vec3_t *a, + video_xr_vec3_t *b); + +/* h, its slots and edges kept, placed on those edges as they are now: + * its held one, of a screen width x height metres at pose, and + * target's. Its line runs midway between their ways through the held + * edge's middle (point), flat and gap from the target's edge to it, so + * it lies midway across and in depth; crossed, across on the edge drawn + * over the other (the held one's if over). */ +void video_xr_hinge_place(video_xr_hinge_t *h, const video_xr_pose_t *pose, + float width, float height, const video_xr_quad_t *target, bool over); + +/* Two screens at rest where a hinge would hold one of them carried: + * slot's edge edge (0 top, 1 bottom, 2 right, 3 left) on target's edge + * on. */ +typedef struct video_xr_rest +{ + unsigned char slot; + unsigned char edge; + unsigned char target; + unsigned char on; +} video_xr_rest_t; + +/* The screens of set but those in skip (slots, as bits) resting + * exactly in a hinge, as video_xr_snap_hinge's targets go: each pair + * whose poses pass the test that engages a carried screen's hinge, gap + * metres apart, with either carried (the earlier in set first if + * both), where that hinge held whole, nothing else pulling, leaves the + * carried one unmoved; up to max into rest. Returns how many. */ +unsigned video_xr_rest_hinges(const video_xr_quad_set_t *set, + unsigned skip, float gap, video_xr_rest_t *rest, unsigned max); + +/* The hinge a screen in slot, width x height metres resting at pose, is + * taken in: of the hinges it rests in carried, on screens of set but + * those in skip, gap metres apart, the one video_xr_snap_hinge ranks + * best, if holding it whole there, nothing else pulling yet, moves + * nothing. False with none. */ +bool video_xr_rest_hinge(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_pose_t *pose, video_xr_hinge_t *hinge); + +/* Where a hand taking a screen width x height in slot, resting at pose + * in h's hinge (video_xr_rest_hinge), gap metres the set gap, holds it, + * into pose: touching, pushed VIDEO_XR_SNAP_STOP_ARM past where the stop + * starts, which holds it there, so a let-go after a drift short of that + * stays touching; else where it rests. */ +void video_xr_rest_take(const video_xr_quad_set_t *set, unsigned slot, + float width, float height, float gap, const video_xr_hinge_t *h, + video_xr_pose_t *pose); + +/* A screen let go hinged settles: its edge to the set gap from within + * VIDEO_XR_HINGE_SETTLE metres of it, apart; touching from the stop's + * hold; level along the edge from within the level pull's reach; else + * where it is. */ +#define VIDEO_XR_HINGE_SETTLE 0.03f + +/* Where screen slot, width x height metres, its hand putting it at + * hand, settles in hinge h (its target's slot and both edges) on set's + * screens but those in skip, gap metres the set gap: h held whole from + * there, nothing else pulling, then to its settle points, into pose. + * False if h does not engage it there or what it gives is not exact + * (video_xr_rest_hinges would not list it). */ +bool video_xr_hinge_settle(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_hinge_t *h, const video_xr_pose_t *hand, + video_xr_pose_t *pose); + +/* video_xr_hinge_settle for g's item where aim puts it with no snap. */ +bool video_xr_grab_settle(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_hinge_t *h, video_xr_pose_t *pose); + +/* video_xr_grab_pose with targets and no settings, also giving the + * hinge it holds (VIDEO_XR_SNAP_HINGE in the snaps returned, never + * with a clamp holding it back, nor unless the item's own edge is + * within VIDEO_XR_SNAP_DIST metres of the target's plane and + * VIDEO_XR_SNAP_ANGLE degrees of the hinge's line, at a gap the hinge + * holds whole). */ +unsigned video_xr_grab_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start, video_xr_pose_t *pose, + video_xr_hinge_t *hinge); + +/* Whether g's item, where it is with no snap, has its edge h->edge where + * h's hinge could take it again, against h's target where targets has + * it now: within reach metres of its plane, at a gap the hinge + * reaches, fades included, or past touching where the stop holds it + * whole (video_xr_grab_through), and overlapping its edge along the + * line. The stick swings about it within VIDEO_XR_SNAP_DIST; the hinge + * a hold was taken in keeps it within VIDEO_XR_HINGE_REACH, past where + * it takes hold (video_xr_grab_kept). */ +#define VIDEO_XR_HINGE_REACH (1.5f * VIDEO_XR_SNAP_DIST) +bool video_xr_grab_near_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h, float reach); + +/* Whether g's item, where it is with no snap, has its edge h->edge + * pushed past touching h's target farther than the stop holds it + * whole, VIDEO_XR_SNAP_STOP_ARM less with g->through: a push that lets + * the hinge go. */ +bool video_xr_grab_through(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h); + +/* Whether g still carries the hinge it was taken in (g->rest_slot): its + * edge within VIDEO_XR_HINGE_REACH of where that hinge could take it + * again (video_xr_grab_near_hinge), or past that while its fade from + * start is short of whole (video_xr_snap_fade), so leaving the reach + * does not hop it. Writes that hinge's slots and edges to kept. */ +bool video_xr_grab_kept(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_pose_t *start, video_xr_hinge_t *kept); + +/* The hinge the main thread marks for g's item (g->mark_slot) into h, + * placed on set's edges as they are with the item at pose. False with + * no mark, or its target in holding (a bit per slot a hand holds) or + * not in set's quads. */ +bool video_xr_grab_marked(const video_xr_grab_t *g, + const video_xr_pose_t *pose, const video_xr_quad_set_t *set, + unsigned holding, video_xr_hinge_t *h); + +/* g holds its item at pose for aim, its place and its turn. */ +void video_xr_grab_put(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_pose_t *pose); + +/* g holds its item in h's plane as video_xr_grab_put_line, at its own + * angles, or turned (an engaged turn to flat or square) at those of + * pose, the shown pose h came with, about its edge h->edge's middle: + * only what the snap holds whole, so it still shows as pose. */ +void video_xr_grab_put_hinge(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h, const video_xr_pose_t *pose, bool turned); + +/* g holds its item where it is with no snap, moved straight out of the + * target's plane until the middle of its edge h->edge is in it: its + * turn and its gap kept, but a gap the click holds goes to g's set + * one. */ +void video_xr_grab_put_line(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h); + +/* g's item, where it is with no snap, turned deg degrees about its own + * edge h->edge (about h->dir, its angle from flat growing by deg). */ +void video_xr_grab_swing(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h, double deg); + +/* video_xr_grab_turn, but with h (unless NULL) its axis h->axis swings + * the item about its own edge h->edge instead, as far, keeping its gap. */ +unsigned video_xr_grab_turn_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float x, + float y, double dt); + +/* video_xr_grab_step for an item hinged by h: h->axis holds at a step + * of the angle from flat, every size degrees; the other axis holds at + * none, the hinge's snap winning; a turn to flat or square that would + * engage wins over both. */ +unsigned video_xr_grab_step_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + unsigned axes, float *rest, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start); + +/* h->axis if g's item, rest as video_xr_grab_step_hinge keeps it, is + * outside every step's hold of its angle from flat; else 0. */ +unsigned video_xr_grab_off_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + const float *rest); + +/* Snapping fades in from a grab: 0 at start, the item's pose as it was + * grabbed, rising evenly to 1 once pose is VIDEO_XR_SNAP_FADE_DIST + * metres or VIDEO_XR_SNAP_FADE_ANGLE degrees from it, whichever comes + * first, so a screen taken near a line does not hop onto it. */ +#define VIDEO_XR_SNAP_FADE_DIST 0.05f +#define VIDEO_XR_SNAP_FADE_ANGLE 10.0f +float video_xr_snap_fade(const video_xr_pose_t *start, + const video_xr_pose_t *pose); + +/* Edit Smoothing's filter for a hand's aim: a One Euro filter (Casiez + * et al. 2012) on its position and its turn, smoothing a slow hand + * hard and a fast one little. */ +typedef struct video_xr_smooth +{ + video_xr_pose_t raw; /* the last step's raw pose */ + video_xr_pose_t pose; /* and what it gave */ + float speed; /* metres a second, filtered */ + float turn; /* radians a second, filtered */ + bool on; /* false: the next step starts afresh */ +} video_xr_smooth_t; + +/* A step jumping further than these starts afresh. */ +#define VIDEO_XR_SMOOTH_JUMP_M 0.5f +#define VIDEO_XR_SMOOTH_JUMP_DEG 45.0f +/* The speeds' own cutoff in Hz. */ +#define VIDEO_XR_SMOOTH_DCUTOFF 1.0f + +/* s one step on, dt seconds after the last, to raw, as level (enum + * video_openxr_edit_smoothing) smooths: the pose into out. Off, a first + * step (s->on false, or dt not above 0) or a jump: out is raw, and the + * filter starts from it. */ +void video_xr_smooth_step(video_xr_smooth_t *s, unsigned level, + const video_xr_pose_t *raw, double dt, video_xr_pose_t *out); + void video_xr_pose_identity(video_xr_pose_t *pose); /* out = q applied to v; out may be v. */ @@ -153,6 +614,17 @@ bool video_xr_anchor_from_head(const video_xr_pose_t *head, void video_xr_compose(const video_xr_pose_t *a, const video_xr_pose_t *b, video_xr_pose_t *out); +/* out = a's inverse: video_xr_compose(a, out) is the identity; out may + * be a. */ +void video_xr_pose_invert(const video_xr_pose_t *a, video_xr_pose_t *out); + +/* q's turn in degrees, in the order an item's adjustments turn it: yaw + * about +Y, pitch about +X, roll about +Z. Pitch is within [-90, 90]; + * straight up or down, roll is 0 and yaw takes the whole turn about the + * vertical. */ +void video_xr_euler(const video_xr_quat_t *q, float *yaw, float *pitch, + float *roll); + /* Lazy Follow: the screens stay until the head's heading is this far * from theirs, or the head this far from where they were placed from; * then they glide back, closing about two thirds of the way each @@ -180,6 +652,13 @@ unsigned video_xr_image_dims(float width_m, float height_m, float distance, bool video_xr_shrinks(unsigned image_dims, unsigned source_dims, unsigned rotation); +/* A slot's image size: while editing, current (the image it has, 0 for + * none) if planned is within 0.8 to 1.25 times it on both axes, so a + * screen being moved is not given a new swapchain at each distance; + * planned otherwise. */ +unsigned video_xr_keep_dims(unsigned planned, unsigned current, + bool editing); + void video_xr_place(const video_xr_params_t *p, video_xr_quad_set_t *out); /* Where a ray (origin + t * dir, t >= 0) meets a quad's front face: @@ -187,6 +666,15 @@ void video_xr_place(const video_xr_params_t *p, video_xr_quad_set_t *out); bool video_xr_ray_hit(const video_xr_quad_t *q, const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, float *u, float *v, float *t); +/* Whether eye is behind q's plane, where runtimes draw nothing of a + * quad layer. */ +bool video_xr_quad_behind(const video_xr_quad_t *q, + const video_xr_vec3_t *eye); + +/* pose turned half round its own up: in the same plane, facing the + * other way. */ +void video_xr_turn_back(video_xr_pose_t *pose); + /* A point on a quad as packed-frame pointer coordinates: any copy of a * screen lands in its EYE_NONE or left view at the same fraction, a * whole frame spreads over the frame. map and frame_dims are the @@ -219,38 +707,199 @@ void video_xr_place_window(const video_xr_params_t *p, bool menu, video_xr_quad_set_t *out, video_views_rect_t rects[VIDEO_XR_MAX_SLOTS][2]); -/* The laser's dot image is this many pixels square. */ -#define VIDEO_XR_CURSOR_DIM 32 +/* The laser's image: its parts side by side, a clear column between + * each and each centred on the middle row (GL's rows run bottom-up): + * dot, outlines, handle bars, beam strips, back, hinge strip, tags, cut + * outlines. */ +#define VIDEO_XR_CURSOR_DIM 32 +#define VIDEO_XR_OUTLINE_DIM 256 +#define VIDEO_XR_OUTLINE_BAND 2 +#define VIDEO_XR_OUTLINE_EDGE (3 * VIDEO_XR_OUTLINE_BAND) +#define VIDEO_XR_OUTLINE_FAINT 64 +#define VIDEO_XR_HANDLE_COLS 128 +#define VIDEO_XR_HANDLE_ROWS 8 +#define VIDEO_XR_BEAM_LEN 128 +#define VIDEO_XR_BEAM_ROWS 4 +#define VIDEO_XR_HINGE_COLS 128 +#define VIDEO_XR_HINGE_ROWS 4 +#define VIDEO_XR_TAGS (VIDEO_XR_MAX_SLOTS + 1) +#define VIDEO_XR_TAG_COLS 128 +#define VIDEO_XR_TAG_ROWS \ + (VIDEO_XR_TAG_TEXT_ROWS + 2 * VIDEO_XR_TAG_INSET) +#define VIDEO_XR_OUTLINE_X(lit) \ + (VIDEO_XR_CURSOR_DIM + 1 + (lit) * (VIDEO_XR_OUTLINE_DIM + 1)) +#define VIDEO_XR_HANDLE_X(lit) \ + (VIDEO_XR_OUTLINE_X(2) + (lit) * (VIDEO_XR_HANDLE_COLS + 1)) +#define VIDEO_XR_BEAM_X(miss) \ + (VIDEO_XR_HANDLE_X(2) + (miss) * (VIDEO_XR_BEAM_LEN + 1)) +#define VIDEO_XR_BACK_DIM 8 +#define VIDEO_XR_BACK_MARGIN 4 +#define VIDEO_XR_BACK_CELL (VIDEO_XR_BACK_DIM + 2 * VIDEO_XR_BACK_MARGIN) +/* Before the tags, so a runtime too narrow for the tags keeps these. */ +#define VIDEO_XR_BACK_X VIDEO_XR_BEAM_X(2) +#define VIDEO_XR_HINGE_X (VIDEO_XR_BACK_X + VIDEO_XR_BACK_CELL + 1) +#define VIDEO_XR_TAG_X(tag) \ + (VIDEO_XR_HINGE_X + VIDEO_XR_HINGE_COLS + 1 \ + + (tag) * (VIDEO_XR_TAG_COLS + 1)) +/* An outline for each cut a hinge makes (top, bottom, right, left, top + * and bottom, right and left), faint ones then bright, each an outline's + * square without the rims of its cut edges, VIDEO_XR_OUTLINE_CROP + * texels deep, so a cut changes only the outline's image. */ +#define VIDEO_XR_OUTLINE_CUTS 6 +#define VIDEO_XR_CUT_X(cut, lit) \ + (VIDEO_XR_TAG_X(VIDEO_XR_TAGS) \ + + ((lit) * VIDEO_XR_OUTLINE_CUTS + (cut)) * (VIDEO_XR_OUTLINE_DIM + 1)) +#define VIDEO_XR_CURSOR_W (VIDEO_XR_CUT_X(0, 2) - 1) +/* The image without the cut outlines, then without the tags too, for + * runtimes whose swapchains are narrower. */ +#define VIDEO_XR_CURSOR_TAGGED_W (VIDEO_XR_TAG_X(VIDEO_XR_TAGS) - 1) +#define VIDEO_XR_CURSOR_UNTAGGED_W (VIDEO_XR_TAG_X(0) - 1) +#define VIDEO_XR_CURSOR_H VIDEO_XR_OUTLINE_DIM +#define VIDEO_XR_CURSOR_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_CURSOR_DIM) / 2) +#define VIDEO_XR_HANDLE_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_HANDLE_ROWS) / 2) +#define VIDEO_XR_BEAM_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_BEAM_ROWS) / 2) +#define VIDEO_XR_TAG_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_TAG_ROWS) / 2) +#define VIDEO_XR_BACK_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_BACK_CELL) / 2) +#define VIDEO_XR_HINGE_Y ((VIDEO_XR_CURSOR_H - VIDEO_XR_HINGE_ROWS) / 2) +/* Rows of the hinge's column nothing is drawn in, either way up. */ +#define VIDEO_XR_HINGE_HIDDEN_Y 0 +/* The beam's strips: white at this level, fading in over their first + * FADE_IN texels; the one into nothing out over its last FADE_OUT. */ +#define VIDEO_XR_BEAM_LEVEL 160 +#define VIDEO_XR_BEAM_FADE_IN 8 +#define VIDEO_XR_BEAM_FADE_OUT 32 +/* A back's grey, opaque. */ +#define VIDEO_XR_BACK_LEVEL 40 +/* A hinge's line: green, red and blue equal, at this alpha. */ +#define VIDEO_XR_HINGE_RB 60 +#define VIDEO_XR_HINGE_G 220 +#define VIDEO_XR_HINGE_LEVEL 166 -/* Its pixels, RGBA8 premultiplied: a white dot with a dark rim and a - * one-pixel soft edge, grey so RGBA and BGRA hold the same bytes. px - * holds VIDEO_XR_CURSOR_DIM squared times four bytes. */ +/* Its pixels, RGBA8 premultiplied, grey or with red equal to blue, so + * RGBA and BGRA hold the same bytes: a white dot with a dark rim and a + * one-pixel soft edge; outline rims and handle bars in black and white, + * the faint ones at VIDEO_XR_OUTLINE_FAINT alpha; white beam strips; + * the back's grey cell; the hinge's green strip. px holds + * VIDEO_XR_CURSOR_W by VIDEO_XR_CURSOR_H times four bytes. */ void video_xr_cursor_pixels(uint8_t *px); +/* A tag's words: light over a dark halo this many texels wide, kept + * this far inside its cell, at this level and alpha; drawn at this + * font size, in rows for a line half again as tall (Noto Sans's is + * 1.44 times its size). */ +#define VIDEO_XR_TAG_HALO 2 +#define VIDEO_XR_TAG_INSET (VIDEO_XR_TAG_HALO + 1) +#define VIDEO_XR_TAG_FAINT 160 +#define VIDEO_XR_TAG_FONT 16 +#define VIDEO_XR_TAG_TEXT_ROWS (VIDEO_XR_TAG_FONT * 3 / 2) + +/* Tag tag's cell of the laser's image from cov, the cell's text as + * coverage (VIDEO_XR_TAG_COLS by VIDEO_XR_TAG_ROWS, 0 to 255): light + * text over its dark halo; bottom_up writes the rows last first, for + * an image whose rows run bottom up, as GL's. */ +void video_xr_tag_pixels(uint8_t *px, unsigned tag, const uint8_t *cov, + bool bottom_up); + +/* The cut outlines' cells of the laser's image; bottom_up swaps the + * top's and the bottom's, for an image whose rows run bottom up. */ +void video_xr_cut_pixels(uint8_t *px, bool bottom_up); + /* The laser's dot: this wide per metre along the ray, never narrower * than VIDEO_XR_CURSOR_MIN, just in front of the quad it is on. */ #define VIDEO_XR_CURSOR_SCALE 0.012f #define VIDEO_XR_CURSOR_MIN 0.005f #define VIDEO_XR_CURSOR_LIFT 0.002f -/* Whether a laser (enum video_openxr_laser) can point at q. Auto: the - * menu or a window while the menu is open, and screens 1 and up. - * Always: every screen and a whole frame too. Off: nothing. */ +/* Whether a laser (enum video_openxr_laser, or VIDEO_XR_LIVE_EDIT) can + * point at q. Auto: the menu or a window while the menu is open, and + * screens 1 and up. Always: every screen and a whole frame too. Off: + * nothing. Edit mode's: every item. */ +#define VIDEO_XR_LIVE_EDIT VIDEO_OPENXR_LASER_LAST bool video_xr_quad_live(const video_xr_quad_t *q, unsigned laser, bool menu_open); +/* Edit mode takes the menu by a handle bar centred under its bottom + * edge, in its plane: this fraction of its width, this many metres + * tall and this many below its outline. A ray within + * VIDEO_XR_HANDLE_MARGIN metres of the bar on any side takes it where + * it meets nothing else. */ +#define VIDEO_XR_HANDLE_WIDTH 0.4f +#define VIDEO_XR_HANDLE_HEIGHT 0.03f +#define VIDEO_XR_HANDLE_GAP 0.005f +#define VIDEO_XR_HANDLE_MARGIN 0.03f + +/* The menu quad's handle: the menu's quad, moved and sized to it. */ +void video_xr_menu_handle(const video_xr_quad_t *menu, video_xr_quad_t *out); + /* The nearest live quad the ray meets, as an index into set->quads, or * -1; of a stereo screen's two quads, the first. t is in units of - * dir's length. */ + * dir's length. Edit mode's laser passes fixed slots' quads and meets + * the menu only around its handle, at (u, v) in the menu's own frame: + * on the bar before anything, even a nearer screen; in its margin, + * which on a small menu reaches onto the menu's bottom edge, only where + * it meets nothing else. Through the menu, what is behind. Edit mode + * meets either face, so an item turned away stays in reach. */ int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, bool menu_open, const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, float *u, float *v, float *t); +/* video_xr_pick with edit mode's laser, for a hand whose laser has the + * menu's handle stuck (stuck): its margin then wins over anything the + * ray meets, as the bar does. *stick, unless NULL: whether it stays + * stuck, the ray on the bar, or stuck and still in its margin. */ +int video_xr_pick_edit(const video_xr_quad_set_t *set, bool stuck, + const video_xr_vec3_t *origin, const video_xr_vec3_t *dir, + float *u, float *v, float *t, bool *stick); + /* The dot where a ray dist metres long meets q at (u, v): its pose, * facing as q does. Returns its width in metres. */ float video_xr_cursor(const video_xr_quad_t *q, float u, float v, float dist, video_xr_pose_t *pose); +/* The laser's beam: this wide, and this long with nothing under the + * laser. */ +#define VIDEO_XR_BEAM_WIDTH 0.004f +#define VIDEO_XR_BEAM_REACH 1.5f + +/* A beam len metres long from o along the unit ray d: its pose at its + * middle, its x down the ray, turned about the ray so its face is + * towards eye. */ +void video_xr_beam(const video_xr_vec3_t *o, const video_xr_vec3_t *d, + float len, const video_xr_vec3_t *eye, video_xr_pose_t *pose); + +/* Edit mode's tag: this tall, as wide as its cell's shape, this far + * above the hand's aim. */ +#define VIDEO_XR_TAG_HEIGHT 0.02f +#define VIDEO_XR_TAG_LIFT 0.05f + +/* The tag over a hand at hand: its pose, its face towards eye and its + * x level; with eye straight above or below, any level x. */ +void video_xr_tag(const video_xr_vec3_t *hand, const video_xr_vec3_t *eye, + video_xr_pose_t *pose); + +/* Edit mode's outline round q: q, grown so the rim's inner edge, with + * the half texel filtering spreads it by, is VIDEO_XR_OUTLINE_GAP + * metres outside q's on every side. */ +#define VIDEO_XR_OUTLINE_GAP 0.005f +void video_xr_outline(const video_xr_quad_t *q, video_xr_quad_t *out); + +/* Outline o, as video_xr_outline made it and turned to the head when + * back, without the rims of its item's edges in hide (bit e for edge + * e: 0 top, 1 bottom, 2 right, 3 left; one, or two opposite): turned a + * quarter about its face for a top or bottom, then cut to the *w texels + * of its square's x from *x. VIDEO_XR_OUTLINE_CROP texels go at a + * side, so filtering's half texel there meets its clear middle; never + * its y, which GL's bottom-up rows would flip. */ +#define VIDEO_XR_OUTLINE_CROP 8 +void video_xr_outline_crop(video_xr_quad_t *o, unsigned hide, bool back, + unsigned *x, unsigned *w); + +/* Where in the laser's image at full width the outline without the + * rims in hide shows, as video_xr_outline_crop takes hide and back: + * VIDEO_XR_OUTLINE_X(lit) for none, else its VIDEO_XR_CUT_X cell, a top + * or bottom over a side. The outline keeps its pose and size. */ +unsigned video_xr_outline_cell(unsigned hide, bool back, unsigned lit); + /* Headset pacing. The XR thread's filter over each frame's predicted * display period: a period is published once VIDEO_XR_PERIODS frames * agree within 1% of their median, and again when a new median is more diff --git a/input/common/input_openxr.c b/input/common/input_openxr.c index 9b8c85af4e..09ef34f738 100644 --- a/input/common/input_openxr.c +++ b/input/common/input_openxr.c @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -36,6 +37,7 @@ #include "../../gfx/video_views.h" #include "../../gfx/common/openxr_frame.h" #include "../../gfx/video_driver.h" +#include "../../msg_hash.h" #include "../../verbosity.h" #ifdef HAVE_MENU @@ -50,17 +52,45 @@ * INPUT_OPENXR_HAPTIC_DURATION (ns). */ #define INPUT_OPENXR_HAPTIC_REFRESH 500000 #define INPUT_OPENXR_HAPTIC_DURATION 1000000000 +/* A snap's buzz as it takes hold: its strength and length (ns). */ +#define INPUT_OPENXR_SNAP_TICK_AMP 0.5f +#define INPUT_OPENXR_SNAP_TICK_NS 20000000 +/* A hinge's as it takes hold, longer and stronger; snaps' due while it + * plays are skipped. */ +#define INPUT_OPENXR_HINGE_BUZZ_AMP 0.8f +#define INPUT_OPENXR_HINGE_BUZZ_NS 150000000 +/* How long a held screen's hinge stays marked once out of where a + * let-go would settle in it (us): a shaking hand never flickers it. */ +#define INPUT_OPENXR_HINGE_DEBOUNCE_US 150000 +/* The most each poll seen out adds to that (us): a stall saw no hand. */ +#define INPUT_OPENXR_HINGE_OUT_STEP_US 50000 /* Half a stick's travel. */ #define INPUT_OPENXR_YIELD_STICK 0x4000 +/* Polls in a row with neither of edit mode's grab nor its menu bound + * that end it: a runtime can read a binding inactive for a sync around + * a controller change. */ +#define INPUT_OPENXR_EDIT_UNBOUND 3 /* Touch's one menu button opens the RetroArch menu once held this long * (us). */ #define INPUT_OPENXR_MENU_HOLD 1000000 +/* Edit mode's longest stick step (s): a stalled poll does not fling the + * item. */ +#define INPUT_OPENXR_EDIT_MAX_DT 0.1 +/* A let-go puts the item where, turned and as large as, it showed this + * long ago (us), before the grip's twitch. The trail keeps an entry a + * frame, INPUT_OPENXR_EDIT_TRAIL_US apart at least; the rewind stops at + * a stall, entries more than INPUT_OPENXR_EDIT_GAP frames apart. */ +#define INPUT_OPENXR_EDIT_REWIND_US 80000 +#define INPUT_OPENXR_EDIT_TRAIL 24 +#define INPUT_OPENXR_EDIT_TRAIL_US 10000 +#define INPUT_OPENXR_EDIT_GAP 2.5 enum input_openxr_set { INPUT_OPENXR_SET_COMBINED = 0, INPUT_OPENXR_SET_SEPARATE, INPUT_OPENXR_SET_POINTER, + INPUT_OPENXR_SET_EDIT, INPUT_OPENXR_SETS }; @@ -90,6 +120,7 @@ enum input_openxr_action IXA_C_LASER, IXA_C_VIEW, IXA_C_START_MENU, + IXA_C_EDIT, IXA_S_B, IXA_S_A, IXA_S_R, @@ -100,10 +131,19 @@ enum input_openxr_action IXA_S_RECENTER, IXA_S_LASER, IXA_S_VIEW, + IXA_S_EDIT, IXA_AIM, IXA_GRIP, IXA_PALM, IXA_RUMBLE, + IXA_E_GRAB, + IXA_E_TILT, + IXA_E_STICK, + IXA_E_RESET, + IXA_E_MENU, + IXA_E_EDIT, + IXA_E_NEXT, + IXA_E_PREV, IXA_COUNT }; @@ -142,6 +182,7 @@ static const input_openxr_action_def_t input_openxr_actions[IXA_COUNT] = { { INPUT_OPENXR_SET_COMBINED, "laser", "Laser Pointer", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_COMBINED, "view", "Headset View", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_COMBINED, "start_menu", "Start (Held: RetroArch Menu)", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, + { INPUT_OPENXR_SET_COMBINED, "edit_screens", "Edit Screens", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_SEPARATE, "b", "B", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, { INPUT_OPENXR_SET_SEPARATE, "a", "A", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, { INPUT_OPENXR_SET_SEPARATE, "r", "R", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, @@ -152,16 +193,26 @@ static const input_openxr_action_def_t input_openxr_actions[IXA_COUNT] = { { INPUT_OPENXR_SET_SEPARATE, "recenter", "Recenter", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_SEPARATE, "laser", "Laser Pointer", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_SEPARATE, "view", "Headset View", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, + { INPUT_OPENXR_SET_SEPARATE, "edit_screens", "Edit Screens", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, { INPUT_OPENXR_SET_POINTER, "aim", "Aim", XR_ACTION_TYPE_POSE_INPUT, true }, { INPUT_OPENXR_SET_POINTER, "grip", "Grip", XR_ACTION_TYPE_POSE_INPUT, true }, { INPUT_OPENXR_SET_POINTER, "palm", "Palm", XR_ACTION_TYPE_POSE_INPUT, true }, - { INPUT_OPENXR_SET_POINTER, "rumble", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT, true } + { INPUT_OPENXR_SET_POINTER, "rumble", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT, true }, + { INPUT_OPENXR_SET_EDIT, "grab", "Grab", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, + { INPUT_OPENXR_SET_EDIT, "tilt", "Tilt", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, + { INPUT_OPENXR_SET_EDIT, "stick", "Distance and Size", XR_ACTION_TYPE_VECTOR2F_INPUT, true }, + { INPUT_OPENXR_SET_EDIT, "reset", "Reset", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, + { INPUT_OPENXR_SET_EDIT, "menu", "RetroArch Menu", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, + { INPUT_OPENXR_SET_EDIT, "edit_screens", "Edit Screens", XR_ACTION_TYPE_BOOLEAN_INPUT, false }, + { INPUT_OPENXR_SET_EDIT, "select_next", "Select Next", XR_ACTION_TYPE_BOOLEAN_INPUT, true }, + { INPUT_OPENXR_SET_EDIT, "select_previous", "Select Previous", XR_ACTION_TYPE_BOOLEAN_INPUT, true } }; static const char *const input_openxr_set_names[INPUT_OPENXR_SETS] = { - "combined", "separate", "pointer" }; + "combined", "separate", "pointer", "edit" }; static const char *const input_openxr_set_labels[INPUT_OPENXR_SETS] = { - "Combined Controllers", "Separate Controllers", "Laser and Rumble" }; + "Combined Controllers", "Separate Controllers", "Laser and Rumble", + "Edit Screens" }; typedef struct input_openxr_bind { @@ -297,26 +348,91 @@ static const input_openxr_bind_t input_openxr_simple[] = { IXB_POINTER }; +/* Edit mode's: the grip grabs, the trigger tilts, the stick (Vive: the + * trackpad) pushes and resizes, the main face button resets, the menu + * button opens the RetroArch menu, which ends it, and spare buttons + * choose the next or previous item. Edit Screens is the user's to + * bind. */ +static const input_openxr_bind_t input_openxr_index_edit[] = { + IXB_BOTH(IXA_E_GRAB, "squeeze/value"), + IXB_BOTH(IXA_E_TILT, "trigger/value"), + IXB_BOTH(IXA_E_STICK, "thumbstick"), + IXB_BOTH(IXA_E_RESET, "a/click"), + IXB_L(IXA_E_MENU, "trackpad/force"), + IXB_R(IXA_E_NEXT, "b/click"), + IXB_L(IXA_E_PREV, "b/click") +}; + +static const input_openxr_bind_t input_openxr_touch_edit[] = { + IXB_BOTH(IXA_E_GRAB, "squeeze/value"), + IXB_BOTH(IXA_E_TILT, "trigger/value"), + IXB_BOTH(IXA_E_STICK, "thumbstick"), + IXB_L(IXA_E_RESET, "x/click"), + IXB_R(IXA_E_RESET, "a/click"), + IXB_L(IXA_E_MENU, "menu/click"), + IXB_R(IXA_E_NEXT, "b/click"), + IXB_L(IXA_E_PREV, "y/click") +}; + +/* The left hand has no face buttons: its D-pad resets (down) and + * chooses (right, left). */ +static const input_openxr_bind_t input_openxr_frame_edit[] = { + IXB_BOTH(IXA_E_GRAB, "squeeze/value"), + IXB_BOTH(IXA_E_TILT, "trigger/value"), + IXB_BOTH(IXA_E_STICK, "thumbstick"), + IXB_L(IXA_E_RESET, "dpad_down/click"), + IXB_R(IXA_E_RESET, "a/click"), + IXB_R(IXA_E_MENU, "thumbstick/click"), + IXB_L(IXA_E_NEXT, "dpad_right/click"), + IXB_L(IXA_E_PREV, "dpad_left/click") +}; + +static const input_openxr_bind_t input_openxr_vive_edit[] = { + IXB_BOTH(IXA_E_GRAB, "squeeze/click"), + IXB_BOTH(IXA_E_TILT, "trigger/value"), + IXB_BOTH(IXA_E_STICK, "trackpad"), + IXB_BOTH(IXA_E_RESET, "trackpad/click"), + IXB_L(IXA_E_MENU, "menu/click"), + IXB_R(IXA_E_NEXT, "menu/click") +}; + +static const input_openxr_bind_t input_openxr_simple_edit[] = { + IXB_BOTH(IXA_E_GRAB, "select/click"), + IXB_L(IXA_E_MENU, "menu/click") +}; + typedef struct input_openxr_profile { const char *path; const input_openxr_bind_t *binds; + const input_openxr_bind_t *edit; unsigned count; + unsigned edit_count; /* Only with XR_VALVE_frame_controller_interaction. */ bool frame; } input_openxr_profile_t; static const input_openxr_profile_t input_openxr_profiles[] = { { "/interaction_profiles/valve/index_controller", - input_openxr_index, ARRAY_SIZE(input_openxr_index), false }, + input_openxr_index, input_openxr_index_edit, + ARRAY_SIZE(input_openxr_index), ARRAY_SIZE(input_openxr_index_edit), + false }, { "/interaction_profiles/oculus/touch_controller", - input_openxr_touch, ARRAY_SIZE(input_openxr_touch), false }, + input_openxr_touch, input_openxr_touch_edit, + ARRAY_SIZE(input_openxr_touch), ARRAY_SIZE(input_openxr_touch_edit), + false }, { "/interaction_profiles/valve/frame_controller_valve", - input_openxr_frame, ARRAY_SIZE(input_openxr_frame), true }, + input_openxr_frame, input_openxr_frame_edit, + ARRAY_SIZE(input_openxr_frame), ARRAY_SIZE(input_openxr_frame_edit), + true }, { "/interaction_profiles/htc/vive_controller", - input_openxr_vive, ARRAY_SIZE(input_openxr_vive), false }, + input_openxr_vive, input_openxr_vive_edit, + ARRAY_SIZE(input_openxr_vive), ARRAY_SIZE(input_openxr_vive_edit), + false }, { "/interaction_profiles/khr/simple_controller", - input_openxr_simple, ARRAY_SIZE(input_openxr_simple), false } + input_openxr_simple, input_openxr_simple_edit, + ARRAY_SIZE(input_openxr_simple), ARRAY_SIZE(input_openxr_simple_edit), + false } }; typedef struct input_openxr_pad @@ -344,6 +460,19 @@ enum input_openxr_role INPUT_OPENXR_ROLE_SPENT /* nothing */ }; +/* A hand's Edit Smoothing, the XR thread's: its filter, the frame and + * space it was last stepped for, what that gave, and the recenters it + * had seen. */ +typedef struct input_openxr_smooth +{ + video_xr_smooth_t filter; + video_xr_pose_t pose; + XrTime time; + XrSpace space; + int recenters; + bool tracked; +} input_openxr_smooth_t; + typedef struct input_openxr { /* Written by the session hooks, while neither the poll nor the XR @@ -358,6 +487,9 @@ typedef struct input_openxr XrSpace grip[INPUT_OPENXR_HANDS]; XrSpace palm[INPUT_OPENXR_HANDS]; XrHandTrackerEXT trackers[INPUT_OPENXR_HANDS]; + /* The profiles that refused edit mode's bindings. */ + XrPath edit_refused[ARRAY_SIZE(input_openxr_profiles)]; + unsigned num_edit_refused; PFN_xrStringToPath StringToPath; PFN_xrPathToString PathToString; PFN_xrCreateActionSet CreateActionSet; @@ -394,8 +526,13 @@ typedef struct input_openxr bool recenter; bool laser_toggle; bool view_toggle; - /* Touch's menu button: held since start_menu_since. */ + bool edit_toggle; + bool edit_ok; /* a profile took edit mode's bindings */ + bool edit_warned; /* its unavailability logged */ + /* Touch's menu button: held since start_menu_since; spent by edit + * mode's menu press until let go. */ bool start_menu_held; + bool start_menu_spent; retro_time_t start_menu_since; float haptic[INPUT_OPENXR_HANDS]; /* the amplitude last accepted */ float haptic_attempt[INPUT_OPENXR_HANDS]; @@ -426,15 +563,102 @@ typedef struct input_openxr uint16_t last_buttons[INPUT_OPENXR_PADS]; unsigned last_sticks[INPUT_OPENXR_PADS]; - /* XR thread. */ + /* XR thread: the dots, the beams, edit mode's outlines, the menu's + * handle, the hinges' lines and the tag, and Edit Smoothing's + * filters and the aims they published. */ XrCompositionLayerQuad cursors[INPUT_OPENXR_HANDS]; + XrCompositionLayerQuad beams[INPUT_OPENXR_HANDS]; + XrCompositionLayerQuad outlines[VIDEO_XR_MAX_SLOTS + 1]; + XrCompositionLayerQuad hinge_lines[OPENXR_HINGE_LAYERS]; + XrCompositionLayerQuad tag; + input_openxr_smooth_t smooth[INPUT_OPENXR_HANDS]; + openxr_aims_t aims; } input_openxr_t; static input_openxr_t input_openxr_st; -/* The main thread's laser mode and menu, for the XR thread's dots. */ -#define INPUT_OPENXR_LASER_MASK 3 -#define INPUT_OPENXR_MENU_OPEN 4 +typedef struct input_openxr_trail +{ + video_xr_pose_t shown; + float width; + float height; + float scale; + XrTime t; /* its frame's display time */ + /* Where the hand put it, unsnapped, and the hinge a let-go there + * settles in: its target's slot plus one (0, none) and both edges. */ + video_xr_pose_t hand; + unsigned char settle_slot; + unsigned char settle_on; + unsigned char settle_edge; +} input_openxr_trail_t; + +/* A hand in edit mode. */ +typedef struct input_openxr_hold +{ + video_xr_grab_t g; + video_xr_pose_t start; + video_xr_pose_t aim; + input_openxr_trail_t trail[INPUT_OPENXR_EDIT_TRAIL]; + unsigned trails; /* entries held */ + unsigned trail_at; /* the newest */ + retro_time_t polled; /* its last tracked poll (us) */ + video_xr_hinge_t lid; /* the hinge last swung on, while lidded */ + video_xr_hinge_t mark; /* while buzzed, where a let-go settles */ + retro_time_t buzz_end; + retro_time_t out; /* seen out of it since (us), 0 in */ + float scale; + float scale0; + float width0; + float height0; + float rest[2]; + unsigned armed; + unsigned steps; + bool pushed; + bool hinged; + bool lidded; + bool buzzed; + bool grip; + bool reset; +} input_openxr_hold_t; + +/* Edit Screens, main thread. Kept over sessions: a session's end stops + * it at the next poll. */ +typedef struct input_openxr_edit +{ + retro_time_t last; /* the last poll's, for the stick's step */ + input_openxr_hold_t hand[INPUT_OPENXR_HANDS]; + openxr_grabs_t grabs; /* as the frame loop is given them */ + unsigned seq; /* grabs let go so far, for the frame snapshot */ + unsigned unbound; /* polls in a row with its grab and menu unbound */ + int chosen; /* the item Select Next chose: a slot, -1 none */ + unsigned stuck; /* hands whose laser has the handle stuck, bits */ + unsigned tag; /* the hand that pressed Select last, for the tag */ + bool on; + bool paused; /* edit mode paused the content */ + bool changed; /* it wrote a setting */ + bool snap; /* Snap to Screens */ + float step; /* Snap to Angles in degrees, 0 Off */ + unsigned char gap; /* Hinge Gap in whole centimetres */ + unsigned smoothing; /* Edit Smoothing, as edit mode started */ + /* Its screens at rest as it last counted their hinges: their slots, + * as bits, each one's first quad, and whether a count is due. */ + unsigned rest_shown; + video_xr_quad_t rest_at[VIDEO_XR_MAX_SLOTS]; + bool rests_due; + bool next; /* last poll's Select Next and Previous */ + bool prev; +} input_openxr_edit_t; + +static input_openxr_edit_t input_openxr_edit; + +/* The main thread's laser mode, menu, Laser Beam and edit mode's Edit + * Smoothing, for the XR thread's dots, beams and aims. */ +#define INPUT_OPENXR_LASER_MASK 3 +#define INPUT_OPENXR_MENU_OPEN 4 +#define INPUT_OPENXR_BEAM_SHIFT 3 +#define INPUT_OPENXR_BEAM_MASK 3 +#define INPUT_OPENXR_SMOOTH_SHIFT 5 +#define INPUT_OPENXR_SMOOTH_MASK 3 static retro_atomic_int_t input_openxr_pointer_config; /* A core's rumble per pad, strong in the high half and weak in the @@ -507,13 +731,17 @@ static bool input_openxr_load(input_openxr_t *st, && INPUT_OPENXR_PROC(st, get, inst, StopHapticFeedback); } -/* One profile's suggestions; a runtime without the profile, or that - * refuses a path, only loses that profile. */ -static void input_openxr_suggest(input_openxr_t *st, +/* One profile's suggestions, edit mode's and the palm's with them. A + * runtime that refuses either keeps the rest; one without the profile, + * or that refuses a path, only loses that profile. Returns whether edit + * mode's were taken. */ +static bool input_openxr_suggest(input_openxr_t *st, const input_openxr_profile_t *p) { unsigned i; unsigned count; + unsigned palms; + unsigned edit = p->edit_count; XrResult res; XrPath profile = XR_NULL_PATH; XrActionSuggestedBinding binds[INPUT_OPENXR_MAX_BINDS @@ -524,24 +752,28 @@ static void input_openxr_suggest(input_openxr_t *st, "/user/hand/right/input/palm_ext/pose" }; if (p->count > INPUT_OPENXR_MAX_BINDS) - return; + return false; + /* Past one suggestion's room, the rest go without edit mode's. */ + if (p->count + edit > INPUT_OPENXR_MAX_BINDS) + edit = 0; if (XR_FAILED(res = st->StringToPath(st->instance, p->path, &profile))) { RARCH_WARN("[OpenXR] No path %s (%d).\n", p->path, (int)res); - return; + return false; } - for (i = 0; i < p->count; i++) + for (i = 0; i < p->count + edit; i++) { - binds[i].action = st->actions[p->binds[i].action]; - if (XR_FAILED(res = st->StringToPath(st->instance, p->binds[i].path, + const input_openxr_bind_t *b = (i < p->count) + ? &p->binds[i] : &p->edit[i - p->count]; + binds[i].action = st->actions[b->action]; + if (XR_FAILED(res = st->StringToPath(st->instance, b->path, &binds[i].binding))) { - RARCH_WARN("[OpenXR] No path %s (%d).\n", p->binds[i].path, - (int)res); - return; + RARCH_WARN("[OpenXR] No path %s (%d).\n", b->path, (int)res); + return false; } } - count = p->count; + count = p->count + edit; if (st->palm_pose) for (i = 0; i < INPUT_OPENXR_PALM_BINDS; i++) { @@ -550,6 +782,7 @@ static void input_openxr_suggest(input_openxr_t *st, &binds[count].binding))) count++; } + palms = count - p->count - edit; sb.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING; sb.next = NULL; sb.interactionProfile = profile; @@ -557,17 +790,46 @@ static void input_openxr_suggest(input_openxr_t *st, sb.suggestedBindings = binds; res = st->SuggestInteractionProfileBindings(st->instance, &sb); - if (XR_FAILED(res) && count > p->count) + /* Refused: without the palm, then without edit mode's, then the rest + * on their own. */ + if (XR_FAILED(res) && palms && edit) + { + sb.countSuggestedBindings = p->count + edit; + if (XR_SUCCEEDED(res = st->SuggestInteractionProfileBindings( + st->instance, &sb))) + palms = 0; + } + if (XR_FAILED(res) && edit) + { + for (i = 0; i < palms; i++) + binds[p->count + i] = binds[p->count + edit + i]; + sb.countSuggestedBindings = p->count + palms; + if (XR_SUCCEEDED(res = st->SuggestInteractionProfileBindings( + st->instance, &sb))) + edit = 0; + } + if (XR_FAILED(res) && palms) { sb.countSuggestedBindings = p->count; res = st->SuggestInteractionProfileBindings(st->instance, &sb); + edit = 0; + palms = 0; } if (XR_FAILED(res)) + { RARCH_WARN("[OpenXR] Bindings refused for %s (%d).\n", p->path, (int)res); - else - RARCH_LOG("[OpenXR] Bindings suggested for %s%s.\n", p->path, - sb.countSuggestedBindings > p->count ? ", with the palm" : ""); + return false; + } + if (edit < p->edit_count) + { + RARCH_WARN("[OpenXR] Edit Screens bindings refused for %s.\n", + p->path); + st->edit_refused[st->num_edit_refused++] = profile; + } + RARCH_LOG("[OpenXR] Bindings suggested for %s%s.\n", p->path, + palms ? ", with the palm" : ""); + return edit > 0; } static void input_openxr_release(input_openxr_t *st) @@ -714,11 +976,12 @@ static void input_openxr_session_created(void *user, } for (i = 0; i < ARRAY_SIZE(input_openxr_profiles); i++) - if (!input_openxr_profiles[i].frame || h->frame_controller) - input_openxr_suggest(st, &input_openxr_profiles[i]); + if ( (!input_openxr_profiles[i].frame || h->frame_controller) + && input_openxr_suggest(st, &input_openxr_profiles[i])) + st->edit_ok = true; - /* All three are attached; each poll syncs the pad set in use and the - * pointer set. */ + /* All four are attached; each poll syncs the pointer set and the pad + * set in use, or edit mode's. */ ai.type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO; ai.next = NULL; ai.countActionSets = INPUT_OPENXR_SETS; @@ -817,6 +1080,33 @@ static bool input_openxr_bool(input_openxr_t *st, unsigned a, XrPath hand) && s.isActive && s.currentState; } +/* Whether a boolean action is bound to anything either hand holds. */ +static bool input_openxr_bound(input_openxr_t *st, unsigned a) +{ + XrActionStateGetInfo gi; + XrActionStateBoolean s; + input_openxr_get_info(st, a, XR_NULL_PATH, &gi); + s.type = XR_TYPE_ACTION_STATE_BOOLEAN; + s.next = NULL; + s.currentState = XR_FALSE; + s.isActive = XR_FALSE; + return XR_SUCCEEDED(st->GetActionStateBoolean(st->session, &gi, &s)) + && s.isActive; +} + +/* Edit mode cannot be used: the desktop told every time, the log once a + * session. */ +static void input_openxr_edit_unavailable(input_openxr_t *st, + const char *why) +{ + const char *msg = msg_hash_to_str(MSG_OPENXR_EDIT_UNAVAILABLE); + if (!st->edit_warned) + RARCH_WARN("[OpenXR] Edit Screens unavailable: %s.\n", why); + st->edit_warned = true; + runloop_msg_queue_push(msg, strlen(msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); +} + static float input_openxr_float(input_openxr_t *st, unsigned a, XrPath hand) { XrActionStateGetInfo gi; @@ -852,16 +1142,20 @@ static void input_openxr_stick(input_openxr_t *st, unsigned a, XrPath hand, } /* Touch's one menu button: Start while it is held, and the RetroArch - * menu too when let go after a second. */ + * menu too when let go after a second. A press that opened the menu + * from edit mode is neither until let go. */ static void input_openxr_start_menu(input_openxr_t *st, bool down, input_openxr_pad_t *pad) { retro_time_t now; if (!down) { - st->start_menu_held = false; + st->start_menu_held = false; + st->start_menu_spent = false; return; } + if (st->start_menu_spent) + return; now = cpu_features_get_time_usec(); if (!st->start_menu_held) { @@ -895,6 +1189,7 @@ static void input_openxr_read_combined(input_openxr_t *st) st->recenter = input_openxr_bool(st, IXA_C_RECENTER, XR_NULL_PATH); st->laser_toggle = input_openxr_bool(st, IXA_C_LASER, XR_NULL_PATH); st->view_toggle = input_openxr_bool(st, IXA_C_VIEW, XR_NULL_PATH); + st->edit_toggle = input_openxr_bool(st, IXA_C_EDIT, XR_NULL_PATH); input_openxr_start_menu(st, input_openxr_bool(st, IXA_C_START_MENU, XR_NULL_PATH), pad); } @@ -925,6 +1220,7 @@ static void input_openxr_read_separate(input_openxr_t *st) st->recenter = input_openxr_bool(st, IXA_S_RECENTER, XR_NULL_PATH); st->laser_toggle = input_openxr_bool(st, IXA_S_LASER, XR_NULL_PATH); st->view_toggle = input_openxr_bool(st, IXA_S_VIEW, XR_NULL_PATH); + st->edit_toggle = input_openxr_bool(st, IXA_S_EDIT, XR_NULL_PATH); } static bool input_openxr_pressing(enum input_openxr_role role) @@ -969,39 +1265,144 @@ static void input_openxr_triggers(input_openxr_t *st, float threshold) } } -/* A hand's aim ray in space at time, when it is tracked. Either +/* A space's pose in another at time, when the runtime finds it. Either * thread: it only reads what the session hooks wrote. */ -static bool input_openxr_ray(const input_openxr_t *st, unsigned h, - XrSpace space, XrTime time, video_xr_vec3_t *o, video_xr_vec3_t *d) +static bool input_openxr_locate(const input_openxr_t *st, XrSpace what, + XrSpace space, XrTime time, video_xr_pose_t *pose) { XrSpaceLocation loc; - video_xr_quat_t q; - video_xr_vec3_t fwd; XrSpaceLocationFlags valid = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT; loc.type = XR_TYPE_SPACE_LOCATION; loc.next = NULL; loc.locationFlags = 0; - if ( !st->aim[h] || !time - || XR_FAILED(st->LocateSpace(st->aim[h], space, time, &loc)) + if ( !what || !time + || XR_FAILED(st->LocateSpace(what, space, time, &loc)) || (loc.locationFlags & valid) != valid) return false; - q.x = loc.pose.orientation.x; - q.y = loc.pose.orientation.y; - q.z = loc.pose.orientation.z; - q.w = loc.pose.orientation.w; - o->x = loc.pose.position.x; - o->y = loc.pose.position.y; - o->z = loc.pose.position.z; - /* An aim pose's ray is its -Z. */ + pose->orientation.x = loc.pose.orientation.x; + pose->orientation.y = loc.pose.orientation.y; + pose->orientation.z = loc.pose.orientation.z; + pose->orientation.w = loc.pose.orientation.w; + pose->position.x = loc.pose.position.x; + pose->position.y = loc.pose.position.y; + pose->position.z = loc.pose.position.z; + return true; +} + +/* A hand's aim pose in space at time, when it is tracked. */ +static bool input_openxr_aim(const input_openxr_t *st, unsigned h, + XrSpace space, XrTime time, video_xr_pose_t *pose) +{ + return input_openxr_locate(st, st->aim[h], space, time, pose); +} + +/* An aim pose's ray: from its position along its -Z. */ +static void input_openxr_aim_ray(const video_xr_pose_t *pose, + video_xr_vec3_t *o, video_xr_vec3_t *d) +{ + video_xr_vec3_t fwd; + *o = pose->position; fwd.x = 0.0f; fwd.y = 0.0f; fwd.z = -1.0f; - video_xr_rotate(&q, &fwd, d); + video_xr_rotate(&pose->orientation, &fwd, d); +} + +/* A hand's aim ray in space at time, when it is tracked. */ +static bool input_openxr_ray(const input_openxr_t *st, unsigned h, + XrSpace space, XrTime time, video_xr_vec3_t *o, video_xr_vec3_t *d) +{ + video_xr_pose_t pose; + if (!input_openxr_aim(st, h, space, time, &pose)) + return false; + input_openxr_aim_ray(&pose, o, d); return true; } +/* Edit Smoothing's level for the frame loop from the main thread's + * config: edit mode's, else Off. */ +static unsigned input_openxr_smoothing(int config) +{ + if ((config & INPUT_OPENXR_LASER_MASK) != VIDEO_XR_LIVE_EDIT) + return VIDEO_OPENXR_EDIT_SMOOTHING_OFF; + return ((unsigned)config >> INPUT_OPENXR_SMOOTH_SHIFT) + & INPUT_OPENXR_SMOOTH_MASK; +} + +/* The frame loop's aim for hand h at time in space: in edit mode with + * Edit Smoothing on, its filter's, stepped once a frame whichever hook + * asks first and published for the main thread; else as located. Lost, + * recentred or in another space, the filter starts afresh. */ +static bool input_openxr_frame_pose(input_openxr_t *st, unsigned h, + XrSpace space, XrTime time, video_xr_pose_t *pose) +{ + video_xr_pose_t raw; + input_openxr_smooth_t *s = &st->smooth[h]; + unsigned level = input_openxr_smoothing( + retro_atomic_load_acquire_int(&input_openxr_pointer_config)); + if (!level || !st->frame) + { + /* The next edit mode with it on starts afresh. */ + s->filter.on = false; + s->time = 0; + return input_openxr_aim(st, h, space, time, pose); + } + if (s->time != time || s->space != space) + { + int recenters = openxr_frame_recenters(st->frame); + double dt = (s->time && time > s->time && s->space == space) + ? (double)(time - s->time) / 1000000000.0 : 0.0; + s->tracked = input_openxr_aim(st, h, space, time, &raw); + if (!s->tracked || recenters != s->recenters) + s->filter.on = false; + if (s->tracked) + { + video_xr_smooth_step(&s->filter, level, &raw, dt, &s->pose); + st->aims.pose[h] = s->pose; + st->aims.valid |= 1u << h; + } + else + st->aims.valid &= ~(1u << h); + st->aims.time[h] = time; + s->time = time; + s->space = space; + s->recenters = recenters; + openxr_frame_publish_aims(st->frame, &st->aims); + } + if (s->tracked) + *pose = s->pose; + return s->tracked; +} + +/* First in each headset frame: in edit mode with Edit Smoothing on, + * both hands' filters stepped and published whatever the layers then + * draw; else every filter afresh and any aims published taken back, + * so the main thread never reads an earlier edit's. */ +static void input_openxr_frame_aims(input_openxr_t *st, int config, + XrSpace space, XrTime time) +{ + unsigned h; + video_xr_pose_t pose; + if (input_openxr_smoothing(config)) + { + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + input_openxr_frame_pose(st, h, space, time, &pose); + return; + } + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + st->smooth[h].filter.on = false; + st->smooth[h].time = 0; + } + if (st->aims.valid) + { + st->aims.valid = 0; + openxr_frame_publish_aims(st->frame, &st->aims); + } +} + /* A point on a screen's quad as the core's pointer coordinates: into * the core's view map when it has one, over the whole frame when not. * A window has the frame where the video driver's viewport puts it, as @@ -1236,44 +1637,499 @@ static void input_openxr_yield(input_openxr_t *st, unsigned laser, st->yield = true; } -/* The XR thread, each headset frame: a dot where each hand points at a - * live quad, from its own locate at the frame's display time. */ +/* One of edit mode's layers: the laser's image's w by h part from x, y, + * over q, just in front of it. */ +static const XrCompositionLayerBaseHeader *input_openxr_edit_layer( + XrCompositionLayerQuad *l, const video_xr_quad_t *q, XrSpace space, + XrSwapchain cursor, int32_t x, int32_t y, int32_t w, int32_t h) +{ + video_xr_vec3_t lift, world; + lift.x = 0.0f; + lift.y = 0.0f; + lift.z = VIDEO_XR_CURSOR_LIFT; + video_xr_rotate(&q->pose.orientation, &lift, &world); + l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; + l->next = NULL; + l->subImage.imageArrayIndex = 0; + l->layerFlags = + XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + l->space = space; + l->eyeVisibility = XR_EYE_VISIBILITY_BOTH; + l->subImage.swapchain = cursor; + l->subImage.imageRect.offset.x = x; + l->subImage.imageRect.offset.y = y; + l->subImage.imageRect.extent.width = w; + l->subImage.imageRect.extent.height = h; + l->pose.orientation.x = q->pose.orientation.x; + l->pose.orientation.y = q->pose.orientation.y; + l->pose.orientation.z = q->pose.orientation.z; + l->pose.orientation.w = q->pose.orientation.w; + l->pose.position.x = q->pose.position.x + world.x; + l->pose.position.y = q->pose.position.y + world.y; + l->pose.position.z = q->pose.position.z + world.z; + l->size.width = q->width; + l->size.height = q->height; + return (const XrCompositionLayerBaseHeader*)l; +} + +/* Edge's rim of slot to hide, unless one beside it already is: one crop + * hides one edge or two opposite. */ +static void input_openxr_hinge_hide(unsigned *hide, unsigned slot, + unsigned edge) +{ + unsigned beside = (edge < 2) ? 12u : 3u; + if (!(hide[slot] & beside)) + hide[slot] |= 1u << edge; +} + +/* A hidden hinge slot's line: VIDEO_XR_HINGE_WIDTH along the x of + * slot's screen from its middle, else of set's first quad; false with + * no quad. */ +static bool input_openxr_hinge_hidden(const video_xr_quad_set_t *set, + unsigned slot, video_xr_vec3_t *a, video_xr_vec3_t *b) +{ + unsigned i; + video_xr_vec3_t x; + const video_xr_quad_t *q = set->num_quads ? &set->quads[0] : NULL; + for (i = 0; i < set->num_quads; i++) + if ( set->quads[i].kind != VIDEO_XR_QUAD_MENU + && set->quads[i].slot == slot) + { + q = &set->quads[i]; + break; + } + if (!q) + return false; + x.x = VIDEO_XR_HINGE_WIDTH; + x.y = 0.0f; + x.z = 0.0f; + video_xr_rotate(&q->pose.orientation, &x, &x); + *a = q->pose.position; + b->x = a->x + x.x; + b->y = a->y + x.y; + b->z = a->z + x.z; + return true; +} + +/* This frame's hinge slots, one for each hand holding a screen, hand 0 + * first, then one for each pair at rest: in hide, by slot, the edges + * whose rims go, each held screen's paired edge and its target's, then + * both edges of each pair at rest no hand holds (of a screen's two + * adjacent ones, the first); from a to b each slot's line, bit k of + * shown set where slot k's shows. A slot with no line shown is hidden + * in place, so a mark or a wobble moves no layer. Returns the slots. */ +static unsigned input_openxr_hinges(const openxr_frame_t *f, + const video_xr_quad_set_t *set, unsigned *hide, video_xr_vec3_t *a, + video_xr_vec3_t *b, unsigned *shown) +{ + unsigned h, i, r; + unsigned holding = 0; + unsigned lines = 0; + memset(hide, 0, VIDEO_XR_MAX_SLOTS * sizeof(*hide)); + *shown = 0; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + const video_xr_hinge_t *k = &f->hinges[h]; + const video_xr_quad_t *held = NULL; + const video_xr_quad_t *target = NULL; + bool hinged = ((f->hinged >> h) & 1u) != 0; + bool line; + if ( f->grabs.hand[h].state != VIDEO_XR_GRAB_HELD + || f->grabs.hand[h].slot >= VIDEO_XR_MENU_SLOT) + continue; + if (hinged) + { + hide[f->grabs.hand[h].slot] |= 1u << k->edge; + input_openxr_hinge_hide(hide, k->slot, k->on); + } + for (i = 0; i < set->num_quads; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if (q->kind == VIDEO_XR_QUAD_MENU) + continue; + if (!held && q->slot == f->grabs.hand[h].slot) + held = q; + if (!target && q->slot == k->slot) + target = q; + } + line = hinged && held && target + && video_xr_hinge_line(k, held->width, held->height, target, + &a[lines], &b[lines]); + if (line) + *shown |= 1u << lines; + if ( line || input_openxr_hinge_hidden(set, + f->grabs.hand[h].slot, &a[lines], &b[lines])) + lines++; + } + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + if ( f->grabs.hand[h].state == VIDEO_XR_GRAB_HELD + && f->grabs.hand[h].slot < VIDEO_XR_MAX_SLOTS) + holding |= 1u << f->grabs.hand[h].slot; + for (r = 0; r < f->grabs.num_rests && r < OPENXR_REST_HINGES; r++) + { + video_xr_hinge_t k; + const video_xr_rest_t *p = &f->grabs.rests[r]; + const video_xr_quad_t *held = NULL; + const video_xr_quad_t *target = NULL; + bool over = false; + bool line = false; + bool idle = + !(((holding >> p->slot) | (holding >> p->target)) & 1u); + for (i = 0; i < set->num_quads; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if (q->kind == VIDEO_XR_QUAD_MENU) + continue; + if (!held && q->slot == p->slot) + { + held = q; + over = target != NULL; + } + if (!target && q->slot == p->target) + target = q; + } + if (idle && held && target) + { + input_openxr_hinge_hide(hide, p->slot, p->edge); + input_openxr_hinge_hide(hide, p->target, p->on); + memset(&k, 0, sizeof(k)); + k.edge = p->edge; + k.slot = p->target; + k.on = p->on; + /* On the edges as they are; crossed, on the one drawn later. */ + video_xr_hinge_place(&k, &held->pose, held->width, held->height, + target, over); + line = video_xr_hinge_line(&k, held->width, held->height, target, + &a[lines], &b[lines]); + } + if (line) + *shown |= 1u << lines; + if ( line || input_openxr_hinge_hidden(set, p->slot, &a[lines], + &b[lines])) + lines++; + } + return lines; +} + +/* Edit mode's outlines after layers' first n, of the menu or of every + * other item: one an item (a stereo screen's two quads are one, a fixed + * slot none), each just outside its item without the rims of its edges + * in hide (by slot), the menu's handle after its outline: bright for + * the slots in lit, faint for the rest; up to cap in all; turned to the + * head with an item the frame shows by its back. With the cut outlines + * in the image, a cut changes the outline's cell alone, never its quad. */ +static unsigned input_openxr_outlines(input_openxr_t *st, + const video_xr_quad_set_t *set, XrSpace space, XrSwapchain cursor, + unsigned lit, const unsigned *hide, bool menu, + const XrCompositionLayerBaseHeader **layers, unsigned n, unsigned cap) +{ + unsigned i; + unsigned done = 0; + bool cells = openxr_frame_cursor_w(st->frame) == VIDEO_XR_CURSOR_W; + for (i = 0; i < set->num_quads && n < cap; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + video_xr_quad_t outline; + unsigned bright; + unsigned x, w; + bool back = ((st->frame->backs >> i) & 1u) != 0; + if ( q->slot >= VIDEO_XR_MAX_SLOTS || (done & (1u << q->slot)) + || set->slots[q->slot].fixed + || (q->kind == VIDEO_XR_QUAD_MENU) != menu) + continue; + done |= 1u << q->slot; + bright = (lit >> q->slot) & 1u; + video_xr_outline(q, &outline); + if (back) + video_xr_turn_back(&outline.pose); + if (cells) + { + x = video_xr_outline_cell(hide[q->slot], back, bright); + w = VIDEO_XR_OUTLINE_DIM; + } + else + { + video_xr_outline_crop(&outline, hide[q->slot], back, &x, &w); + x += VIDEO_XR_OUTLINE_X(bright); + } + layers[n] = input_openxr_edit_layer(&st->outlines[n], &outline, + space, cursor, (int32_t)x, 0, (int32_t)w, VIDEO_XR_OUTLINE_DIM); + n++; + if (q->kind == VIDEO_XR_QUAD_MENU && n < cap) + { + video_xr_quad_t handle; + video_xr_menu_handle(q, &handle); + if (back) + video_xr_turn_back(&handle.pose); + layers[n] = input_openxr_edit_layer(&st->outlines[n], &handle, + space, cursor, VIDEO_XR_HANDLE_X(bright), VIDEO_XR_HANDLE_Y, + VIDEO_XR_HANDLE_COLS, VIDEO_XR_HANDLE_ROWS); + n++; + } + } + return n; +} + +/* Whether a laser in this mode has anything in set to point at. */ +static bool input_openxr_any_live(const video_xr_quad_set_t *set, + unsigned laser, bool menu_open) +{ + unsigned i; + for (i = 0; i < set->num_quads; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + if ( video_xr_quad_live(q, laser, menu_open) + && !(laser == VIDEO_XR_LIVE_EDIT && set->slots[q->slot].fixed)) + return true; + } + return false; +} + +/* A hand's beam from o along d, reach metres to its dot when it hit + * something, facing eye. */ +static const XrCompositionLayerBaseHeader *input_openxr_beam_layer( + XrCompositionLayerQuad *l, const video_xr_vec3_t *o, + const video_xr_vec3_t *d, float reach, bool hit, + const video_xr_vec3_t *eye, XrSpace space, XrSwapchain cursor) +{ + video_xr_pose_t pose; + video_xr_beam(o, d, reach, eye, &pose); + l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; + l->next = NULL; + l->subImage.imageArrayIndex = 0; + l->layerFlags = + XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + l->space = space; + l->eyeVisibility = XR_EYE_VISIBILITY_BOTH; + l->subImage.swapchain = cursor; + l->subImage.imageRect.offset.x = VIDEO_XR_BEAM_X(hit ? 0 : 1); + l->subImage.imageRect.offset.y = VIDEO_XR_BEAM_Y; + l->subImage.imageRect.extent.width = VIDEO_XR_BEAM_LEN; + l->subImage.imageRect.extent.height = VIDEO_XR_BEAM_ROWS; + l->pose.orientation.x = pose.orientation.x; + l->pose.orientation.y = pose.orientation.y; + l->pose.orientation.z = pose.orientation.z; + l->pose.orientation.w = pose.orientation.w; + l->pose.position.x = pose.position.x; + l->pose.position.y = pose.position.y; + l->pose.position.z = pose.position.z; + l->size.width = reach; + l->size.height = VIDEO_XR_BEAM_WIDTH; + return (const XrCompositionLayerBaseHeader*)l; +} + +/* p moved VIDEO_XR_HINGE_LIFT toward eye: a runtime need not draw + * overlapping quads in order, and SteamVR draws screens over a line on + * their seam. */ +static void input_openxr_hinge_lift(const video_xr_vec3_t *p, + const video_xr_vec3_t *eye, video_xr_vec3_t *out) +{ + video_xr_vec3_t t; + float n, s; + t.x = eye->x - p->x; + t.y = eye->y - p->y; + t.z = eye->z - p->z; + n = (float)sqrt(t.x * t.x + t.y * t.y + t.z * t.z); + s = (n > VIDEO_XR_HINGE_LIFT) ? VIDEO_XR_HINGE_LIFT / n : 0.0f; + out->x = p->x + t.x * s; + out->y = p->y + t.y * s; + out->z = p->z + t.z * s; +} + +/* A hinge's line from a to b, each end lifted toward eye and the ribbon + * facing it; the strip a texel in from each end, so filtering there + * meets green alone; hidden, from rows of its column nothing is drawn + * in. */ +static const XrCompositionLayerBaseHeader *input_openxr_hinge_layer( + XrCompositionLayerQuad *l, const video_xr_vec3_t *a, + const video_xr_vec3_t *b, const video_xr_vec3_t *eye, + XrSpace space, XrSwapchain cursor, bool hidden) +{ + video_xr_pose_t pose; + video_xr_vec3_t p, q, d; + float len; + input_openxr_hinge_lift(a, eye, &p); + input_openxr_hinge_lift(b, eye, &q); + d.x = q.x - p.x; + d.y = q.y - p.y; + d.z = q.z - p.z; + len = (float)sqrt(d.x * d.x + d.y * d.y + d.z * d.z); + d.x /= len; + d.y /= len; + d.z /= len; + video_xr_beam(&p, &d, len, eye, &pose); + l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; + l->next = NULL; + l->subImage.imageArrayIndex = 0; + l->layerFlags = + XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + l->space = space; + l->eyeVisibility = XR_EYE_VISIBILITY_BOTH; + l->subImage.swapchain = cursor; + l->subImage.imageRect.offset.x = VIDEO_XR_HINGE_X + 1; + l->subImage.imageRect.offset.y = hidden + ? VIDEO_XR_HINGE_HIDDEN_Y : VIDEO_XR_HINGE_Y; + l->subImage.imageRect.extent.width = VIDEO_XR_HINGE_COLS - 2; + l->subImage.imageRect.extent.height = VIDEO_XR_HINGE_ROWS; + l->pose.orientation.x = pose.orientation.x; + l->pose.orientation.y = pose.orientation.y; + l->pose.orientation.z = pose.orientation.z; + l->pose.orientation.w = pose.orientation.w; + l->pose.position.x = pose.position.x; + l->pose.position.y = pose.position.y; + l->pose.position.z = pose.position.z; + l->size.width = len; + l->size.height = VIDEO_XR_HINGE_WIDTH; + return (const XrCompositionLayerBaseHeader*)l; +} + +/* Edit mode's tag over the hand at o, facing eye: tag's cell, the + * chosen item's words. */ +static const XrCompositionLayerBaseHeader *input_openxr_tag_layer( + XrCompositionLayerQuad *l, const video_xr_vec3_t *o, + const video_xr_vec3_t *eye, unsigned tag, XrSpace space, + XrSwapchain cursor) +{ + video_xr_pose_t pose; + video_xr_tag(o, eye, &pose); + l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; + l->next = NULL; + l->subImage.imageArrayIndex = 0; + l->layerFlags = + XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + l->space = space; + l->eyeVisibility = XR_EYE_VISIBILITY_BOTH; + l->subImage.swapchain = cursor; + l->subImage.imageRect.offset.x = VIDEO_XR_TAG_X(tag); + l->subImage.imageRect.offset.y = VIDEO_XR_TAG_Y; + l->subImage.imageRect.extent.width = VIDEO_XR_TAG_COLS; + l->subImage.imageRect.extent.height = VIDEO_XR_TAG_ROWS; + l->pose.orientation.x = pose.orientation.x; + l->pose.orientation.y = pose.orientation.y; + l->pose.orientation.z = pose.orientation.z; + l->pose.orientation.w = pose.orientation.w; + l->pose.position.x = pose.position.x; + l->pose.position.y = pose.position.y; + l->pose.position.z = pose.position.z; + l->size.width = VIDEO_XR_TAG_HEIGHT + * VIDEO_XR_TAG_COLS / VIDEO_XR_TAG_ROWS; + l->size.height = VIDEO_XR_TAG_HEIGHT; + return (const XrCompositionLayerBaseHeader*)l; +} + +/* Edit mode's bright items, from the slot each hand's dot is on (-1: + * none): every held item; with nothing chosen, those pointed at; else + * the chosen item, and what a hand points at while the other holds the + * chosen item. */ +static unsigned input_openxr_edit_lit(const openxr_grabs_t *g, + const int *pointed) +{ + unsigned h; + unsigned lit = 0; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + const video_xr_grab_t *other = &g->hand[h ^ 1]; + if ( g->hand[h].state == VIDEO_XR_GRAB_HELD + && g->hand[h].slot < VIDEO_XR_MAX_SLOTS) + lit |= 1u << g->hand[h].slot; + if ( pointed[h] >= 0 + && (!g->chosen + || (other->state == VIDEO_XR_GRAB_HELD + && other->slot == g->chosen - 1))) + lit |= 1u << pointed[h]; + } + if (g->chosen) + lit |= 1u << (g->chosen - 1); + return lit; +} + +/* The XR thread, each headset frame, from each hand's own locate: its + * dot on a live quad; in edit mode, in the layers left, the outlines + * (screens' under the screens, but rims a hinge pairs), the hinge lines + * and the chosen item's tag; then the beams, as Laser Beam says. */ static unsigned input_openxr_frame_layers(void *user, XrTime time, - const XrCompositionLayerBaseHeader **layers, unsigned cap) + const video_xr_quad_set_t *set, XrSpace space, + const XrCompositionLayerBaseHeader **layers, unsigned cap, + unsigned *under) { unsigned h; XrSwapchain cursor; - video_xr_quad_set_t set; + video_xr_pose_t head; + video_xr_pose_t aim; + video_xr_vec3_t o[INPUT_OPENXR_HANDS]; + video_xr_vec3_t d[INPUT_OPENXR_HANDS]; + float reach[INPUT_OPENXR_HANDS]; + bool hit[INPUT_OPENXR_HANDS]; + int pointed[INPUT_OPENXR_HANDS]; + int ray_of[INPUT_OPENXR_HANDS]; + bool live, tagged, beaming; + const openxr_grabs_t *g; + unsigned lit; + unsigned hide[VIDEO_XR_MAX_SLOTS]; + video_xr_vec3_t from[OPENXR_HINGE_LAYERS]; + video_xr_vec3_t to[OPENXR_HINGE_LAYERS]; + unsigned lines = 0; + unsigned shown = 0; unsigned n = 0; - XrSpace space = XR_NULL_HANDLE; + unsigned dots = 0; + unsigned rays = 0; input_openxr_t *st = &input_openxr_st; int config = retro_atomic_load_acquire_int( &input_openxr_pointer_config); int laser = config & INPUT_OPENXR_LASER_MASK; bool menu_open = (config & INPUT_OPENXR_MENU_OPEN) != 0; + unsigned beam = ((unsigned)config >> INPUT_OPENXR_BEAM_SHIFT) + & INPUT_OPENXR_BEAM_MASK; (void)user; + *under = 0; + if (st->frame) + input_openxr_frame_aims(st, config, space, time); if ( !st->frame || laser == VIDEO_OPENXR_LASER_OFF || !openxr_session_focused(st->frame->session) || !(cursor = openxr_frame_cursor(st->frame))) return 0; - openxr_frame_get_shown_quads(st->frame, &set, &space); - for (h = 0; h < INPUT_OPENXR_HANDS && n < cap; h++) + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + pointed[h] = -1; + ray_of[h] = -1; + } + for (h = 0; h < INPUT_OPENXR_HANDS && dots < cap; h++) { int q; - float u, v, t, size; + float u, v, size; + video_xr_quad_t on; + float t = 0.0f; video_xr_pose_t pose; - video_xr_vec3_t o, d; - XrCompositionLayerQuad *l = &st->cursors[h]; - if ( !input_openxr_ray(st, h, space, time, &o, &d) - || (q = video_xr_pick(&set, (unsigned)laser, menu_open, - &o, &d, &u, &v, &t)) < 0) + XrCompositionLayerQuad *l = &st->cursors[dots]; + if (!input_openxr_frame_pose(st, h, space, time, &aim)) continue; - size = video_xr_cursor(&set.quads[q], u, v, t, &pose); + input_openxr_aim_ray(&aim, &o[rays], &d[rays]); + q = (laser == VIDEO_XR_LIVE_EDIT) + ? video_xr_pick_edit(set, + ((st->frame->grabs.stuck >> h) & 1u) != 0, &o[rays], + &d[rays], &u, &v, &t, NULL) + : video_xr_pick(set, (unsigned)laser, menu_open, &o[rays], + &d[rays], &u, &v, &t); + hit[rays] = q >= 0; + reach[rays] = (q >= 0) ? t : VIDEO_XR_BEAM_REACH; + ray_of[h] = (int)rays; + rays++; + if (q < 0) + continue; + pointed[h] = (int)set->quads[q].slot; + on = set->quads[q]; + /* On a back, facing the head as the back does. */ + if ((st->frame->backs >> q) & 1u) + { + video_xr_turn_back(&on.pose); + u = 1.0f - u; + } + size = video_xr_cursor(&on, u, v, t, &pose); l->type = XR_TYPE_COMPOSITION_LAYER_QUAD; l->next = NULL; l->subImage.imageRect.offset.x = 0; - l->subImage.imageRect.offset.y = 0; + l->subImage.imageRect.offset.y = VIDEO_XR_CURSOR_Y; l->subImage.imageArrayIndex = 0; l->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; @@ -1291,12 +2147,69 @@ static unsigned input_openxr_frame_layers(void *user, XrTime time, l->pose.position.z = pose.position.z; l->size.width = size; l->size.height = size; - layers[n++] = - (const XrCompositionLayerBaseHeader*)l; + dots++; } + if (laser == VIDEO_XR_LIVE_EDIT && cap > OPENXR_KEPT_EXTRA_LAYERS) + { + lines = input_openxr_hinges(st->frame, set, hide, from, to, + &shown); + lit = input_openxr_edit_lit(&st->frame->grabs, pointed); + n = input_openxr_outlines(st, set, space, cursor, lit, hide, + false, layers, 0, cap - OPENXR_KEPT_EXTRA_LAYERS); + *under = n; + n = input_openxr_outlines(st, set, space, cursor, lit, hide, + true, layers, n, cap - OPENXR_KEPT_EXTRA_LAYERS); + if (lines > cap - OPENXR_KEPT_EXTRA_LAYERS - n) + lines = cap - OPENXR_KEPT_EXTRA_LAYERS - n; + } + /* The lines in what the outlines leave, the tag in what they leave, + * the beams in what it leaves; drawn lines and beams first, so the + * tag is over them. */ + g = &st->frame->grabs; + live = input_openxr_any_live(set, (unsigned)laser, menu_open); + tagged = live && laser == VIDEO_XR_LIVE_EDIT + && g->tag < INPUT_OPENXR_HANDS && ray_of[g->tag] >= 0 + && n + lines + OPENXR_KEPT_EXTRA_LAYERS < cap + && openxr_frame_cursor_w(st->frame) >= VIDEO_XR_CURSOR_TAGGED_W; + beaming = live && rays + && n + lines + (tagged ? 1u : 0u) + OPENXR_KEPT_EXTRA_LAYERS < cap + && (beam == VIDEO_OPENXR_LASER_BEAM_ALWAYS + || (beam == VIDEO_OPENXR_LASER_BEAM_EDIT + && laser == VIDEO_XR_LIVE_EDIT)); + if ( (lines || tagged || beaming) + && input_openxr_locate(st, st->frame->session->view_space, space, + time, &head)) + { + for (h = 0; h < lines; h++) + layers[n++] = input_openxr_hinge_layer(&st->hinge_lines[h], + &from[h], &to[h], &head.position, space, cursor, + !((shown >> h) & 1u)); + if (beaming) + for (h = 0; h < rays && n + (tagged ? 1u : 0u) + + OPENXR_KEPT_EXTRA_LAYERS < cap; h++) + layers[n++] = input_openxr_beam_layer(&st->beams[h], &o[h], + &d[h], reach[h], hit[h], &head.position, space, cursor); + if (tagged) + layers[n++] = input_openxr_tag_layer(&st->tag, &o[ray_of[g->tag]], + &head.position, (g->chosen < VIDEO_XR_TAGS) ? g->chosen : 0, + space, cursor); + } + for (h = 0; h < dots; h++) + layers[n++] = (const XrCompositionLayerBaseHeader*)&st->cursors[h]; return n; } +/* The XR thread: a hand's aim pose at a frame's time, for what it + * holds; edit mode's smoothed as Edit Smoothing says. */ +static bool input_openxr_frame_aim(void *user, unsigned hand, + XrSpace space, XrTime time, video_xr_pose_t *pose) +{ + (void)user; + return hand < INPUT_OPENXR_HANDS + && input_openxr_frame_pose(&input_openxr_st, hand, space, time, + pose); +} + /* Rumble into each hand's haptics: in Combined player 1's strong motor * is the left hand and its weak the right; in Separate each player's * hand takes the stronger of its two. Paused, the refresh would keep @@ -1636,32 +2549,1034 @@ static const char *input_openxr_joypad_name(unsigned pad) return input_openxr_inner->name ? input_openxr_inner->name(pad) : NULL; } +/* Whether set shows slot's item, and as one. */ +static bool input_openxr_edit_shown(const video_xr_quad_set_t *set, + unsigned slot) +{ + unsigned i; + if (set->slots[slot].fixed) + return false; + for (i = 0; i < set->num_quads; i++) + if (set->quads[i].slot == slot) + return true; + return false; +} + +/* The item after chosen, step 1, or before it, -1: the shown items by + * slot, so the screens by number and the menu last, then the laser + * (-1). */ +static int input_openxr_edit_cycle(const video_xr_quad_set_t *set, + int chosen, int step) +{ + unsigned n; + int i = (chosen < 0) ? VIDEO_XR_MAX_SLOTS : chosen; + for (n = 0; n < VIDEO_XR_MAX_SLOTS; n++) + { + i = (i + step + VIDEO_XR_MAX_SLOTS + 1) % (VIDEO_XR_MAX_SLOTS + 1); + if (i == VIDEO_XR_MAX_SLOTS) + return -1; + if (input_openxr_edit_shown(set, (unsigned)i)) + return i; + } + return -1; +} + +/* What hand h's grip and Reset act on, as an index into st->quads: the + * chosen item, wherever it is, unless the other hand holds it; else + * what h's laser is on. -1: nothing. */ +static int input_openxr_edit_target(const input_openxr_t *st, + const input_openxr_edit_t *e, unsigned h) +{ + unsigned i; + const video_xr_grab_t *other = &e->hand[h ^ 1].g; + if ( e->chosen < 0 + || (other->state == VIDEO_XR_GRAB_HELD + && other->slot == (unsigned)e->chosen)) + return st->hit[h]; + for (i = 0; i < st->quads.num_quads; i++) + if (st->quads.quads[i].slot == (unsigned)e->chosen) + return (int)i; + return -1; +} + +/* A held item where its hand last put it, both eyes of a stereo screen. */ +static void input_openxr_edit_place(video_xr_quad_set_t *set, + const video_xr_grab_t *g) +{ + unsigned i; + for (i = 0; i < set->num_quads; i++) + if (set->quads[i].slot == g->slot) + { + set->quads[i].pose = g->last; + set->quads[i].width = g->width; + set->quads[i].height = g->height; + } +} + +/* An item's place, where the menu's rows write it. */ +static void input_openxr_edit_write(input_openxr_edit_t *e, + const input_openxr_hold_t *hold, const video_screen_xr_t *values) +{ + if (hold->g.slot == VIDEO_XR_MENU_SLOT) + config_set_video_menu_xr(values); + else + config_set_video_screen_xr(hold->g.slot, values); + e->changed = true; +} + +/* The slots the hands hold, as bits: nothing snaps to them. */ +static unsigned input_openxr_edit_holding(const input_openxr_edit_t *e) +{ + unsigned h; + unsigned bits = 0; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + if ( e->hand[h].g.state == VIDEO_XR_GRAB_HELD + && e->hand[h].g.slot < VIDEO_XR_MAX_SLOTS) + bits |= 1u << e->hand[h].g.slot; + return bits; +} + +/* hold's item where it shows as the other hand takes a screen that + * pulled it, the screens in was held before: so it does not hop, and + * carries on from there free of that pull. */ +static void input_openxr_edit_rebase(input_openxr_edit_t *e, + input_openxr_hold_t *hold, const video_xr_quad_set_t *targets, + unsigned was) +{ + video_xr_pose_t at, now; + if (hold->g.state != VIDEO_XR_GRAB_HELD || !targets) + return; + video_xr_grab_pose(&hold->g, &hold->aim, targets, was, &hold->start, + &at, NULL, NULL); + video_xr_grab_pose(&hold->g, &hold->aim, targets, + input_openxr_edit_holding(e), &hold->start, &now, NULL, NULL); + if (memcmp(&at, &now, sizeof(at))) + video_xr_grab_put(&hold->g, &hold->aim, &at); +} + +/* The screens no hand holds that rest in a hinge, for the frame loop: + * counted again only when one came, went, moved or was let go, or edit + * mode began. */ +static void input_openxr_edit_rests(input_openxr_edit_t *e, + const video_xr_quad_set_t *set) +{ + unsigned i; + unsigned skip = input_openxr_edit_holding(e); + unsigned shown = 0; + bool due = e->rests_due; + for (i = 0; i < set->num_quads; i++) + { + const video_xr_quad_t *q = &set->quads[i]; + video_xr_quad_t *at; + if ( q->slot >= VIDEO_XR_MENU_SLOT || set->slots[q->slot].fixed + || (((skip | shown) >> q->slot) & 1u)) + continue; + shown |= 1u << q->slot; + at = &e->rest_at[q->slot]; + if ( memcmp(&at->pose, &q->pose, sizeof(q->pose)) + || at->width != q->width || at->height != q->height) + { + *at = *q; + due = true; + } + } + if (shown != e->rest_shown) + due = true; + e->rest_shown = shown; + e->rests_due = false; + if (due) + e->grabs.num_rests = (e->on && e->snap) + ? video_xr_rest_hinges(set, skip, e->gap * 0.01f, e->grabs.rests, + OPENXR_REST_HINGES) + : 0; +} + +/* A buzz on hand h, duration ns at amp. */ +static void input_openxr_edit_tick(input_openxr_t *st, unsigned h, + XrDuration duration, float amp) +{ + XrHapticActionInfo info; + XrHapticVibration vib; + info.type = XR_TYPE_HAPTIC_ACTION_INFO; + info.next = NULL; + info.action = st->actions[IXA_RUMBLE]; + info.subactionPath = st->hands[h]; + vib.type = XR_TYPE_HAPTIC_VIBRATION; + vib.next = NULL; + vib.duration = duration; + vib.frequency = XR_FREQUENCY_UNSPECIFIED; + vib.amplitude = amp; + st->ApplyHapticFeedback(st->session, &info, + (const XrHapticBaseHeader*)&vib); +} + +/* The snaps whose buzz may still fire after a move that snapped: one + * that took hold is spent until its axis, or the gap, pulls no more. */ +static unsigned input_openxr_edit_armed(unsigned armed, unsigned snapped) +{ + const unsigned buzz = VIDEO_XR_SNAP_ALL | VIDEO_XR_SNAP_GAP; + unsigned pulling = (snapped >> VIDEO_XR_SNAP_PULL_SHIFT) & buzz; + return (armed & ~snapped & buzz) | (buzz & ~pulling); +} + +/* The hinge a let-go of hold's item, where aim puts it now, settles in + * on targets: hinge if it does (NULL: none), else its mark if that + * does; NULL: neither. */ +static const video_xr_hinge_t *input_openxr_edit_settles( + const input_openxr_edit_t *e, const input_openxr_hold_t *hold, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *hinge) +{ + video_xr_pose_t pose; + if ( hinge + && video_xr_grab_settle(&hold->g, aim, targets, + input_openxr_edit_holding(e), hinge, &pose)) + return hinge; + if ( hold->buzzed + && video_xr_grab_settle(&hold->g, aim, targets, + input_openxr_edit_holding(e), &hold->mark, &pose)) + return &hold->mark; + return NULL; +} + +/* hold's trail gains, dated by the frame its aim is from, where its + * item shows now, where aim puts it unsnapped, and whether a let-go + * there settles in its hinge, or else its mark, on targets (NULL: + * none); its newest's frame again replaces it. */ +static void input_openxr_edit_trail(const input_openxr_edit_t *e, + input_openxr_hold_t *hold, XrTime frame, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets) +{ + input_openxr_trail_t *at; + const video_xr_hinge_t *h; + const input_openxr_trail_t *newest = &hold->trail[hold->trail_at]; + const input_openxr_trail_t *before = &hold->trail[(hold->trail_at + + INPUT_OPENXR_EDIT_TRAIL - 1) % INPUT_OPENXR_EDIT_TRAIL]; + if ( !hold->trails + || (frame > newest->t && (hold->trails < 2 + || newest->t - before->t + >= (XrTime)INPUT_OPENXR_EDIT_TRAIL_US * 1000))) + { + hold->trail_at = (hold->trail_at + 1) % INPUT_OPENXR_EDIT_TRAIL; + if (hold->trails < INPUT_OPENXR_EDIT_TRAIL) + hold->trails++; + } + at = &hold->trail[hold->trail_at]; + at->t = frame; + at->shown = hold->g.last; + at->width = hold->g.width; + at->height = hold->g.height; + at->scale = hold->scale; + at->settle_slot = 0; + at->settle_on = 0; + at->settle_edge = 0; + video_xr_grab_pose(&hold->g, aim, NULL, 0, NULL, &at->hand, NULL, + NULL); + h = input_openxr_edit_settles(e, hold, aim, targets, + hold->hinged ? &hold->lid : NULL); + if (h) + { + at->settle_slot = (unsigned char)(h->slot + 1); + at->settle_on = (unsigned char)h->on; + at->settle_edge = (unsigned char)h->edge; + } +} + +/* Whether hold's trail entry at follows a stall in st's frames. */ +static bool input_openxr_edit_stalled(input_openxr_t *st, + const input_openxr_hold_t *hold, unsigned at) +{ + unsigned before = (at + INPUT_OPENXR_EDIT_TRAIL - 1) + % INPUT_OPENXR_EDIT_TRAIL; + XrDuration period = openxr_frame_period(st->frame); + if (period < (XrDuration)INPUT_OPENXR_EDIT_TRAIL_US * 2000) + period = (XrDuration)INPUT_OPENXR_EDIT_TRAIL_US * 2000; + return (double)(hold->trail[at].t - hold->trail[before].t) + > INPUT_OPENXR_EDIT_GAP * (double)period; +} + +/* hold's newest trail entry INPUT_OPENXR_EDIT_REWIND_US before frame, + * else its oldest, but never past a stall in st's frames. */ +static const input_openxr_trail_t *input_openxr_edit_rewind( + input_openxr_t *st, const input_openxr_hold_t *hold, XrTime frame) +{ + unsigned i; + unsigned at = hold->trail_at; + for (i = 1; i < hold->trails && frame - hold->trail[at].t + < (XrTime)INPUT_OPENXR_EDIT_REWIND_US * 1000; i++) + { + if (input_openxr_edit_stalled(st, hold, at)) + break; + at = (at + INPUT_OPENXR_EDIT_TRAIL - 1) % INPUT_OPENXR_EDIT_TRAIL; + } + return &hold->trail[at]; +} + +/* hold's item put at pose as large as its trail's entry at shows it + * (NULL: as large as now), with no snap, from its last aim. */ +static void input_openxr_edit_put(input_openxr_edit_t *e, + input_openxr_hold_t *hold, const input_openxr_trail_t *at, + const video_xr_pose_t *pose) +{ + video_screen_xr_t values; + if (at) + { + hold->g.width = at->width; + hold->g.height = at->height; + hold->scale = at->scale; + } + video_xr_grab_put(&hold->g, &hold->aim, pose); + video_views_xr_reset(&values, 1); + video_xr_grab_pose(&hold->g, &hold->aim, NULL, 0, NULL, + &hold->g.last, &values, NULL); + values.scale = hold->scale; + input_openxr_edit_write(e, hold, &values); +} + +/* Where hold's item, let go while its mark shows, settles in it on + * targets: from its trail's entry *at if a let-go there settles in the + * mark, else from the newest entry where one does, *at then that. */ +static bool input_openxr_edit_settle(const input_openxr_edit_t *e, + const input_openxr_hold_t *hold, const video_xr_quad_set_t *targets, + const input_openxr_trail_t **at, video_xr_pose_t *pose) +{ + unsigned i; + unsigned n = hold->trail_at; + const input_openxr_trail_t *from = *at; + if (!hold->buzzed || !targets) + return false; + for (i = 0; i <= hold->trails; i++) + { + if ( from->settle_slot == hold->mark.slot + 1u + && from->settle_on == hold->mark.on + && from->settle_edge == hold->mark.edge) + { + if (!video_xr_hinge_settle(targets, hold->g.slot, + input_openxr_edit_holding(e) & ~(1u << hold->g.slot), + from->width, from->height, e->gap * 0.01f, &hold->mark, + &from->hand, pose)) + return false; + *at = from; + return true; + } + from = &hold->trail[n]; + n = (n + INPUT_OPENXR_EDIT_TRAIL - 1) % INPUT_OPENXR_EDIT_TRAIL; + } + return false; +} + +/* hold's mark, if buzzed, in its grab for the frame loop. */ +static void input_openxr_edit_show_mark(input_openxr_hold_t *hold) +{ + hold->g.mark_slot = hold->buzzed + ? (unsigned char)(hold->mark.slot + 1) : 0; + hold->g.mark_on = hold->buzzed ? (unsigned char)hold->mark.on : 0; + hold->g.mark_edge = hold->buzzed ? (unsigned char)hold->mark.edge : 0; +} + +/* Hand h grabs st->quads' quad target at when (us), aim from frame: + * the item keeps its place in the aim's frame, wherever the hand is, + * its yaw from the aim's heading, and its pitch and roll. A second hand + * on an item does nothing. */ +static void input_openxr_edit_grab(input_openxr_t *st, + input_openxr_edit_t *e, unsigned h, int target, + const video_xr_pose_t *aim, bool tilt, retro_time_t when, + XrTime frame) +{ + unsigned was; + video_xr_pose_t inv; + video_screen_xr_t now; + const video_xr_quad_t *q = &st->quads.quads[target]; + const video_xr_vec3_t *base = &st->quads.slots[q->slot].base; + input_openxr_hold_t *hold = &e->hand[h]; + input_openxr_hold_t *other = &e->hand[h ^ 1]; + if (other->g.state == VIDEO_XR_GRAB_HELD && other->g.slot == q->slot) + return; + was = input_openxr_edit_holding(e); + memset(&hold->g, 0, sizeof(hold->g)); + video_xr_pose_invert(aim, &inv); + video_xr_compose(&inv, &q->pose, &hold->g.rel); + video_xr_grab_hold(&hold->g, aim, &q->pose.orientation); + hold->g.last = q->pose; + hold->g.base = *base; + hold->g.width = q->width; + hold->g.height = q->height; + hold->g.slot = q->slot; + hold->g.state = VIDEO_XR_GRAB_HELD; + hold->g.tilt = tilt; + hold->g.gap = e->gap; + hold->start = q->pose; + hold->aim = *aim; + hold->armed = VIDEO_XR_SNAP_ALL | VIDEO_XR_SNAP_GAP; + /* Taken at a step, it buzzes only once it has left it. */ + hold->rest[0] = 0.0f; + hold->rest[1] = 0.0f; + hold->steps = 0; + hold->pushed = false; + hold->hinged = false; + hold->lidded = false; + hold->buzzed = false; + /* Taken where it rests hinged, it carries that hinge from the start, + * its buzz and its snaps' spent: nothing new took hold. */ + if ( e->snap && q->slot < VIDEO_XR_MENU_SLOT + && video_xr_rest_hinge(&st->quads, q->slot, + input_openxr_edit_holding(e), q->width, q->height, + e->gap * 0.01f, &q->pose, &hold->lid)) + { + video_xr_rest_take(&st->quads, q->slot, q->width, q->height, + e->gap * 0.01f, &hold->lid, &hold->start); + video_xr_compose(&inv, &hold->start, &hold->g.rel); + hold->g.rest_slot = (unsigned char)(hold->lid.slot + 1); + hold->g.rest_on = (unsigned char)hold->lid.on; + hold->g.rest_edge = (unsigned char)hold->lid.edge; + hold->armed = 0; + hold->lidded = true; + hold->buzzed = true; + hold->mark = hold->lid; + hold->out = 0; + input_openxr_edit_show_mark(hold); + } + if (q->slot != VIDEO_XR_MENU_SLOT) + config_get_video_screen_xr(q->slot, &now); + else if (!config_get_video_menu_xr(&now)) + { + /* Unplaced, it is sized by its distance against its spot's: + * placed, that scale keeps it as large. */ + const video_xr_vec3_t *p = &q->pose.position; + double spot = sqrt((double)base->x * base->x + + (double)base->y * base->y + (double)base->z * base->z); + if (spot > 0.0) + now.scale = (float)(sqrt((double)p->x * p->x + + (double)p->y * p->y + (double)p->z * p->z) / spot); + video_views_xr_normalize(&now); + } + hold->scale = now.scale; + hold->scale0 = now.scale; + hold->width0 = q->width; + hold->height0 = q->height; + hold->trails = 0; + hold->polled = when; + input_openxr_edit_trail(e, hold, frame, aim, NULL); + /* Taken hinged, a let-go settles in that hinge from the start. */ + hold->trail[hold->trail_at].settle_slot = hold->g.rest_slot; + hold->trail[hold->trail_at].settle_on = hold->g.rest_on; + hold->trail[hold->trail_at].settle_edge = hold->g.rest_edge; + input_openxr_edit_rebase(e, other, e->snap ? &st->quads : NULL, was); +} + +/* Whether hinge takes hold anew: not the one hold's mark shows. */ +static bool input_openxr_edit_attach(const input_openxr_hold_t *hold, + const video_xr_hinge_t *hinge) +{ + return !hold->buzzed || hold->mark.slot != hinge->slot + || hold->mark.on != hinge->on || hold->mark.edge != hinge->edge; +} + +/* A held item to where the hand puts it, stepped by the free stick and + * snapped to targets unless NULL. Returns the snaps engaged or pulling + * (video_xr_snap); *stepped: a step took hold with its buzz armed; + * *attached: its mark took hold anew. */ +static unsigned input_openxr_edit_move(input_openxr_edit_t *e, + input_openxr_hold_t *hold, const video_xr_pose_t *aim, + const video_xr_vec3_t *head, bool tilt, const int16_t *stick, + const int16_t *free_stick, double dt, + const video_xr_quad_set_t *targets, bool *stepped, bool *attached, + retro_time_t now) +{ + unsigned snapped, axes, held; + video_xr_pose_t inv, pose; + video_screen_xr_t values; + video_xr_hinge_t hinge; + const video_xr_hinge_t *c; + bool hinged = false; + float x = (float)free_stick[0] / 32767.0f; + float y = -(float)free_stick[1] / 32767.0f; + bool pushed = x * x + y * y + > VIDEO_XR_EDIT_DEADZONE * VIDEO_XR_EDIT_DEADZONE; + *attached = false; + hold->aim = *aim; + video_xr_pose_invert(aim, &inv); + if (tilt && !hold->g.tilt) + video_xr_compose(&inv, &hold->g.last, &hold->g.rel); + else if (!tilt && hold->g.tilt) + video_xr_grab_hold(&hold->g, aim, &hold->g.last.orientation); + hold->g.tilt = tilt; + /* The stick's Y is down, as the RetroPad's. */ + video_xr_grab_stick(&hold->g, (float)stick[0] / 32767.0f, + -(float)stick[1] / 32767.0f, dt, &hold->scale); + hold->g.width = hold->width0 * hold->scale / hold->scale0; + hold->g.height = hold->height0 * hold->scale / hold->scale0; + /* Only a stick let go leaves the item where it shows: a wavering + * push that drops an axis for a poll would stay at its step. */ + if (!pushed) + { + hold->rest[0] = 0.0f; + hold->rest[1] = 0.0f; + } + /* A hinge the snap holds now: the stick swings about its edge. An + * engaged turn's angle sticks as that stick lets go or starts a + * push, and a hinge's place on its line with it, so the turn or + * swing goes on from there; never by carrying alone, which can pull + * it out, nor a fading pull, which would hop it into the snap. */ + if (targets) + { + snapped = video_xr_grab_hinge(&hold->g, aim, targets, + input_openxr_edit_holding(e), &hold->start, &pose, &hinge); + hinged = (snapped & VIDEO_XR_SNAP_HINGE) != 0; + /* Ends as a carried hinge does: out of reach (once the fade is + * whole), another hinge taking hold, or the other hand taking its + * target. */ + if (hold->g.rest_slot) + { + video_xr_hinge_t kept; + if ( !video_xr_grab_kept(&hold->g, aim, targets, &hold->start, + &kept) + || ((input_openxr_edit_holding(e) >> kept.slot) & 1u) + || (hinged && (hinge.slot != kept.slot + || hinge.on != kept.on || hinge.edge != kept.edge))) + hold->g.rest_slot = 0; + } + /* The one state the buzz, lid, rims and line follow: a hinge a + * let-go would settle into takes it at once; seen out of that + * range INPUT_OPENXR_HINGE_DEBOUNCE_US on end, it lets go, so a + * shaking hand never flickers it. */ + c = input_openxr_edit_settles(e, hold, aim, targets, + hinged ? &hinge : NULL); + *attached = c && input_openxr_edit_attach(hold, c); + if (*attached) + { + hold->mark = *c; + hold->buzzed = true; + } + if (c) + hold->out = 0; + else if (hold->buzzed) + { + retro_time_t gap = now - hold->polled; + if (!hold->out) + hold->out = now; + else if (gap > INPUT_OPENXR_HINGE_OUT_STEP_US) + hold->out += gap - INPUT_OPENXR_HINGE_OUT_STEP_US; + if (now - hold->out >= INPUT_OPENXR_HINGE_DEBOUNCE_US) + hold->buzzed = false; + } + input_openxr_edit_show_mark(hold); + if (pushed != hold->pushed) + { + if (hinged) + video_xr_grab_put_hinge(&hold->g, aim, &hinge, &pose, + (snapped & VIDEO_XR_SNAP_TURN) != 0); + else if (snapped & VIDEO_XR_SNAP_TURN) + { + if (hold->g.tilt) + { + video_xr_compose(&inv, &pose, &pose); + hold->g.rel.orientation = pose.orientation; + } + else + video_xr_grab_hold(&hold->g, aim, &pose.orientation); + } + } + /* Mid-push, a hinge taking hold puts its edge on the line, its + * angles the hand's: the turn about the middle when it drops + * would carry it off. */ + else if (pushed && hinged && !hold->hinged) + video_xr_grab_put_line(&hold->g, aim, &hinge); + } + hold->pushed = pushed; + /* Hinged, the steps count from flat: across a change, the stick's + * carry over a step counts no more. */ + if (hinged != hold->hinged) + { + hold->rest[0] = 0.0f; + hold->rest[1] = 0.0f; + hold->hinged = hinged; + } + /* As the snaps' buzz, armed by where the item is, however it got + * there. */ + if (hinged) + { + hold->lid = hinge; + hold->lidded = true; + hold->g.through = 0; + hold->steps |= (video_xr_grab_off_step(&hold->g, aim, head, e->step, + hold->rest) & ~hinge.axis) + | video_xr_grab_off_hinge(&hold->g, aim, &hinge, e->step, + hold->rest); + axes = video_xr_grab_turn_hinge(&hold->g, aim, &hinge, x, y, dt); + held = video_xr_grab_step_hinge(&hold->g, aim, &hinge, e->step, + axes, hold->rest, targets, input_openxr_edit_holding(e), + &hold->start); + } + else + { + const video_xr_hinge_t *lid = NULL; + /* Pushed past the stop's hold, the hinge ends with it, and the + * stop holds short until one takes hold, so a wobble there + * neither takes it back nor buzzes. While marked, as its mark + * keeps that far off the plane too. */ + if ( hold->buzzed + && video_xr_grab_through(&hold->g, aim, targets, &hold->mark)) + hold->g.through = 1; + /* Out of its hinge's reach, its mark gone, it turns as any free + * screen. */ + if ( hold->lidded && !hold->buzzed + && !video_xr_grab_near_hinge(&hold->g, aim, targets, + &hold->lid, VIDEO_XR_SNAP_DIST)) + hold->lidded = false; + if (hold->lidded) + lid = &hold->lid; + hold->steps |= video_xr_grab_off_step(&hold->g, aim, head, e->step, + hold->rest); + axes = video_xr_grab_turn_lid(&hold->g, aim, lid, x, y, dt); + held = video_xr_grab_step(&hold->g, aim, head, e->step, axes, + hold->rest, targets, input_openxr_edit_holding(e), + &hold->start, lid); + } + *stepped = (held & hold->steps) != 0; + hold->steps &= ~held; + video_views_xr_reset(&values, 1); + if (video_xr_grab_pose(&hold->g, aim, targets, + input_openxr_edit_holding(e), &hold->start, &pose, &values, + &snapped)) + video_xr_compose(&inv, &pose, &hold->g.rel); + hold->g.last = pose; + values.scale = hold->scale; + input_openxr_edit_write(e, hold, &values); + return snapped; +} + +/* A hold ends. Let go, the item stays where the hand left it, which its + * settings hold already, and the count rides the next frame snapshot; + * the frame loop shows it there until quads planned at that count do, + * even once the hand holds another. Gone from the quads, nothing is + * left to show. */ +static void input_openxr_edit_let_go(input_openxr_edit_t *e, + input_openxr_hold_t *hold, bool shown) +{ + hold->buzzed = false; + hold->out = 0; + hold->trails = 0; + input_openxr_edit_show_mark(hold); + if (!shown) + { + hold->g.state = VIDEO_XR_GRAB_NONE; + return; + } + e->seq++; + hold->g.release_seq = e->seq; + hold->g.state = VIDEO_XR_GRAB_RELEASED; + e->grabs.let_go[hold->g.slot] = hold->g; + video_driver_headset_edit(e->on, e->seq); +} + +/* What the hands hold and where they took it, the chosen item and + * Snap to Screens, for the frame loop. */ +static void input_openxr_edit_publish(input_openxr_t *st, + input_openxr_edit_t *e) +{ + if (!st->frame) + return; + e->grabs.hand[0] = e->hand[0].g; + e->grabs.hand[1] = e->hand[1].g; + e->grabs.start[0] = e->hand[0].start; + e->grabs.start[1] = e->hand[1].start; + e->grabs.chosen = (unsigned)(e->chosen + 1); + e->grabs.stuck = e->stuck; + e->grabs.tag = e->tag; + e->grabs.snap = e->snap; + e->grabs.editing = e->on; + openxr_frame_publish_grabs(st->frame, &e->grabs); +} + +/* Every hold let go where it is, or settled in its mark. */ +static void input_openxr_edit_let_go_all(input_openxr_edit_t *e) +{ + unsigned h; + bool let_go = false; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + if (e->hand[h].g.state == VIDEO_XR_GRAB_HELD) + { + video_xr_pose_t pose; + input_openxr_hold_t *hold = &e->hand[h]; + const input_openxr_trail_t *at = &hold->trail[hold->trail_at]; + if (input_openxr_edit_settle(e, hold, + e->snap ? &input_openxr_st.quads : NULL, &at, &pose)) + input_openxr_edit_put(e, hold, at, &pose); + input_openxr_edit_let_go(e, hold, true); + input_openxr_edit_place(&input_openxr_st.quads, &hold->g); + let_go = true; + } + if (let_go) + input_openxr_edit_rests(e, &input_openxr_st.quads); + if (let_go || e->on) + input_openxr_edit_publish(&input_openxr_st, e); +} + +/* The main button on an item no hand holds: a screen as Reset Screen + * leaves it, the menu back where it opens unplaced. */ +static void input_openxr_edit_reset(input_openxr_edit_t *e, unsigned slot) +{ + unsigned h; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + if ( e->hand[h].g.state == VIDEO_XR_GRAB_HELD + && e->hand[h].g.slot == slot) + return; + if (slot == VIDEO_XR_MENU_SLOT) + config_set_video_menu_xr(NULL); + else + config_set_video_screen_xr(slot, NULL); + e->changed = true; +} + +/* Edit mode's start: the content paused if While Editing Screens asks + * and nothing paused it already, held screens snapping if Snap to + * Screens asks, and the desktop told. Video hears of it first, so the + * frame the pause draws shows edit mode. */ +static void input_openxr_edit_start(input_openxr_edit_t *e, bool pause, + bool snap, float step, unsigned char gap, unsigned smoothing) +{ + unsigned h; + const char *msg = msg_hash_to_str(MSG_OPENXR_EDIT_ON); + uint32_t flags = runloop_get_flags(); + e->on = true; + e->snap = snap; + e->step = step; + e->gap = gap; + e->smoothing = smoothing; + e->changed = false; + e->unbound = 0; + e->paused = false; + e->last = 0; + e->chosen = -1; + e->stuck = 0; + e->tag = 0; + e->rests_due = true; + /* Held as it starts (the menu's OK on A, which is Reset): no press + * until let go. */ + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + e->hand[h].grip = true; + e->hand[h].reset = true; + e->hand[h].buzzed = false; + e->hand[h].out = 0; + e->hand[h].trails = 0; + } + e->next = true; + e->prev = true; + RARCH_LOG("[OpenXR] Edit Screens on.\n"); + video_driver_headset_edit(true, e->seq); + if ( pause + && (flags & RUNLOOP_FLAG_CORE_RUNNING) + && !(flags & RUNLOOP_FLAG_PAUSED)) + { + input_driver_platform_request(INPUT_PLATFORM_PAUSE); + e->paused = (runloop_get_flags() & RUNLOOP_FLAG_PAUSED) != 0; + } + /* After the pause's own notice, which would clear it; long enough + * to read all the controls. */ + runloop_msg_queue_push(msg, strlen(msg), 1, 480, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); +} + +/* Its end: what the hands hold let go, the content resumed only if edit + * mode paused it and it still is, and the desktop told whether settings + * changed. */ +static void input_openxr_edit_stop(input_openxr_edit_t *e) +{ + const char *msg = msg_hash_to_str(e->changed + ? MSG_OPENXR_EDIT_UNSAVED : MSG_OPENXR_EDIT_OFF); + e->chosen = -1; + e->stuck = 0; + e->tag = 0; + input_openxr_edit_let_go_all(e); + e->on = false; + /* No hinges at rest outside edit mode, from its next frame. */ + e->grabs.num_rests = 0; + input_openxr_edit_publish(&input_openxr_st, e); + if (e->paused && (runloop_get_flags() & RUNLOOP_FLAG_PAUSED)) + input_driver_platform_request(INPUT_PLATFORM_UNPAUSE); + e->paused = false; + RARCH_LOG("[OpenXR] Edit Screens off.\n"); + runloop_msg_queue_push(msg, strlen(msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); + video_driver_headset_edit(false, e->seq); +} + +/* Edit mode's poll, in the screens' frame: Select Next and Previous + * choose an item; a grip grabs it, or what the hand points at, and the + * other hand's stick turns it; the main button resets it. The menu and + * Edit Screens come from its own set; nothing reaches the pads. */ +static void input_openxr_edit_poll(input_openxr_t *st, + input_openxr_edit_t *e) +{ + unsigned h; + bool next, prev; + video_xr_pose_t screens, inv; + openxr_aims_t aims; + XrSpace space = XR_NULL_HANDLE; + XrTime time = openxr_frame_predicted_time(st->frame); + retro_time_t now = cpu_features_get_time_usec(); + double dt = (e->last && now > e->last) + ? (double)(now - e->last) / 1000000.0 : 0.0; + + e->last = now; + if (dt > INPUT_OPENXR_EDIT_MAX_DT) + dt = INPUT_OPENXR_EDIT_MAX_DT; + st->start_menu_held = false; + input_openxr_triggers_lost(st); + st->menu_toggle = input_openxr_bool(st, IXA_E_MENU, XR_NULL_PATH); + if (st->menu_toggle) + st->start_menu_spent = true; + st->edit_toggle = input_openxr_bool(st, IXA_E_EDIT, XR_NULL_PATH); + next = input_openxr_bool(st, IXA_E_NEXT, XR_NULL_PATH); + prev = input_openxr_bool(st, IXA_E_PREV, XR_NULL_PATH); + openxr_frame_get_quads(st->frame, &st->quads); + openxr_frame_get_screens(st->frame, &screens, &space); + video_xr_pose_invert(&screens, &inv); + /* Edit Smoothing: the aims the frame loop built its last frame + * from, so what is let go is where it was shown. */ + memset(&aims, 0, sizeof(aims)); + if (e->smoothing) + openxr_frame_get_aims(st->frame, &aims); + /* Items where the headset shows them: let go and not yet in the + * quads, as the frame loop holds them; held, where the hand put + * them. */ + for (h = 0; h < VIDEO_XR_MAX_SLOTS; h++) + { + const video_xr_grab_t *g = &e->grabs.let_go[h]; + if ( g->state == VIDEO_XR_GRAB_RELEASED + && !st->quads.slots[h].fixed + && (int)(st->quads.slots[h].edit_seq - g->release_seq) < 0) + input_openxr_edit_place(&st->quads, g); + } + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + if (e->hand[h].g.state == VIDEO_XR_GRAB_HELD) + input_openxr_edit_place(&st->quads, &e->hand[h].g); + /* The choice: the laser again once its item is gone; a step each + * press. */ + if ( e->chosen >= 0 + && !input_openxr_edit_shown(&st->quads, (unsigned)e->chosen)) + e->chosen = -1; + /* The tag goes to the hand that pressed, the left if both did. */ + if (next && !e->next) + { + e->chosen = input_openxr_edit_cycle(&st->quads, e->chosen, 1); + e->tag = input_openxr_bool(st, IXA_E_NEXT, st->hands[0]) ? 0 : 1; + } + if (prev && !e->prev) + { + e->chosen = input_openxr_edit_cycle(&st->quads, e->chosen, -1); + e->tag = input_openxr_bool(st, IXA_E_PREV, st->hands[0]) ? 0 : 1; + } + e->next = next; + e->prev = prev; + + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + float u, v, t; + int target; + bool sticks; + int16_t stick[2]; + video_xr_pose_t aim; + video_xr_vec3_t o, d; + const video_xr_quad_set_t *targets = e->snap ? &st->quads : NULL; + input_openxr_hold_t *hold = &e->hand[h]; + XrPath hand = st->hands[h]; + bool grip = input_openxr_bool(st, IXA_E_GRAB, hand); + bool reset = input_openxr_bool(st, IXA_E_RESET, hand); + bool tilt = input_openxr_bool(st, IXA_E_TILT, hand); + bool tracked = e->smoothing + ? ((aims.valid >> h) & 1u) != 0 + : input_openxr_aim(st, h, space, time, &aim); + bool grabbed = grip && !hold->grip; + bool reset_pressed = reset && !hold->reset; + /* The frame its aim is from, which dates its trail. */ + XrTime frame = e->smoothing ? aims.time[h] : time; + + if (e->smoothing && tracked) + aim = aims.pose[h]; + hold->grip = grip; + hold->reset = reset; + sticks = false; + if (tracked) + { + video_xr_compose(&inv, &aim, &aim); + input_openxr_aim_ray(&aim, &o, &d); + st->hit[h] = video_xr_pick_edit(&st->quads, + ((e->stuck >> h) & 1u) != 0, &o, &d, &u, &v, &t, &sticks); + } + e->stuck = sticks ? (e->stuck | (1u << h)) : (e->stuck & ~(1u << h)); + target = input_openxr_edit_target(st, e, h); + if (hold->g.state == VIDEO_XR_GRAB_HELD) + { + const video_xr_vec3_t *base = &st->quads.slots[hold->g.slot].base; + /* Its spot moved (the core's layout changed): the hold's + * settings are from the old one, so it ends as if gone. */ + bool shown = input_openxr_edit_shown(&st->quads, hold->g.slot) + && base->x == hold->g.base.x && base->y == hold->g.base.y + && base->z == hold->g.base.z; + /* Its mark's target gone or taken by the other hand, the mark + * ends at once, the hand tracked or not, so letting that go + * takes it again only anew, with the buzz. */ + if ( hold->buzzed + && (!input_openxr_edit_shown(&st->quads, hold->mark.slot) + || ((input_openxr_edit_holding(e) >> hold->mark.slot) + & 1u))) + { + hold->buzzed = false; + hold->out = 0; + input_openxr_edit_show_mark(hold); + } + /* Its lid's target taken ends the lid too, hinged elsewhere or + * not. */ + if ( hold->lidded + && ((input_openxr_edit_holding(e) >> hold->lid.slot) & 1u)) + hold->lidded = false; + if (!grip || !shown) + { + if (shown) + { + video_xr_pose_t pose; + const input_openxr_trail_t *at; + /* A new frame's aims first, as that frame shows it. */ + if (tracked && frame > hold->trail[hold->trail_at].t) + { + video_xr_grab_hinge(&hold->g, &aim, targets, + input_openxr_edit_holding(e), &hold->start, &pose, + NULL); + hold->g.last = pose; + input_openxr_edit_trail(e, hold, frame, &aim, targets); + } + /* As it showed before the grip began to open: the hand + * twitches as it does. */ + at = input_openxr_edit_rewind(st, hold, frame); + /* Exactly into the hinge it showed, so it rests there + * green, among the screens held once it is let go. */ + if (input_openxr_edit_settle(e, hold, targets, &at, &pose)) + input_openxr_edit_put(e, hold, at, &pose); + /* Smoothed, where it shows: smoothing damps the twitch, + * and a rewind would step back by what it lags. */ + else if (e->smoothing) + { + pose = hold->g.last; + input_openxr_edit_put(e, hold, NULL, &pose); + } + else + input_openxr_edit_put(e, hold, at, &at->shown); + } + input_openxr_edit_let_go(e, hold, shown); + /* Where it rests now, for the hinges counted below. */ + if (shown) + input_openxr_edit_place(&st->quads, &hold->g); + } + /* Untracked, the item holds still. */ + else if (tracked) + { + unsigned snapped; + bool stepped; + bool attached; + int16_t free_stick[2]; + video_xr_pose_t head; + const video_xr_vec3_t *at = NULL; + stick[0] = 0; + stick[1] = 0; + free_stick[0] = 0; + free_stick[1] = 0; + input_openxr_stick(st, IXA_E_STICK, hand, stick); + /* The other hand's stick turns and tilts it while that hand + * holds nothing. */ + if (e->hand[h ^ 1].g.state != VIDEO_XR_GRAB_HELD) + input_openxr_stick(st, IXA_E_STICK, st->hands[h ^ 1], + free_stick); + if (input_openxr_locate(st, st->frame->session->view_space, + space, time, &head)) + { + video_xr_compose(&inv, &head, &head); + at = &head.position; + } + snapped = input_openxr_edit_move(e, hold, &aim, at, tilt, stick, + free_stick, dt, targets, &stepped, &attached, now); + /* A new apply cuts the one playing short: ticks due inside + * the hinge's buzz are skipped, their snaps still spent. */ + if (attached) + { + input_openxr_edit_tick(st, h, INPUT_OPENXR_HINGE_BUZZ_NS, + INPUT_OPENXR_HINGE_BUZZ_AMP); + hold->buzz_end = now + INPUT_OPENXR_HINGE_BUZZ_NS / 1000; + } + else if (((snapped & hold->armed) || stepped) + && now >= hold->buzz_end) + input_openxr_edit_tick(st, h, INPUT_OPENXR_SNAP_TICK_NS, + INPUT_OPENXR_SNAP_TICK_AMP); + hold->armed = input_openxr_edit_armed(hold->armed, snapped); + hold->polled = now; + input_openxr_edit_trail(e, hold, frame, &aim, targets); + } + } + /* Taken from anywhere, the hand's aim is needed: untracked, it + * takes nothing. */ + else if (grabbed && tracked && target >= 0) + input_openxr_edit_grab(st, e, h, target, &aim, tilt, now, frame); + else if (reset_pressed && target >= 0) + input_openxr_edit_reset(e, st->quads.quads[target].slot); + } + input_openxr_edit_rests(e, &st->quads); + input_openxr_edit_publish(st, e); +} + void input_openxr_poll(unsigned max_users, unsigned controllers, - unsigned laser, float threshold) + unsigned laser, unsigned beam, float threshold) { unsigned p; XrResult res; XrActionsSyncInfo si; XrActiveActionSet active[2]; - input_openxr_t *st = &input_openxr_st; - uint32_t run_flags = runloop_get_flags(); - bool separate = controllers == VIDEO_OPENXR_CONTROLLERS_SEPARATE; - bool was_pressed = st->gun_pressed || st->menu_pressed; + input_openxr_t *st = &input_openxr_st; + input_openxr_edit_t *e = &input_openxr_edit; + uint32_t run_flags = runloop_get_flags(); + bool separate = controllers == VIDEO_OPENXR_CONTROLLERS_SEPARATE; + bool was_pressed = st->gun_pressed || st->menu_pressed; #ifdef HAVE_MENU - bool menu_open = menu_driver_alive(); + bool menu_open = menu_driver_alive(); #else - bool menu_open = false; + bool menu_open = false; #endif + /* A config can hold any value: past the laser's choices is Auto, and + * never edit mode's; past the beam's, Edit Mode Only. */ + if (laser >= VIDEO_OPENXR_LASER_LAST) + laser = VIDEO_OPENXR_LASER_AUTO; + if (beam >= VIDEO_OPENXR_LASER_BEAM_LAST) + beam = VIDEO_OPENXR_LASER_BEAM_EDIT; + input_openxr_devices(max_users, separate); + /* A pause edit mode made that the user lifted is no longer its own. */ + if (e->paused && !(run_flags & RUNLOOP_FLAG_PAUSED)) + e->paused = false; + + /* Edit mode ends with the session and when the menu opens. It + * starts only between frames (input_openxr_edit_begin()): with late + * polling this poll is inside the core's frame, where a pause would + * draw one. A resume draws none, and paused content runs no frame. */ + if ( e->on + && (menu_open || !st->frame + || openxr_session_ended(st->frame->session))) + input_openxr_edit_stop(e); + /* The XR thread places the dots by these: none while the laser - * yields, which a closed menu or the laser Off ends. */ + * yields, which a closed menu or the laser Off ends; on every item + * while editing, whatever the setting. */ if (!menu_open || laser == VIDEO_OPENXR_LASER_OFF) st->yield = false; retro_atomic_store_release_int(&input_openxr_pointer_config, - (st->yield ? VIDEO_OPENXR_LASER_OFF : (int)laser) - | (menu_open ? INPUT_OPENXR_MENU_OPEN : 0)); + (e->on ? VIDEO_XR_LIVE_EDIT + : ((st->yield ? VIDEO_OPENXR_LASER_OFF : (int)laser) + | (menu_open ? INPUT_OPENXR_MENU_OPEN : 0))) + | ((int)beam << INPUT_OPENXR_BEAM_SHIFT) + | (e->on ? (int)(e->smoothing & INPUT_OPENXR_SMOOTH_MASK) + << INPUT_OPENXR_SMOOTH_SHIFT : 0)); /* Nothing stops the last rumble of a core closed behind a menu that * ran it; no later content may inherit it. */ @@ -1675,6 +3590,7 @@ void input_openxr_poll(unsigned max_users, unsigned controllers, st->recenter = false; st->laser_toggle = false; st->view_toggle = false; + st->edit_toggle = false; st->hit[0] = -1; st->hit[1] = -1; st->ptr_owned = false; @@ -1706,8 +3622,8 @@ void input_openxr_poll(unsigned max_users, unsigned controllers, } st->focused = true; - active[0].actionSet = st->sets[separate - ? INPUT_OPENXR_SET_SEPARATE : INPUT_OPENXR_SET_COMBINED]; + active[0].actionSet = st->sets[e->on ? INPUT_OPENXR_SET_EDIT + : (separate ? INPUT_OPENXR_SET_SEPARATE : INPUT_OPENXR_SET_COMBINED)]; active[0].subactionPath = XR_NULL_PATH; active[1].actionSet = st->sets[INPUT_OPENXR_SET_POINTER]; active[1].subactionPath = XR_NULL_PATH; @@ -1733,17 +3649,38 @@ void input_openxr_poll(unsigned max_users, unsigned controllers, separate ? "separate" : "combined"); st->mode = (int)separate; } - if (separate) - input_openxr_read_separate(st); + /* The runtime may bind none of edit mode's actions (a binding the + * user saved before they existed): nothing would grab or leave. */ + if (e->on) + { + if ( input_openxr_bound(st, IXA_E_GRAB) + || input_openxr_bound(st, IXA_E_MENU)) + e->unbound = 0; + else if (++e->unbound >= INPUT_OPENXR_EDIT_UNBOUND) + { + input_openxr_edit_stop(e); + input_openxr_edit_unavailable(st, "the controllers' bindings " + "have none of its actions"); + } + } + if (e->on) + input_openxr_edit_poll(st, e); else - input_openxr_read_combined(st); - input_openxr_yield(st, laser, menu_open, threshold); - if (st->yield) - laser = VIDEO_OPENXR_LASER_OFF; - retro_atomic_store_release_int(&input_openxr_pointer_config, - (int)laser | (menu_open ? INPUT_OPENXR_MENU_OPEN : 0)); - input_openxr_laser_poll(st, laser, menu_open, threshold, was_pressed); - input_openxr_triggers(st, threshold); + { + if (separate) + input_openxr_read_separate(st); + else + input_openxr_read_combined(st); + input_openxr_yield(st, laser, menu_open, threshold); + if (st->yield) + laser = VIDEO_OPENXR_LASER_OFF; + retro_atomic_store_release_int(&input_openxr_pointer_config, + (int)laser | (menu_open ? INPUT_OPENXR_MENU_OPEN : 0) + | ((int)beam << INPUT_OPENXR_BEAM_SHIFT)); + input_openxr_laser_poll(st, laser, menu_open, threshold, + was_pressed); + input_openxr_triggers(st, threshold); + } input_openxr_haptics(st, separate, (run_flags & RUNLOOP_FLAG_PAUSED) ? true : false); } @@ -1826,9 +3763,99 @@ bool input_openxr_hotkey(unsigned id) return st->laser_toggle; if (id == RARCH_HEADSET_VIEW_TOGGLE) return st->view_toggle; + if (id == RARCH_HEADSET_EDIT_SCREENS) + return st->edit_toggle; return false; } +/* Whether a hand holds a controller whose profile refused edit mode's + * bindings: in edit mode it could not leave it. */ +static bool input_openxr_edit_refused(input_openxr_t *st) +{ + unsigned h, i; + for (h = 0; h < INPUT_OPENXR_HANDS; h++) + { + XrInteractionProfileState ps; + ps.type = XR_TYPE_INTERACTION_PROFILE_STATE; + ps.next = NULL; + if (XR_FAILED(st->GetCurrentInteractionProfile(st->session, + st->hands[h], &ps))) + continue; + for (i = 0; i < st->num_edit_refused; i++) + if (ps.interactionProfile == st->edit_refused[i]) + return true; + } + return false; +} + +bool input_openxr_edit_toggle(void) +{ + const char *why = NULL; + input_openxr_edit_t *e = &input_openxr_edit; + input_openxr_t *st = &input_openxr_st; + if (e->on) + { + input_openxr_edit_stop(e); + return false; + } + if (!input_openxr_active() || !st->frame) + why = "no headset controllers"; + else if (openxr_session_ended(st->frame->session)) + why = "the session has ended"; + else if (!st->edit_ok || input_openxr_edit_refused(st)) + why = "the runtime refused its bindings"; + if (why) + { + input_openxr_edit_unavailable(st, why); + return false; + } + return true; +} + +void input_openxr_edit_begin(bool pause, bool snap, unsigned snap_angles, + unsigned hinge_gap, unsigned smoothing) +{ + static const float steps[VIDEO_OPENXR_EDIT_SNAP_ANGLES_LAST] = + { 0.0f, 15.0f, 30.0f, 45.0f, 90.0f }; + static const unsigned char gaps[VIDEO_OPENXR_EDIT_HINGE_GAP_LAST] = + { 0, 1, 2, 3, 5, 10 }; + input_openxr_edit_t *e = &input_openxr_edit; + if (e->on || !input_openxr_st.frame) + return; +#ifdef HAVE_MENU + /* Without content the menu cannot close, and would end it at once. */ + if (menu_driver_alive()) + { + const char *msg = msg_hash_to_str(MSG_OPENXR_EDIT_NO_CONTENT); + runloop_msg_queue_push(msg, strlen(msg), 1, 180, true, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); + return; + } +#endif + /* A config can hold any value: past Snap to Angles', 15 degrees; + * past Hinge Gap's, 2 cm; past Edit Smoothing's, Medium. */ + if (snap_angles >= VIDEO_OPENXR_EDIT_SNAP_ANGLES_LAST) + snap_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; + if (hinge_gap >= VIDEO_OPENXR_EDIT_HINGE_GAP_LAST) + hinge_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_2; + if (smoothing >= VIDEO_OPENXR_EDIT_SMOOTHING_LAST) + smoothing = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + input_openxr_edit_start(e, pause, snap, steps[snap_angles], + gaps[hinge_gap], smoothing); +} + +void input_openxr_edit_end(void) +{ + input_openxr_edit_t *e = &input_openxr_edit; + if (e->on) + input_openxr_edit_stop(e); +} + +void input_openxr_edit_release(void) +{ + input_openxr_edit_let_go_all(&input_openxr_edit); +} + bool input_openxr_pointer(unsigned port, unsigned device, unsigned idx, unsigned id, int16_t *res) { @@ -2113,6 +4140,7 @@ void input_openxr_register(void) hooks.session_destroying = input_openxr_session_destroying; hooks.frame_layers = input_openxr_frame_layers; hooks.locate_hands = input_openxr_locate_hands; + hooks.aim = input_openxr_frame_aim; hooks.user = NULL; openxr_frame_set_input_hooks(&hooks); } diff --git a/input/common/input_openxr.h b/input/common/input_openxr.h index f91501c447..ac08323478 100644 --- a/input/common/input_openxr.h +++ b/input/common/input_openxr.h @@ -45,10 +45,10 @@ struct rarch_joypad_driver; void input_openxr_register(void); /* Syncs the controllers, and connects or disconnects their devices; - * once per input poll, with Max Users, the Headset Controllers and - * Laser Pointer settings and the axis threshold. */ + * once per input poll, with Max Users, the Headset Controllers, Laser + * Pointer and Laser Beam settings and the axis threshold. */ void input_openxr_poll(unsigned max_users, unsigned controllers, - unsigned laser, float threshold); + unsigned laser, unsigned beam, float threshold); /* @joypad, or while the devices have slots a joypad driver that * answers theirs and passes every other to @joypad. */ @@ -69,10 +69,29 @@ void input_openxr_reannounce(void); * headset's. */ bool input_openxr_autoconfigure(unsigned slot); -/* RARCH_MENU_TOGGLE, RARCH_HEADSET_RECENTER, RARCH_LASER_POINTER_TOGGLE - * or RARCH_HEADSET_VIEW_TOGGLE, from the headset's own actions. */ +/* RARCH_MENU_TOGGLE, RARCH_HEADSET_RECENTER, RARCH_LASER_POINTER_TOGGLE, + * RARCH_HEADSET_VIEW_TOGGLE or RARCH_HEADSET_EDIT_SCREENS, from the + * headset's own actions. */ bool input_openxr_hotkey(unsigned id); +/* Edit Screens, main thread, between frames: a pause draws a frame. + * Toggle: stops edit mode, false; otherwise whether it can start, the + * desktop told when not. Begin, once the menu was asked to close: on at + * once, pausing running content when @pause (While Editing Screens) + * asks, held screens snapping when @snap (Snap to Screens) does; + * Snap to Angles' step, @snap_angles, Hinge Gap, @hinge_gap, and Edit + * Smoothing's level, @smoothing, hold until edit mode ends; a menu that + * stayed open (no content) is told instead. End: off at once, as a + * video reinit wants. */ +bool input_openxr_edit_toggle(void); +void input_openxr_edit_begin(bool pause, bool snap, unsigned snap_angles, + unsigned hinge_gap, unsigned smoothing); +void input_openxr_edit_end(void); + +/* Main thread: what each hand holds is let go where it is, as before a + * recenter. Edit mode stays on. */ +void input_openxr_edit_release(void); + /* The laser as RETRO_DEVICE_POINTER or RETRO_DEVICE_LIGHTGUN on port 0, * while it can point (the headset focused, a screen live for the * laser): true, with *res set. A miss reads as offscreen. */ diff --git a/input/input_defines.h b/input/input_defines.h index 64f6f07a23..9dc7f244c4 100644 --- a/input/input_defines.h +++ b/input/input_defines.h @@ -191,6 +191,7 @@ enum RARCH_HEADSET_RECENTER, RARCH_LASER_POINTER_TOGGLE, RARCH_HEADSET_VIEW_TOGGLE, + RARCH_HEADSET_EDIT_SCREENS, RARCH_FPS_TOGGLE, RARCH_STATISTICS_TOGGLE, RARCH_AI_SERVICE, diff --git a/input/input_driver.c b/input/input_driver.c index e2590e0c48..fbebe74d78 100644 --- a/input/input_driver.c +++ b/input/input_driver.c @@ -12427,6 +12427,9 @@ static void input_hotkeys_maybe_held(input_driver_state_t *input_st, if (input_openxr_hotkey(RARCH_HEADSET_VIEW_TOGGLE)) maybe[RARCH_HEADSET_VIEW_TOGGLE >> 5] |= (1u << (RARCH_HEADSET_VIEW_TOGGLE & 31)); + if (input_openxr_hotkey(RARCH_HEADSET_EDIT_SCREENS)) + maybe[RARCH_HEADSET_EDIT_SCREENS >> 5] |= + (1u << (RARCH_HEADSET_EDIT_SCREENS & 31)); #endif } @@ -15342,6 +15345,19 @@ static void input_driver_platform_do(enum input_platform_request req) if (input_driver_mouse_grabbed()) command_event(CMD_EVENT_GRAB_MOUSE_TOGGLE, NULL); break; + case INPUT_PLATFORM_PAUSE: +#ifdef HAVE_CHEEVOS + /* As the Pause hotkey: hardcore allows a pause only so often. */ + if ( rcheevos_hardcore_active() + && !(runloop_get_flags() & RUNLOOP_FLAG_PAUSED) + && !rcheevos_is_pause_allowed()) + break; +#endif + command_event(CMD_EVENT_PAUSE, NULL); + break; + case INPUT_PLATFORM_UNPAUSE: + command_event(CMD_EVENT_UNPAUSE, NULL); + break; default: break; } @@ -15963,7 +15979,8 @@ void input_driver_poll(void) input->poll(input_st->current_data); #ifdef HAVE_OPENXR input_openxr_poll(max_users, settings->uints.video_openxr_controllers, - settings->uints.video_openxr_laser, input_axis_threshold); + settings->uints.video_openxr_laser, + settings->uints.video_openxr_laser_beam, input_axis_threshold); #endif input_poll_generation++; input_port_keys_at_poll(input_st->current_driver, diff --git a/input/input_driver.h b/input/input_driver.h index 81e4b88b3b..78bd2e90df 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -1353,8 +1353,9 @@ void input_driver_set_nonblocking(bool on); /* What a platform asks of the frontend through its input driver: the * home or power button, a panic combination, the system closing the - * app, its window going away and coming back. The driver says what - * happened; the frontend decides what is done. */ + * app, its window going away and coming back, the content paused and + * resumed. The driver says what happened; the frontend decides what is + * done. */ enum input_platform_request { INPUT_PLATFORM_QUIT = 0, /* quit as the Quit hotkey does */ @@ -1362,6 +1363,8 @@ enum input_platform_request INPUT_PLATFORM_MENU_TOGGLE, /* the platform's menu gesture */ INPUT_PLATFORM_REINIT_VIDEO, /* the window's surface was lost */ INPUT_PLATFORM_FOCUS_LOST, /* game focus off, mouse let go */ + INPUT_PLATFORM_PAUSE, /* pause the content */ + INPUT_PLATFORM_UNPAUSE, /* resume it */ INPUT_PLATFORM_REQUEST_LAST }; void input_driver_platform_request(enum input_platform_request req); diff --git a/intl/msg_hash_lbl_rows.h b/intl/msg_hash_lbl_rows.h index 985854a2ad..d28baa8579 100644 --- a/intl/msg_hash_lbl_rows.h +++ b/intl/msg_hash_lbl_rows.h @@ -134,6 +134,7 @@ #include "../settings/settings_def_video_upscaler.h" #include "../settings/settings_def_video_stereo.h" #include "../settings/settings_def_video_headset.h" +#include "../settings/settings_def_video_screens.h" #include "../settings/settings_def_rewind.h" #include "../settings/settings_def_playlist_history.h" #include "../settings/settings_def_menu_scroll.h" diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index a406e21998..4a66492a66 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -7742,6 +7742,7 @@ MSG_HASH(MENU_ENUM_LABEL_VALUE_##T, us) #include "../settings/settings_def_video_upscaler.h" #include "../settings/settings_def_video_stereo.h" #include "../settings/settings_def_video_headset.h" +#include "../settings/settings_def_video_screens.h" #undef S_BOOL #undef S_BOOL_NS #undef S_BOOL_H @@ -7923,6 +7924,86 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_OFF, "Off" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_OFF, + "Off" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_EDIT, + "Edit Mode Only" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_ALWAYS, + "Always" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF, + "Off" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_15, + "15°" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_30, + "30°" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_45, + "45°" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_90, + "90°" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_0, + "0 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_1, + "1 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_2, + "2 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_3, + "3 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_5, + "5 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_10, + "10 cm" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_OFF, + "Off" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_LOW, + "Low" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, + "Medium" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, + "High" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_OFF, + "Keep Running" + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_ON, + "Pause" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_PLACEMENT_FIXED, "Fixed in Room" @@ -12544,6 +12625,14 @@ MSG_HASH( MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_VIEW_TOGGLE, "Cycles what the core is told the headset currently displays: Immersive (head-tracked), Stereoscopic (a 3D screen) or Flat. Returns to core's default on core closure." ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, + "Edit Headset Screens" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_EDIT_SCREENS, + "Starts or stops moving the headset's screens and menu with its controllers." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, "Show FPS (Toggle)" @@ -34435,6 +34524,38 @@ MSG_HASH( MSG_OPENXR_HEADSET_VIEW_FLAT, "Flat" ) +MSG_HASH( + MSG_OPENXR_EDIT_ON, + "Editing headset screens: grip grabs, the stick moves and resizes, the trigger tilts it with your hand, the other hand's stick turns and tilts it, the main button resets." + ) +MSG_HASH( + MSG_OPENXR_EDIT_OFF, + "Stopped editing headset screens." + ) +MSG_HASH( + MSG_OPENXR_EDIT_UNSAVED, + "Stopped editing headset screens. Changes are not saved yet." + ) +MSG_HASH( + MSG_OPENXR_EDIT_UNAVAILABLE, + "Edit Screens is unavailable: the headset's controllers cannot be used for it." + ) +MSG_HASH( + MSG_OPENXR_EDIT_NO_CONTENT, + "Edit Screens needs content running: load content first." + ) +MSG_HASH( + MSG_OPENXR_EDIT_TAG_LASER, + "Laser" + ) +MSG_HASH( + MSG_OPENXR_EDIT_TAG_SCREEN, + "Screen %u" + ) +MSG_HASH( + MSG_OPENXR_EDIT_TAG_MENU, + "Menu" + ) /* Lakka */ diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index f2bc628b76..618865afb2 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -363,6 +363,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_video_filter_toggle, ME DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_headset_recenter, MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_RECENTER) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_laser_pointer_toggle, MENU_ENUM_SUBLABEL_INPUT_META_LASER_POINTER_TOGGLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_headset_view_toggle, MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_VIEW_TOGGLE) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_headset_edit_screens, MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_EDIT_SCREENS) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_fps_toggle, MENU_ENUM_SUBLABEL_INPUT_META_FPS_TOGGLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_statistics_toggle, MENU_ENUM_SUBLABEL_INPUT_META_STATISTICS_TOGGLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_ai_service, MENU_ENUM_SUBLABEL_INPUT_META_AI_SERVICE) @@ -1853,6 +1854,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case RARCH_HEADSET_VIEW_TOGGLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_headset_view_toggle); return 0; + case RARCH_HEADSET_EDIT_SCREENS: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_headset_edit_screens); + return 0; case RARCH_FPS_TOGGLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_fps_toggle); return 0; @@ -2150,9 +2154,28 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, { MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_DISTANCE }, { MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_WIDTH }, { MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_PLACEMENT }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_MENU_FOLLOW, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_MENU_FOLLOW }, { MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_PERF_BOOST }, { MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_CONTROLLERS }, { MENU_ENUM_LABEL_VIDEO_OPENXR_LASER, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_LASER }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_LASER_BEAM }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_EDIT_PAUSE }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_EDIT_SNAP }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_HINGE_GAP, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_EDIT_HINGE_GAP }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SMOOTHING, MENU_ENUM_SUBLABEL_VIDEO_OPENXR_EDIT_SMOOTHING }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_SELECTED }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_X }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_Y }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_Z }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_YAW }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_PITCH }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_ROLL }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_SCALE }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_VISIBLE }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_RESET }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_RESET_ALL }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_XR_EDIT }, { MENU_ENUM_LABEL_VIDEO_SCREEN_WINDOW_MODE, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_WINDOW_MODE }, { MENU_ENUM_LABEL_VIDEO_SCREEN_WINDOW_MONITOR, MENU_ENUM_SUBLABEL_VIDEO_SCREEN_WINDOW_MONITOR }, { MENU_ENUM_LABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS, MENU_ENUM_SUBLABEL_FILE_BROWSER_OPEN_UWP_PERMISSIONS }, diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 96f6e90a9d..c5febc3d88 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -9400,7 +9400,8 @@ unsigned menu_displaylist_build_list( #ifndef HAVE_OPENXR else if ( key == RARCH_HEADSET_RECENTER || key == RARCH_LASER_POINTER_TOGGLE - || key == RARCH_HEADSET_VIEW_TOGGLE) + || key == RARCH_HEADSET_VIEW_TOGGLE + || key == RARCH_HEADSET_EDIT_SCREENS) continue; #endif /* Show combo entries before normal binds */ @@ -12565,34 +12566,62 @@ unsigned menu_displaylist_build_list( PARSE_ONLY_BOOL, false) == 0) count++; #endif - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_REFRESH_RATE, - PARSE_ONLY_UINT, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST, - PARSE_ONLY_BOOL, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE, - PARSE_ONLY_FLOAT, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH, - PARSE_ONLY_FLOAT, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT, - PARSE_ONLY_UINT, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS, - PARSE_ONLY_UINT, false) == 0) - count++; - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - MENU_ENUM_LABEL_VIDEO_OPENXR_LASER, - PARSE_ONLY_UINT, false) == 0) - count++; + /* Edit mode needs the headset's output and controllers. */ +#ifdef ANDROID + if (video_context_driver_headset_inline()) +#else + if (settings->bools.video_openxr_enable) +#endif + { + static const menu_displaylist_settings_row_t edit_rows[] = { + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT, PARSE_ACTION, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP, PARSE_ONLY_BOOL, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_HINGE_GAP, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SMOOTHING, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE, PARSE_ONLY_BOOL, false }, + }; + count += menu_displaylist_parse_settings_rows(list, settings, + edit_rows, (unsigned)ARRAY_SIZE(edit_rows)); + } + { + static const menu_displaylist_settings_row_t headset_rows[] = { + { MENU_ENUM_LABEL_VIDEO_OPENXR_REFRESH_RATE, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST, PARSE_ONLY_BOOL, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_LASER, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_OPENXR_MENU_FOLLOW, PARSE_ONLY_BOOL, false }, + }; + rarch_setting_t *screen = menu_setting_find_enum( + MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED); + count += menu_displaylist_parse_settings_rows(list, settings, + headset_rows, (unsigned)ARRAY_SIZE(headset_rows)); + if (screen && screen->value.target.unsigned_integer) + { + static const menu_displaylist_settings_row_t screen_rows[] = { + { MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED, PARSE_ONLY_UINT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE, PARSE_ONLY_BOOL, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE, PARSE_ONLY_FLOAT, false }, + { MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET, PARSE_ACTION, false }, + }; + count += menu_displaylist_parse_settings_rows(list, settings, + screen_rows, (unsigned)ARRAY_SIZE(screen_rows)); + } + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL, + PARSE_ACTION, false) == 0) + count++; + } } #endif break; diff --git a/menu/menu_driver.h b/menu/menu_driver.h index 14ce19a689..5da1dbd4cf 100644 --- a/menu/menu_driver.h +++ b/menu/menu_driver.h @@ -543,6 +543,7 @@ struct menu_state /* unsigned alignment */ unsigned input_dialog_kb_idx; + unsigned video_screen_selected; menu_dialog_t dialog_st; enum menu_input_dialog_kb_text_type input_dialog_kb_text_type; #ifdef HAVE_RUNAHEAD diff --git a/menu/menu_setting.c b/menu/menu_setting.c index b371a144b4..d6243a6860 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -354,9 +354,7 @@ SETTINGS_ACTION_SETTER(sel, action_select_handler_t) SETTINGS_ACTION_SETTER(left, action_left_handler_t) SETTINGS_ACTION_SETTER(right, action_right_handler_t) SETTINGS_ACTION_SETTER(change, change_handler_t) -#ifdef ANDROID SETTINGS_ACTION_SETTER(read, change_handler_t) -#endif SETTINGS_ACTION_SETTER(repr, get_string_representation_t) #define SETTINGS_ACTION_SET(field, s, fn) \ @@ -3952,6 +3950,134 @@ static size_t setting_get_string_representation_video_openxr_laser( return 0; } +static size_t setting_get_string_representation_video_openxr_laser_beam( + rarch_setting_t *setting, char *s, size_t len) +{ + if (setting) + { + switch (*setting->value.target.unsigned_integer) + { + case VIDEO_OPENXR_LASER_BEAM_OFF: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_OFF), len); + case VIDEO_OPENXR_LASER_BEAM_EDIT: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_EDIT), len); + case VIDEO_OPENXR_LASER_BEAM_ALWAYS: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_ALWAYS), + len); + } + } + return 0; +} + +static size_t setting_get_string_representation_video_openxr_edit_snap_angles( + rarch_setting_t *setting, char *s, size_t len) +{ + if (setting) + { + switch (*setting->value.target.unsigned_integer) + { + case VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF), + len); + case VIDEO_OPENXR_EDIT_SNAP_ANGLES_15: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_15), + len); + case VIDEO_OPENXR_EDIT_SNAP_ANGLES_30: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_30), + len); + case VIDEO_OPENXR_EDIT_SNAP_ANGLES_45: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_45), + len); + case VIDEO_OPENXR_EDIT_SNAP_ANGLES_90: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_90), + len); + } + } + return 0; +} + +static size_t setting_get_string_representation_video_openxr_edit_hinge_gap( + rarch_setting_t *setting, char *s, size_t len) +{ + if (setting) + { + switch (*setting->value.target.unsigned_integer) + { + case VIDEO_OPENXR_EDIT_HINGE_GAP_0: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_0), + len); + case VIDEO_OPENXR_EDIT_HINGE_GAP_1: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_1), + len); + case VIDEO_OPENXR_EDIT_HINGE_GAP_2: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_2), + len); + case VIDEO_OPENXR_EDIT_HINGE_GAP_3: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_3), + len); + case VIDEO_OPENXR_EDIT_HINGE_GAP_5: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_5), + len); + case VIDEO_OPENXR_EDIT_HINGE_GAP_10: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_10), + len); + } + } + return 0; +} + +static size_t setting_get_string_representation_video_openxr_edit_smoothing( + rarch_setting_t *setting, char *s, size_t len) +{ + if (setting) + { + switch (*setting->value.target.unsigned_integer) + { + case VIDEO_OPENXR_EDIT_SMOOTHING_OFF: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_OFF), + len); + case VIDEO_OPENXR_EDIT_SMOOTHING_LOW: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_LOW), + len); + case VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM), + len); + case VIDEO_OPENXR_EDIT_SMOOTHING_HIGH: + return strlcpy(s, msg_hash_to_str( + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_HIGH), + len); + } + } + return 0; +} + +static size_t setting_get_string_representation_video_openxr_edit_pause( + rarch_setting_t *setting, char *s, size_t len) +{ + if (!setting) + return 0; + return strlcpy(s, msg_hash_to_str(*setting->value.target.boolean + ? MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_ON + : MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_OFF), len); +} + static size_t setting_get_string_representation_video_openxr_placement( rarch_setting_t *setting, char *s, size_t len) { @@ -9376,6 +9502,10 @@ static const enum settings_list_type settings_list_build_order[] = SETTINGS_LIST_MANUAL_CONTENT_SCAN }; +static bool setting_append_list_video_screen_settings( + rarch_setting_t **list, rarch_setting_info_t *list_info, + const char *parent_group); + static bool setting_append_list(settings_t *settings, enum settings_list_type type, rarch_setting_t **list, rarch_setting_info_t *list_info, const char *parent_group); @@ -10267,6 +10397,194 @@ static size_t get_string_representation_input_mouse_index( return _len; } +static unsigned setting_video_screen_mask(void) +{ + video_driver_state_t *video_st = video_state_get_ptr(); + const video_views_map_t *map = &video_st->views_core; + unsigned i, mask = 0; + /* A frame without a map is screen 1. */ + if (!video_st->views_presented) + return 1u; + for (i = 0; i < map->num_views && i < RETRO_VIDEO_VIEWS_MAX; i++) + if (map->views[i].screen < RETRO_VIDEO_VIEWS_MAX) + mask |= 1u << map->views[i].screen; +#ifdef HAVE_OPENXR + if ( video_driver_vr_content_active() + && (video_driver_xr_headset() & VIDEO_XR_HEADSET_TRACKING)) + { + int eyes[2]; + if (video_views_eye_pair(map, eyes)) + mask &= ~(1u << map->views[eyes[0]].screen); + } +#endif + return mask; +} + +static unsigned setting_video_screen_bind(rarch_setting_t *setting) +{ + settings_t *settings = config_get_ptr(); + struct menu_state *menu_st = menu_state_get_ptr(); + unsigned screen = menu_st->video_screen_selected; + unsigned mask = setting_video_screen_mask(); + if (screen >= RETRO_VIDEO_VIEWS_MAX || !(mask & (1u << screen))) + { + for (screen = 0; screen < RETRO_VIDEO_VIEWS_MAX; screen++) + if (mask & (1u << screen)) + break; + menu_st->video_screen_selected = screen < RETRO_VIDEO_VIEWS_MAX + ? screen : 0; + } + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) + { + setting->value.target.unsigned_integer = mask + ? &menu_st->video_screen_selected : NULL; + return mask; + } + if (!mask) + return 0; + screen = menu_st->video_screen_selected; + setting->index_offset = screen; + switch (setting->enum_idx) + { + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X: + setting->value.target.fraction = &settings->floats.video_screen_xr_x[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y: + setting->value.target.fraction = &settings->floats.video_screen_xr_y[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z: + setting->value.target.fraction = &settings->floats.video_screen_xr_z[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW: + setting->value.target.fraction = &settings->floats.video_screen_xr_yaw[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH: + setting->value.target.fraction = &settings->floats.video_screen_xr_pitch[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL: + setting->value.target.fraction = &settings->floats.video_screen_xr_roll[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE: + setting->value.target.fraction = &settings->floats.video_screen_xr_scale[screen]; + break; + case MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE: + setting->value.target.boolean = &settings->bools.video_screen_xr_visible[screen]; + break; + } + return mask; +} + +static void setting_video_screen_read(rarch_setting_t *setting) +{ + setting_video_screen_bind(setting); +} + +static void setting_video_screen_change(rarch_setting_t *setting) +{ + if (setting->enum_idx != MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) + config_get_ptr()->flags |= SETTINGS_FLG_MODIFIED; +} + +static size_t setting_video_screen_repr(rarch_setting_t *setting, + char *s, size_t len) +{ + if (!setting_video_screen_bind(setting)) + return strlcpy(s, "", len); + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) + return snprintf(s, len, "%u", menu_state_get_ptr()->video_screen_selected + 1); + if (setting->type == ST_FLOAT) + return setting_get_string_representation_st_float(setting, s, len); + if (setting->type == ST_BOOL) + return setting_get_string_representation_st_bool(setting, s, len); + return strlcpy(s, "", len); +} + +static int setting_video_screen_step(rarch_setting_t *setting, + size_t idx, bool right) +{ + unsigned mask = setting_video_screen_bind(setting); + if (!mask) + return -1; + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) + { + struct menu_state *menu_st = menu_state_get_ptr(); + unsigned screen = menu_st->video_screen_selected; + unsigned i; + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + screen = (screen + (right ? 1 : RETRO_VIDEO_VIEWS_MAX - 1)) + % RETRO_VIDEO_VIEWS_MAX; + if (mask & (1u << screen)) + break; + } + if (i == RETRO_VIDEO_VIEWS_MAX) + return -1; + menu_st->video_screen_selected = screen; + menu_st->flags |= MENU_ST_FLAG_PREVENT_POPULATE + | MENU_ST_FLAG_ENTRIES_NEED_REFRESH; + return 0; + } + if (setting->type == ST_BOOL) + return setting_bool_action_ok_default(setting, idx, false); + if (right) + return setting_fraction_action_right_default(setting, idx, false); + return setting_fraction_action_left_default(setting, idx, false); +} + +static int setting_video_screen_left(rarch_setting_t *setting, + size_t idx, bool wraparound) +{ + return setting_video_screen_step(setting, idx, false); +} + +static int setting_video_screen_right(rarch_setting_t *setting, + size_t idx, bool wraparound) +{ + return setting_video_screen_step(setting, idx, true); +} + +static int setting_video_screen_start(rarch_setting_t *setting) +{ + if (!setting_video_screen_bind(setting)) + return -1; + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) + { + menu_state_get_ptr()->video_screen_selected = RETRO_VIDEO_VIEWS_MAX; + setting_video_screen_bind(setting); + return 0; + } + return setting_generic_action_start_default(setting); +} + +static int setting_video_screen_reset(rarch_setting_t *setting, + size_t idx, bool wraparound) +{ + settings_t *settings = config_get_ptr(); + struct menu_state *menu_st = menu_state_get_ptr(); + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL) + { + config_reset_video_screen_xr(settings, 0, RETRO_VIDEO_VIEWS_MAX); + config_reset_video_menu_xr(settings); + } + else + { + if (!setting_video_screen_bind(setting)) + return -1; + config_reset_video_screen_xr(settings, menu_st->video_screen_selected, 1); + } + settings->flags |= SETTINGS_FLG_MODIFIED; + menu_st->flags |= MENU_ST_FLAG_PREVENT_POPULATE + | MENU_ST_FLAG_ENTRIES_NEED_REFRESH; + return 0; +} + +static int setting_video_screen_edit(rarch_setting_t *setting, + size_t idx, bool wraparound) +{ + command_event(CMD_EVENT_HEADSET_EDIT_SCREENS, NULL); + return 0; +} + static void general_read_handler(rarch_setting_t *setting) { settings_t *settings = config_get_ptr(); @@ -13495,6 +13813,10 @@ static const setting_desc_t upscaler_desc[] = { }; #endif +static const setting_desc_t video_screens_desc[] = { +#include "../settings/settings_def_video_screens.h" +}; + static const setting_desc_t headset_desc[] = { /* GENERATED: rows come from settings_def_video_headset.h in order. */ #include "../settings/settings_def_video_headset.h" @@ -16502,6 +16824,7 @@ static void settings_build_video( setting_append_list_video_screen_windows(settings, list, list_info, &group_info, &subgroup_info, parent_group); #endif + setting_append_list_video_screen_settings(list, list_info, parent_group); END_SUB_GROUP(list, list_info, parent_group); @@ -19644,6 +19967,54 @@ static void settings_build_desc_group( GROUP_END(); } +static bool setting_append_list_video_screen_settings( + rarch_setting_t **list, rarch_setting_info_t *list_info, + const char *parent_group) +{ + settings_t *settings = config_get_ptr(); + rarch_setting_group_info_t group_info; + rarch_setting_group_info_t subgroup_info; + unsigned first = list_info->index; + int i; + group_info.name = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_SETTINGS); + subgroup_info.name = "Stereo 3D & Screens"; + CONFIG_UINT(list, list_info, &menu_state_get_ptr()->video_screen_selected, + MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED, + MENU_ENUM_LABEL_VALUE_VIDEO_SCREEN_SELECTED, 0, + &group_info, &subgroup_info, parent_group, + setting_video_screen_change, setting_video_screen_read); + settings_list_add_desc(list, list_info, settings, video_screens_desc + 1, + ARRAY_SIZE(video_screens_desc) - 1, &group_info, &subgroup_info, + parent_group); + for (i = first; i < list_info->index; i++) + { + rarch_setting_t *setting = &(*list)[i]; + SETTINGS_ACTION_SET(read, setting, setting_video_screen_read) + SETTINGS_ACTION_SET(change, setting, setting_video_screen_change) + if (setting->enum_idx == MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT) + { + SETTINGS_ACTION_SET(ok, setting, setting_video_screen_edit) + SETTINGS_ACTION_SET(sel, setting, setting_video_screen_edit) + } + else if (setting->type == ST_ACTION) + { + SETTINGS_ACTION_SET(ok, setting, setting_video_screen_reset) + SETTINGS_ACTION_SET(sel, setting, setting_video_screen_reset) + } + else + { + SETTINGS_ACTION_SET(repr, setting, setting_video_screen_repr) + SETTINGS_ACTION_SET(left, setting, setting_video_screen_left) + SETTINGS_ACTION_SET(right, setting, setting_video_screen_right) + SETTINGS_ACTION_SET(ok, setting, setting_video_screen_right) + SETTINGS_ACTION_SET(sel, setting, setting_video_screen_right) + SETTINGS_ACTION_SET(start, setting, setting_video_screen_start) + } + setting_video_screen_bind(setting); + } + return true; +} + static const settings_build_entry_t settings_build_registry[] = { { SETTINGS_LIST_MAIN_MENU, settings_build_main_menu, NULL, 0, MSG_UNKNOWN, MSG_UNKNOWN, MSG_UNKNOWN }, { SETTINGS_LIST_DRIVERS, settings_build_drivers, NULL, 0, MSG_UNKNOWN, MSG_UNKNOWN, MSG_UNKNOWN }, diff --git a/msg_hash.h b/msg_hash.h index ba1a4d9dfb..2dfe934703 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -759,6 +759,14 @@ enum msg_hash_enums MSG_OPENXR_HEADSET_VIEW_IMMERSIVE, MSG_OPENXR_HEADSET_VIEW_STEREOSCOPIC, MSG_OPENXR_HEADSET_VIEW_FLAT, + MSG_OPENXR_EDIT_ON, + MSG_OPENXR_EDIT_OFF, + MSG_OPENXR_EDIT_UNSAVED, + MSG_OPENXR_EDIT_UNAVAILABLE, + MSG_OPENXR_EDIT_NO_CONTENT, + MSG_OPENXR_EDIT_TAG_LASER, + MSG_OPENXR_EDIT_TAG_SCREEN, + MSG_OPENXR_EDIT_TAG_MENU, MSG_IOS_TOUCH_MOUSE_ENABLED, MSG_IOS_TOUCH_MOUSE_DISABLED, @@ -2763,6 +2771,7 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_RECENTER, MENU_ENUM_LABEL_VALUE_INPUT_META_LASER_POINTER_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_VIEW_TOGGLE, + MENU_ENUM_LABEL_VALUE_INPUT_META_HEADSET_EDIT_SCREENS, MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_STATISTICS_TOGGLE, MENU_ENUM_LABEL_VALUE_INPUT_META_AI_SERVICE, @@ -2890,6 +2899,7 @@ enum msg_hash_enums MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_RECENTER, MENU_ENUM_SUBLABEL_INPUT_META_LASER_POINTER_TOGGLE, MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_VIEW_TOGGLE, + MENU_ENUM_SUBLABEL_INPUT_META_HEADSET_EDIT_SCREENS, MENU_ENUM_SUBLABEL_INPUT_META_FPS_TOGGLE, MENU_ENUM_SUBLABEL_INPUT_META_STATISTICS_TOGGLE, MENU_ENUM_SUBLABEL_INPUT_META_AI_SERVICE, @@ -9403,6 +9413,7 @@ enum msg_hash_enums #include "settings/settings_def_video_upscaler.h" #include "settings/settings_def_video_stereo.h" #include "settings/settings_def_video_headset.h" +#include "settings/settings_def_video_screens.h" #undef S_BOOL #undef S_BOOL_NS #undef S_UINT @@ -9493,6 +9504,26 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_AUTO, MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_ALWAYS, MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_OFF, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_OFF, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_EDIT, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_LASER_BEAM_ALWAYS, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_15, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_30, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_45, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SNAP_ANGLES_90, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_0, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_1, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_2, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_3, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_5, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_HINGE_GAP_10, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_OFF, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_LOW, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_OFF, + MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_EDIT_PAUSE_ON, MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_PLACEMENT_FIXED, MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_PLACEMENT_FOLLOW, MENU_ENUM_LABEL_VALUE_VIDEO_OPENXR_PLACEMENT_LAZY, diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index a65930e9fc..3d46daa8ba 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -1400,9 +1400,28 @@ extern const struct msg_hash_us_lbl_blob_s msg_hash_us_lbl_blob; #define MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE) #define MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH) #define MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_MENU_FOLLOW_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_MENU_FOLLOW) #define MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST) #define MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS) #define MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_LASER) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_HINGE_GAP_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_HINGE_GAP) +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SMOOTHING_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SMOOTHING) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL) +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT) #define MENU_ENUM_LABEL_REWIND_ENABLE_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_REWIND_ENABLE) #define MENU_ENUM_LABEL_REWIND_GRANULARITY_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_REWIND_GRANULARITY) #define MENU_ENUM_LABEL_PLAYLIST_SHOW_HISTORY_ICONS_STR (msg_hash_us_lbl_blob.m_MENU_ENUM_LABEL_PLAYLIST_SHOW_HISTORY_ICONS) @@ -3145,9 +3164,28 @@ extern const struct msg_hash_us_lbl_blob_s msg_hash_us_lbl_blob; #define MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE_STR "video_openxr_distance" #define MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH_STR "video_openxr_width" #define MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT_STR "video_openxr_placement" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_MENU_FOLLOW_STR "video_openxr_menu_follow" #define MENU_ENUM_LABEL_VIDEO_OPENXR_PERF_BOOST_STR "video_openxr_perf_boost" #define MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS_STR "video_openxr_controllers" #define MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_STR "video_openxr_laser" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM_STR "video_openxr_laser_beam" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE_STR "video_openxr_edit_pause" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_STR "video_openxr_edit_snap" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SNAP_ANGLES_STR "video_openxr_edit_snap_angles" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_HINGE_GAP_STR "video_openxr_edit_hinge_gap" +#define MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_SMOOTHING_STR "video_openxr_edit_smoothing" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_SELECTED_STR "video_screen_selected" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_X_STR "video_screen_xr_x" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Y_STR "video_screen_xr_y" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_Z_STR "video_screen_xr_z" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_YAW_STR "video_screen_xr_yaw" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_PITCH_STR "video_screen_xr_pitch" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_ROLL_STR "video_screen_xr_roll" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_SCALE_STR "video_screen_xr_scale" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_VISIBLE_STR "video_screen_xr_visible" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_STR "video_screen_xr_reset" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_RESET_ALL_STR "video_screen_xr_reset_all" +#define MENU_ENUM_LABEL_VIDEO_SCREEN_XR_EDIT_STR "video_screen_xr_edit" #define MENU_ENUM_LABEL_REWIND_ENABLE_STR "rewind_enable" #define MENU_ENUM_LABEL_REWIND_GRANULARITY_STR "rewind_granularity" #define MENU_ENUM_LABEL_PLAYLIST_SHOW_HISTORY_ICONS_STR "playlist_show_history_icons" diff --git a/retroarch.c b/retroarch.c index d9fc14f0c8..8bfbb32f69 100644 --- a/retroarch.c +++ b/retroarch.c @@ -1785,7 +1785,9 @@ void drivers_init( video_st->frame_time_count = 0; #ifdef HAVE_OPENXR - /* Before video makes a headset session. */ + /* A video reinit ends edit mode; then video makes a headset + * session. */ + input_openxr_edit_end(); input_openxr_register(); #endif video_driver_lock_new(); @@ -6534,6 +6536,10 @@ bool command_event(enum event_command cmd, void *data) } break; case CMD_EVENT_HEADSET_RECENTER: +#ifdef HAVE_OPENXR + /* A grab ends first, where the hand left its item. */ + input_openxr_edit_release(); +#endif video_driver_headset_recenter(); break; case CMD_EVENT_HEADSET_VIEW_TOGGLE: @@ -6596,6 +6602,25 @@ bool command_event(enum event_command cmd, void *data) #endif } break; + case CMD_EVENT_HEADSET_EDIT_SCREENS: +#ifdef HAVE_OPENXR + /* Edit mode starts here, between frames, as its pause draws a + * frame; the menu closes first. */ + if (input_openxr_edit_toggle()) + { +#ifdef HAVE_MENU + if (menu_st->flags & MENU_ST_FLAG_ALIVE) + retroarch_menu_running_finished(false); +#endif + input_openxr_edit_begin( + settings->bools.video_openxr_edit_pause, + settings->bools.video_openxr_edit_snap, + settings->uints.video_openxr_edit_snap_angles, + settings->uints.video_openxr_edit_hinge_gap, + settings->uints.video_openxr_edit_smoothing); + } +#endif + break; case CMD_EVENT_NONE: return false; diff --git a/retroarch.cfg b/retroarch.cfg index 130e8420ad..6aef7e3550 100644 --- a/retroarch.cfg +++ b/retroarch.cfg @@ -647,6 +647,9 @@ # Cycles what the core is told the headset shows: Immersive, Stereoscopic or Flat. # input_headset_view_toggle = v +# Starts or stops moving the headset's screens and menu with its controllers. +# input_headset_edit_screens = + # Enable other hotkeys. # If this hotkey is bound to either keyboard, joybutton or joyaxis, # all other hotkeys will be disabled unless this hotkey is also held at the same time. diff --git a/runloop.c b/runloop.c index 7145e8d304..7068af5ced 100644 --- a/runloop.c +++ b/runloop.c @@ -8853,6 +8853,9 @@ static enum runloop_state_enum runloop_check_state( /* Check headset view hotkey */ HOTKEY_CHECK(RARCH_HEADSET_VIEW_TOGGLE, CMD_EVENT_HEADSET_VIEW_TOGGLE, true, NULL); + /* Check headset edit hotkey: here, so it works in the menu too */ + HOTKEY_CHECK(RARCH_HEADSET_EDIT_SCREENS, CMD_EVENT_HEADSET_EDIT_SCREENS, true, NULL); + /* Check netplay host hotkey */ HOTKEY_CHECK(RARCH_NETPLAY_HOST_TOGGLE, CMD_EVENT_NETPLAY_HOST_TOGGLE, true, NULL); diff --git a/samples/config/defaults_target/Makefile b/samples/config/defaults_target/Makefile index 00855db6c9..fe5479e83a 100644 --- a/samples/config/defaults_target/Makefile +++ b/samples/config/defaults_target/Makefile @@ -6,7 +6,8 @@ CFLAGS += -O1 -Wall all: defaults_target_test -defaults_target_test: defaults_target_test.c $(ROOT)/configuration.c $(ROOT)/configuration.h +defaults_target_test: defaults_target_test.c $(ROOT)/configuration.c $(ROOT)/configuration.h \ + $(ROOT)/gfx/video_views.c $(ROOT)/gfx/video_views.h $(CC) $(CPPFLAGS) $(CFLAGS) -fwhole-program $< $(LDFLAGS) -lm -o $@ check: defaults_target_test diff --git a/samples/config/defaults_target/defaults_target_test.c b/samples/config/defaults_target/defaults_target_test.c index 7159fe6739..58d51e4155 100644 --- a/samples/config/defaults_target/defaults_target_test.c +++ b/samples/config/defaults_target/defaults_target_test.c @@ -4,6 +4,7 @@ * defaults without ever swapping the pointer other threads read. */ #include "../../../configuration.c" #include "../../../libretro-common/compat/compat_strl.c" +#include "../../../gfx/video_views.c" #include diff --git a/samples/gfx/gl3_shader_status/gl3_chain_alloc_test.c b/samples/gfx/gl3_shader_status/gl3_chain_alloc_test.c index 807458aae6..b5fa215261 100644 --- a/samples/gfx/gl3_shader_status/gl3_chain_alloc_test.c +++ b/samples/gfx/gl3_shader_status/gl3_chain_alloc_test.c @@ -40,19 +40,32 @@ static void *test_realloc(void *p, size_t size) return realloc(p, size); } -/* Reflect the stock shader's Source sampler, with no uniform buffers. */ +/* Reflect the stock shader's Source sampler, with no uniform buffers. + * With replay_reflect each pass also reads PassFeedback0 and + * OriginalHistory1, on units 1 and 2. */ static slang_texture_semantic_meta texture_meta[SLANG_NUM_TEXTURE_SEMANTICS]; +static slang_texture_semantic_meta history_meta[2]; +static bool replay_reflect; static bool test_reflection_init(slang_reflection *r) { unsigned i; memset(r, 0, sizeof(*r)); memset(texture_meta, 0, sizeof(texture_meta)); + memset(history_meta, 0, sizeof(history_meta)); for (i = 0; i < SLANG_NUM_TEXTURE_SEMANTICS; i++) { r->semantic_textures[i].data = &texture_meta[i]; r->semantic_textures[i].size = 1; memset(&texture_meta[i].location, -1, sizeof(texture_meta[i].location)); } + if (replay_reflect) + { + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].data = + history_meta; + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].size = 2; + memset(&history_meta[0].location, -1, sizeof(history_meta[0].location)); + memset(&history_meta[1].location, -1, sizeof(history_meta[1].location)); + } return true; } static void test_reflection_free(slang_reflection *r) @@ -61,6 +74,13 @@ static bool test_reflect(const struct slang_blob *v, const struct slang_blob *f, slang_reflection *r) { r->semantic_textures[SLANG_TEXTURE_SEMANTIC_SOURCE].data[0].texture = true; + if (replay_reflect) + { + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_PASS_FEEDBACK].data[0].texture = true; + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_PASS_FEEDBACK].data[0].binding = 1; + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].data[1].texture = true; + r->semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].data[1].binding = 2; + } return v && v->nwords && f && f->nwords; } static unsigned test_miplevels(unsigned w, unsigned h) @@ -79,6 +99,18 @@ static unsigned next_texture = 1; static int live_textures, live_framebuffers, live_buffers, live_shaders, live_programs; static unsigned mipmaps, trilinear, storage_levels, draws; static GLuint bound_texture, mip_texture; +/* What each draw saw: the textures on units 0-2, each texture's draws + * into it so far (its generation), and what draws write. */ +#define TEX_NAMES 4096 +static unsigned active_unit; +static GLuint unit_texture[3]; +static unsigned tex_gen[TEX_NAMES]; +static GLuint fb_texture[TEX_NAMES]; +static GLuint bound_fb; +static GLuint seen_texture[3]; +static unsigned seen_gen[3]; +static uint64_t seen_count; +static const uint64_t *watched_count; static void APIENTRY mock_gen_textures(GLsizei n, GLuint *names) { GLsizei i; @@ -88,7 +120,12 @@ static void APIENTRY mock_gen_textures(GLsizei n, GLuint *names) static void APIENTRY mock_delete_textures(GLsizei n, const GLuint *names) { (void)names; live_textures -= n; } static void APIENTRY mock_bind_texture(GLenum target, GLuint name) -{ (void)target; bound_texture = name; } +{ + (void)target; + bound_texture = name; + if (active_unit < 3) + unit_texture[active_unit] = name; +} static void APIENTRY mock_tex_parameter(GLenum target, GLenum pname, GLint value) { (void)target; @@ -97,7 +134,24 @@ static void APIENTRY mock_tex_parameter(GLenum target, GLenum pname, GLint value trilinear++; } static void APIENTRY mock_draw(GLenum mode, GLint first, GLsizei count) -{ (void)mode; (void)first; (void)count; draws++; } +{ + unsigned u; + GLuint target = bound_fb < TEX_NAMES ? fb_texture[bound_fb] : 0; + (void)mode; + (void)first; + (void)count; + draws++; + for (u = 0; u < 3; u++) + { + seen_texture[u] = unit_texture[u]; + seen_gen[u] = unit_texture[u] < TEX_NAMES + ? tex_gen[unit_texture[u]] : 0; + } + if (watched_count) + seen_count = *watched_count; + if (target && target < TEX_NAMES) + tex_gen[target]++; +} #define slang_reflection_init test_reflection_init #define slang_reflection_free test_reflection_free @@ -142,18 +196,26 @@ static void APIENTRY mock_gen_framebuffers(GLsizei n, GLuint *names) static void APIENTRY mock_delete_framebuffers(GLsizei n, const GLuint *names) { (void)names; live_framebuffers -= n; } static void APIENTRY mock_bind_framebuffer(GLenum target, GLuint name) -{ (void)target; (void)name; } +{ (void)target; bound_fb = name; } static GLenum APIENTRY mock_framebuffer_status(GLenum target) { (void)target; return GL_FRAMEBUFFER_COMPLETE; } static void APIENTRY mock_framebuffer_texture(GLenum target, GLenum attach, GLenum textarget, GLuint texture, GLint level) -{ (void)target; (void)attach; (void)textarget; (void)texture; (void)level; } +{ + (void)target; + (void)attach; + (void)textarget; + (void)level; + if (bound_fb < TEX_NAMES) + fb_texture[bound_fb] = texture; +} static void APIENTRY mock_storage(GLenum target, GLsizei levels, GLenum format, GLsizei width, GLsizei height) { (void)target; (void)format; (void)width; (void)height; storage_levels = levels; } static void APIENTRY mock_mipmap(GLenum target) { (void)target; mipmaps++; mip_texture = bound_texture; } -static void APIENTRY mock_active_texture(GLenum target) { (void)target; } +static void APIENTRY mock_active_texture(GLenum target) +{ active_unit = target - GL_TEXTURE0; } static void APIENTRY mock_attrib(GLuint index) { (void)index; } static void APIENTRY mock_attrib_pointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *p) @@ -231,6 +293,7 @@ static struct slang_blob *stock_blob(const uint32_t *words, size_t len) /* @passes passes binding @vs/@fs by reference, as a preset's passes do * once the blob table hands the same stages to each of them, with the * text rslc printed for them. */ +static bool chain_replay; static struct gl3_filter_chain *chain_of(unsigned passes, struct slang_blob *vs, struct slang_blob *fs) { @@ -239,6 +302,7 @@ static struct gl3_filter_chain *chain_of(unsigned passes, unsigned i; if (!chain) return NULL; + chain->replay = chain_replay; pass_info.scale_type_x = GLSLANG_FILTER_CHAIN_SCALE_VIEWPORT; pass_info.scale_type_y = GLSLANG_FILTER_CHAIN_SCALE_VIEWPORT; pass_info.scale_x = 1.0f; @@ -405,6 +469,118 @@ static void test_shrink_chain(void) } } +/* The history copy, drawn into its framebuffer's texture. */ +extern void (*test_framebuffer_copied)(unsigned fb_id); +static void framebuffer_copied(unsigned fb_id) +{ + if (fb_id < TEX_NAMES && fb_texture[fb_id] < TEX_NAMES) + tex_gen[fb_texture[fb_id]]++; +} + +/* One frame through @chain: its input @in, count @count, every pass, + * and the frame ended. */ +static void replay_frame(struct gl3_filter_chain *chain, GLuint in, + uint64_t count) +{ + struct gl3_filter_chain_texture texture; + struct gl3_viewport vp; + float mvp[16]; + memset(&texture, 0, sizeof(texture)); + texture.image = in; + texture.dims = VIDEO_SCALE_PACK(64, 32); + texture.format = GL_RGBA8; + vp.pos = 0; + vp.dims = VIDEO_SCALE_PACK(64, 32); + gl3_build_default_matrix(mvp); + gl3_filter_chain_set_frame_count(chain, count); + gl3_filter_chain_set_input_texture(chain, &texture); + gl3_filter_chain_build_offscreen_passes(chain, &vp); + mock_bind_framebuffer(GL_FRAMEBUFFER, 0); + gl3_filter_chain_build_viewport_pass(chain, &vp, mvp); + gl3_filter_chain_end_frame(chain); +} + +/* A replay of the last frame's final pass, in a later frame and at + * another size, reads what that pass read: the same textures - the + * first pass's output, its feedback, the history - with nothing drawn + * into them since, and that frame's count. Feedback does not swap. The + * history keeps a spare slot, so the frame's end did not write over the + * one the pass read. */ +static void test_replay(void) +{ + GLuint texture[3]; + unsigned gen[3], u; + struct gl3_filter_chain *chain; + struct gl3_framebuffer *fb, *feedback; + struct gl3_viewport vp; + float mvp[16]; + uint32_t words[] = { 0x07230203u, 0x00010000u }; + struct slang_blob *vs = stock_blob(words, sizeof(words)); + struct slang_blob *fs = stock_blob(words, sizeof(words)); + + /* Created to replay: one history texture more than read. Any other + * chain keeps only what its passes read. */ + replay_reflect = true; + chain = chain_of(2, vs, fs); + CHECK(chain && chain->num_original_history == 1, + "a chain made without replay keeps %u history framebuffers", + chain ? (unsigned)chain->num_original_history : 0); + gl3_chain_free(chain); + chain_replay = true; + chain = chain_of(2, vs, fs); + chain_replay = false; + replay_reflect = false; + slang_blob_unref(vs); + slang_blob_unref(fs); + CHECK(chain, "replay chain not built"); + if (!chain) + return; + watched_count = &chain->passes[1]->frame_count; + test_framebuffer_copied = framebuffer_copied; + CHECK(chain->num_original_history == 2 + && chain->common.num_original_history == 1, + "history: %u framebuffers for %u read", + (unsigned)chain->num_original_history, + (unsigned)chain->common.num_original_history); + CHECK(!gl3_filter_chain_replay_ready(chain), + "a chain with no frame drawn is ready to replay"); + replay_frame(chain, 0x1001, 1); + replay_frame(chain, 0x1002, 2); + for (u = 0; u < 3; u++) + { + texture[u] = seen_texture[u]; + gen[u] = seen_gen[u]; + } + CHECK(seen_count == 2, "frame 2 drew with count %u", (unsigned)seen_count); + CHECK(gl3_filter_chain_replay_ready(chain), "no replay after a frame"); + + fb = chain->passes[0]->framebuffer; + feedback = chain->passes[0]->framebuffer_feedback; + gl3_filter_chain_set_frame_count(chain, 3); + vp.pos = 0; + vp.dims = VIDEO_SCALE_PACK(80, 40); + gl3_build_default_matrix(mvp); + mock_bind_framebuffer(GL_FRAMEBUFFER, 0); + draws = 0; + gl3_filter_chain_build_viewport_pass_replay(chain, &vp, mvp); + CHECK(draws == 1, "replay drew %u passes", draws); + for (u = 0; u < 3; u++) + CHECK(seen_texture[u] == texture[u] && seen_gen[u] == gen[u], + "replay unit %u: texture %u gen %u, want %u gen %u", u, + seen_texture[u], seen_gen[u], texture[u], gen[u]); + CHECK(seen_count == 2, "replay drew with count %u, want 2", + (unsigned)seen_count); + CHECK(chain->passes[1]->frame_count == 3, + "replay left the count at %u", + (unsigned)chain->passes[1]->frame_count); + CHECK(chain->passes[0]->framebuffer == fb + && chain->passes[0]->framebuffer_feedback == feedback, + "replay swapped the feedback buffers"); + watched_count = NULL; + test_framebuffer_copied = NULL; + gl3_chain_free(chain); +} + int main(void) { unsigned allocs, k; @@ -466,6 +642,7 @@ int main(void) test_shrink_chain(); test_shared_programs(); test_unread_luts(); + test_replay(); if (failures) { diff --git a/samples/gfx/gl3_shader_status/stubs.c b/samples/gfx/gl3_shader_status/stubs.c index 1881fdbc3f..8380a6353b 100644 --- a/samples/gfx/gl3_shader_status/stubs.c +++ b/samples/gfx/gl3_shader_status/stubs.c @@ -42,7 +42,13 @@ void input_driver_set_shader_uses_sensors(int on) { (void)on; } int input_driver_read_sensor_snapshot(void *s) { (void)s; return 0; } int video_shader_load_preset_into_shader(const char *p, void *s) { (void)p; (void)s; return 0; } -void gl3_framebuffer_copy(void) { } +/* A test that follows what is drawn into framebuffers hooks this. */ +void (*test_framebuffer_copied)(unsigned fb_id); +void gl3_framebuffer_copy(unsigned fb_id) +{ + if (test_framebuffer_copied) + test_framebuffer_copied(fb_id); +} void gl3_framebuffer_copy_partial(void) { } uint32_t gl3_get_cross_compiler_target_version(void) { return 430; } int gl3_spirv_binary_supported(void) { return 0; } diff --git a/samples/gfx/gl3_xr_draw/.gitignore b/samples/gfx/gl3_xr_draw/.gitignore new file mode 100644 index 0000000000..30bcfa4ed5 --- /dev/null +++ b/samples/gfx/gl3_xr_draw/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/samples/gfx/gl3_xr_draw/Makefile b/samples/gfx/gl3_xr_draw/Makefile new file mode 100644 index 0000000000..38e0381115 --- /dev/null +++ b/samples/gfx/gl3_xr_draw/Makefile @@ -0,0 +1,27 @@ +CC ?= cc +PYTHON ?= python3 +BUILD_DIR ?= .build +REPO_ROOT := ../../.. +TARGET := $(BUILD_DIR)/gl3_xr_draw_test$(EXE) + +CFLAGS += -Wall -Wextra -pedantic -std=c89 -Werror=declaration-after-statement \ + -Werror=implicit-function-declaration -Wno-long-long \ + -I$(REPO_ROOT) -I$(REPO_ROOT)/libretro-common/include \ + -I$(REPO_ROOT)/gfx/include -I$(BUILD_DIR) + +all: $(TARGET) + +$(BUILD_DIR)/driver_code.inc: extract.py $(REPO_ROOT)/gfx/drivers/gl3.c + @mkdir -p $(BUILD_DIR) + $(PYTHON) extract.py $@ + +$(TARGET): gl3_xr_draw_test.c $(BUILD_DIR)/driver_code.inc + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) + +check: $(TARGET) + $(RUNNER) $(TARGET) + +clean: + rm -f $(BUILD_DIR)/driver_code.inc $(TARGET) + +.PHONY: all check clean diff --git a/samples/gfx/gl3_xr_draw/extract.py b/samples/gfx/gl3_xr_draw/extract.py new file mode 100644 index 0000000000..5be9a9e12d --- /dev/null +++ b/samples/gfx/gl3_xr_draw/extract.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Exercise GL3's headset draw: what it draws straight into the +runtime's textures, and when a chain ends a frame.""" +from pathlib import Path +import re +import sys + +root = Path(__file__).resolve().parents[3] +driver = (root / 'gfx/drivers/gl3.c').read_text() + + +def function(source, name): + masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"', + lambda m: ' ' * len(m[0]), source, flags=re.S) + match = re.search(r'static (?:bool|void) ' + re.escape(name) + + r'\([^;]*?\)\s*\{', masked) + if not match: + raise RuntimeError('function missing: ' + name) + end = masked.index('{', match.start()) + 1 + depth = 1 + while depth: + depth += (masked[end] == '{') - (masked[end] == '}') + end += 1 + return source[match.start():end] + + +parts = [function(driver, name) for name in + ('gl3_xr_same', 'gl3_xr_keep', 'gl3_xr_draw_screen', + 'gl3_xr_draw')] +Path(sys.argv[1]).write_text('\n\n'.join(parts) + '\n') diff --git a/samples/gfx/gl3_xr_draw/gl3_xr_draw_test.c b/samples/gfx/gl3_xr_draw/gl3_xr_draw_test.c new file mode 100644 index 0000000000..2a5a168058 --- /dev/null +++ b/samples/gfx/gl3_xr_draw/gl3_xr_draw_test.c @@ -0,0 +1,392 @@ +/* GL3's headset draw, copied out of gfx/drivers/gl3.c, with GL, the + * runtime's textures and the chain's passes stubbed: each screen is + * drawn straight into the texture the runtime gives, a new frame ends + * the chain's frame, and a repeat draws a screen again only when it + * moved or changed size, by replaying the last frame's final pass. */ +#include +#include +#include +#include +#include "gfx/video_views.h" +#include "gfx/video_defines.h" +#include "gfx/video_xr.h" + +typedef unsigned GLuint; +typedef int GLint; +typedef unsigned GLenum; +typedef unsigned char GLboolean; +typedef float GLfloat; +typedef int GLsizei; +typedef unsigned GLbitfield; + +#define GL_FRAMEBUFFER 0x8D40 +#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6 +#define GL_VIEWPORT 0x0BA2 +#define GL_SCISSOR_TEST 0x0C11 +#define GL_BLEND 0x0BE2 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_COLOR_BUFFER_BIT 0x00004000 + +struct gl3_filter_chain_texture +{ + GLuint image; + unsigned dims; + unsigned padded_dims; + unsigned origin; + GLenum format; +}; + +struct gl3_viewport +{ + unsigned pos; + unsigned dims; +}; + +typedef struct gl3_filter_chain gl3_filter_chain_t; +typedef struct gl_openxr gl_openxr_t; + +typedef struct +{ + video_views_map_t views; + bool frame_repeat; +} video_frame_info_t; + +typedef struct gl3 +{ + struct + { + GLuint tex[RETRO_VIDEO_VIEWS_MAX]; + unsigned tex_dims[RETRO_VIDEO_VIEWS_MAX]; + GLenum tex_format[RETRO_VIDEO_VIEWS_MAX]; + } views; + struct + { + gl_openxr_t *host; + video_xr_quad_set_t set; + struct + { + video_xr_slot_t info; + video_xr_quad_t quads[4]; + unsigned num_quads; + bool published; + } shown[VIDEO_XR_MAX_SLOTS]; + float mvp[16]; + GLuint fbo; + bool proj_shown; + } xr; + unsigned rotation_raw; +} gl3_t; + +static unsigned failures; + +/* The runtime's textures are 0x100 + slot * 2 + layer; any other name + * attached is an image of the driver's own. */ +#define RUNTIME_TEX(s, l) (0x100u + (s) * 2u + (l)) + +static unsigned offscreen_passes, viewport_passes, replays; +static unsigned frames_ended, last_pass_dims; +static bool chain_drawn = true; +static unsigned runtime_attaches, own_attaches; +static unsigned published[VIDEO_XR_MAX_SLOTS]; +static video_xr_quad_set_t planned; + +static void glGetIntegerv(GLenum pname, GLint *v) +{ + (void)pname; + v[0] = 0; + if (pname == GL_VIEWPORT) + v[1] = v[2] = v[3] = 0; +} +static GLboolean glIsEnabled(GLenum cap) { (void)cap; return 0; } +static void glEnable(GLenum cap) { (void)cap; } +static void glDisable(GLenum cap) { (void)cap; } +static void glGenFramebuffers(GLsizei n, GLuint *fbos) +{ + (void)n; + fbos[0] = 1; +} +static void glBindFramebuffer(GLenum target, GLuint fbo) +{ + (void)target; + (void)fbo; +} +static void glFramebufferTexture2D(GLenum target, GLenum att, + GLenum textarget, GLuint tex, GLint level) +{ + (void)target; + (void)att; + (void)textarget; + (void)level; + if (!tex) + return; + if (tex >= RUNTIME_TEX(0, 0) + && tex <= RUNTIME_TEX(VIDEO_XR_MAX_SLOTS - 1, 1)) + runtime_attaches++; + else + own_attaches++; +} +static void glViewport(GLint x, GLint y, GLsizei w, GLsizei h) +{ + (void)x; + (void)y; + (void)w; + (void)h; +} +static void glClearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) +{ + (void)r; + (void)g; + (void)b; + (void)a; +} +static void glClear(GLbitfield mask) { (void)mask; } + +static void gl3_filter_chain_set_input_texture(gl3_filter_chain_t *chain, + const struct gl3_filter_chain_texture *in) +{ + (void)chain; + (void)in; +} +static void gl3_filter_chain_build_offscreen_passes( + gl3_filter_chain_t *chain, const struct gl3_viewport *vp) +{ + (void)chain; + (void)vp; + offscreen_passes++; +} +static bool gl3_filter_chain_replay_ready(const gl3_filter_chain_t *chain) +{ + (void)chain; + return chain_drawn; +} +static void gl3_filter_chain_build_viewport_pass_replay( + gl3_filter_chain_t *chain, const struct gl3_viewport *vp, + const float *mvp) +{ + (void)chain; + (void)mvp; + replays++; + last_pass_dims = vp->dims; +} +static void gl3_filter_chain_build_viewport_pass(gl3_filter_chain_t *chain, + const struct gl3_viewport *vp, const float *mvp) +{ + (void)chain; + (void)mvp; + viewport_passes++; + last_pass_dims = vp->dims; +} +static void gl3_filter_chain_end_frame(gl3_filter_chain_t *chain) +{ + (void)chain; + frames_ended++; +} + +static void gl3_xr_plan(gl3_t *gl, const video_frame_info_t *video_info, + bool views, bool eyes, unsigned frame_dims, unsigned src_dims, + unsigned ui_dims) +{ + (void)video_info; + (void)views; + (void)eyes; + (void)frame_dims; + (void)src_dims; + (void)ui_dims; + gl->xr.set = planned; +} +bool video_xr_shrinks(unsigned image_dims, unsigned source_dims, + unsigned rotation) +{ + (void)image_dims; + (void)source_dims; + (void)rotation; + return false; +} +static void gl3_xr_forget(gl3_t *gl, unsigned s, bool yield) +{ + (void)yield; + gl->xr.shown[s].published = false; +} +static unsigned gl3_xr_eye_screens(const video_frame_info_t *video_info) +{ + (void)video_info; + return 0; +} +unsigned gl_openxr_slot_refused(gl_openxr_t *xr, unsigned slot) +{ + (void)xr; + (void)slot; + return 0; +} +bool gl_openxr_slot_target(gl_openxr_t *xr, unsigned slot, + unsigned dims, unsigned layers, uint32_t textures[2]) +{ + (void)xr; + (void)dims; + (void)layers; + textures[0] = RUNTIME_TEX(slot, 0); + textures[1] = RUNTIME_TEX(slot, 1); + return true; +} +void gl_openxr_slot_publish(gl_openxr_t *xr, unsigned slot, + const video_xr_quad_set_t *quads, const void *projection) +{ + (void)xr; + (void)quads; + (void)projection; + published[slot]++; +} +void gl_openxr_slot_forget(gl_openxr_t *xr, unsigned slot) +{ + (void)xr; + (void)slot; +} +static void gl3_xr_copy_ui(gl3_t *gl, unsigned s, GLuint tex, + unsigned ui_dims) +{ + (void)gl; + (void)s; + (void)tex; + (void)ui_dims; +} +static bool gl3_xr_draw_projection(gl3_t *gl, + const video_frame_info_t *video_info) +{ + (void)gl; + (void)video_info; + return true; +} + +#define OPENXR_PROJECTION_SLOT VIDEO_XR_MAX_SLOTS + +#include "driver_code.inc" + +#define CHECK(cond) do { if (!(cond)) { \ + printf("FAIL line %u: %s\n", (unsigned)__LINE__, #cond); \ + failures++; } } while (0) + +/* Any chain: the pass steps are stubbed. */ +#define FRAME_CHAIN ((gl3_filter_chain_t*)(uintptr_t)1) + +/* @screens screens drawn whole, no menu. */ +static void plan(unsigned dims, unsigned screens, float x) +{ + unsigned s; + memset(&planned, 0, sizeof(planned)); + for (s = 0; s < screens; s++) + { + planned.slots[s].dims = dims; + planned.slots[s].layers = 1; + planned.slots[s].view[0] = -1; + planned.quads[s].slot = s; + planned.quads[s].pose.position.x = x; + } + planned.num_quads = screens; +} + +static void draw(gl3_t *gl, bool new_frame) +{ + video_frame_info_t info; + struct gl3_filter_chain_texture frame; + memset(&info, 0, sizeof(info)); + memset(&frame, 0, sizeof(frame)); + frame.image = 0x10; + frame.dims = VIDEO_SCALE_PACK(64, 32); + info.frame_repeat = !new_frame; + gl3_xr_draw(gl, &info, new_frame, true, false, NULL, FRAME_CHAIN, NULL, + &frame, frame.dims, 0); +} + +static void reset(gl3_t *gl) +{ + memset(gl, 0, sizeof(*gl)); + offscreen_passes = viewport_passes = replays = 0; + frames_ended = last_pass_dims = 0; + chain_drawn = true; + runtime_attaches = own_attaches = 0; + memset(published, 0, sizeof(published)); +} + +/* A new frame: each screen straight into the runtime's texture, one + * final pass each, and the chain's frame ended. */ +static void test_direct(void) +{ + gl3_t gl; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + reset(&gl); + plan(dims, 2, 0.0f); + draw(&gl, true); + CHECK(offscreen_passes == 2 && viewport_passes == 2); + CHECK(frames_ended == 2 && replays == 0); + CHECK(runtime_attaches == 2 && own_attaches == 0); + CHECK(published[0] == 1 && published[1] == 1); +} + +/* A repeat of what was shown draws nothing. */ +static void test_repeat_same(void) +{ + gl3_t gl; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + reset(&gl); + plan(dims, 1, 0.0f); + draw(&gl, true); + viewport_passes = frames_ended = runtime_attaches = 0; + draw(&gl, false); + CHECK(viewport_passes == 0 && replays == 0 && frames_ended == 0); + CHECK(published[0] == 1); +} + +/* Resized or moved while paused: the last frame's final pass replayed + * at the new size straight into the runtime's texture. No other pass + * runs and no frame ends, so shader feedback and history stay where + * that frame left them. */ +static void test_repeat_changed(void) +{ + gl3_t gl; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 40); + reset(&gl); + plan(dims, 1, 0.0f); + draw(&gl, true); + offscreen_passes = viewport_passes = frames_ended = 0; + runtime_attaches = own_attaches = 0; + plan(changed, 1, 0.0f); + draw(&gl, false); + CHECK(replays == 1 && offscreen_passes == 0 && viewport_passes == 0); + CHECK(frames_ended == 0 && last_pass_dims == changed); + CHECK(runtime_attaches == 1 && own_attaches == 0); + replays = 0; + plan(changed, 1, 0.5f); + draw(&gl, false); + CHECK(replays == 1 && offscreen_passes == 0 && frames_ended == 0); + CHECK(published[0] == 3); +} + +/* A chain with no frame drawn since it was built has nothing to replay: + * its passes run, and no frame ends. */ +static void test_repeat_unready(void) +{ + gl3_t gl; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 40); + reset(&gl); + plan(dims, 1, 0.0f); + draw(&gl, true); + offscreen_passes = viewport_passes = frames_ended = 0; + chain_drawn = false; + plan(changed, 1, 0.0f); + draw(&gl, false); + CHECK(offscreen_passes == 1 && viewport_passes == 1); + CHECK(replays == 0 && frames_ended == 0); +} + +int main(void) +{ + test_direct(); + test_repeat_same(); + test_repeat_changed(); + test_repeat_unready(); + printf("%u failed (4 GL3 headset draw cases)\n", failures); + return failures ? 1 : 0; +} diff --git a/samples/gfx/video_views/video_views_test.c b/samples/gfx/video_views/video_views_test.c index 185e72515d..48aaf47f37 100644 --- a/samples/gfx/video_views/video_views_test.c +++ b/samples/gfx/video_views/video_views_test.c @@ -32,6 +32,75 @@ static int failures = 0; printf(" FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ failures++; } } while (0) +static void test_screen_defaults(void) +{ + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX + 1]; + unsigned i; + memset(screens, 0, sizeof(screens)); + screens[RETRO_VIDEO_VIEWS_MAX].scale = 7.0f; + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX + 1); + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + CHECK(screens[i].x == 0.0f && screens[i].y == 0.0f + && screens[i].z == 0.0f); + CHECK(screens[i].yaw == 0.0f && screens[i].pitch == 0.0f + && screens[i].roll == 0.0f); + CHECK(screens[i].scale == 1.0f && screens[i].visible); + } + CHECK(screens[RETRO_VIDEO_VIEWS_MAX].scale == 7.0f); + screens[1].scale = 3.0f; + video_views_xr_reset(screens, 1); + CHECK(screens[1].scale == 3.0f); + video_views_xr_reset(NULL, RETRO_VIDEO_VIEWS_MAX); + video_views_xr_normalize(NULL); +} + +static void test_screen_normalize(void) +{ + video_screen_xr_t s; + float invalid[3]; + unsigned i; + video_views_xr_reset(&s, 1); + s.x = -10.0f; + s.y = 10.0f; + s.z = 2.5f; + s.yaw = -180.0f; + s.pitch = 180.0f; + s.roll = 45.0f; + s.scale = 0.1f; + s.visible = false; + video_views_xr_normalize(&s); + CHECK(s.x == -10.0f && s.y == 10.0f && s.z == 2.5f); + CHECK(s.yaw == -180.0f && s.pitch == 180.0f && s.roll == 45.0f); + CHECK(s.scale == 0.1f && !s.visible); + s.x = -11.0f; + s.y = 11.0f; + s.z = -11.0f; + s.yaw = -181.0f; + s.pitch = 181.0f; + s.roll = -181.0f; + s.scale = 11.0f; + video_views_xr_normalize(&s); + CHECK(s.x == -10.0f && s.y == 10.0f && s.z == -10.0f); + CHECK(s.yaw == -180.0f && s.pitch == 180.0f && s.roll == -180.0f); + CHECK(s.scale == 10.0f); + s.scale = 0.0f; + video_views_xr_normalize(&s); + CHECK(s.scale == 0.1f); + invalid[0] = (float)HUGE_VAL; + invalid[1] = -(float)HUGE_VAL; + invalid[2] = (float)sqrt(-1.0); + for (i = 0; i < 3; i++) + { + s.x = s.y = s.z = invalid[i]; + s.yaw = s.pitch = s.roll = s.scale = invalid[i]; + video_views_xr_normalize(&s); + CHECK(s.x == 0.0f && s.y == 0.0f && s.z == 0.0f); + CHECK(s.yaw == 0.0f && s.pitch == 0.0f && s.roll == 0.0f); + CHECK(s.scale == 1.0f && !s.visible); + } +} + static struct retro_video_view mkview(unsigned x, unsigned y, unsigned w, unsigned h, unsigned screen, unsigned eye) { @@ -1315,6 +1384,8 @@ static void test_ui_helpers(void) int main(void) { + test_screen_defaults(); + test_screen_normalize(); test_validate(); test_frame_helpers(); test_eye_pair(); diff --git a/samples/gfx/video_xr/video_xr_test.c b/samples/gfx/video_xr/video_xr_test.c index 7ac462b31b..c40380e28f 100644 --- a/samples/gfx/video_xr/video_xr_test.c +++ b/samples/gfx/video_xr/video_xr_test.c @@ -93,6 +93,1882 @@ static int dims_is(unsigned dims, unsigned w, unsigned h) return VIDEO_SCALE_W(dims) == w && VIDEO_SCALE_H(dims) == h; } +static void test_screen_defaults(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t base, set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + unsigned layout, rotation, window; + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(800, 480); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + for (layout = 0; layout < VIDEO_SCREEN_LAYOUT_LAST; layout++) + for (rotation = 0; rotation < 4; rotation++) + { + p.screen_layout = layout; + p.rotation = rotation; + p.screens = NULL; + video_xr_place(&p, &base); + p.screens = screens; + video_xr_place(&p, &set); + CHECK(memcmp(&base, &set, sizeof(set)) == 0); + } + /* Without a map too, the frame and gl's window. */ + p.map = NULL; + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + for (window = 0; window < 2; window++) + { + p.window = window ? true : false; + p.screens = NULL; + video_xr_place(&p, &base); + /* Placed alike; without the settings it is no item. */ + CHECK(base.slots[0].fixed); + base.slots[0].fixed = false; + p.screens = screens; + video_xr_place(&p, &set); + CHECK(memcmp(&base, &set, sizeof(set)) == 0); + } + p.window = false; + /* Without a map the whole frame is screen 1: hidden, only the menu. */ + screens[0].visible = false; + screens[0].x = 2.0f; + p.map = NULL; + video_xr_place(&p, &set); + CHECK(set.num_quads == 1 && set.quads[0].kind == VIDEO_XR_QUAD_MENU); + screens[0].visible = true; + video_xr_place(&p, &set); + CHECK(set.num_quads == 2); + CHECK(pos_is(&set.quads[0], 2.0, 0.0, -1.8)); + CHECK(NEAR(set.quads[0].width, 1.6f)); +} + +static void test_screen_transform(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_xr_vec3_t axis, result, origin, dir; + float u, v, t; + int16_t fx, fy; + map_3ds(&map); + params_init(&p, &map); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + p.screens = screens; + screens[1].scale = 0.5f; + screens[1].x = 0.25f; + screens[1].z = 0.4f; + video_xr_place(&p, &set); + CHECK(set.num_quads == 3); + CHECK(NEAR(set.quads[2].width, 0.64f)); + CHECK(NEAR(set.quads[2].height, 0.48f)); + CHECK(pos_is(&set.quads[2], 0.25, -0.992, -1.4)); + CHECK(pos_is(&set.quads[0], 0.0, 0.0, -1.8)); + CHECK(set.slots[0].view[0] == 0 && set.slots[0].view[1] == 1); + CHECK(pos_is(&set.quads[1], 0.0, 0.0, -1.8)); + CHECK(NEAR(set.quads[0].width, 1.6) && NEAR(set.quads[1].height, 0.96)); + axis.x = axis.y = 0.0f; + axis.z = 1.0f; + screens[1].yaw = 90.0f; + video_xr_place(&p, &set); + video_xr_rotate(&set.quads[2].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 1.0) && NEAR(result.y, 0.0) && NEAR(result.z, 0.0)); + origin.x = 1.25f; + origin.y = -0.87248f; + origin.z = -1.24064f; + dir.x = -1.0f; + dir.y = dir.z = 0.0f; + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_AUTO, false, + &origin, &dir, &u, &v, &t) == 2); + CHECK(NEAR(u, 0.251) && NEAR(v, 0.251) && NEAR(t, 1.0)); + CHECK(video_xr_quad_to_frame(&set.quads[2], u, v, &map, + VIDEO_SCALE_PACK(800, 480), &fx, &fy)); + CHECK(fx == -6522 && fy == 8276); + screens[1].yaw = 0.0f; + screens[1].pitch = 90.0f; + video_xr_place(&p, &set); + video_xr_rotate(&set.quads[2].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 0.0) && NEAR(result.y, -1.0) && NEAR(result.z, 0.0)); + screens[1].pitch = 0.0f; + screens[1].roll = 90.0f; + axis.x = 1.0f; + axis.z = 0.0f; + video_xr_place(&p, &set); + video_xr_rotate(&set.quads[2].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 0.0) && NEAR(result.y, 1.0) && NEAR(result.z, 0.0)); + screens[1].yaw = screens[1].pitch = 90.0f; + video_xr_place(&p, &set); + video_xr_rotate(&set.quads[2].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 1.0) && NEAR(result.y, 0.0) && NEAR(result.z, 0.0)); + screens[1].pitch = screens[1].roll = 0.0f; + p.anchor.orientation.y = (float)sin(M_PI / 4.0); + p.anchor.orientation.w = (float)cos(M_PI / 4.0); + p.anchor.position.x = 3.0f; + p.anchor.position.y = 2.0f; + p.anchor.position.z = 1.0f; + video_xr_place(&p, &set); + CHECK(pos_is(&set.quads[2], 1.6, 1.008, 0.75)); + axis.x = 0.0f; + axis.z = 1.0f; + video_xr_rotate(&set.quads[2].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 0.0) && NEAR(result.y, 0.0) && NEAR(result.z, -1.0)); +} + +static void test_screen_visibility(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_views_rect_t rects[VIDEO_XR_MAX_SLOTS][2]; + video_viewport_t vp; + map_3ds(&map); + params_init(&p, &map); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + p.screens = screens; + p.ui_dims = VIDEO_SCALE_PACK(800, 480); + screens[1].visible = false; + video_xr_place(&p, &set); + CHECK(set.num_quads == 3); + CHECK(pos_is(&set.quads[0], 0.0, 0.0, -1.8)); + CHECK(pos_is(&set.quads[1], 0.0, 0.0, -1.8)); + CHECK(set.quads[2].kind == VIDEO_XR_QUAD_MENU); + CHECK(pos_is(&set.quads[2], 0.0, 0.0, -1.7)); + CHECK(set.slots[1].dims == 0 && set.slots[1].layers == 0); + CHECK(set.slots[1].view[0] == -1 && set.slots[1].view[1] == -1); + screens[0].visible = false; + video_xr_place(&p, &set); + CHECK(set.num_quads == 1 && set.quads[0].kind == VIDEO_XR_QUAD_MENU); + screens[1].visible = true; + screens[0].scale = 10.0f; + screens[0].y = 5.0f; + video_xr_place(&p, &set); + CHECK(set.num_quads == 2 && set.quads[0].screen == 1); + CHECK(pos_is(&set.quads[0], 0.0, -0.992, -1.8)); + screens[0].visible = true; + p.skip_screens = 1u; + video_xr_place(&p, &set); + CHECK(set.num_quads == 2 && set.quads[0].screen == 1); + CHECK(set.slots[0].dims == 0); + CHECK(pos_is(&set.quads[0], 0.0, -0.992, -1.8)); + memset(&vp, 0, sizeof(vp)); + vp.dims = vp.full_dims = VIDEO_SCALE_PACK(800, 480); + screens[0].visible = screens[1].visible = false; + video_xr_place_window(&p, true, false, &vp, vp.dims, &set, rects); + CHECK(set.num_quads == 2); + CHECK(pos_is(&set.quads[0], 0.0, 0.0, -1.8)); + CHECK(NEAR(set.quads[0].width, 1.6)); +} + +static void test_screen_identity(void) +{ + video_views_map_t map; + struct retro_video_view v[3]; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + map_3ds(&map); + v[0] = map.views[2]; + v[1] = map.views[1]; + v[2] = map.views[0]; + CHECK(video_views_validate(v, 3, &map)); + params_init(&p, &map); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + screens[1].x = 0.25f; + screens[1].scale = 0.5f; + p.screens = screens; + video_xr_place(&p, &set); + CHECK(set.num_quads == 3 && set.quads[2].screen == 1); + CHECK(pos_is(&set.quads[2], 0.25, -0.992, -1.8)); + CHECK(NEAR(set.quads[2].width, 0.64)); + CHECK(set.slots[0].view[0] == 2 && set.slots[0].view[1] == 1); + CHECK(set.slots[1].view[0] == 0); +} + +static void test_screen_density(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + map_3ds(&map); + params_init(&p, &map); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + p.screens = screens; + screens[1].scale = 0.5f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 352, 264)); + screens[1].x = 0.25f; + screens[1].z = 0.4f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 365, 274)); + p.anchor.position.x = 10.0f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 365, 274)); + screens[1].scale = 0.1f; + screens[1].x = screens[1].z = 0.0f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 320, 240)); + p.stock = true; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 72, 54)); + p.px_per_rad = 0.0f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 320, 240)); + p.stock = false; + p.px_per_rad = 1000.0f; + screens[1].scale = 1.0f; + screens[1].y = 0.992f; + screens[1].z = 1.8f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 2986, 2240)); + p.max_dim = 500; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[1].dims, 500, 375)); +} + +static int vec_is(const video_xr_vec3_t *v, double x, double y, double z) +{ + return NEAR(v->x, x) && NEAR(v->y, y) && NEAR(v->z, z); +} + +/* Degrees, the same turn either side of +-180. */ +static int angle_near(float a, float b) +{ + double d = fmod(fabs((double)a - (double)b), 360.0); + return d < 0.01 || d > 359.99; +} + +/* Eight screens of 160x100 stacked, one view each. */ +static void map_8(video_views_map_t *map) +{ + struct retro_video_view v[RETRO_VIDEO_VIEWS_MAX]; + unsigned s; + for (s = 0; s < RETRO_VIDEO_VIEWS_MAX; s++) + v[s] = mkview(0, s * 100, 160, 100, s, RETRO_VIDEO_VIEW_EYE_NONE); + CHECK(video_views_validate(v, RETRO_VIDEO_VIEWS_MAX, map)); +} + +static void test_pose_invert(void) +{ + video_xr_pose_t a, inv, id; + a.orientation.x = 0.1f; + a.orientation.y = 0.3f; + a.orientation.z = -0.2f; + a.orientation.w = (float)sqrt(0.86); + a.position.x = 1.0f; + a.position.y = -2.0f; + a.position.z = 0.5f; + video_xr_pose_invert(&a, &inv); + video_xr_compose(&a, &inv, &id); + CHECK(NEAR(id.position.x, 0.0) && NEAR(id.position.y, 0.0) + && NEAR(id.position.z, 0.0) && NEAR(fabs(id.orientation.w), 1.0)); + video_xr_compose(&inv, &a, &id); + CHECK(NEAR(id.position.x, 0.0) && NEAR(id.position.z, 0.0) + && NEAR(fabs(id.orientation.w), 1.0)); + /* In place. */ + inv = a; + video_xr_pose_invert(&inv, &inv); + video_xr_compose(&a, &inv, &id); + CHECK(NEAR(id.position.y, 0.0) && NEAR(fabs(id.orientation.w), 1.0)); +} + +/* The same turn: q and -q are one. */ +static int quat_same(const video_xr_quat_t *a, const video_xr_quat_t *b) +{ + return fabs((double)a->x * b->x + (double)a->y * b->y + + (double)a->z * b->z + (double)a->w * b->w) > 0.9999; +} + +static void test_euler(void) +{ + static const float yaws[] = { -170.0f, -90.0f, -30.0f, 0.0f, + 45.0f, 120.0f, 179.0f }; + static const float pitches[] = { -80.0f, -45.0f, 0.0f, 30.0f, 85.0f }; + static const float rolls[] = { -150.0f, -60.0f, 0.0f, 10.0f, + 90.0f, 175.0f }; + unsigned i, j, k; + video_xr_vec3_t base; + video_screen_xr_t s, got; + video_xr_pose_t pose, again; + + base.x = base.y = base.z = 0.0f; + for (i = 0; i < sizeof(yaws) / sizeof(yaws[0]); i++) + for (j = 0; j < sizeof(pitches) / sizeof(pitches[0]); j++) + for (k = 0; k < sizeof(rolls) / sizeof(rolls[0]); k++) + { + video_views_xr_reset(&s, 1); + s.yaw = yaws[i]; + s.pitch = pitches[j]; + s.roll = rolls[k]; + video_xr_item_pose(&base, &s, &pose); + video_xr_euler(&pose.orientation, &got.yaw, &got.pitch, + &got.roll); + CHECK(angle_near(got.yaw, s.yaw) && angle_near(got.pitch, s.pitch) + && angle_near(got.roll, s.roll)); + } + + /* Straight up the roll folds into the yaw: the same turn. */ + video_views_xr_reset(&s, 1); + s.yaw = 30.0f; + s.pitch = 90.0f; + s.roll = 20.0f; + video_xr_item_pose(&base, &s, &pose); + video_views_xr_reset(&got, 1); + video_xr_euler(&pose.orientation, &got.yaw, &got.pitch, &got.roll); + CHECK(fabs(got.pitch - 90.0f) < 0.05 && got.roll == 0.0f); + CHECK(angle_near(got.yaw, 10.0f)); + video_xr_item_pose(&base, &got, &again); + CHECK(quat_same(&pose.orientation, &again.orientation)); + /* Straight down it adds. */ + s.pitch = -90.0f; + video_xr_item_pose(&base, &s, &pose); + video_views_xr_reset(&got, 1); + video_xr_euler(&pose.orientation, &got.yaw, &got.pitch, &got.roll); + CHECK(fabs(got.pitch + 90.0f) < 0.05 && got.roll == 0.0f); + CHECK(angle_near(got.yaw, 50.0f)); + video_xr_item_pose(&base, &got, &again); + CHECK(quat_same(&pose.orientation, &again.orientation)); +} + +static void test_layout(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_layout_t lay; + video_xr_quad_set_t set; + + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(800, 480); + video_xr_layout(&p, &lay); + CHECK(lay.count == 2); + CHECK(vec_is(&lay.pos[0], 0.0, 0.0, -1.8)); + CHECK(NEAR(lay.width[0], 1.6) && NEAR(lay.height[0], 0.96)); + CHECK(vec_is(&lay.pos[1], 0.0, -0.992, -1.8)); + CHECK(NEAR(lay.width[1], 1.28) && NEAR(lay.height[1], 0.96)); + CHECK(lay.native[1] == VIDEO_SCALE_PACK(320, 240)); + CHECK(vec_is(&lay.menu, 0.0, 0.0, -1.7)); + + /* video_xr_place starts from the same spots, and says so. */ + video_xr_place(&p, &set); + CHECK(vec_is(&set.slots[0].base, 0.0, 0.0, -1.8)); + CHECK(vec_is(&set.slots[1].base, 0.0, -0.992, -1.8)); + CHECK(vec_is(&set.slots[VIDEO_XR_MENU_SLOT].base, 0.0, 0.0, -1.7)); + + p.screen_layout = VIDEO_SCREEN_LAYOUT_HORIZONTAL; + video_xr_layout(&p, &lay); + CHECK(vec_is(&lay.pos[1], 1.472, 0.0, -1.8)); + + /* A whole frame is screen 0's spot. */ + params_init(&p, NULL); + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + video_xr_layout(&p, &lay); + CHECK(lay.count == 1 && vec_is(&lay.pos[0], 0.0, 0.0, -1.8)); + CHECK(NEAR(lay.width[0], 1.6) && NEAR(lay.height[0], 0.96)); + video_xr_place(&p, &set); + CHECK(vec_is(&set.slots[0].base, 0.0, 0.0, -1.8)); +} + +/* Each screen's quad turned back into settings gives its settings: + * both layouts, three scales, all eight screens, a turned anchor. */ +static void test_settings_identity(void) +{ + static const float scales[] = { 0.5f, 1.0f, 2.5f }; + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_xr_layout_t lay; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_xr_pose_t inv; + unsigned layout, k, s, i; + + map_8(&map); + params_init(&p, &map); + p.anchor.orientation.y = (float)sin(M_PI / 8.0); + p.anchor.orientation.w = (float)cos(M_PI / 8.0); + p.anchor.position.x = 0.4f; + p.anchor.position.z = -0.3f; + video_xr_pose_invert(&p.anchor, &inv); + for (layout = 0; layout < VIDEO_SCREEN_LAYOUT_LAST; layout++) + for (k = 0; k < sizeof(scales) / sizeof(scales[0]); k++) + { + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + for (s = 0; s < RETRO_VIDEO_VIEWS_MAX; s++) + { + screens[s].x = 0.1f * (float)s - 0.3f; + screens[s].y = 0.05f * (float)s; + screens[s].z = -0.2f + 0.07f * (float)s; + screens[s].yaw = -40.0f + 11.0f * (float)s; + screens[s].pitch = 25.0f - 9.0f * (float)s; + screens[s].roll = 5.0f * (float)s - 15.0f; + screens[s].scale = scales[k]; + } + p.screen_layout = layout; + p.screens = screens; + video_xr_place(&p, &set); + video_xr_layout(&p, &lay); + CHECK(set.num_quads == RETRO_VIDEO_VIEWS_MAX); + for (i = 0; i < set.num_quads; i++) + { + video_screen_xr_t got; + video_xr_pose_t local; + const video_xr_quad_t *q = &set.quads[i]; + s = q->screen; + video_xr_compose(&inv, &q->pose, &local); + video_xr_item_settings(&set.slots[s].base, &local, &got); + CHECK(NEAR(got.x, screens[s].x) && NEAR(got.y, screens[s].y) + && NEAR(got.z, screens[s].z)); + CHECK(angle_near(got.yaw, screens[s].yaw) + && angle_near(got.pitch, screens[s].pitch) + && angle_near(got.roll, screens[s].roll)); + CHECK(NEAR(q->width / lay.width[s], scales[k])); + } + } +} + +static void test_grab(void) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose, inv; + video_screen_xr_t s; + + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + /* An item 1.8 m ahead at its own spot, held from the eyes. */ + g.base.z = -1.8f; + video_xr_pose_identity(&g.rel); + g.rel.position.z = -1.8f; + g.turn = g.rel.orientation; + CHECK(!video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL)); + CHECK(NEAR(pose.position.z, -1.8) && NEAR(s.x, 0.0) && NEAR(s.z, 0.0)); + /* The hand moves: the item with it. */ + aim.position.x = 0.2f; + CHECK(!video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL)); + CHECK(NEAR(s.x, 0.2) && NEAR(s.y, 0.0) && NEAR(s.z, 0.0)); + /* The wrist rolls 30 degrees: level, the item does not... */ + aim.orientation.z = (float)sin(M_PI / 12.0); + aim.orientation.w = (float)cos(M_PI / 12.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.roll, 0.0f) && angle_near(s.yaw, 0.0f) + && angle_near(s.pitch, 0.0f)); + /* ...tilting, it does. */ + g.tilt = true; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.roll, 30.0f)); + /* Level again at the roll it was left at. */ + g.tilt = false; + video_xr_grab_hold(&g, &aim, &pose.orientation); + aim.orientation.z = 0.0f; + aim.orientation.w = 1.0f; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.roll, 30.0f)); + /* At a limit the item stops: 10 m right of its spot at most. */ + aim.position.x = 10.5f; + CHECK(video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL)); + CHECK(NEAR(s.x, 10.0) && NEAR(pose.position.x, 10.0)); + /* Held again from there, as the caller does, it resumes from the + * limit, not from where the hand went. */ + video_xr_pose_invert(&aim, &inv); + video_xr_compose(&inv, &pose, &g.rel); + aim.position.x = 10.4f; + CHECK(!video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL)); + CHECK(NEAR(s.x, 9.9)); + /* out may be NULL: only the pose. */ + CHECK(!video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL)); + /* The ray rises 20 degrees: the item rides up with it, its pitch + * held... */ + video_xr_pose_identity(&aim); + video_xr_pose_identity(&g.rel); + g.turn = g.rel.orientation; + g.rel.position.z = -1.8f; + aim.orientation.x = (float)sin(M_PI / 18.0); + aim.orientation.w = (float)cos(M_PI / 18.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.pitch, 0.0f) && angle_near(s.yaw, 0.0f) + && angle_near(s.roll, 0.0f)); + CHECK(NEAR(pose.position.y, 1.8 * sin(M_PI / 9.0))); + /* ...tilting, it follows... */ + g.tilt = true; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.pitch, 20.0f)); + /* ...and stays at the pitch it was left at. */ + g.tilt = false; + video_xr_grab_hold(&g, &aim, &pose.orientation); + video_xr_pose_identity(&aim); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.pitch, 20.0f) && angle_near(s.roll, 0.0f)); +} + +/* A stick axis past the deadzone, as a fraction of the travel left. */ +static double past_deadzone(double v) +{ + double a = (fabs(v) - VIDEO_XR_EDIT_DEADZONE) + / (1.0 - VIDEO_XR_EDIT_DEADZONE); + if (a <= 0.0) + return 0.0; + return (v < 0.0) ? -a : a; +} + +static void test_grab_stick(void) +{ + video_xr_grab_t g; + float scale = 1.0f; + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&g.rel); + g.rel.position.z = -1.0f; + /* Inside the deadzone: nothing. */ + video_xr_grab_stick(&g, 0.1f, 0.15f, 1.0, &scale); + CHECK(NEAR(g.rel.position.z, -1.0) && scale == 1.0f); + /* Full push for half a second, then half past the deadzone for one. */ + video_xr_grab_stick(&g, 0.0f, 1.0f, 0.5, &scale); + CHECK(NEAR(g.rel.position.z, -1.75)); + video_xr_grab_stick(&g, 0.0f, 0.6f, 1.0, &scale); + CHECK(NEAR(g.rel.position.z, -2.5)); + /* Pulled in, no nearer than the reach. */ + video_xr_grab_stick(&g, 0.0f, -1.0f, 10.0, &scale); + CHECK(NEAR(g.rel.position.z, -VIDEO_XR_EDIT_REACH)); + /* Right doubles it in a second, left halves it, within the range. */ + video_xr_grab_stick(&g, 1.0f, 0.0f, 1.0, &scale); + CHECK(NEAR(scale, 2.0)); + video_xr_grab_stick(&g, -1.0f, 0.0f, 2.0, &scale); + CHECK(NEAR(scale, 0.5)); + video_xr_grab_stick(&g, 1.0f, 0.0f, 10.0, &scale); + CHECK(scale == 10.0f); + video_xr_grab_stick(&g, -1.0f, 0.0f, 20.0, &scale); + CHECK(NEAR(scale, 0.1)); + /* 15 degrees off right pushes too: no lock to the nearest axis. */ + g.rel.position.z = -1.0f; + scale = 1.0f; + video_xr_grab_stick(&g, (float)cos(M_PI / 12.0), (float)sin(M_PI / 12.0), + 1.0, &scale); + CHECK(NEAR(g.rel.position.z, -1.0 - 1.5 * past_deadzone(sin(M_PI / 12.0))) + && NEAR(scale, pow(2.0, past_deadzone(cos(M_PI / 12.0))))); +} + +/* The free hand's stick turns the held item about the vertical through + * its middle: right anticlockwise seen from above, as fast as the stick + * is pushed past the deadzone; a pitched item keeps its pitch. */ +static void test_grab_turn(void) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + video_xr_pose_identity(&g.rel); + g.rel.position.x = 0.3f; + g.rel.position.z = -1.8f; + g.base = g.rel.position; + g.turn = g.rel.orientation; + /* Inside the deadzone: nothing. */ + video_xr_grab_turn(&g, &aim, 0.15f, 0.0f, 1.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 0.0f)); + /* Full right for half a second: 45 degrees, in place. */ + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 0.5); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 45.0f) && NEAR(s.x, 0.0) && NEAR(s.y, 0.0) + && NEAR(s.z, 0.0)); + /* Half past the deadzone left for a second: back 45. */ + video_xr_grab_turn(&g, &aim, -0.6f, 0.0f, 1.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 0.0f)); + /* Pitched 15 degrees, a quarter turn keeps the pitch, no roll. */ + g.turn.x = (float)sin(M_PI / 24.0); + g.turn.y = 0.0f; + g.turn.w = (float)cos(M_PI / 24.0); + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 1.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 90.0f) && angle_near(s.pitch, 15.0f) + && angle_near(s.roll, 0.0f)); +} + +/* aim at the anchor turned to this yaw, pitch and roll. */ +static void turn_aim(video_xr_pose_t *aim, float yaw, float pitch, + float roll) +{ + video_xr_vec3_t at; + video_screen_xr_t s; + at.x = 0.0f; + at.y = 0.0f; + at.z = 0.0f; + video_views_xr_reset(&s, 1); + s.yaw = yaw; + s.pitch = pitch; + s.roll = roll; + video_xr_item_pose(&at, &s, aim); +} + +/* g holding an item 1.8 m ahead at this yaw, pitch and roll, taken + * with aim. */ +static void turn_grab(video_xr_grab_t *g, const video_xr_pose_t *aim, + float yaw, float pitch, float roll) +{ + video_xr_pose_t item, inv; + video_screen_xr_t s; + memset(g, 0, sizeof(*g)); + g->base.z = -1.8f; + video_views_xr_reset(&s, 1); + s.yaw = yaw; + s.pitch = pitch; + s.roll = roll; + video_xr_item_pose(&g->base, &s, &item); + video_xr_pose_invert(aim, &inv); + video_xr_compose(&inv, &item, &g->rel); + video_xr_grab_hold(g, aim, &item.orientation); +} + +/* Whether ten steps of 0.1 s of the free hand's stick at full right + * each turn g's item, held by aim, 9 degrees of shown yaw, keeping its + * shown pitch and roll and its middle. */ +static int turns_evenly(video_xr_grab_t *g, const video_xr_pose_t *aim) +{ + video_xr_pose_t start, pose; + video_screen_xr_t s0, s; + unsigned i; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &start, &s0, NULL); + for (i = 1; i <= 10; i++) + { + video_xr_grab_turn(g, aim, 1.0f, 0.0f, 0.1); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, &s, NULL); + /* Read back near straight up or down, a float turn's pitch is + * good to a few hundredths of a degree. */ + if (!angle_near(s.yaw, s0.yaw + 9.0f * (float)i) + || fabs((double)s.pitch - s0.pitch) > 0.1 + || !angle_near(s.roll, s0.roll) + || !vec_is(&pose.position, start.position.x, + start.position.y, start.position.z)) + return 0; + } + return 1; +} + +/* The turn is about the vertical through the item's middle whatever its + * pitch and wherever the ray points, carried level or tilting. */ +static void test_grab_turn_pitched(void) +{ + video_xr_grab_t g; + video_xr_pose_t taken, aim; + video_xr_pose_identity(&taken); + video_xr_pose_identity(&aim); + turn_grab(&g, &taken, 0.0f, 60.0f, 0.0f); + CHECK(turns_evenly(&g, &aim)); + turn_grab(&g, &taken, 0.0f, 80.0f, 0.0f); + CHECK(turns_evenly(&g, &aim)); + /* Floor and ceiling screens. */ + turn_grab(&g, &taken, 30.0f, 90.0f, 0.0f); + CHECK(turns_evenly(&g, &aim)); + turn_grab(&g, &taken, 30.0f, -90.0f, 0.0f); + CHECK(turns_evenly(&g, &aim)); + /* Upright, the ray raised 60 degrees since the grab. */ + aim.orientation.x = (float)sin(M_PI / 6.0); + aim.orientation.w = (float)cos(M_PI / 6.0); + turn_grab(&g, &taken, 0.0f, 0.0f, 0.0f); + CHECK(turns_evenly(&g, &aim)); + turn_grab(&g, &taken, 0.0f, 0.0f, 0.0f); + g.tilt = true; + CHECK(turns_evenly(&g, &aim)); + /* Rolled, taken by a rolled aim and turned by another. */ + turn_aim(&taken, 10.0f, 15.0f, 25.0f); + turn_aim(&aim, -20.0f, 35.0f, -40.0f); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + CHECK(turns_evenly(&g, &aim)); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + g.tilt = true; + CHECK(turns_evenly(&g, &aim)); +} + +/* Whether an upright item, taken square to a level ray and turned a + * quarter with the ray raised this many degrees, keeps that yaw as the + * ray comes back down a degree at a time. */ +static int keeps_turn(unsigned raised) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + unsigned i; + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 40.0f, 0.0f, 0.0f); + turn_aim(&aim, 40.0f, (float)raised, 0.0f); + if (!turns_evenly(&g, &aim)) + return 0; + for (i = 0; i <= raised; i++) + { + turn_aim(&aim, 40.0f, (float)(raised - i), 0.0f); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + if (!angle_near(s.yaw, 130.0f) || !angle_near(s.pitch, 0.0f) + || !angle_near(s.roll, 0.0f)) + return 0; + } + return 1; +} + +/* Carried, a turn made with the ray raised stays as the ray comes down: + * squared up there, the item is still square, inside + * VIDEO_XR_SNAP_ANGLE, once lowered. */ +static void test_grab_turn_lowered(void) +{ + CHECK(keeps_turn(60)); + CHECK(keeps_turn(20)); +} + +/* Tilting, the turned item rides the hand from where the turn left it: + * turned side-on to the ray, raising the ray rolls it. */ +static void test_grab_turn_tilt_ray(void) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 40.0f, 0.0f, 0.0f); + g.tilt = true; + CHECK(turns_evenly(&g, &aim)); + turn_aim(&aim, 40.0f, 30.0f, 0.0f); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 130.0f) && angle_near(s.pitch, 0.0f) + && angle_near(s.roll, 30.0f)); + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 130.0f) && angle_near(s.pitch, 0.0f) + && angle_near(s.roll, 0.0f)); +} + +/* The trigger pressed or let go with g's hand at aim, as the hand's + * move does it, where it last put the item. */ +static void carry_tilt(video_xr_grab_t *g, const video_xr_pose_t *aim, + bool tilt) +{ + video_xr_pose_t inv; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &g->last, NULL, NULL); + video_xr_pose_invert(aim, &inv); + if (tilt && !g->tilt) + video_xr_compose(&inv, &g->last, &g->rel); + else if (!tilt && g->tilt) + video_xr_grab_hold(g, aim, &g->last.orientation); + g->tilt = tilt; +} + +/* Degrees between two turns. */ +static double turn_apart(const video_xr_quat_t *a, const video_xr_quat_t *b) +{ + double x = (double)a->w * b->x - (double)a->x * b->w + - (double)a->y * b->z + (double)a->z * b->y; + double y = (double)a->w * b->y + (double)a->x * b->z + - (double)a->y * b->w - (double)a->z * b->x; + double z = (double)a->w * b->z - (double)a->x * b->y + + (double)a->y * b->x - (double)a->z * b->w; + double w = (double)a->w * b->w + (double)a->x * b->x + + (double)a->y * b->y + (double)a->z * b->z; + return 2.0 * atan2(sqrt(x * x + y * y + z * z), fabs(w)) * 180.0 / M_PI; +} + +/* Whether g shows its item at this yaw, pitch and roll for aim. */ +static int carried_at(const video_xr_grab_t *g, const video_xr_pose_t *aim, + float yaw, float pitch, float roll) +{ + video_xr_pose_t pose; + video_screen_xr_t s; + video_views_xr_reset(&s, 1); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, &s, NULL); + return angle_near(s.yaw, yaw) && angle_near(s.pitch, pitch) + && angle_near(s.roll, roll); +} + +/* Carried, an item keeps its yaw from the hand's heading: raising, + * lowering or twisting the hand leaves it, and turning the heading + * turns it as much. */ +static void test_carry_heading(void) +{ + unsigned i; + video_xr_grab_t g; + video_xr_pose_t aim; + int kept = 1; + /* Square to a hand at 40, turned 45 by the stick, the ray raised + * 60 and lowered again a degree at a time. */ + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 40.0f, 0.0f, 0.0f); + for (i = 0; i < 5; i++) + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 0.1); + for (i = 0; i <= 120; i++) + { + turn_aim(&aim, 40.0f, (float)(i <= 60 ? i : 120 - i), 0.0f); + if (!carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)) + kept = 0; + } + CHECK(kept); + /* Taken 45 off the ray, the ray raised 60. */ + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 85.0f, 0.0f, 0.0f); + turn_aim(&aim, 40.0f, 60.0f, 0.0f); + CHECK(carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)); + /* The wrist twisted 30, the ray level and raised 30. */ + turn_aim(&aim, 40.0f, 0.0f, 30.0f); + CHECK(carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)); + turn_aim(&aim, 40.0f, 30.0f, 30.0f); + CHECK(carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)); + /* The heading turned 30, the ray raised and twisted. */ + turn_aim(&aim, 70.0f, 20.0f, -10.0f); + CHECK(carried_at(&g, &aim, 115.0f, 0.0f, 0.0f)); + /* A pitched and rolled item likewise. */ + turn_aim(&aim, -20.0f, 10.0f, 5.0f); + turn_grab(&g, &aim, 25.0f, 30.0f, -20.0f); + turn_aim(&aim, 10.0f, 60.0f, 30.0f); + CHECK(carried_at(&g, &aim, 55.0f, 30.0f, -20.0f)); +} + +/* A full turn of the stick, a degree at a time, brings an item back + * where it began; each step turns it a degree about the vertical. Near + * straight down and rolled, the readback of its yaw is unsure of the + * roll, which must not add up. */ +static void test_carry_full_turn(void) +{ + unsigned k; + video_xr_grab_t g; + video_xr_pose_t taken, aim, start, pose, want, about; + double worst = 0.0; + turn_aim(&taken, -59.2f, 22.3f, 29.2f); + turn_aim(&aim, -44.9f, 14.0f, 18.7f); + turn_grab(&g, &taken, -14.429f, -89.743f, -31.768f); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &start, NULL, NULL); + video_xr_pose_identity(&about); + for (k = 1; k <= 360; k++) + { + double d; + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 1.0 / 90.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + about.orientation.y = (float)sin(k * M_PI / 360.0); + about.orientation.w = (float)cos(k * M_PI / 360.0); + video_xr_compose(&about, &start, &want); + d = turn_apart(&want.orientation, &pose.orientation); + if (!(d <= worst)) + worst = d; + } + CHECK(worst < 0.01); + CHECK(turn_apart(&start.orientation, &pose.orientation) < 0.1); + CHECK(vec_is(&pose.position, start.position.x, start.position.y, + start.position.z)); +} + +/* A turn of deg degrees about +X (axis 0) or +Y (axis 1). */ +static void axis_turn(video_xr_pose_t *out, int axis, double deg) +{ + video_xr_pose_identity(out); + if (axis) + out->orientation.y = (float)sin(deg * M_PI / 360.0); + else + out->orientation.x = (float)sin(deg * M_PI / 360.0); + out->orientation.w = (float)cos(deg * M_PI / 360.0); +} + +static double dist(const video_xr_vec3_t *a, const video_xr_vec3_t *b) +{ + double x = (double)a->x - b->x; + double y = (double)a->y - b->y; + double z = (double)a->z - b->z; + return sqrt(x * x + y * y + z * z); +} + +/* How far the top and bottom edges' middles of an item 0.9 m high at + * pose are from at. */ +static void edges_from(const video_xr_pose_t *pose, + const video_xr_vec3_t *at, double *top, double *bottom) +{ + video_xr_pose_t edge, p; + video_xr_pose_identity(&edge); + edge.position.y = 0.45f; + video_xr_compose(pose, &edge, &p); + *top = dist(&p.position, at); + edge.position.y = -0.45f; + video_xr_compose(pose, &edge, &p); + *bottom = dist(&p.position, at); +} + +/* The free hand's stick up tips the held item's top away from the user + * about its own left-right line through its middle, past the deadzone + * as the turn is; down tips it back. */ +static void test_grab_tilt(void) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + double top0, bottom0, top, bottom; + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + video_xr_pose_identity(&g.rel); + g.rel.position.z = -1.8f; + g.base = g.rel.position; + g.turn = g.rel.orientation; + /* Inside the deadzone: nothing. */ + video_xr_grab_turn(&g, &aim, 0.0f, 0.15f, 1.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(turn_apart(&pose.orientation, &aim.orientation) < 0.01); + edges_from(&pose, &aim.position, &top0, &bottom0); + /* Full up for 0.1 s, a screen facing the user at the aim: 9 degrees, + * its top edge farther and its bottom edge nearer, in place. */ + video_xr_grab_turn(&g, &aim, 0.0f, 1.0f, 0.1); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + edges_from(&pose, &aim.position, &top, &bottom); + CHECK(top > top0 + 0.03 && bottom < bottom0 - 0.03); + CHECK(angle_near(s.pitch, -9.0f) && angle_near(s.yaw, 0.0f) + && angle_near(s.roll, 0.0f) && NEAR(s.x, 0.0) && NEAR(s.y, 0.0) + && NEAR(s.z, 0.0)); + /* Half past the deadzone down for a second: 45 back. */ + video_xr_grab_turn(&g, &aim, 0.0f, -0.6f, 1.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.pitch, 36.0f) && angle_near(s.yaw, 0.0f) + && angle_near(s.roll, 0.0f)); +} + +/* Whether ten steps of 0.1 s of the free hand's stick full up each tip + * g's item, held by aim, 9 degrees more about its own left-right line, + * keeping its middle. */ +static int tilts_evenly(video_xr_grab_t *g, const video_xr_pose_t *aim) +{ + video_xr_pose_t start, pose, tip, want; + unsigned i; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &start, NULL, NULL); + for (i = 1; i <= 10; i++) + { + video_xr_grab_turn(g, aim, 0.0f, 1.0f, 0.1); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + axis_turn(&tip, 0, -9.0 * i); + video_xr_compose(&start, &tip, &want); + if (!(turn_apart(&want.orientation, &pose.orientation) < 0.01) + || !vec_is(&pose.position, start.position.x, + start.position.y, start.position.z)) + return 0; + } + return 1; +} + +/* Carried or tilting with the trigger, upright or turned every way. */ +static void test_grab_tilt_evenly(void) +{ + video_xr_grab_t g; + video_xr_pose_t taken, aim; + video_xr_pose_identity(&taken); + video_xr_pose_identity(&aim); + turn_grab(&g, &taken, 0.0f, 0.0f, 0.0f); + CHECK(tilts_evenly(&g, &aim)); + turn_grab(&g, &taken, 0.0f, 0.0f, 0.0f); + g.tilt = true; + CHECK(tilts_evenly(&g, &aim)); + /* Rolled, taken by a rolled aim and tilted by another. */ + turn_aim(&taken, 10.0f, 15.0f, 25.0f); + turn_aim(&aim, -20.0f, 35.0f, -40.0f); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + CHECK(tilts_evenly(&g, &aim)); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + g.tilt = true; + CHECK(tilts_evenly(&g, &aim)); +} + +/* Whether the stick right and up together for 0.1 s turns g's item 9 + * degrees about the vertical and tips it 9 about its own left-right + * line at once, keeping its middle. */ +static int turns_and_tilts(video_xr_grab_t *g, const video_xr_pose_t *aim) +{ + video_xr_pose_t start, pose, about, tip, want; + video_xr_grab_pose(g, aim, NULL, 0, NULL, &start, NULL, NULL); + video_xr_grab_turn(g, aim, 1.0f, 1.0f, 0.1); + video_xr_grab_pose(g, aim, NULL, 0, NULL, &pose, NULL, NULL); + axis_turn(&about, 1, 9.0); + axis_turn(&tip, 0, -9.0); + video_xr_compose(&start, &tip, &want); + want.position = about.position; + video_xr_compose(&about, &want, &want); + return turn_apart(&want.orientation, &pose.orientation) < 0.01 + && vec_is(&pose.position, start.position.x, start.position.y, + start.position.z); +} + +static void test_grab_turn_and_tilt(void) +{ + video_xr_grab_t g; + video_xr_pose_t taken, aim; + turn_aim(&taken, 10.0f, 15.0f, 25.0f); + turn_aim(&aim, -20.0f, 35.0f, -40.0f); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + CHECK(turns_and_tilts(&g, &aim)); + turn_grab(&g, &taken, 20.0f, 40.0f, 30.0f); + g.tilt = true; + CHECK(turns_and_tilts(&g, &aim)); +} + +/* The free hand's stick pushed full, deg degrees anticlockwise from + * right, for half a second: within VIDEO_XR_EDIT_TURN_LOCK degrees of + * left or right it only turns an item facing the user, within that of + * up or down it only tilts it, and between it does both. */ +static void test_grab_turn_lock(void) +{ + static const struct + { + float deg; + int turns, tilts; + } pushes[] = + { + { 15.0f, 1, 0 }, { 165.0f, 1, 0 }, { 195.0f, 1, 0 }, + { 345.0f, 1, 0 }, { 75.0f, 0, 1 }, { 105.0f, 0, 1 }, + { 255.0f, 0, 1 }, { 285.0f, 0, 1 }, { 45.0f, 1, 1 }, + { 135.0f, 1, 1 }, { 225.0f, 1, 1 }, { 315.0f, 1, 1 }, + { VIDEO_XR_EDIT_TURN_LOCK - 1.0f, 1, 0 }, + { VIDEO_XR_EDIT_TURN_LOCK + 1.0f, 1, 1 }, + { 91.0f - VIDEO_XR_EDIT_TURN_LOCK, 0, 1 }, + { 89.0f - VIDEO_XR_EDIT_TURN_LOCK, 1, 1 }, + { 180.0f + VIDEO_XR_EDIT_TURN_LOCK - 1.0f, 1, 0 }, + { 269.0f + VIDEO_XR_EDIT_TURN_LOCK, 0, 1 } + }; + unsigned i; + for (i = 0; i < sizeof(pushes) / sizeof(pushes[0]); i++) + { + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + double r = pushes[i].deg * M_PI / 180.0; + float x = (float)cos(r); + float y = (float)sin(r); + float yaw, pitch; + yaw = pushes[i].turns ? (float)(past_deadzone(x) * 45.0) : 0.0f; + pitch = pushes[i].tilts ? (float)(-past_deadzone(y) * 45.0) : 0.0f; + video_xr_pose_identity(&aim); + turn_grab(&g, &aim, 0.0f, 0.0f, 0.0f); + video_xr_grab_turn(&g, &aim, x, y, 0.5); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + if (!(angle_near(s.yaw, yaw) && angle_near(s.pitch, pitch) + && angle_near(s.roll, 0.0f))) + { + printf(" FAIL %s:%d: stick at %.1f deg: yaw %.2f pitch %.2f," + " want %.2f %.2f\n", __FILE__, __LINE__, pushes[i].deg, + s.yaw, s.pitch, yaw, pitch); + failures++; + } + } +} + +/* An item turned 45 degrees tips about its own left-right line, not the + * world's: its yaw stays and it takes no roll. */ +static void test_grab_tilt_turned(void) +{ + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_screen_xr_t s; + video_xr_pose_identity(&aim); + turn_grab(&g, &aim, 45.0f, 0.0f, 0.0f); + video_xr_grab_turn(&g, &aim, 0.0f, 1.0f, 30.0 / VIDEO_XR_EDIT_TURN_RATE); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 45.0f) && angle_near(s.pitch, -30.0f) + && angle_near(s.roll, 0.0f)); + /* Turned 45 by the stick, tilting with the trigger. */ + turn_grab(&g, &aim, 0.0f, 0.0f, 0.0f); + g.tilt = true; + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 0.5); + video_xr_grab_turn(&g, &aim, 0.0f, 1.0f, 30.0 / VIDEO_XR_EDIT_TURN_RATE); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + CHECK(angle_near(s.yaw, 45.0f) && angle_near(s.pitch, -30.0f) + && angle_near(s.roll, 0.0f)); +} + +/* A full tilt of the stick, a degree at a time, through straight up + * and down with no stop, brings an item back where it began; each step + * tips it a degree about its own left-right line. */ +static void test_tilt_full_turn(void) +{ + unsigned k, tilt; + video_xr_grab_t g; + video_xr_pose_t taken, aim, start, pose, tip, want; + for (tilt = 0; tilt < 2; tilt++) + { + double worst = 0.0; + turn_aim(&taken, -59.2f, 22.3f, 29.2f); + turn_aim(&aim, -44.9f, 14.0f, 18.7f); + turn_grab(&g, &taken, 30.0f, 20.0f, -15.0f); + g.tilt = tilt != 0; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &start, NULL, NULL); + for (k = 1; k <= 360; k++) + { + double d; + video_xr_grab_turn(&g, &aim, 0.0f, 1.0f, 1.0 / 90.0); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + axis_turn(&tip, 0, -(double)k); + video_xr_compose(&start, &tip, &want); + d = turn_apart(&want.orientation, &pose.orientation); + if (!(d <= worst)) + worst = d; + } + CHECK(worst < 0.01); + CHECK(turn_apart(&start.orientation, &pose.orientation) < 0.1); + CHECK(vec_is(&pose.position, start.position.x, start.position.y, + start.position.z)); + } +} + +/* Whether the ray, at heading 40 and twisted roll, swept in 0.1 degree + * steps from from to to and back, moves g's item less than a degree a + * step and never to NaN; and, untwisted, keeps it at yaw. */ +static int sweeps_smoothly(const video_xr_grab_t *g, float roll, int from, + int to, float yaw) +{ + int i; + video_xr_pose_t aim, pose, prev; + video_screen_xr_t s; + int n = to > from ? to - from : from - to; + int step = to > from ? 1 : -1; + for (i = 0; i <= 2 * n; i++) + { + int at = from + step * (i <= n ? i : 2 * n - i); + turn_aim(&aim, 40.0f, (float)at / 10.0f, roll); + video_views_xr_reset(&s, 1); + video_xr_grab_pose(g, &aim, NULL, 0, NULL, &pose, &s, NULL); + if (!(fabs(pose.orientation.w) <= 1.0f) || !(s.yaw == s.yaw)) + return 0; + if (i && !(turn_apart(&prev.orientation, &pose.orientation) < 1.0)) + return 0; + if (roll == 0.0f && !angle_near(s.yaw, yaw)) + return 0; + prev = pose; + } + return 1; +} + +/* Pointing at the ceiling or the floor, a little past them, or + * twisting there: nothing jumps or fails. Untwisted, the item keeps its + * yaw; twisted, it is back on it once the ray is lowered. */ +static void test_carry_vertical(void) +{ + unsigned i, r; + video_xr_grab_t g; + video_xr_pose_t aim, pose, prev; + static const float rolls[] = { 0.0f, 30.0f, -60.0f }; + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 85.0f, 0.0f, 0.0f); + for (r = 0; r < 3; r++) + { + int past = r ? 910 : 930; + CHECK(sweeps_smoothly(&g, rolls[r], 600, past, 85.0f)); + CHECK(sweeps_smoothly(&g, rolls[r], -600, -past, 85.0f)); + turn_aim(&aim, 40.0f, 60.0f, rolls[r]); + CHECK(carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)); + } + for (r = 0; r < 2; r++) + { + int ok = 1; + for (i = 0; i <= 720; i++) + { + turn_aim(&aim, 40.0f, r ? -90.0f : 90.0f, (float)i * 0.5f); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + if (!(fabs(pose.orientation.w) <= 1.0f) || (i + && !(turn_apart(&prev.orientation, &pose.orientation) + < 1.0))) + ok = 0; + prev = pose; + } + CHECK(ok); + } +} + +/* Some heading must jump on the path over the top: an untwisted ray + * turns the item round once tipped 5 degrees past straight up or down. */ +static void test_carry_flip(void) +{ + int up; + video_xr_grab_t g; + video_xr_pose_t aim; + turn_aim(&aim, 40.0f, 0.0f, 0.0f); + turn_grab(&g, &aim, 85.0f, 0.0f, 0.0f); + for (up = 1; up >= -1; up -= 2) + { + turn_aim(&aim, 40.0f, (float)up * 94.0f, 0.0f); + CHECK(carried_at(&g, &aim, 85.0f, 0.0f, 0.0f)); + turn_aim(&aim, 40.0f, (float)up * 96.0f, 0.0f); + CHECK(carried_at(&g, &aim, -95.0f, 0.0f, 0.0f)); + } +} + +/* Taking an item, and pressing or letting go of the trigger, leave it + * where it is: at any pitch, straight up or down too, and rolled; with + * the hand near level, past 80 degrees, or straight up or down. */ +static void test_carry_still(void) +{ + static const float pitches[] = { 0.0f, 45.0f, -70.0f, 89.8f, -89.9f, + 90.0f, -90.0f }; + static const float rolls[] = { 0.0f, 30.0f, -120.0f }; + /* The hand's yaw, pitch and roll at the grab, the trigger pressed + * and the trigger let go. */ + static const float aims[3][3][3] = { + { { -30.0f, 25.0f, 15.0f }, { 10.0f, -40.0f, -35.0f }, + { 60.0f, 50.0f, 70.0f } }, + { { -30.0f, 84.0f, 15.0f }, { 10.0f, 87.0f, -35.0f }, + { 60.0f, -82.0f, 70.0f } }, + { { -30.0f, 90.0f, 15.0f }, { 10.0f, 90.0f, -35.0f }, + { 60.0f, -90.0f, 70.0f } } + }; + unsigned i, j; + int still = 1; + for (i = 0; i < sizeof(pitches) / sizeof(pitches[0]); i++) + for (j = 0; j < 9; j++) + { + video_xr_grab_t g; + video_xr_pose_t aim, item, pose, before; + video_screen_xr_t s; + const float (*a)[3] = aims[j / 3]; + video_views_xr_reset(&s, 1); + s.yaw = 20.0f; + s.pitch = pitches[i]; + s.roll = rolls[j % 3]; + turn_aim(&aim, a[0][0], a[0][1], a[0][2]); + turn_grab(&g, &aim, s.yaw, s.pitch, s.roll); + video_xr_item_pose(&g.base, &s, &item); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + if (turn_apart(&item.orientation, &pose.orientation) > 0.001 + || !vec_is(&pose.position, item.position.x, + item.position.y, item.position.z)) + still = 0; + /* The trigger pressed with the hand moved, let go with it + * moved again. */ + turn_aim(&aim, a[1][0], a[1][1], a[1][2]); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &before, NULL, NULL); + carry_tilt(&g, &aim, true); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + if (turn_apart(&before.orientation, &pose.orientation) > 0.001) + still = 0; + turn_aim(&aim, a[2][0], a[2][1], a[2][2]); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &before, NULL, NULL); + carry_tilt(&g, &aim, false); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + if (turn_apart(&before.orientation, &pose.orientation) > 0.001 + || !vec_is(&pose.position, before.position.x, + before.position.y, before.position.z)) + still = 0; + } + CHECK(still); +} + +static void test_menu_handle(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_xr_quad_t menu, h, rim; + video_xr_vec3_t o, d; + video_xr_pose_t pose; + float u, v, t; + unsigned i; + + /* Under the menu's outline, 5 mm clear, 40% as wide, 3 cm tall. */ + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_xr_place(&p, &set); + CHECK(set.quads[3].kind == VIDEO_XR_QUAD_MENU); + video_xr_menu_handle(&set.quads[3], &h); + CHECK(pos_is(&h, 0.0, -0.5309, -1.7)); + CHECK(NEAR(h.width, 0.64) && NEAR(h.height, 0.03)); + CHECK(h.kind == VIDEO_XR_QUAD_MENU && h.slot == VIDEO_XR_MENU_SLOT); + CHECK(NEAR(h.pose.orientation.w, 1.0)); + + /* Its down is the menu's: rolled a quarter turn, towards +X. */ + memset(&menu, 0, sizeof(menu)); + menu.pose.orientation.z = (float)sin(M_PI / 4.0); + menu.pose.orientation.w = (float)cos(M_PI / 4.0); + menu.pose.position.x = 1.0f; + menu.pose.position.y = 2.0f; + menu.pose.position.z = 3.0f; + menu.width = 1.0f; + menu.height = 0.5f; + menu.kind = VIDEO_XR_QUAD_MENU; + video_xr_menu_handle(&menu, &h); + CHECK(pos_is(&h, 1.2886, 2.0, 3.0)); + CHECK(NEAR(h.width, 0.4) && NEAR(h.pose.orientation.z, sin(M_PI / 4.0))); + + /* Picked there, 10 cm along it, the point is the menu's; through + * the menu, nothing. */ + memset(&set, 0, sizeof(set)); + set.quads[0] = menu; + set.num_quads = 1; + o.x = 1.2886f; o.y = 2.1f; o.z = 4.0f; + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); + CHECK(NEAR(u, 0.6) && NEAR(v, 1.0773) && NEAR(t, 1.0)); + video_xr_cursor(&set.quads[0], u, v, t, &pose); + CHECK(NEAR(pose.position.x, 1.2886) && NEAR(pose.position.y, 2.1)); + o.x = 1.0f; o.y = 2.0f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == -1); + + /* A menu placed a quarter turn left: its handle 1.7 m to the left, + * 16 cm along it towards -Z, nearer than screen 1 behind. */ + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + p.anchor.orientation.y = (float)sin(M_PI / 4.0); + p.anchor.orientation.w = (float)cos(M_PI / 4.0); + video_xr_place(&p, &set); + o.x = o.y = o.z = 0.0f; + d.x = -1.7f; d.y = -0.5309f; d.z = -0.16f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 3); + CHECK(NEAR(u, 0.6) && NEAR(v, 1.0531) && NEAR(t, 1.0)); + + /* Its top 5 mm below the outline's outer edge, which grows with the + * menu. */ + for (i = 0; i < 3; i++) + { + memset(&menu, 0, sizeof(menu)); + video_xr_pose_identity(&menu.pose); + menu.width = 0.5f + (float)i; + menu.height = 0.3f + 0.6f * (float)i; + menu.kind = VIDEO_XR_QUAD_MENU; + video_xr_outline(&menu, &rim); + video_xr_menu_handle(&menu, &h); + CHECK(fabs(-rim.height * 0.5 - 0.005 + - (h.pose.position.y + h.height * 0.5)) < 1e-5); + } +} + +/* Edit mode takes the menu by its handle bar before any screen, even a + * nearer one; within VIDEO_XR_HANDLE_MARGIN of the bar only where the + * ray meets nothing else. */ +static void test_handle_first(void) +{ + video_xr_quad_set_t set; + video_xr_vec3_t o, d; + float u, v, t; + + /* The menu 1.6 x 0.96 at 1.7 m: its bar 0.64 x 0.03, centred 0.531 + * below its middle. A screen 1.28 x 0.96 at 1.5 m whose top edge is + * 7 mm under the bar's middle. */ + memset(&set, 0, sizeof(set)); + video_xr_pose_identity(&set.quads[0].pose); + set.quads[0].pose.position.y = -1.018f; + set.quads[0].pose.position.z = -1.5f; + set.quads[0].width = 1.28f; + set.quads[0].height = 0.96f; + set.quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set.quads[0].slot = 1; + video_xr_pose_identity(&set.quads[1].pose); + set.quads[1].pose.position.z = -1.7f; + set.quads[1].width = 1.6f; + set.quads[1].height = 0.96f; + set.quads[1].kind = VIDEO_XR_QUAD_MENU; + set.quads[1].slot = VIDEO_XR_MENU_SLOT; + set.num_quads = 2; + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + o.x = 0.0f; o.z = 0.0f; + + /* On the bar, through the nearer screen: the menu. */ + o.y = -0.541f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 1); + CHECK(NEAR(u, 0.5) && NEAR(t, 1.7) && NEAR(v, 0.5 + 0.541 / 0.96)); + /* 1.5 cm under the bar, in its margin over the screen: the screen. */ + o.y = -0.561f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); + CHECK(NEAR(t, 1.5)); + /* In the margin over nothing, 2.5 cm past the bar's end or 1.5 cm + * above it: the menu. */ + o.x = 0.345f; + o.y = -0.531f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 1); + CHECK(NEAR(u, 0.5 + 0.345 / 1.6)); + o.x = 0.0f; + o.y = -0.501f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 1); + CHECK(NEAR(v, 0.5 + 0.501 / 0.96) && NEAR(t, 1.7)); + /* Past the margin: the screen below, nothing beside. */ + o.x = 0.0f; + o.y = -0.586f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); + CHECK(NEAR(t, 1.5)); + o.x = 0.36f; + o.y = -0.531f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == -1); + /* Outside edit mode the bar is nothing: the screen. */ + o.x = 0.0f; + o.y = -0.541f; + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_ALWAYS, false, + &o, &d, &u, &v, &t) == 0); +} + +/* Edit mode's sticky pick: a hand whose laser was on the bar keeps the + * menu while the ray stays in the bar's 3 cm margin, even over the + * nearer screen; it lets go past the margin. Only the bar sticks: the + * margin over nothing takes the menu but does not stick, and arriving + * from the screen the screen keeps its whole area. */ +static void test_handle_sticky(void) +{ + video_xr_quad_set_t set; + video_xr_vec3_t o, d; + float u, v, t; + bool stick = false; + + memset(&set, 0, sizeof(set)); + video_xr_pose_identity(&set.quads[0].pose); + set.quads[0].pose.position.y = -1.018f; + set.quads[0].pose.position.z = -1.5f; + set.quads[0].width = 1.28f; + set.quads[0].height = 0.96f; + set.quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set.quads[0].slot = 1; + video_xr_pose_identity(&set.quads[1].pose); + set.quads[1].pose.position.z = -1.7f; + set.quads[1].width = 1.6f; + set.quads[1].height = 0.96f; + set.quads[1].kind = VIDEO_XR_QUAD_MENU; + set.quads[1].slot = VIDEO_XR_MENU_SLOT; + set.num_quads = 2; + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + o.x = 0.0f; o.z = 0.0f; + + /* On the bar: the menu, and it sticks. */ + o.y = -0.541f; + CHECK(video_xr_pick_edit(&set, false, &o, &d, &u, &v, &t, &stick) == 1 + && stick); + /* Stuck, 1.5 cm under the bar over the screen: still the menu. */ + o.y = -0.561f; + CHECK(video_xr_pick_edit(&set, true, &o, &d, &u, &v, &t, &stick) == 1 + && stick); + CHECK(NEAR(t, 1.7) && NEAR(v, 0.5 + 0.561 / 0.96) && NEAR(u, 0.5)); + /* Not stuck there: the screen, as video_xr_pick. */ + CHECK(video_xr_pick_edit(&set, false, &o, &d, &u, &v, &t, &stick) == 0 + && !stick && NEAR(t, 1.5)); + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, &o, &d, &u, &v, + &t) == 0); + /* Stuck, past the margin: the screen, unstuck. */ + o.y = -0.586f; + CHECK(video_xr_pick_edit(&set, true, &o, &d, &u, &v, &t, &stick) == 0 + && !stick); + /* Stuck, beside the bar past its margin, over nothing: nothing. */ + o.x = 0.36f; + o.y = -0.531f; + CHECK(video_xr_pick_edit(&set, true, &o, &d, &u, &v, &t, &stick) == -1 + && !stick); + /* In the margin over nothing: the menu, but it does not stick. */ + o.x = 0.345f; + CHECK(video_xr_pick_edit(&set, false, &o, &d, &u, &v, &t, &stick) == 1 + && !stick); + /* Stuck with the menu gone: the screen, unstuck. */ + o.x = 0.0f; + o.y = -0.561f; + set.num_quads = 1; + CHECK(video_xr_pick_edit(&set, true, &o, &d, &u, &v, &t, &stick) == 0 + && !stick); + /* stick may be NULL. */ + set.num_quads = 2; + CHECK(video_xr_pick_edit(&set, true, &o, &d, &u, &v, &t, NULL) == 1); +} + +static void test_live_edit(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_xr_vec3_t o, d; + video_xr_pose_t pose; + float u, v, t; + unsigned i; + + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_xr_place(&p, &set); + /* Every item, screen 0 and the closed menu too. */ + for (i = 0; i < set.num_quads; i++) + CHECK(video_xr_quad_live(&set.quads[i], VIDEO_XR_LIVE_EDIT, false)); + o.x = o.y = o.z = 0.0f; + /* Straight ahead the menu covers screen 0: through it, screen 0. */ + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); + /* Its handle, nearer than screen 1 behind it, takes the menu; the + * point is in the menu's frame, so the dot lands on the handle. */ + d.x = 0.0f; d.y = -0.5309f; d.z = -1.7f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 3); + CHECK(NEAR(u, 0.5) && NEAR(v, 1.0531) && NEAR(t, 1.0)); + video_xr_cursor(&set.quads[3], u, v, t, &pose); + CHECK(NEAR(pose.position.x, 0.0) && NEAR(pose.position.y, -0.5309)); + /* The handle is edit mode's alone. */ + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_AUTO, true, + &o, &d, &u, &v, &t) == 2); + /* Through the gap between the menu and its handle, in the bar's + * margin and just above it: what is behind, screen 1. */ + o.y = 1.455f; + d.x = 0.0f; d.y = -1.15f; d.z = -1.0f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 2); + o.y = 1.472f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 2); + /* From the head, screen 1's top middle, 3.2 cm under screen 0: above + * and below the bar, in its margin, screen 1; on the bar, the + * menu. */ + o.y = 0.0f; + d.x = 0.0f; d.y = -0.53f; d.z = -1.8f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 2); + CHECK(NEAR(t, 1.0)); + d.y = -0.595f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 2); + d.y = -0.56f; + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 3); + /* Other lasers still see the whole menu. */ + o.y = 0.0f; + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_AUTO, true, + &o, &d, &u, &v, &t) == 3); + + /* A whole frame and a window are items too. */ + params_init(&p, NULL); + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + video_xr_place(&p, &set); + CHECK(video_xr_quad_live(&set.quads[0], VIDEO_XR_LIVE_EDIT, false)); + p.window = true; + video_xr_place(&p, &set); + CHECK(video_xr_quad_live(&set.quads[0], VIDEO_XR_LIVE_EDIT, false)); +} + +/* A frame shown whole without the screens' settings, a views core's + * fallback, is no item for edit mode, which passes it; other lasers + * still meet it. A no-map frame with the settings, screens and the + * menu are items. */ +static void test_edit_fixed(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_xr_vec3_t o, d; + float u, v, t; + unsigned s; + + o.x = o.y = o.z = 0.0f; + d.x = 0.0f; d.y = 0.0f; d.z = -1.0f; + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + params_init(&p, NULL); + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_xr_place(&p, &set); + CHECK(set.num_quads == 2 && set.slots[0].fixed + && !set.slots[VIDEO_XR_MENU_SLOT].fixed); + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == -1); + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_ALWAYS, false, + &o, &d, &u, &v, &t) == 0); + /* gl's window, shown whole while the menu is open or the eyes are + * tracked, likewise. */ + p.window = true; + video_xr_place(&p, &set); + CHECK(set.slots[0].fixed); + p.window = false; + p.screens = screens; + video_xr_place(&p, &set); + CHECK(!set.slots[0].fixed); + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); + + map_3ds(&map); + params_init(&p, &map); + p.screens = screens; + video_xr_place(&p, &set); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + CHECK(!set.slots[s].fixed); + CHECK(video_xr_pick(&set, VIDEO_XR_LIVE_EDIT, false, + &o, &d, &u, &v, &t) == 0); +} + +/* A frame without a map takes screen 1's place, turn and size; gl's + * window too while the menu is closed, but not with it open over it. */ +static void test_frame_as_screen(void) +{ + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_views_rect_t rects[VIDEO_XR_MAX_SLOTS][2]; + video_viewport_t vp; + video_xr_vec3_t axis, result; + + params_init(&p, NULL); + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + p.frame_aspect = 800.0f / 480.0f; + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + screens[0].x = 0.25f; + screens[0].y = 0.1f; + screens[0].z = 0.3f; + screens[0].yaw = 90.0f; + screens[0].scale = 1.5f; + screens[1].x = 5.0f; + p.screens = screens; + video_xr_place(&p, &set); + CHECK(set.num_quads == 1 && set.quads[0].kind == VIDEO_XR_QUAD_FRAME); + CHECK(pos_is(&set.quads[0], 0.25, 0.1, -1.5)); + CHECK(NEAR(set.quads[0].width, 2.4) && NEAR(set.quads[0].height, 1.44)); + CHECK(dims_is(set.slots[0].dims, 1335, 801)); + axis.x = axis.y = 0.0f; + axis.z = 1.0f; + video_xr_rotate(&set.quads[0].pose.orientation, &axis, &result); + CHECK(NEAR(result.x, 1.0) && NEAR(result.z, 0.0)); + /* Clamped to the setting's range. */ + screens[0].scale = 20.0f; + video_xr_place(&p, &set); + CHECK(NEAR(set.quads[0].width, 16.0)); + screens[0].scale = 1.5f; + + p.window = true; + memset(&vp, 0, sizeof(vp)); + vp.dims = vp.full_dims = VIDEO_SCALE_PACK(800, 480); + video_xr_place_window(&p, false, false, &vp, vp.dims, &set, rects); + CHECK(set.num_quads == 1 && set.quads[0].kind == VIDEO_XR_QUAD_WINDOW); + CHECK(pos_is(&set.quads[0], 0.25, 0.1, -1.5)); + video_xr_place_window(&p, true, false, &vp, vp.dims, &set, rects); + CHECK(pos_is(&set.quads[0], 0.0, 0.0, -1.8)); + CHECK(NEAR(set.quads[0].width, 1.6)); + screens[0].visible = false; + video_xr_place_window(&p, true, false, &vp, vp.dims, &set, rects); + CHECK(set.num_quads == 1); + p.window = false; + video_xr_place(&p, &set); + CHECK(set.num_quads == 0); +} + +/* By default the menu at its spot, wherever screen 1 is, as before + * screens moved. With menu_follow: 10 cm in front of screen 1 and + * facing as it does, after the screens, as large to the eye as at its + * spot and no nearer than 0.4 m; at its own place once placed; at its + * spot when screen 1 does not float or show, or would leave it facing + * away. */ +static void test_menu_spot(void) +{ + video_views_map_t map; + video_xr_params_t p; + video_xr_quad_set_t set; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + video_screen_xr_t menu; + const video_xr_quad_t *m; + + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + p.screens = screens; + screens[0].z = 0.6f; + screens[0].yaw = 30.0f; + video_xr_place(&p, &set); + CHECK(set.num_quads == 4); + m = &set.quads[set.num_quads - 1]; + CHECK(m->kind == VIDEO_XR_QUAD_MENU && pos_is(m, 0.0, 0.0, -1.7)); + CHECK(NEAR(m->pose.orientation.w, 1.0)); + CHECK(NEAR(m->width, 1.6) && NEAR(m->height, 0.96)); + + p.menu_follow = true; + video_xr_place(&p, &set); + CHECK(set.num_quads == 4); + m = &set.quads[set.num_quads - 1]; + CHECK(m->kind == VIDEO_XR_QUAD_MENU); + CHECK(pos_is(m, 0.05, 0.0, -1.11340)); + CHECK(NEAR(m->pose.orientation.y, 0.25882) && NEAR(m->pose.orientation.w, 0.96593)); + /* 1.1145 m away: 1.6 x 1.1145 / 1.7 wide. */ + CHECK(NEAR(m->width, 1.04896) && NEAR(m->height, 0.62938)); + CHECK(vec_is(&set.slots[VIDEO_XR_MENU_SLOT].base, 0.0, 0.0, -1.7)); + /* Its image is the one at its spot. */ + p.px_per_rad = 200.0f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[VIDEO_XR_MENU_SLOT].dims, 352, 211)); + + /* Far and enlarged: as wide to the eye, the same image. */ + screens[0].z = -8.0f; + screens[0].yaw = 0.0f; + screens[0].scale = 5.0f; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(pos_is(m, 0.0, 0.0, -9.7)); + CHECK(NEAR(m->width, 9.12941) && NEAR(m->height, 5.47765)); + CHECK(dims_is(set.slots[VIDEO_XR_MENU_SLOT].dims, 352, 211)); + p.px_per_rad = 1000.0f; + + /* Pulled to the eyes: 0.4 m out along the line from the anchor, or + * straight ahead when at it; behind them, facing away, at its spot. */ + screens[0].scale = 1.0f; + screens[0].z = 1.6f; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(pos_is(m, 0.0, 0.0, -0.4)); + CHECK(NEAR(m->width, 0.37647) && NEAR(m->height, 0.22588)); + screens[0].z = 1.7f; + video_xr_place(&p, &set); + CHECK(pos_is(&set.quads[set.num_quads - 1], 0.0, 0.0, -0.4)); + screens[0].z = 2.0f; + video_xr_place(&p, &set); + CHECK(pos_is(&set.quads[set.num_quads - 1], 0.0, 0.0, -1.7)); + screens[0].z = 2.5f; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(pos_is(m, 0.0, 0.0, -1.7)); + CHECK(NEAR(m->width, 1.6) && NEAR(m->height, 0.96)); + CHECK(NEAR(m->pose.orientation.w, 1.0)); + /* Beside the eyes: followed while it faces them (yaw -90 turns the + * face to -X), at its spot when turned away. */ + screens[0].x = 3.0f; + screens[0].z = 1.8f; + screens[0].yaw = -90.0f; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(pos_is(m, 2.9, 0.0, 0.0)); + CHECK(NEAR(m->width, 2.72941)); + screens[0].yaw = 90.0f; + video_xr_place(&p, &set); + CHECK(pos_is(&set.quads[set.num_quads - 1], 0.0, 0.0, -1.7)); + screens[0].x = 0.0f; + screens[0].yaw = 0.0f; + screens[0].z = 1.25f; + video_xr_place(&p, &set); + CHECK(pos_is(&set.quads[set.num_quads - 1], 0.0, 0.0, -0.45)); + screens[0].z = 0.6f; + + /* Placed: from its own spot. */ + video_views_xr_reset(&menu, 1); + menu.x = 0.5f; + menu.z = 0.2f; + menu.scale = 0.5f; + p.menu = &menu; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(m->kind == VIDEO_XR_QUAD_MENU && pos_is(m, 0.5, 0.0, -1.5)); + CHECK(NEAR(m->width, 0.8) && NEAR(m->height, 0.48)); + /* Clamped to the setting's range. */ + menu.scale = 20.0f; + video_xr_place(&p, &set); + CHECK(NEAR(set.quads[set.num_quads - 1].width, 16.0)); + p.menu = NULL; + + /* Screen 1 hidden or the projection's: the menu at its spot. */ + screens[0].visible = false; + screens[0].x = 3.0f; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(m->kind == VIDEO_XR_QUAD_MENU && pos_is(m, 0.0, 0.0, -1.7)); + screens[0].visible = true; + p.skip_screens = 1u; + video_xr_place(&p, &set); + m = &set.quads[set.num_quads - 1]; + CHECK(m->kind == VIDEO_XR_QUAD_MENU && pos_is(m, 0.0, 0.0, -1.7)); + p.skip_screens = 0; + + /* A whole frame is screen 1: the menu goes with it. */ + params_init(&p, NULL); + p.menu_follow = true; + p.frame_dims = VIDEO_SCALE_PACK(800, 480); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + screens[0].z = 0.6f; + p.screens = screens; + video_xr_place(&p, &set); + CHECK(set.num_quads == 2 && set.quads[1].kind == VIDEO_XR_QUAD_MENU); + CHECK(pos_is(&set.quads[1], 0.0, 0.0, -1.1)); + CHECK(NEAR(set.quads[1].width, 1.03529)); + /* Its image to the pixel where sizing it from the moved size would + * round up one more. */ + p.px_per_rad = 355.240692f; + screens[0].z = -8.0f; + screens[0].yaw = -23.0f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[VIDEO_XR_MENU_SLOT].dims, 625, 375)); +} + +/* Every slot of a plan carries the edit count it was planned at, and a + * plan is the same bytes whatever was under it: a driver that compares + * plans byte for byte sees no change that is not one. */ +static void test_edit_seq(void) +{ + static video_xr_quad_set_t a, b; + video_views_map_t map; + video_xr_params_t p; + unsigned s; + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + p.edit_seq = 7; + memset(&a, 0x00, sizeof(a)); + memset(&b, 0xa5, sizeof(b)); + video_xr_place(&p, &a); + video_xr_place(&p, &b); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + CHECK(a.slots[s].edit_seq == 7); + CHECK(!memcmp(&a, &b, sizeof(a))); +} + +/* While editing, a slot keeps its image for a plan within 0.8 to 1.25 + * times it on both axes; out of edit mode every plan is as planned. */ +static void test_keep_dims(void) +{ + static video_xr_quad_set_t plan, set; + video_views_map_t map; + video_xr_params_t p; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + unsigned s, w, h, cur0; + unsigned cur = VIDEO_SCALE_PACK(800, 480); + + for (w = 1; w <= 1300; w += 7) + for (h = 1; h <= 800; h += 5) + { + unsigned d = VIDEO_SCALE_PACK(w, h); + CHECK(video_xr_keep_dims(d, cur, false) == d); + CHECK(video_xr_keep_dims(d, 0, false) == d); + } + CHECK(video_xr_keep_dims(0, cur, false) == 0); + + /* The band's edges, each axis on its own. */ + CHECK(video_xr_keep_dims(VIDEO_SCALE_PACK(640, 384), cur, true) == cur); + CHECK(video_xr_keep_dims(VIDEO_SCALE_PACK(1000, 600), cur, true) == cur); + CHECK(video_xr_keep_dims(VIDEO_SCALE_PACK(801, 479), cur, true) == cur); + CHECK(dims_is(video_xr_keep_dims(VIDEO_SCALE_PACK(639, 480), cur, true), + 639, 480)); + CHECK(dims_is(video_xr_keep_dims(VIDEO_SCALE_PACK(1001, 480), cur, true), + 1001, 480)); + CHECK(dims_is(video_xr_keep_dims(VIDEO_SCALE_PACK(800, 383), cur, true), + 800, 383)); + CHECK(dims_is(video_xr_keep_dims(VIDEO_SCALE_PACK(800, 601), cur, true), + 800, 601)); + /* No image yet; a slot planned empty stays empty. */ + CHECK(dims_is(video_xr_keep_dims(VIDEO_SCALE_PACK(700, 420), 0, true), + 700, 420)); + CHECK(video_xr_keep_dims(0, cur, true) == 0); + + /* Screen 1 20 cm nearer keeps its image while editing; twice as near + * is past the band. Out of edit mode the plan's bytes are its own. */ + map_3ds(&map); + params_init(&p, &map); + p.ui_dims = VIDEO_SCALE_PACK(1600, 960); + video_views_xr_reset(screens, RETRO_VIDEO_VIEWS_MAX); + p.screens = screens; + video_xr_place(&p, &plan); + cur0 = plan.slots[0].dims; + CHECK(dims_is(cur0, 837, 502)); + screens[0].z = 0.2f; + video_xr_place(&p, &set); + CHECK(dims_is(set.slots[0].dims, 928, 557)); + CHECK(video_xr_keep_dims(set.slots[0].dims, cur0, true) == cur0); + screens[0].z = 1.0f; + video_xr_place(&p, &set); + CHECK(video_xr_keep_dims(set.slots[0].dims, cur0, true) + == set.slots[0].dims && set.slots[0].dims != cur0); + memcpy(&set, &plan, sizeof(set)); + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + set.slots[s].dims = video_xr_keep_dims(set.slots[s].dims, + VIDEO_SCALE_PACK(VIDEO_SCALE_W(set.slots[s].dims) + 1, + VIDEO_SCALE_H(set.slots[s].dims) + 1), false); + CHECK(!memcmp(&plan, &set, sizeof(set))); +} + static void test_image_dims(void) { /* 1.6 m at 1.8 m spans 0.8364 rad: 837 px at 1000 px/rad. */ @@ -871,21 +2747,430 @@ static void test_cursor(void) CHECK(NEAR(pose.orientation.y, 0.70711)); } +/* One of a pose's own axes, in its space. */ +static void pose_axis(const video_xr_pose_t *pose, float x, float y, + float z, video_xr_vec3_t *out) +{ + video_xr_vec3_t v; + v.x = x; + v.y = y; + v.z = z; + video_xr_rotate(&pose->orientation, &v, out); +} + +/* The laser's beam: centred on its ray, its x down the ray, turned + * about it so its face is towards the eye; seen end on, still square to + * the ray. */ +static void test_beam(void) +{ + video_xr_vec3_t o, d, eye, ax, ay, az; + video_xr_pose_t pose; + const video_xr_quat_t *q = &pose.orientation; + + o.x = 0.2f; + o.y = -0.3f; + o.z = 0.0f; + d.x = 0.0f; + d.y = 0.0f; + d.z = -1.0f; + eye.x = 0.0f; + eye.y = 0.0f; + eye.z = 0.0f; + video_xr_beam(&o, &d, 1.8f, &eye, &pose); + pose_axis(&pose, 1.0f, 0.0f, 0.0f, &ax); + pose_axis(&pose, 0.0f, 1.0f, 0.0f, &ay); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + CHECK(vec_is(&pose.position, 0.2, -0.3, -0.9)); + CHECK(vec_is(&ax, 0.0, 0.0, -1.0)); + CHECK(vec_is(&az, -0.5547, 0.8321, 0.0)); + CHECK(vec_is(&ay, -0.8321, -0.5547, 0.0)); + CHECK(NEAR(q->x * q->x + q->y * q->y + q->z * q->z + q->w * q->w, 1.0)); + + /* A ray down and to the right, the eye above it. */ + d.x = 0.6f; + d.z = -0.8f; + eye.y = 1.0f; + video_xr_beam(&o, &d, 1.0f, &eye, &pose); + pose_axis(&pose, 1.0f, 0.0f, 0.0f, &ax); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + CHECK(vec_is(&pose.position, 0.5, -0.3, -0.4)); + CHECK(vec_is(&ax, 0.6, 0.0, -0.8)); + CHECK(vec_is(&az, -0.0977, 0.9925, -0.0733)); + + /* The eye on the ray's line: any face square to the ray. */ + o.x = 0.0f; + o.y = 0.0f; + d.x = 0.0f; + d.z = -1.0f; + eye.y = 0.0f; + eye.z = 0.5f; + video_xr_beam(&o, &d, 1.5f, &eye, &pose); + pose_axis(&pose, 1.0f, 0.0f, 0.0f, &ax); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + CHECK(vec_is(&ax, 0.0, 0.0, -1.0)); + CHECK(vec_is(&az, 0.0, 1.0, 0.0)); + d.y = 1.0f; + d.z = 0.0f; + eye.y = -1.0f; + eye.z = 0.0f; + video_xr_beam(&o, &d, 1.0f, &eye, &pose); + pose_axis(&pose, 1.0f, 0.0f, 0.0f, &ax); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + CHECK(vec_is(&ax, 0.0, 1.0, 0.0)); + CHECK(vec_is(&az, 0.0, 0.0, 1.0)); +} + +static uint8_t t_atlas[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + +static const uint8_t *texel(unsigned x, unsigned y) +{ + return t_atlas + ((size_t)y * VIDEO_XR_CURSOR_W + x) * 4; +} + +/* Whether every texel of column x is clear. */ +static int column_clear(unsigned x) +{ + unsigned y; + for (y = 0; y < VIDEO_XR_CURSOR_H; y++) + if (texel(x, y)[3]) + return 0; + return 1; +} + +/* The laser's image: the dot; edit mode's faint and bright outlines, + * each rim three bands, dark, light, dark, clear inside; the menu's + * faint and bright handle bars, the same bands across; the beam's + * strips; a clear column between each. */ static void test_cursor_pixels(void) { - static uint8_t px[VIDEO_XR_CURSOR_DIM * VIDEO_XR_CURSOR_DIM * 4]; - const uint8_t *centre = px + ((size_t)(VIDEO_XR_CURSOR_DIM / 2) - * VIDEO_XR_CURSOR_DIM + VIDEO_XR_CURSOR_DIM / 2) * 4; - const uint8_t *rim = px + ((size_t)2 * VIDEO_XR_CURSOR_DIM - + VIDEO_XR_CURSOR_DIM / 2) * 4; + const unsigned mid = VIDEO_XR_OUTLINE_DIM / 2; + const unsigned b = VIDEO_XR_OUTLINE_BAND; + const unsigned hx = VIDEO_XR_HANDLE_COLS / 2; + const uint8_t *centre = texel(VIDEO_XR_CURSOR_DIM / 2, + VIDEO_XR_CURSOR_Y + VIDEO_XR_CURSOR_DIM / 2); + const uint8_t *rim = texel(VIDEO_XR_CURSOR_DIM / 2, + VIDEO_XR_CURSOR_Y + 2); + const uint8_t *corner = texel(0, VIDEO_XR_CURSOR_Y); + const uint8_t *hit_in = texel(VIDEO_XR_BEAM_X(0), VIDEO_XR_BEAM_Y + 1); + const uint8_t *hit_mid = texel(VIDEO_XR_BEAM_X(0) + + VIDEO_XR_BEAM_LEN / 2, VIDEO_XR_BEAM_Y + 1); + const uint8_t *hit_end = texel(VIDEO_XR_BEAM_X(0) + + VIDEO_XR_BEAM_LEN - 1, VIDEO_XR_BEAM_Y + VIDEO_XR_BEAM_ROWS - 1); + const uint8_t *miss_in = texel(VIDEO_XR_BEAM_X(1), VIDEO_XR_BEAM_Y); + const uint8_t *miss_mid = texel(VIDEO_XR_BEAM_X(1) + + VIDEO_XR_BEAM_LEN / 2, VIDEO_XR_BEAM_Y + 2); + const uint8_t *miss_end = texel(VIDEO_XR_BEAM_X(1) + + VIDEO_XR_BEAM_LEN - 1, VIDEO_XR_BEAM_Y + 1); + unsigned k; - video_xr_cursor_pixels(px); - /* White in the middle, a dark rim, nothing in the corners; grey, so - * RGBA and BGRA are the same bytes. */ + memset(t_atlas, 0x7f, sizeof(t_atlas)); + video_xr_cursor_pixels(t_atlas); + /* The dot: white in the middle, a dark rim, nothing in the corners; + * grey, so RGBA and BGRA are the same bytes. */ CHECK(centre[0] == 255 && centre[1] == 255 && centre[2] == 255 && centre[3] == 255); CHECK(rim[0] == 38 && rim[1] == 38 && rim[2] == 38 && rim[3] == 255); - CHECK(!px[0] && !px[1] && !px[2] && !px[3]); + CHECK(!corner[0] && !corner[1] && !corner[2] && !corner[3]); + CHECK(!texel(4, 4)[3]); + /* Mid-height: the same rows counted from either end. */ + CHECK(VIDEO_XR_CURSOR_Y + VIDEO_XR_CURSOR_DIM + VIDEO_XR_CURSOR_Y + == VIDEO_XR_CURSOR_H); + CHECK(VIDEO_XR_HANDLE_Y * 2 + VIDEO_XR_HANDLE_ROWS == VIDEO_XR_CURSOR_H); + /* Two texels a band at least, so filtering keeps three. */ + CHECK(b >= 2 && VIDEO_XR_OUTLINE_EDGE == 3 * b); + for (k = 0; k < 2; k++) + { + unsigned x0 = VIDEO_XR_OUTLINE_X(k); + unsigned h0 = VIDEO_XR_HANDLE_X(k); + uint8_t a = k ? 255 : VIDEO_XR_OUTLINE_FAINT; + const uint8_t *top_dark = texel(x0 + mid, 0); + const uint8_t *top_light = texel(x0 + mid, b); + const uint8_t *top_inner = texel(x0 + mid, 2 * b); + const uint8_t *inside = texel(x0 + mid, 3 * b); + const uint8_t *left = texel(x0, mid); + const uint8_t *left_lit = texel(x0 + b + 1, mid); + const uint8_t *right = texel(x0 + VIDEO_XR_OUTLINE_DIM - 1, mid); + const uint8_t *bottom = texel(x0 + mid, VIDEO_XR_OUTLINE_DIM - 1); + const uint8_t *bot_light = texel(x0 + mid, + VIDEO_XR_OUTLINE_DIM - 1 - b); + const uint8_t *bar_dark = texel(h0 + hx, VIDEO_XR_HANDLE_Y); + const uint8_t *bar_light = texel(h0 + hx, VIDEO_XR_HANDLE_Y + + VIDEO_XR_HANDLE_ROWS / 2); + const uint8_t *bar_low = texel(h0 + hx, VIDEO_XR_HANDLE_Y + + VIDEO_XR_HANDLE_ROWS - 1); + const uint8_t *bar_end = texel(h0, VIDEO_XR_HANDLE_Y + + VIDEO_XR_HANDLE_ROWS / 2); + CHECK(!top_dark[0] && top_dark[3] == a); + CHECK(top_light[0] == a && top_light[2] == a && top_light[3] == a); + CHECK(!top_inner[0] && top_inner[3] == a); + CHECK(!inside[0] && !inside[3]); + CHECK(!left[0] && left[3] == a); + CHECK(left_lit[0] == a && left_lit[3] == a); + CHECK(!right[0] && right[3] == a); + CHECK(!bottom[0] && bottom[3] == a); + CHECK(bot_light[0] == a && bot_light[3] == a); + CHECK(!bar_dark[0] && bar_dark[3] == a); + CHECK(bar_light[0] == a && bar_light[3] == a); + CHECK(!bar_low[0] && bar_low[3] == a); + CHECK(!bar_end[0] && bar_end[3] == a); + CHECK(!texel(h0 + hx, VIDEO_XR_HANDLE_Y - 1)[3]); + CHECK(!texel(h0 + hx, VIDEO_XR_HANDLE_Y + VIDEO_XR_HANDLE_ROWS)[3]); + } + /* Filtering at a part's edge meets a clear texel, not the next. */ + CHECK(column_clear(VIDEO_XR_CURSOR_DIM)); + CHECK(column_clear(VIDEO_XR_OUTLINE_X(1) - 1)); + CHECK(column_clear(VIDEO_XR_HANDLE_X(0) - 1)); + CHECK(column_clear(VIDEO_XR_HANDLE_X(1) - 1)); + CHECK(column_clear(VIDEO_XR_BEAM_X(0) - 1)); + CHECK(column_clear(VIDEO_XR_BEAM_X(1) - 1)); + CHECK(VIDEO_XR_BEAM_X(1) + VIDEO_XR_BEAM_LEN + 1 == VIDEO_XR_BACK_X); + /* The beam's strips: white, faded in at the hand; the one into + * nothing faded out at its far end too; mid-height, nothing above or + * below them. */ + CHECK(hit_mid[0] == VIDEO_XR_BEAM_LEVEL && hit_mid[2] == VIDEO_XR_BEAM_LEVEL + && hit_mid[3] == VIDEO_XR_BEAM_LEVEL); + CHECK(hit_end[3] == VIDEO_XR_BEAM_LEVEL); + CHECK(hit_in[3] > 0 && hit_in[3] < VIDEO_XR_BEAM_LEVEL / 8 + && hit_in[0] == hit_in[3]); + CHECK(miss_mid[3] == VIDEO_XR_BEAM_LEVEL); + CHECK(miss_in[3] == hit_in[3]); + CHECK(miss_end[3] > 0 && miss_end[3] < VIDEO_XR_BEAM_LEVEL / 32); + CHECK(VIDEO_XR_BEAM_Y * 2 + VIDEO_XR_BEAM_ROWS == VIDEO_XR_CURSOR_H); + CHECK(!texel(VIDEO_XR_BEAM_X(0) + 64, VIDEO_XR_BEAM_Y - 1)[3]); + CHECK(!texel(VIDEO_XR_BEAM_X(1) + 64, VIDEO_XR_BEAM_Y + + VIDEO_XR_BEAM_ROWS)[3]); +} + +/* Edit mode's tags: a cell for each item it can choose after the + * hinge's strip, each centred on the middle row with a clear column + * between, empty until text goes in. Text is light over a dark halo two + * texels wide, at the tag's faint level; bottom up, its rows mirror + * within the cell. */ +static void test_tag_pixels(void) +{ + static uint8_t cov[VIDEO_XR_TAG_COLS * VIDEO_XR_TAG_ROWS]; + const unsigned x0 = VIDEO_XR_TAG_X(3); + const unsigned y0 = VIDEO_XR_TAG_Y; + const unsigned r = VIDEO_XR_TAG_ROWS - 1; + const uint8_t f = VIDEO_XR_TAG_FAINT; + unsigned x, y; + int clear = 1; + CHECK(VIDEO_XR_TAGS == VIDEO_XR_MENU_SLOT + 2); + CHECK(VIDEO_XR_TAG_X(0) == VIDEO_XR_HINGE_X + VIDEO_XR_HINGE_COLS + 1); + CHECK(VIDEO_XR_TAG_X(VIDEO_XR_TAGS - 1) + VIDEO_XR_TAG_COLS + == VIDEO_XR_CURSOR_TAGGED_W); + CHECK(2 * VIDEO_XR_TAG_Y + VIDEO_XR_TAG_ROWS == VIDEO_XR_CURSOR_H); + video_xr_cursor_pixels(t_atlas); + for (x = VIDEO_XR_TAG_X(0) - 1; x < VIDEO_XR_CURSOR_W; x++) + clear &= column_clear(x); + CHECK(clear); + /* A 4 x 2 block, its top left at 60, 9: off the middle, so the rows + * bottom up differ. */ + memset(cov, 0, sizeof(cov)); + for (y = 9; y < 11; y++) + for (x = 60; x < 64; x++) + cov[y * VIDEO_XR_TAG_COLS + x] = 255; + video_xr_tag_pixels(t_atlas, 3, cov, false); + CHECK(texel(x0 + 61, y0 + 10)[0] == f && texel(x0 + 61, y0 + 10)[3] == f); + CHECK(texel(x0 + 58, y0 + 10)[0] == 0 && texel(x0 + 58, y0 + 10)[3] == f); + CHECK(texel(x0 + 57, y0 + 10)[3] == 0); + CHECK(texel(x0 + 61, y0 + 7)[0] == 0 && texel(x0 + 61, y0 + 7)[3] == f); + CHECK(texel(x0 + 61, y0 + 6)[3] == 0); + CHECK(column_clear(x0 - 1) && column_clear(x0 + VIDEO_XR_TAG_COLS)); + /* Bottom up: row y at r - y. */ + video_xr_tag_pixels(t_atlas, 3, cov, true); + CHECK(texel(x0 + 61, y0 + r - 10)[0] == f + && texel(x0 + 61, y0 + r - 7)[0] == 0 + && texel(x0 + 61, y0 + r - 7)[3] == f + && texel(x0 + 61, y0 + r - 6)[3] == 0 + && texel(x0 + 61, y0 + 10)[3] == 0); +} + +/* Edit mode's cut outlines after the tags, a clear column before each: + * for each cut, faint and bright, the whole outline's texels but those + * VIDEO_XR_OUTLINE_CROP in from each cut edge, which are clear, its + * corners included; bottom up, the top's and the bottom's rows swap. */ +static void test_cut_pixels(void) +{ + static const unsigned cuts[VIDEO_XR_OUTLINE_CUTS] = { + 1u, 2u, 4u, 8u, 3u, 12u }; + const unsigned d = VIDEO_XR_OUTLINE_DIM; + const unsigned e = VIDEO_XR_OUTLINE_CROP; + unsigned up, k, c, x, y; + CHECK(VIDEO_XR_CURSOR_W == 5582 && VIDEO_XR_CURSOR_TAGGED_W == 2498 + && VIDEO_XR_CURSOR_UNTAGGED_W == 1208); + CHECK(VIDEO_XR_CUT_X(0, 0) == VIDEO_XR_CURSOR_TAGGED_W + 1); + CHECK(VIDEO_XR_CUT_X(VIDEO_XR_OUTLINE_CUTS - 1, 1) + d + == VIDEO_XR_CURSOR_W); + for (up = 0; up < 2; up++) + { + unsigned bad = 0; + memset(t_atlas, 0x7f, sizeof(t_atlas)); + video_xr_cursor_pixels(t_atlas); + video_xr_cut_pixels(t_atlas, up != 0); + for (k = 0; k < 2; k++) + for (c = 0; c < VIDEO_XR_OUTLINE_CUTS; c++) + { + unsigned x0 = VIDEO_XR_CUT_X(c, k); + unsigned cut = cuts[c]; + if (up) + cut = (cut & 12u) | ((cut & 1u) << 1) | ((cut >> 1) & 1u); + bad += !column_clear(x0 - 1); + for (y = 0; y < d; y++) + for (x = 0; x < d; x++) + { + const uint8_t *t = texel(x0 + x, y); + const uint8_t *whole = texel(VIDEO_XR_OUTLINE_X(k) + x, y); + int gone = ((cut & 1u) && y < e) + || ((cut & 2u) && y >= d - e) + || ((cut & 4u) && x >= d - e) + || ((cut & 8u) && x < e); + bad += gone ? (t[3] != 0) : (memcmp(t, whole, 4) != 0); + } + } + CHECK(!bad); + } +} + +/* A screen's back: a solid cell after the beam strips, before the + * tags, so a runtime too narrow for the tags keeps it; centred on the + * middle row, the square a back shows kept its margin inside, so + * filtering takes in nothing else. Opaque dark grey. */ +static void test_back_pixels(void) +{ + const unsigned x0 = VIDEO_XR_BACK_X; + const unsigned y0 = VIDEO_XR_BACK_Y; + const uint8_t l = VIDEO_XR_BACK_LEVEL; + unsigned x, y; + int solid = 1; + CHECK(VIDEO_XR_BACK_X == VIDEO_XR_BEAM_X(1) + VIDEO_XR_BEAM_LEN + 1); + CHECK(VIDEO_XR_BACK_CELL + == VIDEO_XR_BACK_DIM + 2 * VIDEO_XR_BACK_MARGIN); + CHECK(VIDEO_XR_BACK_MARGIN >= 2); + CHECK(VIDEO_XR_HINGE_X == x0 + VIDEO_XR_BACK_CELL + 1); + CHECK(2 * VIDEO_XR_BACK_Y + VIDEO_XR_BACK_CELL == VIDEO_XR_CURSOR_H); + CHECK(l > 0 && l < 64); + video_xr_cursor_pixels(t_atlas); + for (y = y0; y < y0 + VIDEO_XR_BACK_CELL; y++) + for (x = x0; x < x0 + VIDEO_XR_BACK_CELL; x++) + solid &= texel(x, y)[0] == l && texel(x, y)[1] == l + && texel(x, y)[2] == l && texel(x, y)[3] == 255; + CHECK(solid); + CHECK(column_clear(x0 - 1) && column_clear(x0 + VIDEO_XR_BACK_CELL)); + CHECK(!texel(x0, y0 - 1)[3] && !texel(x0, y0 + VIDEO_XR_BACK_CELL)[3]); +} + +/* A hinge's line: a uniform green strip, red equal to blue so RGBA and + * BGRA hold the same bytes, premultiplied at VIDEO_XR_HINGE_LEVEL; + * after the back's cell and before the tags, so a runtime too narrow + * for the tags keeps it; centred on the middle row, a clear column + * either side. */ +static void test_hinge_pixels(void) +{ + const unsigned x0 = VIDEO_XR_HINGE_X; + const unsigned y0 = VIDEO_XR_HINGE_Y; + const unsigned rb = (VIDEO_XR_HINGE_RB * VIDEO_XR_HINGE_LEVEL + 127) / 255; + const unsigned g = (VIDEO_XR_HINGE_G * VIDEO_XR_HINGE_LEVEL + 127) / 255; + unsigned x, y; + int solid = 1; + CHECK(VIDEO_XR_HINGE_X == VIDEO_XR_BACK_X + VIDEO_XR_BACK_CELL + 1); + CHECK(VIDEO_XR_TAG_X(0) == VIDEO_XR_HINGE_X + VIDEO_XR_HINGE_COLS + 1); + CHECK(VIDEO_XR_CURSOR_UNTAGGED_W == x0 + VIDEO_XR_HINGE_COLS); + CHECK(2 * VIDEO_XR_HINGE_Y + VIDEO_XR_HINGE_ROWS == VIDEO_XR_CURSOR_H); + CHECK(VIDEO_XR_HINGE_G > VIDEO_XR_HINGE_RB && g > rb); + video_xr_cursor_pixels(t_atlas); + for (y = y0; y < y0 + VIDEO_XR_HINGE_ROWS; y++) + for (x = x0; x < x0 + VIDEO_XR_HINGE_COLS; x++) + solid &= texel(x, y)[0] == rb && texel(x, y)[1] == g + && texel(x, y)[2] == rb + && texel(x, y)[3] == VIDEO_XR_HINGE_LEVEL; + CHECK(solid); + CHECK(column_clear(x0 - 1) && column_clear(x0 + VIDEO_XR_HINGE_COLS)); + CHECK(!texel(x0 + 64, y0 - 1)[3] + && !texel(x0 + 64, y0 + VIDEO_XR_HINGE_ROWS)[3]); +} + +/* A quad's back: its plane, its front the other way, its up the same; + * the head is behind it on the far side of its front. Edit mode picks + * by the plane, so a hand behind meets the back where the front is; + * other lasers only the front. */ +static void test_back_pick(void) +{ + video_xr_quad_set_t set; + video_xr_quad_t q; + video_xr_vec3_t o, d, eye, ax, az; + float u = 0.0f, v = 0.0f, t = 0.0f; + memset(&q, 0, sizeof(q)); + q.pose.orientation.y = (float)sin(M_PI / 12.0); + q.pose.orientation.w = (float)cos(M_PI / 12.0); + q.pose.position.z = -1.8f; + q.width = 1.6f; + q.height = 0.96f; + q.kind = VIDEO_XR_QUAD_SCREEN; + q.screen = 1; + set.quads[0] = q; + video_xr_turn_back(&set.quads[0].pose); + ax.x = 1.0f; ax.y = 0.0f; ax.z = 0.0f; + video_xr_rotate(&set.quads[0].pose.orientation, &ax, &ax); + az.x = 0.0f; az.y = 0.0f; az.z = 1.0f; + video_xr_rotate(&set.quads[0].pose.orientation, &az, &az); + CHECK(pos_is(&set.quads[0], 0.0, 0.0, -1.8)); + CHECK(vec_is(&ax, -cos(M_PI / 6.0), 0.0, sin(M_PI / 6.0))); + CHECK(vec_is(&az, -sin(M_PI / 6.0), 0.0, -cos(M_PI / 6.0))); + eye.x = 0.0f; eye.y = 0.3f; eye.z = 0.0f; + CHECK(!video_xr_quad_behind(&q, &eye)); + CHECK(video_xr_quad_behind(&set.quads[0], &eye)); + eye.z = -3.0f; + CHECK(video_xr_quad_behind(&q, &eye)); + CHECK(!video_xr_quad_behind(&set.quads[0], &eye)); + + /* Square on, from behind, 40 cm right and 20 up of its middle. */ + memset(&set, 0, sizeof(set)); + set.quads[0] = q; + video_xr_pose_identity(&set.quads[0].pose); + set.quads[0].pose.position.z = -1.8f; + set.num_quads = 1; + o.x = 0.4f; o.y = 0.2f; o.z = -3.0f; + d.x = 0.0f; d.y = 0.0f; d.z = 1.0f; + CHECK(video_xr_pick_edit(&set, false, &o, &d, &u, &v, &t, NULL) == 0); + CHECK(NEAR(u, 0.75) && NEAR(v, 0.5 - 0.2 / 0.96) && NEAR(t, 1.2)); + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_ALWAYS, true, + &o, &d, &u, &v, &t) == -1); + /* The menu's handle bar too. */ + set.quads[0].kind = VIDEO_XR_QUAD_MENU; + o.x = 0.0f; + o.y = -0.531f; + CHECK(video_xr_pick_edit(&set, false, &o, &d, &u, &v, &t, NULL) == 0); + CHECK(video_xr_pick(&set, VIDEO_OPENXR_LASER_ALWAYS, true, + &o, &d, &u, &v, &t) == -1); +} + +/* The tag: 5 cm over the hand, its face towards the eye, its x level; + * with the eye straight above, facing up. */ +static void test_tag_pose(void) +{ + double n; + video_xr_vec3_t hand, eye, ax, az; + video_xr_pose_t pose; + hand.x = 0.2f; + hand.y = -0.3f; + hand.z = -0.3f; + eye.x = 0.0f; + eye.y = 0.0f; + eye.z = 0.0f; + video_xr_tag(&hand, &eye, &pose); + CHECK(vec_is(&pose.position, 0.2, -0.25, -0.3)); + pose_axis(&pose, 1.0f, 0.0f, 0.0f, &ax); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + n = sqrt(0.2 * 0.2 + 0.25 * 0.25 + 0.3 * 0.3); + CHECK(vec_is(&az, -0.2 / n, 0.25 / n, 0.3 / n)); + CHECK(NEAR(ax.y, 0.0) && ax.x > 0.0f); + eye.x = 0.2f; + eye.y = 1.0f; + eye.z = -0.3f; + video_xr_tag(&hand, &eye, &pose); + pose_axis(&pose, 0.0f, 0.0f, 1.0f, &az); + CHECK(vec_is(&az, 0.0, 1.0, 0.0)); } static int pose_same(const video_xr_pose_t *a, const video_xr_pose_t *b) @@ -899,6 +3184,4379 @@ static int pose_same(const video_xr_pose_t *a, const video_xr_pose_t *b) && a->position.z == b->position.z; } +#define SNAP_ALL (VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_X | VIDEO_XR_SNAP_Y \ + | VIDEO_XR_SNAP_Z) +#define GAP_ALL (SNAP_ALL | VIDEO_XR_SNAP_PULLING(SNAP_ALL) \ + | VIDEO_XR_SNAP_HINGE | VIDEO_XR_SNAP_GAP \ + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP)) + +/* The target: screen 1, 1.6 x 0.9, at 2 m facing ahead. */ +static void snap_set(video_xr_quad_set_t *set) +{ + memset(set, 0, sizeof(*set)); + video_xr_pose_identity(&set->quads[0].pose); + set->quads[0].pose.position.z = -2.0f; + set->quads[0].width = 1.6f; + set->quads[0].height = 0.9f; + set->quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set->num_quads = 1; +} + +/* At x, y, z turned yaw degrees about +Y. */ +static void snap_pose(video_xr_pose_t *pose, double x, double y, + double z, double yaw) +{ + video_xr_pose_identity(pose); + pose->position.x = (float)x; + pose->position.y = (float)y; + pose->position.z = (float)z; + pose->orientation.y = (float)sin(yaw * M_PI / 360.0); + pose->orientation.w = (float)cos(yaw * M_PI / 360.0); +} + +/* Screen 2, 1 x 0.6, held at x, y, z turned yaw degrees about +Y, + * snapped to set. */ +static unsigned snap_at(const video_xr_quad_set_t *set, double x, + double y, double z, double yaw, video_xr_pose_t *pose) +{ + snap_pose(pose, x, y, z, yaw); + return video_xr_snap(set, 1, 0, 1.0f, 0.6f, 1.0f, pose) + & VIDEO_XR_SNAP_ALL; +} + +/* A 0.5 x 0.4 target in slot at x, 0, -2 turned yaw degrees. */ +static void snap_add(video_xr_quad_set_t *set, unsigned slot, double x, + double yaw) +{ + video_xr_quad_t *q = &set->quads[set->num_quads++]; + snap_pose(&q->pose, x, 0.0, -2.0, yaw); + q->width = 0.5f; + q->height = 0.4f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->slot = slot; + q->screen = slot; +} + +/* Screen 3, 0.5 x 0.4, held likewise. */ +static unsigned snap_small_at(const video_xr_quad_set_t *set, double x, + double y, double z, double yaw, video_xr_pose_t *pose) +{ + snap_pose(pose, x, y, z, yaw); + return video_xr_snap(set, 2, 0, 0.5f, 0.4f, 1.0f, pose) + & VIDEO_XR_SNAP_ALL; +} + +static double snap_yaw(const video_xr_pose_t *pose) +{ + float yaw, pitch, roll; + video_xr_euler(&pose->orientation, &yaw, &pitch, &roll); + return yaw; +} + +/* Parallel and near: into its plane, edge to edge or level, each exact + * within its range; a quarter turn about its up meets it at a corner. */ +static void test_snap_lines(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + snap_set(&set); + CHECK(snap_at(&set, 1.308, 0.01, -1.98, 0.0, &p) == SNAP_ALL); + CHECK(vec_is(&p.position, 1.30, 0.0, -2.0)); + CHECK(snap_at(&set, 0.1, 0.16, -1.99, 0.0, &p) + == (VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z)); + CHECK(vec_is(&p.position, 0.1, 0.15, -2.0)); + CHECK(snap_at(&set, 0.0, 0.76, -2.0, 0.0, &p) == SNAP_ALL); + CHECK(vec_is(&p.position, 0.0, 0.75, -2.0)); + CHECK(snap_at(&set, 0.808, 0.01, -1.49, 90.0, &p) == SNAP_ALL); + CHECK(vec_is(&p.position, 0.8, 0.0, -1.5) + && angle_near((float)snap_yaw(&p), 90.0f)); +} + +/* Above the snaps engaged, each axis says whether it pulls at all, + * whole or fading: the hand's buzz re-arms only once its axis lets go + * entirely. */ +static void test_snap_pulling(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + const unsigned no_x = VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Y + | VIDEO_XR_SNAP_Z; + snap_set(&set); + snap_pose(&p, 1.308, 0.01, -1.98, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 1.0f, &p) == GAP_ALL); + /* 1.5 cm off flush: the edge fades, pulling but not engaged. */ + snap_pose(&p, 1.315, 0.0, -2.0, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 1.0f, &p) + == (no_x | VIDEO_XR_SNAP_PULLING(SNAP_ALL) | VIDEO_XR_SNAP_HINGE + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + /* 2.5 cm off: the edge lets go, the rest still hold. */ + snap_pose(&p, 1.325, 0.0, -2.0, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 1.0f, &p) + == (no_x | VIDEO_XR_SNAP_PULLING(no_x) | VIDEO_XR_SNAP_HINGE)); + /* Half faded in: every axis pulls, none is engaged. */ + snap_pose(&p, 1.308, 0.01, -1.98, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 0.5f, &p) + == (VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + /* Too far for any pull. */ + snap_pose(&p, 3.0, 0.0, -2.0, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 1.0f, &p) == 0); + /* Midway from the edge to a screen's centre 8 cm on: X still pulls + * between its two lines, so its buzz stays spent. */ + snap_add(&set, 2, 1.38, 0.0); + snap_pose(&p, 1.34, 0.0, -2.0, 0.0); + CHECK(video_xr_snap(&set, 1, 0, 1.0f, 0.6f, 1.0f, &p) + == (no_x | VIDEO_XR_SNAP_PULLING(SNAP_ALL) | VIDEO_XR_SNAP_HINGE)); +} + +/* Within 5 degrees exactly parallel, to 10 closing at twice the hand's + * turn, past it as held; lines exact to 3 cm, fading to 6, edge to + * edge to 1 either way, fading to 2; a 30 to 60 cm gap fades every + * pull, past 60 none. */ +static void test_snap_zones(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + snap_set(&set); + CHECK(snap_at(&set, 1.30, 0.0, -2.0, 4.0, &p) == SNAP_ALL); + CHECK(fabs(snap_yaw(&p)) < 0.01 && vec_is(&p.position, 1.30, 0.0, -2.0)); + CHECK(snap_at(&set, 1.30, 0.0, -2.0, 7.5, &p) == 0); + CHECK(fabs(snap_yaw(&p) - 5.0) < 0.02); + CHECK(snap_at(&set, 1.30, 0.0, -2.0, 12.0, &p) == 0); + CHECK(fabs(snap_yaw(&p) - 12.0) < 0.01 + && vec_is(&p.position, 1.30, 0.0, -2.0)); + CHECK(snap_at(&set, 1.295, 0.0, -2.0, 0.0, &p) == SNAP_ALL); + CHECK(NEAR(p.position.x, 1.30)); + snap_at(&set, 1.305, 0.0, -2.0, 0.0, &p); + CHECK(NEAR(p.position.x, 1.30)); + snap_at(&set, 1.315, 0.0, -2.0, 0.0, &p); + CHECK(NEAR(p.position.x, 1.31)); + snap_at(&set, 1.325, 0.0, -2.0, 0.0, &p); + CHECK(NEAR(p.position.x, 1.325)); + snap_at(&set, 1.36, 0.0, -2.0, 0.0, &p); + CHECK(NEAR(p.position.x, 1.36)); + CHECK(snap_at(&set, 1.75, 0.0, -2.0, 4.0, &p) == 0); + CHECK(fabs(snap_yaw(&p) - 2.0) < 0.02); + CHECK(snap_at(&set, 1.95, 0.0, -2.0, 4.0, &p) == 0); + CHECK(fabs(snap_yaw(&p) - 4.0) < 0.01 + && vec_is(&p.position, 1.95, 0.0, -2.0)); +} + +/* No target: the menu, a fixed slot, a held slot, the held screen's own + * slot, no set; the menu held snaps to nothing; a stereo pair counts + * once. */ +static void test_snap_targets(void) +{ + video_xr_quad_set_t set, two; + video_xr_pose_t p, q; + snap_set(&set); + set.quads[0].kind = VIDEO_XR_QUAD_MENU; + set.quads[0].slot = VIDEO_XR_MENU_SLOT; + CHECK(snap_at(&set, 1.32, 0.01, -1.98, 0.0, &p) == 0); + CHECK(vec_is(&p.position, 1.32, 0.01, -1.98)); + snap_set(&set); + set.slots[0].fixed = true; + CHECK(snap_at(&set, 1.32, 0.01, -1.98, 0.0, &p) == 0); + snap_set(&set); + video_xr_pose_identity(&p); + p.position.x = 1.32f; + p.position.y = 0.01f; + p.position.z = -1.98f; + CHECK(video_xr_snap(&set, 1, 1u, 1.0f, 0.6f, 1.0f, &p) == 0); + CHECK(video_xr_snap(&set, 0, 0, 1.0f, 0.6f, 1.0f, &p) == 0); + CHECK(video_xr_snap(&set, VIDEO_XR_MENU_SLOT, 0, 1.0f, 0.6f, 1.0f, &p) == 0); + CHECK(video_xr_snap(NULL, 1, 0, 1.0f, 0.6f, 1.0f, &p) == 0); + CHECK(vec_is(&p.position, 1.32, 0.01, -1.98)); + two = set; + two.quads[0].eye = VIDEO_XR_EYE_LEFT; + two.quads[1] = two.quads[0]; + two.quads[1].eye = VIDEO_XR_EYE_RIGHT; + two.num_quads = 2; + snap_at(&set, 1.32, 0.01, -1.98, 0.0, &p); + snap_at(&two, 1.32, 0.01, -1.98, 0.0, &q); + CHECK(!memcmp(&p, &q, sizeof(p))); +} + +/* No jumps: across every zone's edges the snapped pose moves at most + * twice the hand's step, but past touching, where the stop lets go at + * up to 3.4 times. */ +static void test_snap_continuous(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + snap_set(&set); + video_xr_pose_identity(&last); + /* Through the flush line's magnet, both sides: 0.5 mm steps. */ + for (i = 0; i <= 400; i++) + { + snap_at(&set, 1.20 + 0.0005 * i, 0.0, -2.0, 0.0, &p); + if (i) + CHECK(fabs(p.position.x - last.position.x) <= 0.00171); + last = p; + } + /* Turning through 5 and 10 degrees 2 cm off the line. */ + for (i = 0; i <= 240; i++) + { + snap_at(&set, 1.32, 0.0, -2.0, 0.05 * i, &p); + if (i) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 0.101 + && fabs(p.position.x - last.position.x) <= 0.005 + && fabs(p.position.z - last.position.z) <= 0.005); + last = p; + } + /* Away through the 30 and 60 cm gaps, turned 4 degrees. */ + for (i = 0; i <= 450; i++) + { + snap_at(&set, 1.55 + 0.001 * i, 0.0, -2.0, 4.0, &p); + if (i) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 0.02); + last = p; + } + /* Square, through the corner's magnet both ways. */ + for (i = 0; i <= 200; i++) + { + snap_at(&set, 0.70 + 0.001 * i, 0.0, -1.49, 90.0, &p); + if (i) + CHECK(fabs(p.position.x - last.position.x) <= 0.00341 + && fabs(p.position.z - last.position.z) <= 0.00341); + last = p; + } + for (i = 0; i <= 200; i++) + { + snap_at(&set, 0.81, 0.0, -1.40 - 0.001 * i, 90.0, &p); + if (i) + CHECK(fabs(p.position.x - last.position.x) <= 0.0021 + && fabs(p.position.z - last.position.z) <= 0.0021); + last = p; + } +} + +/* Several targets, each ranked by what its pull leaves: in a row of + * screens 5 cm apart, screen 3 held 0.8 cm right of screen 2 lines up + * with it as if alone, in either quad order, though screen 1 is at the + * same angle and on the same lines in its 30-60 cm fade; carried right + * through screen 1's 60 cm edge in 0.5 mm steps, it never turns and no + * step moves it more than twice the hand's. */ +static void test_snap_ranked(void) +{ + unsigned i, o; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + for (o = 0; o < 2; o++) + { + memset(&set, 0, sizeof(set)); + snap_add(&set, o, o ? 0.0 : -0.55, 0.0); + snap_add(&set, !o, o ? -0.55 : 0.0, 0.0); + CHECK(snap_small_at(&set, 0.508, 0.01, -1.99, 3.0, &p) == SNAP_ALL); + CHECK(fabs(snap_yaw(&p)) < 0.01 + && vec_is(&p.position, 0.50, 0.0, -2.0)); + /* Level with both: the tie goes to the whole pull. */ + CHECK(snap_small_at(&set, 0.508, 0.0, -1.99, 3.0, &p) == SNAP_ALL); + video_xr_pose_identity(&last); + for (i = 0; i <= 200; i++) + { + snap_small_at(&set, 0.50 + 0.0005 * i, 0.01, -1.99, 3.0, &p); + if (i) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 0.01 + && fabs(p.position.x - last.position.x) <= 0.00101 + && fabs(p.position.y - last.position.y) <= 0.00101 + && fabs(p.position.z - last.position.z) <= 0.00101); + last = p; + } + /* The whole pull still wins over one a rounding error nearer. */ + set.quads[!o].pose.position.y = 2e-6f; + CHECK(snap_small_at(&set, 0.508, 0.0, -1.99, 3.0, &p) == SNAP_ALL); + } + /* Among whole pulls the smallest correction wins: 0.88 high, 1.5 cm + * low of a 0.9 high target, its bottom edge levels, not its top. */ + snap_set(&set); + snap_pose(&p, 0.01, -0.015, -1.99, 0.0); + CHECK((video_xr_snap(&set, 1, 0, 1.0f, 0.88f, 1.0f, &p) + & VIDEO_XR_SNAP_ALL) == SNAP_ALL + && vec_is(&p.position, 0.0, -0.01, -2.0)); +} + +/* A whole line beats a fading one however near: screen 3 held 0.8 cm + * right of a target at y 0, with another 1 cm higher whose gap is 0.31 + * or 0.45 m, carried up through that one's line in 1 um steps, stays + * level with the whole one in either quad order. */ +static void test_snap_whole_line(void) +{ + unsigned i, j, o; + video_xr_quad_set_t set; + video_xr_pose_t p; + for (j = 0; j < 2; j++) + { + for (o = 0; o < 2; o++) + { + double x = j ? 1.47 : 1.33; + memset(&set, 0, sizeof(set)); + snap_add(&set, o, o ? x : 0.0, 0.0); + snap_add(&set, !o, o ? 0.0 : x, 0.0); + set.quads[!o].pose.position.y = 0.01f; + for (i = 0; i <= 800; i++) + CHECK(snap_small_at(&set, 0.508, 0.0096 + 0.000001 * i, -1.99, + 0.0, &p) == SNAP_ALL && fabs(p.position.y) < 1e-6); + } + } +} + +/* A turn tie: targets 3 degrees either way, the near one whole, the + * far one in its 30-60 cm fade; the near one turns it, in either quad + * order, as it does when the far one is parallel already. Of two in + * the turn's whole snap, the nearer wins, though the other's turn is + * smaller. */ +static void test_snap_turn_tie(void) +{ + unsigned o; + video_xr_quad_set_t set; + video_xr_pose_t p; + for (o = 0; o < 2; o++) + { + memset(&set, 0, sizeof(set)); + snap_add(&set, o, o ? 1.45 : 0.0, o ? 3.0 : -3.0); + snap_add(&set, !o, o ? 0.0 : 1.45, o ? -3.0 : 3.0); + CHECK(snap_small_at(&set, 0.508, 0.0, -2.0, 0.0, &p) + & VIDEO_XR_SNAP_TURN); + CHECK(angle_near((float)snap_yaw(&p), -3.0f)); + set.quads[!o].pose.orientation.y = 0.0f; + set.quads[!o].pose.orientation.w = 1.0f; + CHECK(snap_small_at(&set, 0.508, 0.0, -2.0, 0.0, &p) + & VIDEO_XR_SNAP_TURN); + CHECK(angle_near((float)snap_yaw(&p), -3.0f)); + memset(&set, 0, sizeof(set)); + snap_add(&set, o, o ? 1.04 : 0.0, o ? 1.0 : -4.0); + snap_add(&set, !o, o ? 0.0 : 1.04, o ? -4.0 : 1.0); + CHECK(snap_small_at(&set, 0.508, 0.0, -2.0, 0.0, &p) + & VIDEO_XR_SNAP_TURN); + CHECK(angle_near((float)snap_yaw(&p), -4.0f)); + } +} + +/* A target turned 30 degrees snaps in its own frame: screen 2 held 0.8 cm + * off standing flush right of it, 1 cm high, 2 cm in front and turned + * 3 degrees more lands flush, level, in its plane and parallel. */ +static void test_snap_turned(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + double c = cos(M_PI / 6.0); + double s = sin(M_PI / 6.0); + snap_set(&set); + snap_pose(&set.quads[0].pose, 0.0, 0.0, -2.0, 30.0); + CHECK(snap_at(&set, 1.308 * c + 0.02 * s, 0.01, + -2.0 - 1.308 * s + 0.02 * c, 33.0, &p) == SNAP_ALL); + CHECK(vec_is(&p.position, 1.30 * c, 0.0, -2.0 - 1.30 * s) + && angle_near((float)snap_yaw(&p), 30.0f)); +} + +/* Metres between two poses' middles. */ +static double pose_gap(const video_xr_pose_t *a, const video_xr_pose_t *b) +{ + double dx = (double)a->position.x - b->position.x; + double dy = (double)a->position.y - b->position.y; + double dz = (double)a->position.z - b->position.z; + return sqrt(dx * dx + dy * dy + dz * dz); +} + +/* Screen 1 of the 3DS layout alone, 1.6 x 0.96 at 1.8 m: its bottom + * edge runs along x at y -0.48, z -1.8, its right edge up y at x 0.8. */ +static void hinge_set(video_xr_quad_set_t *set) +{ + memset(set, 0, sizeof(*set)); + video_xr_pose_identity(&set->quads[0].pose); + set->quads[0].pose.position.z = -1.8f; + set->quads[0].width = 1.6f; + set->quads[0].height = 0.96f; + set->quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set->num_quads = 1; +} + +/* deg degrees about x, y, z. */ +static void hinge_turn(video_xr_quat_t *q, double x, double y, double z, + double deg) +{ + double n = sqrt(x * x + y * y + z * z); + double s = sin(deg * M_PI / 360.0); + q->x = (float)(x / n * s); + q->y = (float)(y / n * s); + q->z = (float)(z / n * s); + q->w = (float)cos(deg * M_PI / 360.0); +} + +/* b, then a: a b. */ +static void hinge_then(const video_xr_quat_t *a, const video_xr_quat_t *b, + video_xr_quat_t *out) +{ + video_xr_pose_t pa, pb; + video_xr_pose_identity(&pa); + video_xr_pose_identity(&pb); + pa.orientation = *a; + pb.orientation = *b; + video_xr_compose(&pa, &pb, &pa); + *out = pa.orientation; +} + +/* Turned q, with its own point mx, my at x, y, z. */ +static void hinge_place(video_xr_pose_t *p, const video_xr_quat_t *q, + double mx, double my, double x, double y, double z) +{ + video_xr_vec3_t m, w; + m.x = (float)mx; + m.y = (float)my; + m.z = 0.0f; + video_xr_rotate(q, &m, &w); + p->orientation = *q; + p->position.x = (float)(x - w.x); + p->position.y = (float)(y - w.y); + p->position.z = (float)(z - w.z); +} + +/* The gap hinge_snap and hinge_grab snap with, metres; 0 touching. */ +static float t_gap; + +/* Screen 2 of the 3DS layout, 1.28 x 0.96, held at p, snapped to set. */ +static unsigned hinge_snap(const video_xr_quad_set_t *set, + video_xr_pose_t *p, float fade, video_xr_hinge_t *h) +{ + return video_xr_snap_hinge(set, 1, 0, 1.28f, 0.96f, t_gap, fade, p, h); +} + +/* Screen in slot, w x h, at p, added to set. */ +static void rest_add(video_xr_quad_set_t *set, unsigned slot, float w, + float h, const video_xr_pose_t *p) +{ + video_xr_quad_t *q = &set->quads[set->num_quads++]; + memset(q, 0, sizeof(*q)); + q->pose = *p; + q->width = w; + q->height = h; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->slot = slot; + q->screen = slot; +} + +/* hinge_set's screen 1 and the 3DS layout's screen 2 resting under it, + * tipped fold degrees toward the user about its top edge, that edge + * gap metres under screen 1's bottom edge, in its plane. */ +static void rest_under(video_xr_quad_set_t *set, double fold, double gap) +{ + video_xr_pose_t p; + video_xr_quat_t q; + hinge_set(set); + hinge_turn(&q, 1.0, 0.0, 0.0, -fold); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.48 - gap, -1.8); + rest_add(set, 1, 1.28f, 0.96f, &p); +} + +/* Whether r is slot's edge edge on target's edge on. */ +static int rest_is(const video_xr_rest_t *r, unsigned slot, unsigned edge, + unsigned target, unsigned on) +{ + return r->slot == slot && r->edge == edge && r->target == target + && r->on == on; +} + +/* Screens resting exactly where a hinge holds one carried, each pair + * once: the 3DS pair flat, 2 cm apart and folded 30 degrees, screen 1 + * carried where either can be, screen 2 where only it can (folded 2 cm + * apart, screen 1's edge lies 1 cm off screen 2's plane); side by side; + * a square corner; a 2 x 2 grid's four sides, not its corners. None + * 12 cm apart, 1 mm out of the plane, skipped or fixed; max caps them. + * None only near a hinge: 7 mm into Hinge Gap 0's click, 3.2 cm apart + * at Hinge Gap 2 (inside its click), centres 2 cm off level; 3.2 cm at + * Hinge Gap 0 is a free gap, held there. None inside a settle band off + * its point, which a let-go would move: 4.5 cm at Hinge Gap 2, 2.5 at + * 0, 0.5 at 3, 7.5 at 5; 5.5 cm at Hinge Gap 2 is past it, and touching + * rests at every gap. */ +static void test_rest_hinges(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_rest_t r[8]; + static const double grid[4][2] = { + { -0.8, 0.48 }, { 0.8, 0.48 }, { -0.8, -0.48 }, { 0.8, -0.48 } }; + /* Hinge Gap, apart, metres. */ + static const double band[4][2] = { + { 0.02, 0.045 }, { 0.0, 0.025 }, { 0.03, 0.005 }, { 0.05, 0.075 } }; + static const double gaps[6] = { 0.0, 0.01, 0.02, 0.03, 0.05, 0.10 }; + + rest_under(&set, 0.0, 0.0); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + rest_under(&set, 0.0, 0.02); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + rest_under(&set, 30.0, 0.0); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + rest_under(&set, 30.0, 0.02); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 8) == 1 + && rest_is(&r[0], 1, 0, 0, 1)); + + hinge_set(&set); + snap_pose(&p, 1.44, 0.0, -1.8, 0.0); + rest_add(&set, 1, 1.28f, 0.96f, &p); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 1 + && rest_is(&r[0], 0, 2, 1, 3)); + hinge_set(&set); + snap_pose(&p, 0.8, 0.0, -1.16, -90.0); + rest_add(&set, 1, 1.28f, 0.96f, &p); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 1 + && rest_is(&r[0], 0, 2, 1, 3)); + + memset(&set, 0, sizeof(set)); + for (i = 0; i < 4; i++) + { + snap_pose(&p, grid[i][0], grid[i][1], -1.8, 0.0); + rest_add(&set, i, 1.6f, 0.96f, &p); + } + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 4 + && rest_is(&r[0], 0, 2, 1, 3) && rest_is(&r[1], 0, 1, 2, 0) + && rest_is(&r[2], 1, 1, 3, 0) && rest_is(&r[3], 2, 2, 3, 3)); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 2) == 2); + CHECK(video_xr_rest_hinges(&set, 1u << 0, 0.0f, r, 8) == 2 + && rest_is(&r[0], 1, 1, 3, 0) && rest_is(&r[1], 2, 2, 3, 3)); + + rest_under(&set, 0.0, 0.12); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 0); + rest_under(&set, 0.0, 0.0); + set.quads[1].pose.position.z += 0.001f; + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 0); + rest_under(&set, 0.0, 0.0); + CHECK(video_xr_rest_hinges(&set, 1u << 1, 0.0f, r, 8) == 0); + set.slots[1].fixed = true; + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 0); + + rest_under(&set, 0.0, 0.007); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 0); + rest_under(&set, 0.0, 0.032); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 8) == 0); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + rest_under(&set, 0.0, 0.0); + set.quads[1].pose.position.x = 0.02f; + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 8) == 0); + + for (i = 0; i < 4; i++) + { + rest_under(&set, 0.0, band[i][1]); + CHECK(video_xr_rest_hinges(&set, 0, (float)band[i][0], r, 8) == 0); + } + rest_under(&set, 0.0, 0.055); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + rest_under(&set, 0.0, 0.0); + for (i = 0; i < 6; i++) + CHECK(video_xr_rest_hinges(&set, 0, (float)gaps[i], r, 8) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); +} + +/* Whether a and b are the same pose to rounding. */ +static int pose_near(const video_xr_pose_t *a, const video_xr_pose_t *b) +{ + return fabs(a->position.x - b->position.x) < 1e-5 + && fabs(a->position.y - b->position.y) < 1e-5 + && fabs(a->position.z - b->position.z) < 1e-5 + && fabs(a->orientation.x - b->orientation.x) < 1e-5 + && fabs(a->orientation.y - b->orientation.y) < 1e-5 + && fabs(a->orientation.z - b->orientation.z) < 1e-5 + && fabs(a->orientation.w - b->orientation.w) < 1e-5; +} + +/* The hinge a screen is taken in where it rests, by the same test that + * shows it: the 3DS pair folded 2 cm apart, shown, and taken hinged + * from screen 2's side alone (one-sided at a fold: screen 1's edge lies + * 1 cm off screen 2's plane), not with screen 1 held; neither shown + * nor taken hinged where the hinge, whole, would still move it (7 mm + * into Hinge Gap 0's click); of two, the one the snap ranks best: + * screen 1's touching edge over screen 3's 4 cm away. */ +static void test_rest_hinge(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_hinge_t h; + video_xr_rest_t r[2]; + rest_under(&set, 30.0, 0.02); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 2) == 1 + && rest_is(&r[0], 1, 0, 0, 1)); + CHECK(video_xr_rest_hinge(&set, 1, 0, 1.28f, 0.96f, 0.02f, + &set.quads[1].pose, &h) + && h.slot == 0 && h.on == 1 && h.edge == 0 + && h.axis == VIDEO_XR_STEP_TILT); + CHECK(!video_xr_rest_hinge(&set, 0, 0, 1.6f, 0.96f, 0.02f, + &set.quads[0].pose, &h)); + CHECK(!video_xr_rest_hinge(&set, 1, 1u << 0, 1.28f, 0.96f, 0.02f, + &set.quads[1].pose, &h)); + rest_under(&set, 0.0, 0.007); + CHECK(video_xr_rest_hinges(&set, 0, 0.0f, r, 2) == 0); + CHECK(!video_xr_rest_hinge(&set, 1, 0, 1.28f, 0.96f, 0.0f, + &set.quads[1].pose, &h)); + rest_under(&set, 0.0, 0.0); + snap_pose(&p, 0.0, -0.48 - 0.96 - 0.04 - 0.48, -1.8, 0.0); + rest_add(&set, 2, 1.28f, 0.96f, &p); + CHECK(video_xr_rest_hinge(&set, 1, 0, 1.28f, 0.96f, 0.0f, + &set.quads[1].pose, &h) + && h.slot == 0 && h.on == 1 && h.edge == 0); +} + +/* The 3DS pair resting folded 30 degrees 2 cm apart, screen 2 gripped + * where it rests by an aim at the origin, as edit mode takes it. */ +static void rest_grab(video_xr_quad_set_t *set, video_xr_grab_t *g, + video_xr_pose_t *aim) +{ + rest_under(set, 30.0, 0.02); + memset(g, 0, sizeof(*g)); + video_xr_pose_identity(aim); + g->rel = set->quads[1].pose; + g->turn = g->rel.orientation; + g->last = g->rel; + g->base = g->rel.position; + g->width = 1.28f; + g->height = 0.96f; + g->slot = 1; + g->state = VIDEO_XR_GRAB_HELD; + g->gap = 2; +} + +/* A grab carrying the hinge its screen rested in is hinged whole where + * it was taken, its fade still none, nothing moved; 2 cm toward the + * user it is still held on the line, where the fade alone would not + * hold it; without the hinge, neither. 4.8 cm off, past + * VIDEO_XR_HINGE_REACH, it holds no hinge but its target still pulls + * whole, the fade short of whole; 6 cm off, the hinge kept ends. */ +static void test_grab_kept(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t h; + rest_grab(&set, &g, &aim); + start = g.last; + CHECK(!video_xr_grab_kept(&g, &aim, &set, &start, &h)); + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); + g.rest_slot = 1; + g.rest_on = 1; + g.rest_edge = 0; + CHECK(video_xr_grab_kept(&g, &aim, &set, &start, &h) + && h.slot == 0 && h.on == 1 && h.edge == 0); + CHECK((video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE) + && h.slot == 0 && h.on == 1 && h.edge == 0 + && pose_near(&pose, &start)); + aim.position.z = 0.02f; + CHECK((video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE) + && NEAR(pose.position.z, start.position.z)); + g.rest_slot = 0; + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); + g.rest_slot = 1; + aim.position.z = 0.048f; + CHECK(video_xr_grab_kept(&g, &aim, &set, &start, &h)); + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); + aim.position.z = 0.06f; + CHECK(!video_xr_grab_kept(&g, &aim, &set, &start, &h)); + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); +} + +/* rest_grab's screen 2, the hand x metres past touching screen 1 along + * screen 1's up, its fade whole: the snaps held, its edge's gap below + * screen 1's in *across, and h's hinge near and pushed through. */ +static unsigned through_snap(const video_xr_quad_set_t *set, + const video_xr_grab_t *g, const video_xr_hinge_t *h, double x, + double *across, bool *near, bool *through) +{ + unsigned bits; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t got; + video_xr_vec3_t m, up, w, e; + const video_xr_quad_t *q = &set->quads[0]; + m.x = 0.0f; + m.y = 1.0f; + m.z = 0.0f; + video_xr_rotate(&q->pose.orientation, &m, &up); + video_xr_pose_identity(&aim); + aim.position.x = (float)(up.x * (x + 0.02)); + aim.position.y = (float)(up.y * (x + 0.02)); + aim.position.z = (float)(up.z * (x + 0.02)); + start = g->last; + start.position.y -= 0.3f; + bits = video_xr_grab_hinge(g, &aim, set, 0, &start, &pose, &got); + m.y = g->height * 0.5f; + video_xr_rotate(&pose.orientation, &m, &w); + m.y = -q->height * 0.5f; + video_xr_rotate(&q->pose.orientation, &m, &e); + *across = -(((double)pose.position.x + w.x - q->pose.position.x - e.x) + * up.x + ((double)pose.position.y + w.y - q->pose.position.y - e.y) + * up.y + ((double)pose.position.z + w.z - q->pose.position.z - e.z) + * up.z); + *near = video_xr_grab_near_hinge(g, &aim, set, h, + VIDEO_XR_HINGE_REACH); + *through = video_xr_grab_through(g, &aim, set, h); + return bits; +} + +/* Pushed past touching, rest_grab's screen 2 is hinged touching and its + * hinge near to VIDEO_XR_SNAP_STOP past; past that the push lets it go, + * neither held nor near, overlapping. Once a push has (g->through), from + * about VIDEO_XR_SNAP_STOP_ARM short of there it overlaps a little, + * neither held nor near, and back short of that it is hinged touching; + * apart, as before. Swept in and back, setting and clearing g->through + * as the main thread does, nothing hops: it moves no more than the + * stop's fade, 3.4 times the hand, and the slip as it lets go is + * about a tenth of a millimetre. */ +static void test_grab_through(void) +{ + static const double whole[] = { 0.005, 0.015, 0.029 }; + unsigned i; + double a, last = 0.0; + bool near, through; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim; + video_xr_hinge_t h; + rest_grab(&set, &g, &aim); + memset(&h, 0, sizeof(h)); + h.slot = 0; + h.on = 1; + h.edge = 0; + for (i = 0; i < 3; i++) + { + CHECK((through_snap(&set, &g, &h, whole[i], &a, &near, &through) + & VIDEO_XR_SNAP_HINGE) && fabs(a) < 1e-5 && near + && !through); + } + CHECK(!(through_snap(&set, &g, &h, 0.031, &a, &near, &through) + & VIDEO_XR_SNAP_HINGE) && a < -1e-5 && !near && through); + through_snap(&set, &g, &h, 0.04, &a, &near, &through); + CHECK(a < -0.001 && a > -0.0399 && !near); + through_snap(&set, &g, &h, 0.06, &a, &near, &through); + CHECK(fabs(a + 0.06) < 1e-5); + g.through = 1; + for (i = 0; i < 2; i++) + { + CHECK(!(through_snap(&set, &g, &h, i ? 0.0299 : 0.026, &a, &near, + &through) & VIDEO_XR_SNAP_HINGE) + && a < -1e-5 && a > -0.00015 && !near && through); + } + CHECK((through_snap(&set, &g, &h, 0.024, &a, &near, &through) + & VIDEO_XR_SNAP_HINGE) && fabs(a) < 1e-5 && near && !through); + CHECK((through_snap(&set, &g, &h, -0.023, &a, &near, &through) + & VIDEO_XR_SNAP_HINGE) && fabs(a - 0.02) < 1e-5); + g.through = 0; + for (i = 0; i <= 600; i++) + { + double x = 0.02 + 0.0001 * ((i <= 300) ? i : 600 - i); + bool slip = false; + unsigned bits; + if (i <= 300 && !g.through && x > 0.03 + 1e-5) + { + g.through = 1; + slip = true; + } + bits = through_snap(&set, &g, &h, x, &a, &near, &through); + if (bits & VIDEO_XR_SNAP_HINGE) + g.through = 0; + if (i) + CHECK(fabs(a - last) <= (slip ? 0.00045 : 0.00035)); + CHECK(((bits & VIDEO_XR_SNAP_HINGE) == 0) == (i <= 300 + ? x > 0.03 + 1e-5 : x > 0.025 + 1e-5)); + last = a; + } +} + +/* Through (g->through), rest_grab's screen 2 carried 1 um at a time + * from 2.45 to 2.65 cm past touching, and let go where it shows, saved + * to the micrometre: it rests hinged exactly where it was hinged + * carried. */ +static void test_grab_through_rest(void) +{ + unsigned i; + unsigned odd = 0; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t h; + video_xr_vec3_t m, up; + rest_grab(&set, &g, &aim); + g.through = 1; + m.x = 0.0f; + m.y = 1.0f; + m.z = 0.0f; + video_xr_rotate(&set.quads[0].pose.orientation, &m, &up); + start = g.last; + start.position.y -= 0.3f; + for (i = 0; i <= 2000; i++) + { + double x = 0.0245 + 1e-6 * i; + bool carried, rests; + video_xr_pose_identity(&aim); + aim.position.x = (float)(up.x * (x + 0.02)); + aim.position.y = (float)(up.y * (x + 0.02)); + aim.position.z = (float)(up.z * (x + 0.02)); + carried = (video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, + NULL) & VIDEO_XR_SNAP_HINGE) != 0; + pose.position.x = (float)(floor(pose.position.x * 1e6 + 0.5) / 1e6); + pose.position.y = (float)(floor(pose.position.y * 1e6 + 0.5) / 1e6); + pose.position.z = (float)(floor(pose.position.z * 1e6 + 0.5) / 1e6); + rests = video_xr_rest_hinge(&set, 1, 0, g.width, g.height, 0.02f, + &pose, &h); + odd += (carried != rests) ? 1u : 0u; + } + if (odd) + printf(" through, carried and rest differ at %u of 2001\n", odd); + CHECK(odd == 0); +} + +/* At Hinge Gap 1 the click reaches touching, so a screen held where it + * lies touching would click apart. The 3DS pair resting touching, flat + * or folded 30, rests hinged at Gap 1 as at 0 and 2; taken where the + * hand holds it (video_xr_rest_take, 1 cm past touching, 5 mm at Gap + * 2), it is hinged whole and unmoved. Resting 1 cm apart, the hand + * holds it where it lies. */ +static void test_rest_gap_one(void) +{ + unsigned i; + for (i = 0; i < 3; i++) + { + video_xr_quad_set_t set; + video_xr_rest_t r[4]; + video_xr_hinge_t h; + video_xr_grab_t g; + video_xr_pose_t aim, taken, pose; + const video_xr_pose_t *at = &set.quads[1].pose; + float gap = (i == 2) ? 0.02f : 0.01f; + rest_under(&set, i ? 30.0 : 0.0, 0.0); + CHECK(video_xr_rest_hinges(&set, 0, gap, r, 4) == 1); + CHECK(video_xr_rest_hinge(&set, 1, 0, 1.28f, 0.96f, gap, at, &h) + && h.slot == 0); + taken = *at; + video_xr_rest_take(&set, 1, 1.28f, 0.96f, gap, &h, &taken); + CHECK(fabs(taken.position.y - at->position.y + - ((i == 2) ? 0.005 : 0.01)) < 1e-6 + && fabs(taken.position.z - at->position.z) < 1e-6); + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + g.rel = taken; + g.turn = taken.orientation; + g.last = *at; + g.base = at->position; + g.width = 1.28f; + g.height = 0.96f; + g.slot = 1; + g.state = VIDEO_XR_GRAB_HELD; + g.gap = (unsigned char)(gap * 100.0f + 0.5f); + g.rest_slot = (unsigned char)(h.slot + 1); + g.rest_on = (unsigned char)h.on; + g.rest_edge = (unsigned char)h.edge; + CHECK((video_xr_grab_hinge(&g, &aim, &set, 0, &taken, &pose, NULL) + & VIDEO_XR_SNAP_HINGE) + && fabs(pose.position.x - at->position.x) < 1e-5 + && fabs(pose.position.y - at->position.y) < 1e-5 + && fabs(pose.position.z - at->position.z) < 1e-5); + } + { + video_xr_quad_set_t set; + video_xr_rest_t r[4]; + video_xr_hinge_t h; + video_xr_pose_t taken; + rest_under(&set, 30.0, 0.01); + CHECK(video_xr_rest_hinges(&set, 0, 0.01f, r, 4) == 1); + CHECK(video_xr_rest_hinge(&set, 1, 0, 1.28f, 0.96f, 0.01f, + &set.quads[1].pose, &h)); + taken = set.quads[1].pose; + video_xr_rest_take(&set, 1, 1.28f, 0.96f, 0.01f, &h, &taken); + CHECK(!memcmp(&taken, &set.quads[1].pose, sizeof(taken))); + } +} + +/* Whether set's screen in slot, w x 0.96 where it rests, is taken in + * the hinge of its edge edge on target's edge on, gap metres apart. */ +static int rest_takes(const video_xr_quad_set_t *set, unsigned slot, + float w, float gap, unsigned edge, unsigned target, unsigned on) +{ + video_xr_hinge_t h; + return video_xr_rest_hinge(set, slot, 0, w, 0.96f, gap, + &set->quads[slot].pose, &h) + && h.edge == edge && h.slot == target && h.on == on; +} + +/* Flat, or at Hinge Gap 0, a pair resting hinged is taken hinged from + * either side: the 3DS pair flat touching and 2 cm apart, folded 30 + * degrees touching, and a square corner touching. */ +static void test_rest_hinge_sides(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + rest_under(&set, 0.0, 0.0); + CHECK(rest_takes(&set, 1, 1.28f, 0.0f, 0, 0, 1)); + CHECK(rest_takes(&set, 0, 1.6f, 0.0f, 1, 1, 0)); + rest_under(&set, 0.0, 0.02); + CHECK(rest_takes(&set, 1, 1.28f, 0.02f, 0, 0, 1)); + CHECK(rest_takes(&set, 0, 1.6f, 0.02f, 1, 1, 0)); + rest_under(&set, 30.0, 0.0); + CHECK(rest_takes(&set, 1, 1.28f, 0.0f, 0, 0, 1)); + CHECK(rest_takes(&set, 0, 1.6f, 0.0f, 1, 1, 0)); + hinge_set(&set); + snap_pose(&p, 0.8, 0.0, -1.16, -90.0); + rest_add(&set, 1, 1.28f, 0.96f, &p); + CHECK(rest_takes(&set, 0, 1.6f, 0.0f, 2, 1, 3)); + CHECK(rest_takes(&set, 1, 1.28f, 0.0f, 3, 0, 2)); +} + +/* The pick-up tests the size it is given, as the pairs at rest test + * each quad's own: given the quad's, it agrees with them; 2 cm taller, + * screen 2's top edge 1 cm into the 2 cm gap's click, it is not taken + * hinged. */ +static void test_rest_hinge_size(void) +{ + video_xr_quad_set_t set; + video_xr_hinge_t h; + video_xr_rest_t r[2]; + const video_xr_quad_t *q = &set.quads[1]; + rest_under(&set, 0.0, 0.02); + CHECK(video_xr_rest_hinges(&set, 0, 0.02f, r, 2) == 1 + && rest_is(&r[0], 0, 1, 1, 0)); + CHECK(video_xr_rest_hinge(&set, 1, 0, q->width, q->height, 0.02f, + &q->pose, &h) + && h.slot == 0 && h.on == 1 && h.edge == 0); + CHECK(!video_xr_rest_hinge(&set, 1, 0, q->width, q->height + 0.02f, + 0.02f, &q->pose, &h)); +} + +/* Whether a and b are the same pose to 0.01 mm and 0.01 degrees. */ +static int settle_same(const video_xr_pose_t *a, const video_xr_pose_t *b) +{ + return fabs(a->position.x - b->position.x) <= 1e-5 + && fabs(a->position.y - b->position.y) <= 1e-5 + && fabs(a->position.z - b->position.z) <= 1e-5 + && turn_apart(&a->orientation, &b->orientation) <= 0.01; +} + +/* Whether screen 2, w wide, settled at pose in h from hand, rests there + * exactly: put there in set, the pair is listed at rest and taken in h, + * settling again from where a hand takes it leaves it, and it keeps + * hand's fold. */ +static int settle_rests(video_xr_quad_set_t *set, float w, float gap, + const video_xr_hinge_t *h, const video_xr_pose_t *hand, + const video_xr_pose_t *pose) +{ + video_xr_rest_t r[8]; + video_xr_hinge_t k; + video_xr_pose_t take, again; + set->quads[1].pose = *pose; + set->quads[1].width = w; + take = *pose; + video_xr_rest_take(set, 1, w, 0.96f, gap, h, &take); + return video_xr_rest_hinges(set, 0, gap, r, 8) == 1 + && (rest_is(&r[0], 1, 0, 0, 1) || rest_is(&r[0], 0, 1, 1, 0)) + && video_xr_rest_hinge(set, 1, 0, w, 0.96f, gap, pose, &k) + && k.slot == h->slot && k.edge == h->edge && k.on == h->on + && video_xr_hinge_settle(set, 1, 0, w, 0.96f, gap, h, &take, + &again) + && settle_same(&again, pose) + && turn_apart(&pose->orientation, &hand->orientation) <= 0.01; +} + +#define SETTLE_FREE 100.0 +#define SETTLE_NONE 200.0 + +/* Screen 2 let go under screen 1, flat and folded 30 degrees, in the + * hinge of its top edge on screen 1's bottom: to the set gap from within + * VIDEO_XR_HINGE_SETTLE of it apart, touching from the stop's hold, + * where it is at a free gap past that, and not at all past the hinge's + * reach (Hinge Gap 10 to 11 cm, where it engages). Along, at Hinge Gap + * 2 cm, level from within twice VIDEO_XR_SNAP_DIST: centres, or a + * narrower screen's ends. Turned 4.9 degrees off parallel, by a roll or + * a yaw, it settles parallel; past VIDEO_XR_SNAP_ANGLE, where a grab's + * hinge lets go, not. Folded 7.5 degrees, it settles at the 5 the turn + * to flat's fade shows. None in front of screen 1's plane, overlapping + * too little along, folded past square, with the target skipped, or + * with no set. Each settled pose rests there. 4 m tall and folded 8 + * degrees, its middle too far out of screen 1's plane for the turn's + * whole pull, it does not rest, and the settle refuses the pose it + * gives. */ +static void test_hinge_settle(void) +{ + /* Hinge Gap, the hand apart (pushed below 0), settled apart, in cm. */ + static const double row[][3] = { + { 0.0, 0.0, 0.0 }, { 0.0, 0.3, 0.0 }, { 0.0, 1.5, 0.0 }, + { 0.0, 2.9, 0.0 }, { 0.0, -1.0, 0.0 }, { 0.0, -2.9, 0.0 }, + { 0.0, 3.1, SETTLE_FREE }, { 0.0, 9.9, SETTLE_FREE }, + { 0.0, 10.5, SETTLE_NONE }, { 0.0, -3.2, SETTLE_NONE }, + { 1.0, 0.0, 1.0 }, { 1.0, 3.9, 1.0 }, { 1.0, -0.3, 1.0 }, + { 1.0, -1.0, 0.0 }, { 1.0, -3.4, 0.0 }, + { 1.0, 4.1, SETTLE_FREE }, { 1.0, -3.6, SETTLE_NONE }, + { 2.0, 0.0, 0.0 }, { 2.0, 0.3, 2.0 }, { 2.0, 3.5, 2.0 }, + { 2.0, 4.9, 2.0 }, { 2.0, 5.1, SETTLE_FREE }, + { 2.0, 9.9, SETTLE_FREE }, { 2.0, -2.9, 0.0 }, + { 3.0, 0.5, 3.0 }, { 3.0, 5.9, 3.0 }, { 3.0, 6.1, SETTLE_FREE }, + { 5.0, 1.9, SETTLE_FREE }, { 5.0, 8.1, SETTLE_FREE }, + { 5.0, 2.1, 5.0 }, { 5.0, 7.9, 5.0 }, + { 10.0, 6.9, SETTLE_FREE }, { 10.0, 7.1, 10.0 }, + { 10.0, 11.0, 10.0 }, { 10.0, 11.5, SETTLE_NONE } + }; + /* Screen 2's centre off screen 1's, its width, where it settles. */ + static const double along[][3] = { + { 0.029, 1.28, 0.0 }, { 0.04, 1.28, 0.0 }, { 0.059, 1.28, 0.0 }, + { 0.061, 1.28, 0.061 }, { 0.31, 0.9, 0.35 }, { -0.31, 0.9, -0.35 } + }; + unsigned i, f; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t hand, pose, want, aim; + video_xr_hinge_t h, k; + video_xr_quat_t q; + memset(&h, 0, sizeof(h)); + h.slot = 0; + h.edge = 0; + h.on = 1; + for (f = 0; f < 2; f++) + { + double fold = f ? 30.0 : 0.0; + for (i = 0; i < sizeof(row) / sizeof(row[0]); i++) + { + float gap = (float)(row[i][0] * 0.01); + bool ok; + rest_under(&set, fold, row[i][1] * 0.01); + hand = set.quads[1].pose; + ok = video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, gap, &h, + &hand, &pose); + if (row[i][2] == SETTLE_NONE) + ok = !ok; + else + { + if (row[i][2] == SETTLE_FREE) + want = hand; + else + { + rest_under(&set, fold, row[i][2] * 0.01); + want = set.quads[1].pose; + } + ok = ok && settle_same(&pose, &want) + && settle_rests(&set, 1.28f, gap, &h, &hand, &pose); + } + if (!ok) + { + printf(" FAIL %s:%d: fold %.0f, Hinge Gap %.0f, %.1f cm apart\n", + __FILE__, __LINE__, fold, row[i][0], row[i][1]); + failures++; + } + } + for (i = 0; i < sizeof(along) / sizeof(along[0]); i++) + { + float w = (float)along[i][1]; + rest_under(&set, fold, 0.02); + set.quads[1].width = w; + set.quads[1].pose.position.x += (float)along[i][0]; + hand = set.quads[1].pose; + want = hand; + want.position.x = (float)along[i][2]; + if (!(video_xr_hinge_settle(&set, 1, 0, w, 0.96f, 0.02f, &h, + &hand, &pose) + && settle_same(&pose, &want) + && settle_rests(&set, w, 0.02f, &h, &hand, &pose))) + { + printf(" FAIL %s:%d: fold %.0f, %.1f cm along\n", __FILE__, + __LINE__, fold, along[i][0] * 100.0); + failures++; + } + } + } + + rest_under(&set, 0.0, 0.02); + hand = set.quads[1].pose; + hand.position.z += 0.031f; + CHECK(!video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose)); + hinge_turn(&q, 0.0, 0.0, 1.0, 5.5); + hinge_place(&hand, &q, 0.0, 0.48, 0.0, -0.48 - 0.02, -1.8); + CHECK(!video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose)); + for (i = 0; i < 4; i++) + { + bool ok; + double deg = (i & 1) ? 5.1 : 4.9; + rest_under(&set, 0.0, 0.02); + want = set.quads[1].pose; + hinge_turn(&q, 0.0, (i & 2) ? 1.0 : 0.0, (i & 2) ? 0.0 : 1.0, deg); + hinge_place(&hand, &q, 0.0, 0.48, 0.0, -0.48 - 0.02, -1.8); + ok = video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose); + if ((i & 1) ? ok : !(ok && settle_same(&pose, &want) + && settle_rests(&set, 1.28f, 0.02f, &h, &want, &pose))) + { + printf(" FAIL %s:%d: %s %.1f deg off parallel\n", __FILE__, + __LINE__, (i & 2) ? "yaw" : "roll", deg); + failures++; + } + } + rest_under(&set, 0.0, 0.02); + hand = set.quads[1].pose; + hand.position.x += 1.411f; + CHECK(!video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose)); + hand = set.quads[1].pose; + CHECK(!video_xr_hinge_settle(&set, 1, 1u << 0, 1.28f, 0.96f, 0.02f, + &h, &hand, &pose)); + CHECK(!video_xr_hinge_settle(NULL, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose)); + rest_under(&set, 95.0, 0.02); + CHECK(!video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &set.quads[1].pose, &pose)); + rest_under(&set, 5.0, 0.02); + want = set.quads[1].pose; + rest_under(&set, 7.5, 0.02); + hand = set.quads[1].pose; + CHECK(video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &pose) + && settle_same(&pose, &want)); + hinge_set(&set); + hinge_turn(&q, 1.0, 0.0, 0.0, -8.0); + hinge_place(&want, &q, 0.0, 2.0, 0.0, -0.48, -1.8); + rest_add(&set, 1, 1.28f, 4.0f, &want); + hinge_turn(&q, 1.0, 0.0, 0.0, -9.0); + hinge_place(&hand, &q, 0.0, 2.0, 0.0, -0.48, -1.8); + pose.position.x = 1000.0f; + CHECK(!video_xr_rest_hinge(&set, 1, 0, 1.28f, 4.0f, 0.0f, &want, &k) + && !video_xr_hinge_settle(&set, 1, 0, 1.28f, 4.0f, 0.0f, &h, + &hand, &pose) + && pose.position.x != 1000.0f); + + /* A grab settles from where its aim puts its item. */ + rest_grab(&set, &g, &aim); + g.rel.position.y -= 0.01f; + hand = set.quads[1].pose; + hand.position.y -= 0.01f; + CHECK(video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, 0.02f, &h, + &hand, &want) + && video_xr_grab_settle(&g, &aim, &set, 0, &h, &pose) + && settle_same(&pose, &want) + && settle_same(&want, &set.quads[1].pose)); + CHECK(!video_xr_grab_settle(&g, &aim, NULL, 0, &h, &pose)); +} + +/* Screen 2 resting touching under screen 1, flat and folded 30 degrees, + * taken where the hand holds it (video_xr_rest_take) and let go after + * the hand drifts across the edge: touching while it drifts less than + * VIDEO_XR_SNAP_STOP_ARM apart, so a still re-grab never moves it to + * the gap; past that, by the settle bands. */ +static void test_hinge_settle_taken(void) +{ + /* Hinge Gap in cm, the drift apart (pushed below 0) in mm, settled + * apart in cm. */ + static const double row[][3] = { + { 0.0, 0.02, 0.0 }, { 0.0, 1.0, 0.0 }, { 0.0, 4.9, 0.0 }, + { 0.0, -1.0, 0.0 }, { 0.0, 5.1, 0.0 }, { 0.0, 10.0, 0.0 }, + { 1.0, 0.02, 0.0 }, { 1.0, 1.0, 0.0 }, { 1.0, 4.9, 0.0 }, + { 1.0, -1.0, 0.0 }, { 1.0, 5.1, 1.0 }, { 1.0, 10.0, 1.0 }, + { 2.0, 0.02, 0.0 }, { 2.0, 1.0, 0.0 }, { 2.0, 4.9, 0.0 }, + { 2.0, -1.0, 0.0 }, { 2.0, 5.1, 2.0 }, { 2.0, 10.0, 2.0 }, + { 3.0, 0.02, 0.0 }, { 3.0, 1.0, 0.0 }, { 3.0, 4.9, 0.0 }, + { 3.0, -1.0, 0.0 }, { 3.0, 5.1, 3.0 }, { 3.0, 10.0, 3.0 }, + { 5.0, 0.02, 0.0 }, { 5.0, 1.0, 0.0 }, { 5.0, 4.9, 0.0 }, + { 5.0, -1.0, 0.0 }, { 5.0, 5.1, SETTLE_FREE }, + { 5.0, 10.0, SETTLE_FREE }, + { 10.0, 0.02, 0.0 }, { 10.0, 1.0, 0.0 }, { 10.0, 4.9, 0.0 }, + { 10.0, -1.0, 0.0 }, { 10.0, 5.1, SETTLE_FREE }, + { 10.0, 10.0, SETTLE_FREE } + }; + unsigned i, f; + video_xr_quad_set_t set; + video_xr_pose_t hand, pose, want; + video_xr_hinge_t h; + memset(&h, 0, sizeof(h)); + h.slot = 0; + h.edge = 0; + h.on = 1; + for (f = 0; f < 2; f++) + { + double fold = f ? 30.0 : 0.0; + for (i = 0; i < sizeof(row) / sizeof(row[0]); i++) + { + float gap = (float)(row[i][0] * 0.01); + rest_under(&set, fold, 0.0); + hand = set.quads[1].pose; + video_xr_rest_take(&set, 1, 1.28f, 0.96f, gap, &h, &hand); + hand.position.y -= (float)(row[i][1] * 0.001); + if (row[i][2] == SETTLE_FREE) + want = hand; + else + { + rest_under(&set, fold, row[i][2] * 0.01); + want = set.quads[1].pose; + } + if (!(video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, gap, &h, + &hand, &pose) + && settle_same(&pose, &want) + && settle_rests(&set, 1.28f, gap, &h, &hand, &pose))) + { + printf(" FAIL %s:%d: fold %.0f, Hinge Gap %.0f, %.2f mm " + "apart\n", __FILE__, __LINE__, fold, row[i][0], + row[i][1]); + failures++; + } + } + } +} + +/* Screen 2 held deg degrees off flat with screen 1 in the hinge of + * kind k, its edge gap metres out from screen 1's: 0 its top edge + * under screen 1's bottom, tipped toward the user; 1 its bottom over + * screen 1's top, likewise; 2 its left edge on screen 1's right, + * turned toward the user about the vertical. */ +static void fold_place(unsigned k, double deg, double gap, + video_xr_pose_t *p) +{ + video_xr_quat_t q; + if (k == 0) + { + hinge_turn(&q, 1.0, 0.0, 0.0, -deg); + hinge_place(p, &q, 0.0, 0.48, 0.0, -0.48 - gap, -1.8); + } + else if (k == 1) + { + hinge_turn(&q, 1.0, 0.0, 0.0, deg); + hinge_place(p, &q, 0.0, -0.48, 0.0, 0.48 + gap, -1.8); + } + else + { + hinge_turn(&q, 0.0, 1.0, 0.0, -deg); + hinge_place(p, &q, -0.64, 0.0, 0.8 + gap, 0.0, -1.8); + } +} + +/* Screen 2 of the 3DS layout gripped at pose, held still: its fade + * whole, or none but the hinge it rests in (taken, unless NULL). */ +static void fold_grab(video_xr_grab_t *g, const video_xr_pose_t *pose, + float gap, const video_xr_hinge_t *taken) +{ + memset(g, 0, sizeof(*g)); + g->rel = *pose; + g->turn = pose->orientation; + g->last = *pose; + g->base = pose->position; + g->width = 1.28f; + g->height = 0.96f; + g->slot = 1; + g->state = VIDEO_XR_GRAB_HELD; + g->tilt = true; + g->gap = (unsigned char)(gap * 100.0f + 0.5f); + if (taken) + { + g->rest_slot = (unsigned char)(taken->slot + 1); + g->rest_edge = (unsigned char)taken->edge; + g->rest_on = (unsigned char)taken->on; + } +} + +/* Screen 2 held hinged deg degrees off flat with screen 1, by its top, + * bottom or side edge, or off square at a corner, at Hinge Gap 0 and + * 2: shown where the turn puts it, whole within VIDEO_XR_SNAP_ANGLE, + * then catching up at twice the hand's speed to twice that, let go it + * settles there, rests listed, and taken up it shows unmoved with the + * hand back where it held it. With screen 1 turned, so rounding leaves + * a flat rest a hair off its goal, a rest flat, square or in the fade + * is taken likewise and keeps its hinge. */ +static void test_hinge_fold_rest(void) +{ + static const double flat[] = { 0.0, 1.0, 3.0, 4.9, 5.1, 6.0, 7.5, + 9.0, 9.9, 10.1, 12.0, 30.0 }; + static const double square[] = { 75.0, 79.0, 80.5, 82.0, 83.7, 84.9, + 85.1, 87.0, 89.9, 90.0 }; + /* Screen 1's yaw, pitch and roll; 1 puts screen 2 back through its + * settings. */ + static const float turned[3][4] = { { 61.7f, -8.2f, 0.0f, 0.0f }, + { 123.4f, 5.6f, -7.8f, 0.0f }, { -33.0f, 12.5f, 2.0f, 1.0f } }; + /* fold_place's kind, the fold resting, the hand's fold taking it. */ + static const double rests[4][3] = { { 0.0, 0.0, 0.0 }, + { 0.0, 7.5, 8.75 }, { 2.0, 90.0, 90.0 }, { 2.0, 82.5, 81.25 } }; + static const unsigned he[3] = { 0, 1, 3 }; + static const unsigned te[3] = { 1, 0, 2 }; + unsigned row; + const unsigned nf = sizeof(flat) / sizeof(flat[0]); + const unsigned n = 6 * nf + + 2 * (unsigned)(sizeof(square) / sizeof(square[0])); + for (row = 0; row < n; row++) + { + unsigned i, nr; + video_xr_quad_set_t set; + video_xr_rest_t r[8]; + video_xr_grab_t g; + video_xr_pose_t hand, aim, start, shown, pose, take, again; + video_xr_hinge_t h, k; + double a, off, want, back; + int listed = 0; + int ok; + bool sq = row >= 6 * nf; + unsigned j = sq ? 2 : row / (2 * nf); + float gap = (sq ? (row - 6 * nf) % 2 : (row / nf) % 2) + ? 0.02f : 0.0f; + double deg = sq ? square[(row - 6 * nf) / 2] : flat[row % nf]; + const video_xr_quat_t *q1; + memset(&h, 0, sizeof(h)); + h.edge = he[j]; + h.on = te[j]; + hinge_set(&set); + fold_place(j, deg, gap, &hand); + rest_add(&set, 1, 1.28f, 0.96f, &hand); + q1 = &set.quads[0].pose.orientation; + a = sq ? 90.0 - deg : deg; + off = (a <= VIDEO_XR_SNAP_ANGLE) ? 0.0 + : (a < 2.0 * VIDEO_XR_SNAP_ANGLE) + ? 2.0 * (a - VIDEO_XR_SNAP_ANGLE) : a; + want = sq ? 90.0 - off : off; + back = (a <= VIDEO_XR_SNAP_ANGLE) ? want : deg; + + video_xr_pose_identity(&aim); + fold_grab(&g, &hand, gap, NULL); + start = hand; + start.position.x += 1.0f; + ok = (video_xr_grab_hinge(&g, &aim, &set, 0, &start, &shown, &k) + & VIDEO_XR_SNAP_HINGE) + && k.slot == 0 && k.edge == h.edge && k.on == h.on + && fabs(turn_apart(&shown.orientation, q1) - want) <= 0.01 + && video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, gap, &h, + &hand, &pose) + && settle_same(&pose, &shown); + if (ok) + { + set.quads[1].pose = pose; + nr = video_xr_rest_hinges(&set, 0, gap, r, 8); + for (i = 0; i < nr; i++) + if ( rest_is(&r[i], 1, h.edge, 0, h.on) + || rest_is(&r[i], 0, h.on, 1, h.edge)) + listed = 1; + take = pose; + video_xr_rest_take(&set, 1, 1.28f, 0.96f, gap, &h, &take); + fold_grab(&g, &take, gap, &h); + ok = listed + && (video_xr_grab_hinge(&g, &aim, &set, 0, &pose, &again, &k) + & VIDEO_XR_SNAP_HINGE) + && settle_same(&again, &pose) + && fabs(turn_apart(&take.orientation, q1) - back) <= 0.01; + } + if (!ok) + { + printf(" FAIL %s:%d: %s edge, Hinge Gap %.0f, held %.1f deg " + "off %s\n", __FILE__, __LINE__, + (j == 0) ? "top" : (j == 1) ? "bottom" : "side", + gap * 100.0f, a, sq ? "square" : "flat"); + failures++; + } + } + for (row = 0; row < 24; row++) + { + unsigned i, nr; + video_xr_quad_set_t set; + video_xr_rest_t r[8]; + video_xr_grab_t g; + video_screen_xr_t s; + video_xr_vec3_t base; + video_xr_pose_t rest, take, aim, again, pose; + video_xr_hinge_t h, k; + int listed = 0; + int ok; + const float *t = turned[row / 8]; + const double *f = rests[(row / 2) % 4]; + unsigned j = (unsigned)f[0]; + float gap = (row & 1) ? 0.02f : 0.0f; + const video_xr_quat_t *q1; + memset(&h, 0, sizeof(h)); + h.edge = he[j]; + h.on = te[j]; + memset(&s, 0, sizeof(s)); + s.yaw = t[0]; + s.pitch = t[1]; + s.roll = t[2]; + s.scale = 1.0f; + base.x = 0.0f; + base.y = 0.0f; + base.z = -1.8f; + hinge_set(&set); + video_xr_item_pose(&base, &s, &set.quads[0].pose); + q1 = &set.quads[0].pose.orientation; + fold_place(j, f[1], gap, &rest); + rest.position.z += 1.8f; + video_xr_compose(&set.quads[0].pose, &rest, &rest); + if (f[1] == 0.0) + rest.orientation = *q1; + if (t[3] != 0.0f) + { + memset(&s, 0, sizeof(s)); + s.scale = 1.0f; + video_xr_item_settings(&base, &rest, &s); + video_xr_item_pose(&base, &s, &rest); + } + rest_add(&set, 1, 1.28f, 0.96f, &rest); + nr = video_xr_rest_hinges(&set, 0, gap, r, 8); + for (i = 0; i < nr; i++) + if ( rest_is(&r[i], 1, h.edge, 0, h.on) + || rest_is(&r[i], 0, h.on, 1, h.edge)) + listed = 1; + take = rest; + video_xr_rest_take(&set, 1, 1.28f, 0.96f, gap, &h, &take); + video_xr_pose_identity(&aim); + fold_grab(&g, &take, gap, &h); + ok = listed + && fabs(turn_apart(&take.orientation, q1) - f[2]) <= 0.01 + && fabs(turn_apart(&take.orientation, &rest.orientation) + - fabs(f[2] - f[1])) <= 0.01 + && (video_xr_grab_hinge(&g, &aim, &set, 0, &rest, &again, &k) + & VIDEO_XR_SNAP_HINGE) + && k.slot == 0 && k.edge == h.edge && k.on == h.on + && settle_same(&again, &rest) + && video_xr_hinge_settle(&set, 1, 0, 1.28f, 0.96f, gap, &h, + &take, &pose) + && settle_same(&pose, &rest); + if (!ok) + { + printf(" FAIL %s:%d: screen 1 turned %.1f %.1f %.1f%s, %s " + "edge, Hinge Gap %.0f, resting %.1f deg off flat\n", + __FILE__, __LINE__, t[0], t[1], t[2], + t[3] != 0.0f ? " (from settings)" : "", + j ? "side" : "top", gap * 100.0f, f[1]); + failures++; + } + } +} + +/* The hinge a grab was taken in turns it whole whatever the fade, even + * with a nearer screen still fading in: screen 2 of the flat 3DS pair + * at Hinge Gap 0, its kept hinge on screen 1, held 5 mm toward the user + * and tipped 2 degrees, with a third screen 2 cm right of it in the + * hand's plane; taken there (a fade of none) or where it rests (fading + * in), it is hinged flat where it rests, as without the third. */ +static void test_grab_kept_turn(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, rest, start, pose, p; + video_xr_hinge_t h; + for (i = 0; i < 2; i++) + { + rest_under(&set, 0.0, 0.0); + rest = set.quads[1].pose; + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + g.rel = rest; + g.rel.position.z += 0.005f; + hinge_turn(&g.rel.orientation, 1.0, 0.0, 0.0, 2.0); + g.turn = g.rel.orientation; + g.last = g.rel; + g.base = rest.position; + g.width = 1.28f; + g.height = 0.96f; + g.slot = 1; + g.state = VIDEO_XR_GRAB_HELD; + g.rest_slot = 1; + g.rest_on = 1; + g.rest_edge = 0; + start = i ? rest : g.rel; + CHECK((video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE) + && h.slot == 0 && pose_near(&pose, &rest)); + snap_pose(&p, 1.28 + 0.02, -0.96, -1.795, 0.0); + rest_add(&set, 2, 1.28f, 0.96f, &p); + CHECK((video_xr_grab_hinge(&g, &aim, &set, 0, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE) + && h.slot == 0 && pose_near(&pose, &rest)); + } +} + +#define HINGE_LINES (VIDEO_XR_SNAP_X | VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z) + +/* The 3DS pair: screen 2 tipped 30 degrees toward the user about its + * top edge, that edge 0.8 cm under and 1.3 cm in front of screen 1's + * bottom edge and turned 3 degrees off it, by a yaw or a roll, snaps + * touching and exactly parallel and keeps its 30 degrees: a hinge, its + * top edge on that line, swung by the tilt. */ +static void test_hinge_3ds(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p, want; + video_xr_quat_t tip, off, q; + video_xr_hinge_t h; + hinge_set(&set); + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + hinge_place(&want, &tip, 0.0, 0.48, 0.0, -0.48, -1.8); + for (i = 0; i < 2; i++) + { + unsigned bits; + hinge_turn(&off, 0.0, i ? 0.0 : 1.0, i ? 1.0 : 0.0, 3.0); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.488, -1.787); + memset(&h, 0, sizeof(h)); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) + && (bits & VIDEO_XR_SNAP_ALL) == HINGE_LINES); + CHECK(vec_is(&p.position, 0.0, -0.895692, -1.56) + && turn_apart(&p.orientation, &want.orientation) < 0.01); + CHECK(h.edge == 0 && h.axis == VIDEO_XR_STEP_TILT + && fabs(h.angle + 30.0) < 0.01); + CHECK(vec_is(&h.point, 0.0, -0.48, -1.8) + && vec_is(&h.dir, 1.0, 0.0, 0.0) + && vec_is(&h.flat, 0.0, -1.0, 0.0)); + CHECK(fabs(video_xr_hinge_angle(&h, &p.orientation) + 30.0) < 0.01); + } + /* No hinge out asked: the same pose. */ + hinge_turn(&off, 0.0, 1.0, 0.0, 3.0); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.488, -1.787); + want = p; + hinge_snap(&set, &p, 1.0f, &h); + CHECK((video_xr_snap(&set, 1, 0, 1.28f, 0.96f, 1.0f, &want) + & VIDEO_XR_SNAP_HINGE) && pose_same(&p, &want)); +} + +/* Tipped square about the line, as the 90 degree step leaves it, a + * rounding past square still hinges; 0.05 degrees past does not. */ +static void test_hinge_square_fold(void) +{ + static const double past[3] = { 1e-4, -1e-4, 0.05 }; + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + video_xr_hinge_t h; + hinge_set(&set); + for (i = 0; i < 3; i++) + { + hinge_turn(&q, 1.0, 0.0, 0.0, -90.0 - past[i]); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.48, -1.8); + CHECK(((hinge_snap(&set, &p, 1.0f, &h) & VIDEO_XR_SNAP_HINGE) != 0) + == (i < 2)); + } +} + +/* Side by side like a triptych: screen 2's left edge, 3 degrees tipped + * off upright, 0.8 cm right of and 1.3 cm in front of screen 1's right + * edge, turned 30 degrees toward the user: hinged there, swung by the + * turn. On its side, its top edge hinges the same way. */ +static void test_hinge_side(void) +{ + unsigned bits; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t turn, off, q; + video_xr_hinge_t h; + hinge_set(&set); + hinge_turn(&turn, 0.0, 1.0, 0.0, -30.0); + hinge_turn(&off, 1.0, 0.0, 0.0, 3.0); + hinge_then(&off, &turn, &q); + hinge_place(&p, &q, -0.64, 0.0, 0.808, 0.0, -1.787); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) + && (bits & VIDEO_XR_SNAP_ALL) == HINGE_LINES); + CHECK(vec_is(&p.position, 1.354256, 0.0, -1.48) + && turn_apart(&p.orientation, &turn) < 0.01); + CHECK(h.edge == 3 && h.axis == VIDEO_XR_STEP_TURN + && fabs(h.angle + 30.0) < 0.01 + && vec_is(&h.point, 0.8, 0.0, -1.8) && vec_is(&h.dir, 0.0, 1.0, 0.0) + && vec_is(&h.flat, 1.0, 0.0, 0.0)); + /* Turned a quarter in its own plane, then as before. */ + hinge_turn(&q, 0.0, 0.0, 1.0, 90.0); + hinge_then(&turn, &q, &q); + hinge_then(&off, &q, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.808, 0.0, -1.787); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && h.edge == 0 + && h.axis == VIDEO_XR_STEP_TURN && fabs(h.angle + 30.0) < 0.01); + CHECK(vec_is(&p.position, 1.215692, 0.0, -1.56)); +} + +/* Screens that overlap never hinge: screen 2 flat 1 cm in front of + * screen 1, top edges 1 cm apart, levels as before; folded back over + * it from its bottom edge, 170 or 135 degrees from flat, the pull only + * fades, whole again by 90. */ +static void test_hinge_overlap(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + hinge_set(&set); + video_xr_pose_identity(&p); + p.position.x = 0.1f; + p.position.y = -0.01f; + p.position.z = -1.79f; + CHECK(hinge_snap(&set, &p, 1.0f, NULL) + == (VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Y + | VIDEO_XR_SNAP_Z))); + CHECK(vec_is(&p.position, 0.1, 0.0, -1.8)); + hinge_turn(&q, 1.0, 0.0, 0.0, -170.0); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.79); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) + == (VIDEO_XR_SNAP_PULLING(HINGE_LINES) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + hinge_turn(&q, 1.0, 0.0, 0.0, -135.0); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.79); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) + == (VIDEO_XR_SNAP_PULLING(HINGE_LINES) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + hinge_turn(&q, 1.0, 0.0, 0.0, -90.0); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.79); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE); + CHECK(vec_is(&p.position, 0.0, -0.48, -1.32)); +} + +/* Out of range nothing; in a fade band a part pull, as the other snaps: + * 7 and 4.5 cm under, hinged where held, the gap free; the grab's fade + * half in, half way; 7.5 degrees off parallel, turned to 5; 12, none. + * Along screen 1's edge, 1 cm past its end, none; overlapping it + * 1.5 cm, half way; 4 cm, a hinge. Flat 1.5 cm in, the corner's X line + * leaves 1 cm overlapping: lined up, but no hinge. */ +static void test_hinge_range(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p, was; + video_xr_quat_t tip, off, q; + video_xr_hinge_t h; + hinge_set(&set); + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.55, -1.8); + was = p; + CHECK(hinge_snap(&set, &p, 1.0f, &h) == (VIDEO_XR_SNAP_HINGE + | HINGE_LINES | VIDEO_XR_SNAP_PULLING(HINGE_LINES)) + && pose_same(&p, &was) && fabs(h.gap - 0.07) < 1e-5); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.525, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, &h) == (VIDEO_XR_SNAP_HINGE + | HINGE_LINES | VIDEO_XR_SNAP_PULLING(HINGE_LINES))); + CHECK(vec_is(&p.position, 0.0, -0.940692, -1.56) + && fabs(h.gap - 0.045) < 1e-5); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.49, -1.8); + CHECK(hinge_snap(&set, &p, 0.5f, NULL) + == (VIDEO_XR_SNAP_PULLING(HINGE_LINES) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(vec_is(&p.position, 0.0, -0.900692, -1.56)); + hinge_turn(&off, 0.0, 1.0, 0.0, 7.5); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) + == (VIDEO_XR_SNAP_PULLING(HINGE_LINES) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(fabs(snap_yaw(&p) - 5.0) < 0.02); + hinge_turn(&off, 0.0, 1.0, 0.0, 12.0); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.8); + was = p; + CHECK(hinge_snap(&set, &p, 1.0f, NULL) == 0 && pose_same(&p, &was)); + hinge_place(&p, &tip, 0.0, 0.48, 1.45, -0.49, -1.8); + was = p; + CHECK(hinge_snap(&set, &p, 1.0f, NULL) == 0 && pose_same(&p, &was)); + hinge_place(&p, &tip, 0.0, 0.48, 1.425, -0.49, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) + == VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z + | VIDEO_XR_SNAP_GAP)); + CHECK(vec_is(&p.position, 1.425, -0.900692, -1.56)); + hinge_place(&p, &tip, 0.0, 0.48, 1.4, -0.49, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE); + snap_pose(&p, 1.425, -0.968, -1.8, 0.0); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) == (VIDEO_XR_SNAP_TURN + | VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z + | VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(vec_is(&p.position, 1.43, -0.96, -1.8)); +} + +/* A hinge at 0 and at 90 degrees is the plain flush and corner snap: + * the same poses, to rounding, and also a hinge. Flat: yawed 3 + * degrees, 0.8 cm under and 1.3 cm out. Square: 2 degrees short, + * behind screen 1's right corner, 0.5 cm out and 1 cm back. */ +static void test_hinge_flush_corner(void) +{ + unsigned bits; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + video_xr_hinge_t h; + hinge_set(&set); + hinge_turn(&q, 0.0, 1.0, 0.0, 3.0); + hinge_place(&p, &q, 0.0, 0.48, 0.05, -0.488, -1.787); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK(bits == (VIDEO_XR_SNAP_HINGE | VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Y + | VIDEO_XR_SNAP_Z | VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_GAP | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(fabs(p.position.x - 0.04) < 1e-6 && fabs(p.position.y + 0.96) < 1e-6 + && fabs(p.position.z + 1.8) < 1e-6 + && turn_apart(&p.orientation, &set.quads[0].pose.orientation) < 1e-3); + CHECK(h.edge == 0 && fabs(h.angle) < 1e-3); + hinge_turn(&q, 0.0, 1.0, 0.0, 88.0); + hinge_place(&p, &q, 0.64, 0.0, 0.805, 0.01, -1.81); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK(bits == GAP_ALL); + CHECK(fabs(p.position.x - 0.8) < 1e-6 && fabs(p.position.y) < 1e-6 + && fabs(p.position.z + 1.16) < 1e-6 + && angle_near((float)snap_yaw(&p), 90.0f)); + CHECK(h.edge == 2 && h.axis == VIDEO_XR_STEP_TURN + && fabs(fabs(h.angle) - 90.0) < 1e-3); +} + +/* A turn to the hinge's own screen turns about the shared edge: tipped + * 4.5 degrees about it, on it, screen 2 snaps flat and stays on the + * line, flush, where turning about its middle would lift that edge + * 3.8 cm off it and leave it short of screen 1's plane. */ +static void test_hinge_pivot(void) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + hinge_set(&set); + hinge_turn(&q, 1.0, 0.0, 0.0, -4.5); + hinge_place(&p, &q, 0.0, 0.48, 0.05, -0.48, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE); + CHECK(vec_is(&p.position, 0.04, -0.96, -1.8) + && turn_apart(&p.orientation, &set.quads[0].pose.orientation) < 1e-3); +} + +/* Of several hinges the one its pull leaves nearest wins, as for every + * snap: two screens above screen 2, A level, C turned 2 degrees, their + * bottom edges as one line, screen 2 tipped 30 and yawed 3 under both: + * whole on A beats fading on C, whole on C beats fading on A, and of two + * whole, the nearer (C's 1 cm to A's 2). A screen the whole turn goes + * to keeps it: no hinge turns it from there. */ +static void test_hinge_ranked(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t tip, off, q, c; + video_xr_hinge_t h; + const double ya[3] = { -0.48, -0.385, -0.48 }; + const double yc[3] = { -0.385, -0.48, -0.49 }; + const double yh[3] = { -0.5, -0.5, -0.49 }; + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + hinge_turn(&off, 0.0, 1.0, 0.0, 3.0); + hinge_then(&off, &tip, &q); + hinge_turn(&c, 0.0, 1.0, 0.0, 2.0); + for (i = 0; i < 3; i++) + { + memset(&set, 0, sizeof(set)); + snap_add(&set, 0, -0.32, 0.0); + set.quads[0].pose.position.y = (float)(ya[i] + 0.2); + set.quads[0].width = 0.6f; + snap_add(&set, 2, 0.32, 2.0); + set.quads[1].pose.position.y = (float)(yc[i] + 0.2); + set.quads[1].width = 0.6f; + set.quads[1].pose.position.z = -1.8f; + set.quads[0].pose.position.z = -1.8f; + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.5, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(h.edge == 0 && fabs(h.angle + 30.0) < 0.01); + if (i) + CHECK(NEAR(h.point.y, yh[i]) && vec_is(&h.dir, 0.999391, 0.0, + -0.034899)); + else + CHECK(NEAR(h.point.y, yh[i]) && vec_is(&h.dir, 1.0, 0.0, 0.0)); + } + /* C nearer, 1 cm to A's 2.5, but 7.5 degrees off, its pull fading: + * A, whole at 3, still wins. C turned 10.5 about where its edge line + * crosses under screen 2's middle. */ + set.quads[0].pose.position.y = -0.275f; + hinge_turn(&set.quads[1].pose.orientation, 0.0, 1.0, 0.0, 10.5); + set.quads[1].pose.position.x = (float)(0.32 * cos(10.5 * M_PI / 180.0)); + set.quads[1].pose.position.z = (float)(-1.8 + - 0.32 * sin(10.5 * M_PI / 180.0)); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.5, -1.8); + CHECK(hinge_snap(&set, &p, 1.0f, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(NEAR(h.point.y, -0.5) && vec_is(&h.dir, 1.0, 0.0, 0.0)); + /* A screen 15 cm right of screen 2's place, as it is turned 2 more: + * the turn goes whole to it, and screen 1's hinge 3 degrees off is + * left at that. */ + hinge_set(&set); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.49, -1.8); + { + video_xr_quad_t *t = &set.quads[set.num_quads++]; + video_xr_vec3_t o, w; + *t = set.quads[0]; + hinge_then(&q, &c, &t->pose.orientation); + o.x = 1.04f; + o.y = 0.0f; + o.z = 0.0f; + video_xr_rotate(&q, &o, &w); + t->pose.position.x = p.position.x + w.x; + t->pose.position.y = p.position.y + w.y; + t->pose.position.z = p.position.z + w.z; + t->width = 0.5f; + t->height = 0.4f; + t->slot = 2; + t->screen = 2; + } + CHECK(hinge_snap(&set, &p, 1.0f, NULL) == (SNAP_ALL + | VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(turn_apart(&p.orientation, &set.quads[1].pose.orientation) < 0.02); +} + +/* Turned whole to a screen on its right, screen 2 keeps a hinge to + * screen 1 as that turn leaves it: rolled 5.5 degrees off screen 1, + * its top edge 1 cm under, the other screen rolled 3.9 to 4.9 more, so + * the pair ends 9.4 to 10.4 off parallel. No step hops, and at 10.4 + * the hinge still pulls that edge up toward screen 1's. */ +static void test_hinge_turned_away(void) +{ + unsigned i, bits; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + video_xr_quat_t q; + video_xr_vec3_t o, w; + video_xr_quad_t *t; + hinge_set(&set); + hinge_turn(&q, 0.0, 0.0, 1.0, -5.5); + t = &set.quads[set.num_quads++]; + *t = set.quads[0]; + t->width = 0.5f; + t->height = 0.4f; + t->slot = 2; + t->screen = 2; + o.x = 1.04f; + o.y = 0.06f; + o.z = 0.0f; + video_xr_pose_identity(&last); + for (i = 0; i <= 100; i++) + { + hinge_place(&p, &q, 0.0, 0.48, -0.04, -0.49, -1.8); + video_xr_rotate(&q, &o, &w); + t->pose.position.x = p.position.x + w.x; + t->pose.position.y = p.position.y + w.y; + t->pose.position.z = p.position.z + w.z; + hinge_turn(&t->pose.orientation, 0.0, 0.0, 1.0, -9.4 - 0.01 * i); + bits = hinge_snap(&set, &p, 1.0f, NULL); + CHECK(turn_apart(&p.orientation, &t->pose.orientation) < 1e-3); + if (i) + CHECK(pose_gap(&p, &last) <= 0.0005); + last = p; + } + CHECK(bits == (VIDEO_XR_SNAP_TURN | VIDEO_XR_SNAP_Z + | VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(vec_is(&p.position, -0.088357, -0.963336, -1.8)); +} + +/* A whole line to another screen beats a hinge's part pull: screen 2 + * tipped 7 degrees, its top edge 5 mm under screen 1's bottom edge and + * overlapping it 1.5 cm, a screen parallel to it on its right, carried + * from 3 cm behind screen 1's line to 3 cm in front in 0.1 mm steps. + * It stays in that screen's plane, and no step hops. */ +static void test_hinge_outranked(void) +{ + unsigned i, bits; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + video_xr_quat_t q; + video_xr_vec3_t o, w; + video_xr_quad_t *t; + hinge_set(&set); + hinge_turn(&q, 1.0, 0.0, 0.0, -7.0); + t = &set.quads[set.num_quads++]; + *t = set.quads[0]; + t->width = 0.5f; + t->height = 0.4f; + t->slot = 2; + t->screen = 2; + o.x = 1.04f; + o.y = 0.14f; + o.z = 0.0f; + video_xr_pose_identity(&last); + for (i = 0; i <= 600; i++) + { + hinge_place(&p, &q, 0.0, 0.48, 1.425, -0.485, -1.83 + 0.0001 * i); + video_xr_rotate(&q, &o, &w); + t->pose.orientation = q; + t->pose.position.x = p.position.x + w.x; + t->pose.position.y = p.position.y + w.y; + t->pose.position.z = p.position.z + w.z; + bits = hinge_snap(&set, &p, 1.0f, NULL); + CHECK(bits & VIDEO_XR_SNAP_Z); + if (i) + CHECK(pose_gap(&p, &last) <= 0.0005); + last = p; + } +} + +/* No jumps: screen 2, its top edge 1.5 cm under and 1.3 cm out from + * screen 1's bottom edge and yawed 3, folded from -100 to 100 degrees + * about that edge in quarter degrees; on the line, through flat and + * square's fades in tenths, and tipped to 12 degrees in hundredths + * 1 cm past the end of screen 1's edge, a diagonal neighbour that never + * hinges nor pulls as one; carried away from the line in 0.5 mm steps + * and slid along it from 5 cm of overlap to 2 cm past the end in 1 mm + * steps, at 30 degrees, let go of there, and in flat's fade at 7.5 + * and 9.5. + * No step moves it more than the bound, the hand's own step a few + * times over. */ +static void test_hinge_continuous(void) +{ + unsigned i, j, bits; + video_xr_quad_set_t set; + video_xr_pose_t p, last, was; + video_xr_quat_t tip, off, q; + double most = 0.0; + hinge_set(&set); + video_xr_pose_identity(&last); + hinge_turn(&off, 0.0, 1.0, 0.0, 3.0); + for (i = 0; i <= 800; i++) + { + hinge_turn(&tip, 1.0, 0.0, 0.0, -100.0 + 0.25 * i); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.05, -0.495, -1.787); + hinge_snap(&set, &p, 1.0f, NULL); + if (i) + CHECK(pose_gap(&p, &last) <= 0.0080 + && turn_apart(&p.orientation, &last.orientation) <= 0.5); + last = p; + } + for (j = 0; j < 3; j++) + { + for (i = 0; i <= ((j == 2) ? 1200u : 2000u); i++) + { + if (j == 2) + { + hinge_turn(&q, 1.0, 0.0, 0.0, -0.01 * i); + hinge_place(&p, &q, 0.0, 0.48, 1.45, -0.48, -1.8); + } + else if (j) + { + hinge_turn(&q, 0.0, 1.0, 0.0, -80.0 - 0.01 * i); + hinge_place(&p, &q, -0.64, 0.0, 0.814, 0.01, -1.786); + } + else + { + hinge_turn(&q, 1.0, 0.0, 0.0, -0.1 * i); + hinge_place(&p, &q, 0.0, 0.48, 0.05, -0.48, -1.8); + } + bits = hinge_snap(&set, &p, 1.0f, NULL); + if (j == 2) + CHECK(!(bits & VIDEO_XR_SNAP_HINGE) + && (!i || pose_gap(&p, &last) <= 0.0005)); + if (j == 2 && i == 600 && t_gap == 0.0f) + CHECK(fabs(p.position.x - 1.442) < 1e-5 + && fabs(p.position.y + 0.959006) < 1e-5 + && fabs(p.position.z + 1.763161) < 1e-5); + if (i && pose_gap(&p, &last) > most) + most = pose_gap(&p, &last); + last = p; + } + } + CHECK(most <= 0.0040); + for (j = 0; j < 2; j++) + { + hinge_turn(&tip, 1.0, 0.0, 0.0, j ? -7.5 : -30.0); + hinge_then(&off, &tip, &q); + for (i = 0; i <= 160; i++) + { + hinge_place(&p, &q, 0.0, 0.48, 0.05, -0.48 - 0.0005 * i, -1.8); + hinge_snap(&set, &p, 1.0f, NULL); + if (i) + CHECK(pose_gap(&p, &last) <= 0.0015); + last = p; + } + } + for (j = 0; j < 3; j++) + { + hinge_turn(&tip, 1.0, 0.0, 0.0, (j == 2) ? -9.5 : j ? -7.5 : -30.0); + hinge_then(&off, &tip, &q); + for (i = 0; i <= 70; i++) + { + hinge_place(&p, &q, 0.0, 0.48, 1.39 + 0.001 * i, -0.495, -1.787); + was = p; + bits = hinge_snap(&set, &p, 1.0f, NULL); + if (!i && !j) + CHECK(bits & VIDEO_XR_SNAP_HINGE); + if (i) + CHECK(pose_gap(&p, &last) <= 0.0030); + last = p; + } + if (!j) + CHECK(bits == 0 && pose_same(&p, &was)); + } +} + +/* Rolled 1 to 4 degrees off the line, its edge's middle on it 10 cm + * along, screen 2 swung through flat's fade band under screen 1, or + * square's beside it: that middle keeps its place along the line. */ +static void test_hinge_roll_band(void) +{ + static const double rolls[] = { 1.0, 2.0, 4.0 }; + unsigned k, ri, i; + double most = 0.0; + for (k = 0; k < 2; k++) + for (ri = 0; ri < 3; ri++) + for (i = 0; i <= 240; i++) + { + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t a, b, q; + video_xr_vec3_t m, w; + double along; + hinge_set(&set); + hinge_turn(&b, 0.0, 0.0, 1.0, rolls[ri]); + if (k) + { + hinge_turn(&a, 0.0, 1.0, 0.0, -78.0 - 0.05 * i); + hinge_then(&a, &b, &q); + hinge_place(&p, &q, -0.64, 0.0, 0.8, 0.10, -1.8); + } + else + { + hinge_turn(&a, 1.0, 0.0, 0.0, -12.0 + 0.05 * i); + hinge_then(&b, &a, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.10, -0.48, -1.8); + } + CHECK(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE); + m.x = k ? -0.64f : 0.0f; + m.y = k ? 0.0f : 0.48f; + m.z = 0.0f; + video_xr_rotate(&p.orientation, &m, &w); + along = k ? (double)p.position.y + w.y : (double)p.position.x + w.x; + if (fabs(along - 0.10) > most) + most = fabs(along - 0.10); + } + if (most > 1e-4) + { + printf(" FAIL %s:%d: the edge's middle %.4f m along the line off" + " its place\n", __FILE__, __LINE__, most); + failures++; + } +} + +/* The 3DS pair held: screen 2 flat under screen 1, its top edge on + * screen 1's bottom edge, taken a metre away so its snaps are whole, + * carried or tilting. */ +static void hinge_grab(video_xr_quad_set_t *set, video_xr_grab_t *g, + video_xr_pose_t *start, bool tilt) +{ + hinge_set(set); + memset(g, 0, sizeof(*g)); + video_xr_pose_identity(&g->rel); + g->turn = g->rel.orientation; + g->rel.position.y = -0.96f; + g->rel.position.z = -1.8f; + g->base.y = -0.992f; + g->base.z = -1.8f; + g->width = 1.28f; + g->height = 0.96f; + g->slot = 1; + g->state = VIDEO_XR_GRAB_HELD; + g->tilt = tilt; + g->gap = (unsigned char)(t_gap * 100.0f + 0.5f); + video_xr_pose_identity(start); + start->position.x = 1.0f; +} + +/* Metres from screen 2's top edge middle to screen 1's bottom edge. */ +static double hinge_off_line(const video_xr_pose_t *p) +{ + double dy, dz; + video_xr_vec3_t m, w; + m.x = 0.0f; + m.y = 0.48f; + m.z = 0.0f; + video_xr_rotate(&p->orientation, &m, &w); + dy = (double)p->position.y + w.y + 0.48; + dz = (double)p->position.z + w.z + 1.8; + return sqrt(dy * dy + dz * dz); +} + +/* The hinge a grab holds is whole, its own edge in the hinge's range, + * not only put in the plane by the flat snap's lines: screen 2, 2 cm + * under screen 1's bottom edge, 2.5 cm in front and tipped 3 degrees + * toward the user, snaps into the plane, a hinge at 0, its own edge + * 5 cm off; flat, 1.5 cm in front, 1.5 cm off. The plane and the gap + * count apart: 0.9 cm under and 2.9 cm in front, clicked flush, it is + * whole; 4 cm over, pulled to 2.9, the snap holds no hinge there; at + * Hinge Gap 1, 0.3 cm over, pulled to 0.4 under by the click's fade, + * the hand's own gap is in the stop's whole range: whole. */ +static void test_hinge_whole(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t h; + hinge_grab(&set, &g, &start, false); + video_xr_pose_identity(&aim); + hinge_turn(&g.rel.orientation, 1.0, 0.0, 0.0, 3.0); + g.turn = g.rel.orientation; + g.rel.position.y = -0.98f; + g.rel.position.z = -1.775f; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + CHECK(video_xr_snap_hinge(&set, g.slot, 2u, g.width, g.height, 0.0f, + 1.0f, &pose, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); + hinge_turn(&g.rel.orientation, 1.0, 0.0, 0.0, 0.0); + g.turn = g.rel.orientation; + g.rel.position.z = -1.785f; + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE); + g.rel.position.y = -0.969f; + g.rel.position.z = -1.771f; + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE); + g.rel.position.y = -0.92f; + g.rel.position.z = -1.785f; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + CHECK(!(video_xr_snap_hinge(&set, g.slot, 2u, g.width, g.height, 0.0f, + 1.0f, &pose, &h) & VIDEO_XR_SNAP_HINGE)); + CHECK(NEAR(hinge_off_line(&pose), 0.029)); + CHECK(!(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE)); + g.gap = 1; + g.rel.position.y = -0.957f; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + CHECK(video_xr_snap_hinge(&set, g.slot, 2u, g.width, g.height, 0.01f, + 1.0f, &pose, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(fabs(h.gap - 0.004) < 1e-5); + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h) + & VIDEO_XR_SNAP_HINGE); +} + +/* The free stick down at full travel, polls 10 ms apart, swings the + * hinged screen about the shared edge, never off it: flat until 5 + * degrees, then catching up, held at each 15 degree step from flat + * over its 2 degree window, to 90, past which it lets go; carried or + * tilting alike. */ +static void test_hinge_swing(void) +{ + unsigned i, t, bits, axes, held; + float rest[2]; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t h; + for (t = 0; t < 2; t++) + { + double worst = 0.0; + hinge_grab(&set, &g, &start, t != 0); + video_xr_pose_identity(&aim); + rest[0] = 0.0f; + rest[1] = 0.0f; + bits = video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && h.axis == VIDEO_XR_STEP_TILT + && fabs(h.angle) < 1e-3); + video_xr_grab_put(&g, &aim, &pose); + for (i = 1; i <= 106; i++) + { + bits = video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + if (!(bits & VIDEO_XR_SNAP_HINGE)) + break; + axes = video_xr_grab_turn_hinge(&g, &aim, &h, 0.0f, -1.0f, 0.01); + held = video_xr_grab_step_hinge(&g, &aim, &h, 15.0f, axes, rest, + &set, 2u, &start); + bits = video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + if ((bits & VIDEO_XR_SNAP_HINGE) && hinge_off_line(&pose) > worst) + worst = hinge_off_line(&pose); + if (i == 6) + CHECK(fabs(h.angle - 0.8) < 0.05); + if (i == 12) + CHECK(fabs(h.angle - 10.8) < 0.05 && !held); + if (i >= 15 && i <= 18) + CHECK(fabs(h.angle - 15.0) < 0.01 && held == VIDEO_XR_STEP_TILT); + if (i == 24) + CHECK(fabs(h.angle - 21.6) < 0.05 && !held); + if (i >= 32 && i <= 35) + CHECK(fabs(h.angle - 30.0) < 0.01 && held == VIDEO_XR_STEP_TILT); + if (i >= 98 && i <= 102) + CHECK(fabs(h.angle - 90.0) < 0.01); + } + CHECK(i == 104 && worst < 1e-4); + } +} + +/* The other axis acts as before: the turn about the vertical through + * screen 2's middle, hinged at 30 degrees, twists its edge off the + * line's way, and past the snap's 5 degrees it is no longer hinged. */ +static void test_hinge_other_axis(void) +{ + unsigned i, bits = 0; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose; + video_xr_hinge_t h; + hinge_grab(&set, &g, &start, false); + video_xr_pose_identity(&aim); + video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + video_xr_grab_put(&g, &aim, &pose); + video_xr_grab_swing(&g, &aim, &h, 30.0); + for (i = 0; i < 7; i++) + { + bits = video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + if (i == 5) + CHECK(bits & VIDEO_XR_SNAP_HINGE); + if (bits & VIDEO_XR_SNAP_HINGE) + CHECK(video_xr_grab_turn_hinge(&g, &aim, &h, 1.0f, 0.0f, 0.01) + == VIDEO_XR_STEP_TURN); + } + CHECK(!(bits & VIDEO_XR_SNAP_HINGE)); +} + +/* A step's buzz arms by the angle from flat: inside 15's window not, + * outside it, and back in by the stick's rest. A commit keeps the + * screen's own angle from flat where the flat snap only fades (shown + * 4, its own 7), the snap's where it is whole. */ +static void test_hinge_step_put(void) +{ + float rest[2]; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, pose, under; + video_xr_hinge_t h, at; + hinge_grab(&set, &g, &start, false); + video_xr_pose_identity(&aim); + video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + video_xr_grab_put(&g, &aim, &pose); + rest[0] = 0.0f; + rest[1] = 0.0f; + video_xr_grab_swing(&g, &aim, &h, 15.0); + CHECK(video_xr_grab_off_hinge(&g, &aim, &h, 15.0f, rest) == 0); + video_xr_grab_swing(&g, &aim, &h, 3.0); + CHECK(video_xr_grab_off_hinge(&g, &aim, &h, 15.0f, rest) + == VIDEO_XR_STEP_TILT); + rest[1] = -3.0f; + CHECK(video_xr_grab_off_hinge(&g, &aim, &h, 15.0f, rest) == 0); + CHECK(video_xr_grab_off_hinge(&g, &aim, &h, 0.0f, rest) + == VIDEO_XR_STEP_TILT); + hinge_grab(&set, &g, &start, false); + video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &h); + video_xr_grab_put(&g, &aim, &pose); + video_xr_grab_swing(&g, &aim, &h, 7.0); + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &pose, &at) + & VIDEO_XR_SNAP_HINGE); + CHECK(fabs(at.angle - 4.0) < 0.05); + video_xr_grab_put_hinge(&g, &aim, &at, &pose, false); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &under, NULL, NULL); + CHECK(fabs(video_xr_hinge_angle(&at, &under.orientation) - 7.0) < 0.01 + && hinge_off_line(&under) < 1e-4); + video_xr_grab_put_hinge(&g, &aim, &at, &pose, true); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &under, NULL, NULL); + CHECK(fabs(video_xr_hinge_angle(&at, &under.orientation) - 4.0) < 0.01); +} + +/* Neither commit moves the shown screen, so a stick push or let-go + * never hops it: screen 2 hinged under screen 1 near flat, at 30 + * either way and near square, or beside it near square, rolled up to + * 4.8 degrees, its edge 1.2 or 2.8 cm off the line and 0 to 8 cm along + * it from the centres' level, carried or tilting, turned or not. + * put_line keeps the angle from flat. */ +static void test_hinge_put_still(void) +{ + static const double tips[] = { 2.0, 7.0, -30.0, 30.0, -86.0, -88.0, + -83.0 }; + static const double rolls[] = { 0.0, 2.0, 4.0, 4.8 }; + static const double xs[] = { 0.0, 0.02, 0.035, 0.045, 0.055, 0.06, + 0.08 }; + static const double gaps[] = { 0.012, 0.028 }; + unsigned t, ti, ri, xi, gi, k; + unsigned n[2]; + double moved[2], turned[2], flat = 0.0; + n[0] = n[1] = 0; + moved[0] = moved[1] = turned[0] = turned[1] = 0.0; + for (t = 0; t < 2; t++) + for (ti = 0; ti < 7; ti++) + for (ri = 0; ri < 4; ri++) + for (xi = 0; xi < 7; xi++) + for (gi = 0; gi < 2; gi++) + { + video_xr_quad_set_t set; + video_xr_grab_t g, was; + video_xr_pose_t aim, start, want, shown, now, under; + video_xr_quat_t a, b, q; + video_xr_hinge_t h; + unsigned bits; + double gp = gaps[gi] / sqrt(2.0); + hinge_grab(&set, &g, &start, t != 0); + hinge_turn(&b, 0.0, 0.0, 1.0, rolls[ri]); + if (ti >= 5) + { + hinge_turn(&a, 0.0, 1.0, 0.0, tips[ti]); + hinge_then(&a, &b, &q); + hinge_place(&want, &q, -0.64, 0.0, 0.8 + gp, xs[xi], -1.8 + gp); + } + else + { + hinge_turn(&a, 1.0, 0.0, 0.0, tips[ti]); + hinge_then(&b, &a, &q); + hinge_place(&want, &q, 0.0, 0.48, xs[xi], -0.48 - gp, -1.8 + gp); + } + video_xr_pose_identity(&aim); + hinge_turn(&aim.orientation, 0.0, 1.0, 0.0, 20.0); + aim.position.x = 0.1f; + aim.position.z = 0.2f; + video_xr_grab_put(&g, &aim, &want); + bits = video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &shown, &h); + if (!(bits & VIDEO_XR_SNAP_HINGE)) + continue; + n[(bits & VIDEO_XR_SNAP_TURN) != 0]++; + was = g; + for (k = 0; k < 2; k++) + { + g = was; + if (k) + video_xr_grab_put_hinge(&g, &aim, &h, &shown, + (bits & VIDEO_XR_SNAP_TURN) != 0); + else + video_xr_grab_put_line(&g, &aim, &h); + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &now, NULL) + & VIDEO_XR_SNAP_HINGE); + if (pose_gap(&now, &shown) > moved[k]) + moved[k] = pose_gap(&now, &shown); + if (turn_apart(&now.orientation, &shown.orientation) > turned[k]) + turned[k] = turn_apart(&now.orientation, &shown.orientation); + } + g = was; + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &under, NULL, NULL); + video_xr_grab_put_line(&g, &aim, &h); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &now, NULL, NULL); + if (fabs(video_xr_hinge_angle(&h, &now.orientation) + - video_xr_hinge_angle(&h, &under.orientation)) > flat) + flat = fabs(video_xr_hinge_angle(&h, &now.orientation) + - video_xr_hinge_angle(&h, &under.orientation)); + } + CHECK(n[0] >= 200 && n[1] >= 80); + if ( moved[0] > 1e-4 || turned[0] > 0.01 || moved[1] > 1e-4 + || turned[1] > 0.01 || flat > 0.01) + { + printf(" FAIL %s:%d: put_line moved %.4f m %.3f deg, put_hinge" + " %.4f m %.3f deg, angle from flat %.3f\n", __FILE__, + __LINE__, moved[0], turned[0], moved[1], turned[1], flat); + failures++; + } +} + +/* Side by side, from 0.8 cm past it, the edges click to each Hinge Gap + * exactly; 1.5 cm past 2 cm, half way, still hinged; square, the corner + * leaves the gap between its plane and the other's edge. */ +static void test_gap_click(void) +{ + static const float gaps[] = { 0.0f, 0.01f, 0.02f, 0.03f, 0.05f, + 0.10f }; + unsigned i; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_hinge_t h; + snap_set(&set); + for (i = 0; i < 6; i++) + { + snap_pose(&p, 1.30 + gaps[i] + 0.008, 0.01, -1.98, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, gaps[i], 1.0f, + &p, &h) == GAP_ALL); + CHECK(vec_is(&p.position, 1.30 + gaps[i], 0.0, -2.0) + && fabs(h.gap - gaps[i]) < 1e-5); + } + snap_pose(&p, 1.335, 0.0, -2.0, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, + NULL) == (VIDEO_XR_SNAP_HINGE | VIDEO_XR_SNAP_TURN + | VIDEO_XR_SNAP_Y | VIDEO_XR_SNAP_Z + | VIDEO_XR_SNAP_PULLING(SNAP_ALL) + | VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(NEAR(p.position.x, 1.33)); + snap_pose(&p, 0.828, 0.01, -1.49, 90.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, + NULL) == GAP_ALL); + CHECK(vec_is(&p.position, 0.82, 0.0, -1.5) + && angle_near((float)snap_yaw(&p), 90.0f)); +} + +/* A hinge's line: along it where the held edge overlaps the target's, + * midway across the gap; on the edges at gap 0; none without overlap. + * Hand-built under screen 1 of hinge_set: a 1.28 m edge 6 cm along and + * 2 cm under runs -0.58 to 0.70 at y -0.49, from either end as its way + * turns; 1.2 m along it stops at screen 1's end, 1.5 m along there is + * none; moved and turned with screen 1, the same ends moved. Snapped: + * the 3DS pair folded 30, the square corner, and side by side 2 cm + * apart. */ +static void test_hinge_line(void) +{ + video_xr_quad_set_t set; + video_xr_quad_t moved; + video_xr_pose_t p, inv, at; + video_xr_quat_t tip, off, q; + video_xr_hinge_t h, m; + video_xr_vec3_t a, b; + t_gap = 0.0f; + hinge_set(&set); + memset(&h, 0, sizeof(h)); + h.point.x = 0.06f; + h.point.y = -0.50f; + h.point.z = -1.8f; + h.dir.x = 1.0f; + h.flat.y = -1.0f; + h.gap = 0.02f; + h.edge = 0; + h.on = 1; + CHECK(video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, -0.58, -0.49, -1.8) && vec_is(&b, 0.70, -0.49, -1.8)); + video_xr_pose_identity(&p); + hinge_turn(&p.orientation, 1.0, 2.0, 0.5, 40.0); + p.position.x = 0.3f; + p.position.y = 1.1f; + p.position.z = 0.4f; + moved = set.quads[0]; + video_xr_compose(&p, &set.quads[0].pose, &moved.pose); + m = h; + video_xr_pose_identity(&at); + at.position = h.point; + video_xr_compose(&p, &at, &at); + m.point = at.position; + video_xr_rotate(&p.orientation, &h.dir, &m.dir); + video_xr_rotate(&p.orientation, &h.flat, &m.flat); + CHECK(video_xr_hinge_line(&m, 1.28f, 0.96f, &moved, &a, &b)); + video_xr_pose_invert(&p, &inv); + at.position = a; + video_xr_compose(&inv, &at, &at); + CHECK(vec_is(&at.position, -0.58, -0.49, -1.8)); + at.position = b; + video_xr_compose(&inv, &at, &at); + CHECK(vec_is(&at.position, 0.70, -0.49, -1.8)); + h.dir.x = -1.0f; + CHECK(video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, 0.70, -0.49, -1.8) && vec_is(&b, -0.58, -0.49, -1.8)); + h.dir.x = 1.0f; + h.point.x = 1.2f; + CHECK(video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, 0.56, -0.49, -1.8) && vec_is(&b, 0.8, -0.49, -1.8)); + h.point.x = 1.5f; + CHECK(!video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + h.point.x = 0.06f; + h.point.y = -0.48f; + h.gap = 0.0f; + CHECK(video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, -0.58, -0.48, -1.8) && vec_is(&b, 0.70, -0.48, -1.8)); + /* The 3DS pair as test_hinge_3ds takes it, folded 30, touching. */ + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + hinge_turn(&off, 0.0, 1.0, 0.0, 3.0); + hinge_then(&off, &tip, &q); + hinge_place(&p, &q, 0.0, 0.48, 0.0, -0.488, -1.787); + CHECK(hinge_snap(&set, &p, 1.0f, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, -0.64, -0.48, -1.8) && vec_is(&b, 0.64, -0.48, -1.8)); + /* The square corner of test_hinge_flush_corner, up screen 1's right + * edge. */ + hinge_turn(&q, 0.0, 1.0, 0.0, 88.0); + hinge_place(&p, &q, 0.64, 0.0, 0.805, 0.01, -1.81); + CHECK(hinge_snap(&set, &p, 1.0f, &h) & VIDEO_XR_SNAP_HINGE); + CHECK(h.edge == 2 + && video_xr_hinge_line(&h, 1.28f, 0.96f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, 0.8, -0.48, -1.8) && vec_is(&b, 0.8, 0.48, -1.8)); + /* Side by side at Hinge Gap 2 cm, as test_gap_click clicks it: up + * the gap's middle. */ + snap_set(&set); + snap_pose(&p, 1.30 + 0.02 + 0.008, 0.01, -1.98, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, &h) + & VIDEO_XR_SNAP_HINGE); + CHECK(video_xr_hinge_line(&h, 1.0f, 0.6f, &set.quads[0], &a, &b)); + CHECK(vec_is(&a, 0.81, -0.3, -2.0) && vec_is(&b, 0.81, 0.3, -2.0)); +} + +/* Metres from v to x, y, z. */ +static double pt_off(const video_xr_vec3_t *v, double x, double y, + double z) +{ + double dx = v->x - x; + double dy = v->y - y; + double dz = v->z - z; + return sqrt(dx * dx + dy * dy + dz * dz); +} + +/* Screen 2 (1.28 x 0.96) at p under screen 1 of set, hinged by k as + * kept: placed on its edges, the middle of its line (false without + * one), and its gap. */ +static bool place_mid(const video_xr_quad_set_t *set, + const video_xr_hinge_t *k, const video_xr_pose_t *p, bool over, + video_xr_vec3_t *mid, float *gap) +{ + video_xr_vec3_t a, b; + video_xr_hinge_t h = *k; + video_xr_hinge_place(&h, p, 1.28f, 0.96f, &set->quads[0], over); + if (!video_xr_hinge_line(&h, 1.28f, 0.96f, &set->quads[0], &a, &b)) + return false; + mid->x = (a.x + b.x) * 0.5f; + mid->y = (a.y + b.y) * 0.5f; + mid->z = (a.z + b.z) * 0.5f; + *gap = h.gap; + return true; +} + +/* A kept hinge's line on the real edges: whole, where the snap's own + * hinge draws it, the 3DS pair folded 30 at gaps 0 and 2; 1 cm and + * 0 apart, midway across; 1 to 3 cm crossed, on the held edge drawn + * over the other, else on the other's; 1 cm under and 3.5 cm in front, + * midway in depth too; turned 3 degrees in the plane, its way midway + * and its middle midway between them. */ +static void test_hinge_place(void) +{ + static const float gaps[] = { 0.0f, 0.02f }; + static const double ys[] = { -0.488, -0.508 }; + unsigned i; + float gap; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t tip; + video_xr_hinge_t k, w; + video_xr_vec3_t a, b, c, m; + hinge_set(&set); + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + for (i = 0; i < 2; i++) + { + hinge_place(&p, &tip, 0.0, 0.48, 0.06, ys[i], -1.787); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.28f, 0.96f, gaps[i], 1.0f, + &p, &k) & VIDEO_XR_SNAP_HINGE); + CHECK(video_xr_hinge_line(&k, 1.28f, 0.96f, &set.quads[0], &a, &b)); + w = k; + video_xr_hinge_place(&w, &p, 1.28f, 0.96f, &set.quads[0], true); + CHECK(video_xr_hinge_line(&w, 1.28f, 0.96f, &set.quads[0], &c, &m)); + CHECK( pt_off(&c, a.x, a.y, a.z) < 1e-6 + && pt_off(&m, b.x, b.y, b.z) < 1e-6 + && fabs(w.gap - k.gap) < 1e-6); + CHECK(place_mid(&set, &k, &p, true, &m, &gap) + && pt_off(&m, 0.06, -0.48 - gaps[i] * 0.5, -1.8) < 1e-5); + } + memset(&k, 0, sizeof(k)); + k.point.y = -0.50f; + k.point.z = -1.8f; + k.dir.x = 1.0f; + k.flat.y = -1.0f; + k.gap = 0.02f; + k.on = 1; + video_xr_pose_identity(&p); + for (i = 0; i < 2; i++) + { + p.position.x = -0.2f; + p.position.y = (float)(-0.96 - 0.01 * i); + p.position.z = -1.8f; + CHECK(place_mid(&set, &k, &p, i == 0, &m, &gap) + && pt_off(&m, -0.18, -0.48 - 0.005 * i, -1.8) < 1e-5 + && fabs(gap - 0.01 * i) < 1e-5); + } + for (i = 1; i <= 3; i++) + { + p.position.y = (float)(-0.96 + 0.01 * i); + CHECK(place_mid(&set, &k, &p, true, &m, &gap) + && pt_off(&m, -0.18, -0.48 + 0.01 * i, -1.8) < 1e-5 + && gap < 1e-5); + CHECK(place_mid(&set, &k, &p, false, &m, &gap) + && pt_off(&m, -0.18, -0.48, -1.8) < 1e-5 + && gap < 1e-5); + } + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.49, -1.765); + CHECK(place_mid(&set, &k, &p, true, &m, &gap) + && pt_off(&m, 0.0, -0.485, -1.7825) < 1e-5 + && fabs(gap - sqrt(0.01 * 0.01 + 0.035 * 0.035)) < 1e-5); + hinge_turn(&p.orientation, 0.0, 0.0, 1.0, 3.0); + hinge_place(&p, &p.orientation, 0.0, 0.48, 0.0, -0.49, -1.8); + w = k; + video_xr_hinge_place(&w, &p, 1.28f, 0.96f, &set.quads[0], true); + CHECK( vec_is(&w.dir, cos(1.5 * M_PI / 180.0), + sin(1.5 * M_PI / 180.0), 0.0) + && place_mid(&set, &k, &p, true, &m, &gap)); + CHECK(fabs(m.y + 0.485) < 5e-4 && fabs(m.z + 1.8) < 1e-5); +} + +/* Elsewhere from touching to 10 cm the gap stays where the hand puts + * it, hinged: side by side at 4 and 8 cm, the 3DS pair at 30 degrees + * at 8; 2.8 cm under clicks to 2, 3.5 to 3 (half way); an edge 2 cm + * over the other's, overlapping, does not hinge. */ +static void test_gap_free(void) +{ + unsigned i, bits; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t tip; + video_xr_hinge_t h; + snap_set(&set); + for (i = 0; i < 2; i++) + { + double x = i ? 1.38 : 1.34; + snap_pose(&p, x, 0.01, -1.98, 0.0); + bits = video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, + &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && !(bits & VIDEO_XR_SNAP_X) + && !(bits & VIDEO_XR_SNAP_GAP)); + CHECK(vec_is(&p.position, x, 0.0, -2.0) + && fabs(h.gap - (x - 1.30)) < 1e-5); + } + hinge_set(&set); + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + t_gap = 0.02f; + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.508, -1.787); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_GAP) && (bits & VIDEO_XR_SNAP_HINGE) + && (bits & VIDEO_XR_SNAP_ALL) == HINGE_LINES); + CHECK(vec_is(&p.position, 0.0, -0.915692, -1.56) + && fabs(h.gap - 0.02) < 1e-5 && fabs(h.angle + 30.0) < 0.01); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.515, -1.787); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && !(bits & VIDEO_XR_SNAP_GAP) + && (bits & VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP))); + CHECK(vec_is(&p.position, 0.0, -0.925692, -1.56) + && fabs(h.gap - 0.03) < 1e-5); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.56, -1.787); + CHECK(hinge_snap(&set, &p, 1.0f, &h) == (VIDEO_XR_SNAP_HINGE + | HINGE_LINES | VIDEO_XR_SNAP_PULLING(HINGE_LINES))); + CHECK(vec_is(&p.position, 0.0, -0.975692, -1.56) + && fabs(h.gap - 0.08) < 1e-5); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.445, -1.787); + CHECK(!(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE)); + t_gap = 0.0f; +} + +/* Carried straight out from the other's edge to 15 cm, at 30 and at + * 7.5 degrees from flat, a 2 cm gap set: hinged to 10 cm exactly, then + * fading, with no step over the carry sweep's 1.5 mm. */ +static void test_gap_fade_out(void) +{ + unsigned i, j, last_hinged; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + video_xr_quat_t tip; + hinge_set(&set); + t_gap = 0.02f; + for (j = 0; j < 2; j++) + { + last_hinged = 0; + hinge_turn(&tip, 1.0, 0.0, 0.0, j ? -7.5 : -30.0); + for (i = 0; i <= 300; i++) + { + hinge_place(&p, &tip, 0.0, 0.48, 0.05, -0.48 - 0.0005 * i, + -1.8); + if (hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE) + last_hinged = i; + if (i) + CHECK(pose_gap(&p, &last) <= 0.0015); + last = p; + } + CHECK(last_hinged == 200); + } + t_gap = 0.0f; +} + +/* Layout k's held screen pushed c metres past touching its target's + * edge (c below 0 apart), 2 cm out of plane: flat side by side, the + * 3DS pair folded 30, square at the corner, or the 3DS pair flat; + * snapped with gap set. Returns the snaps, in *across how far past the + * edge it shows. */ +static unsigned stop_snap(unsigned k, double c, float gap, + double *across) +{ + unsigned bits; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t tip; + video_xr_vec3_t m, w; + if (k == 1 || k == 3) + { + hinge_set(&set); + hinge_turn(&tip, 1.0, 0.0, 0.0, (k == 1) ? -30.0 : 0.0); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.48 + c, -1.787); + bits = video_xr_snap_hinge(&set, 1, 0, 1.28f, 0.96f, gap, 1.0f, + &p, NULL); + m.x = 0.0f; + m.y = 0.48f; + m.z = 0.0f; + video_xr_rotate(&p.orientation, &m, &w); + *across = -0.48 - (p.position.y + w.y); + return bits; + } + snap_set(&set); + if (k == 0) + snap_pose(&p, 1.30 - c, 0.01, -1.98, 0.0); + else + snap_pose(&p, 0.80 - c, 0.01, -1.49, 90.0); + bits = video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, gap, 1.0f, &p, + NULL); + *across = p.position.x - ((k == 0) ? 1.30 : 0.80); + return bits; +} + +/* Hinged, pushed past touching it stops there, at every gap set and + * flat, folded or at a corner: whole 0.5 to 2.9 cm past, partly 4 cm + * past, none from 5.5 (6 at Hinge Gap 1, test_gap_stop_one); 3 mm apart + * it is as the gap leaves it (the click at 0, its fade at 2, free at + * 10). Swept from 1 cm apart to 10 past, nothing hops, the click + * handing over to the stop, which lets go at 4 times the hand at + * most. */ +static void test_gap_stop(void) +{ + static const float gaps[] = { 0.0f, 0.01f, 0.02f, 0.10f }; + static const double apart[] = { 0.0, -1.0, 0.006, 0.003 }; + static const double whole[] = { 0.005, 0.01, 0.02, 0.029 }; + unsigned k, i, j; + for (k = 0; k < 3; k++) + for (i = 0; i < 4; i++) + { + double a, last = 0.0; + unsigned bits; + for (j = 0; j < 4; j++) + { + bits = stop_snap(k, whole[j], gaps[i], &a); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && fabs(a) < 1e-5); + } + bits = stop_snap(k, 0.04, gaps[i], &a); + CHECK(!(bits & VIDEO_XR_SNAP_HINGE) && a < -0.001 && a > -0.0399); + for (j = 0; j < 2; j++) + { + double c = ((i == 1) ? 0.06 : 0.055) + 0.005 * j; + stop_snap(k, c, gaps[i], &a); + CHECK(fabs(a + c) < 1e-5); + } + if (apart[i] >= 0.0) + { + bits = stop_snap(k, -0.003, gaps[i], &a); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && fabs(a - apart[i]) < 1e-5); + } + for (j = 0; j <= 220; j++) + { + stop_snap(k, -0.01 + 0.0005 * j, gaps[i], &a); + if (j) + CHECK(fabs(a - last) <= 0.002); + last = a; + } + } +} + +/* Hinge Gap 1: its click's fade shows touching 5 mm past, so the stop + * holds it there from 5 to 35 mm past, flat, folded or at a corner, + * partly at 4.5 cm and not at all from 6; shaken 3 mm about 1 and + * 3 cm past at 8 Hz, it shows touching on every poll. */ +static void test_gap_stop_one(void) +{ + static const double past[] = { 0.005, 0.01, 0.014, 0.025, 0.034 }; + unsigned k, j; + for (k = 0; k < 3; k++) + { + double a; + double worst = 0.0; + for (j = 0; j < 5; j++) + { + unsigned bits = stop_snap(k, past[j], 0.01f, &a); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && fabs(a) < 1e-5); + } + stop_snap(k, 0.045, 0.01f, &a); + CHECK(a < -0.001 && a > -0.0449); + for (j = 0; j < 900; j++) + { + stop_snap(k, ((j < 450) ? 0.01 : 0.03) + 0.003 * sin(j * 0.56), + 0.01f, &a); + if (fabs(a) > worst) + worst = fabs(a); + } + CHECK(worst < 1e-5); + } +} + +/* Flat, at a corner, stacked and folded 30, at Gaps 3, 5 and 10 (no + * click reaches touching): 3 mm apart the stop's tick bits count as + * pulling, 8 mm apart they do not, and past touching the stop holds. */ +static void test_gap_stop_arm(void) +{ + static const unsigned layouts[] = { 0, 2, 3, 1 }; + static const unsigned axes[] = { VIDEO_XR_SNAP_X, VIDEO_XR_SNAP_Z, + VIDEO_XR_SNAP_Y, VIDEO_XR_SNAP_GAP }; + static const float gaps[] = { 0.03f, 0.05f, 0.10f }; + unsigned k, i; + for (k = 0; k < 4; k++) + for (i = 0; i < 3; i++) + { + double a; + unsigned x = axes[k] | VIDEO_XR_SNAP_GAP; + unsigned bits = stop_snap(layouts[k], -0.003, gaps[i], &a); + CHECK( (bits & VIDEO_XR_SNAP_PULLING(x)) == VIDEO_XR_SNAP_PULLING(x) + && !(bits & x)); + bits = stop_snap(layouts[k], -0.008, gaps[i], &a); + CHECK(!(bits & VIDEO_XR_SNAP_PULLING(x))); + bits = stop_snap(layouts[k], 0.002, gaps[i], &a); + CHECK((bits & x) == x && fabs(a) < 1e-5); + } +} + +/* Flat side by side (corner 0) or square at the corner, the held screen + * turned yaw degrees further about the vertical, the middle of its edge + * c metres past touching the target's; snapped with gap set. */ +static unsigned stop_bits_turned(unsigned corner, double yaw, double c, + float gap) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + snap_set(&set); + hinge_turn(&q, 0.0, 1.0, 0.0, (corner ? 90.0 : 0.0) + yaw); + hinge_place(&p, &q, corner ? 0.5 : -0.5, 0.0, 0.80 - c, 0.01, -1.99); + return video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, gap, 1.0f, &p, + NULL); +} + +/* Pushed 0.5 or 5 mm past touching, flat or at a corner, the hand's turn + * shaken 0.2 degrees at 8 Hz about the turn's whole edge, 5 degrees + * either way, at Hinge Gap 2, 3 and 10 cm: on every poll both of the + * stop's tick bits, its axis's and the gap's, count as pulling, so + * neither re-arms and the tick never repeats. */ +static void test_gap_stop_arm_turned(void) +{ + static const float gaps[] = { 0.02f, 0.03f, 0.10f }; + static const double past[] = { 0.0005, 0.005 }; + unsigned corner, i, j, k, side; + for (corner = 0; corner < 2; corner++) + for (side = 0; side < 2; side++) + for (i = 0; i < 3; i++) + for (j = 0; j < 2; j++) + { + unsigned want = VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_GAP + | (corner ? VIDEO_XR_SNAP_Z : VIDEO_XR_SNAP_X)); + unsigned off = 0; + for (k = 0; k < 450; k++) + { + double yaw = (side ? -5.0 : 5.0) * (1.0 + 0.04 * sin(k * 0.56)); + unsigned bits = stop_bits_turned(corner, yaw, past[j], gaps[i]); + if ((bits & want) != want) + off++; + } + if (off) + printf(" %s %+.0f deg, gap %.0f cm, %.1f mm past: %u polls " + "re-arm\n", corner ? "corner" : "flat", side ? -5.0 : 5.0, + gaps[i] * 100.0, past[j] * 1e3, off); + CHECK(off == 0); + } +} + +/* At the corner, the held screen turned yaw degrees past square, its + * edge's middle c metres past touching the target's; snapped with gap + * set. Returns how far past that middle shows. */ +static double turned_stop_snap(double yaw, double c, float gap) +{ + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_quat_t q; + video_xr_vec3_t m, w; + snap_set(&set); + hinge_turn(&q, 0.0, 1.0, 0.0, 90.0 + yaw); + hinge_place(&p, &q, 0.5, 0.0, 0.80 - c, 0.01, -1.99); + video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, gap, 1.0f, &p, NULL); + m.x = 0.5f; + m.y = 0.0f; + m.z = 0.0f; + video_xr_rotate(&p.orientation, &m, &w); + return 0.80 - (p.position.x + w.x); +} + +/* A corner held up to 5 degrees off square either way, at each gap + * set, pushed from touching to 4 cm past in 0.1 mm steps: the stop + * lets go with the hinge, no step over twice the larger beside it (a + * hop), nor over 1.2 mm, the turn's own swing; up to 3 degrees past + * square, where the turn swings the edge back toward touching, none + * over the 0.4 mm the stop's own fade takes. */ +static void test_gap_stop_turned(void) +{ + static const float gaps[] = { 0.0f, 0.01f, 0.02f, 0.05f, 0.10f }; + unsigned y, i, j; + for (y = 0; y <= 20; y++) + for (i = 0; i < 5; i++) + { + double yaw = -5.0 + 0.5 * y; + double most = (yaw >= 0.0 && yaw <= 3.0) ? 0.0004 : 0.0012; + double last = turned_stop_snap(yaw, 0.0, gaps[i]); + double before = 0.0; + double prev = 0.0; + double worst = 0.0; + double hop = 0.0; + for (j = 1; j <= 401; j++) + { + double step = 0.0; + if (j <= 400) + { + double a = turned_stop_snap(yaw, 0.0001 * j, gaps[i]); + step = fabs(a - last); + last = a; + } + if (j >= 2) + { + double side = (before > step) ? before : step; + if (prev - 2.0 * side > hop) + hop = prev - 2.0 * side; + } + if (step > worst) + worst = step; + before = prev; + prev = step; + } + if (hop > 0.00005 || worst > most) + printf(" yaw %+.1f gap %.0f cm: hop %.2f mm, step %.2f mm\n", + yaw, gaps[i] * 100.0, hop * 1e3, worst * 1e3); + CHECK(hop <= 0.00005 && worst <= most); + } +} + +/* Swung by the stick from flat to square, carried or tilting, a 2 cm + * gap set: taken 2.6 cm under it clicks to 2 and stays; 3.5 under, in + * the click's fade, it shows 3 and stays; 6 under, free, it stays 6. */ +static void test_gap_swing(void) +{ + static const double under[] = { 0.026, 0.035, 0.06 }; + static const double shown[] = { 0.02, 0.03, 0.06 }; + unsigned i, k, u; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, want, now; + video_xr_quat_t tip; + video_xr_hinge_t h; + t_gap = 0.02f; + for (u = 0; u < 3; u++) + for (k = 0; k < 2; k++) + { + float angle = 0.0f; + hinge_grab(&set, &g, &start, k != 0); + hinge_turn(&tip, 1.0, 0.0, 0.0, -0.5); + hinge_place(&want, &tip, 0.0, 0.48, 0.0, -0.48 - under[u], -1.8); + video_xr_pose_identity(&aim); + video_xr_grab_put(&g, &aim, &want); + for (i = 0; i < 89; i++) + { + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &now, &h) + & VIDEO_XR_SNAP_HINGE); + CHECK(fabs(h.gap - shown[u]) < 1e-5); + if (!i) + video_xr_grab_put_line(&g, &aim, &h); + angle = h.angle; + video_xr_grab_turn_hinge(&g, &aim, &h, 0.0f, 1.0f, 1.0 / 90.0); + } + CHECK(angle <= -88.0f); + } + t_gap = 0.0f; +} + +/* Screen 1 turned th degrees about its right edge at x 0.8, and the + * small screen parallel 10 cm right of a screen held 1 cm plus the gap + * g right of screen 1, in quad order o. */ +static void gap_nearer_set(video_xr_quad_set_t *set, double th, double g, + unsigned o) +{ + double r = th * M_PI / 180.0; + video_xr_quad_t *q; + memset(set, 0, sizeof(*set)); + if (o) + snap_add(set, 2, 2.16 + g, 0.0); + q = &set->quads[set->num_quads++]; + snap_pose(&q->pose, 0.8 - 0.8 * cos(r), 0.0, -2.0 + 0.8 * sin(r), th); + q->width = 1.6f; + q->height = 0.9f; + q->kind = VIDEO_XR_QUAD_SCREEN; + if (!o) + snap_add(set, 2, 2.16 + g, 0.0); +} + +/* Of two screens, the nearer edge wins and clicks, in either order: + * tipped 30 degrees, 1 cm under A, level or turned 3, and 9 under C (a + * 2 cm gap set, 2.8 and 9); flat, 1 cm right of screen 1 and 9 cm left + * of a small screen, either a little off parallel, or screen 1 turned + * up to 4.9 degrees, which the held screen turns to. Nearer is from + * where the click puts the edge. */ +static void test_gap_nearer(void) +{ + /* Screen 1's yaw and the small screen's. */ + static const double noise[][2] = { + { 0.0, 0.0 }, { 0.1, 0.0 }, { 0.0, 0.1 }, { 0.001, 0.0 } + }; + static const double turned[] = { 0.6, 1.0, 3.0, 4.9 }; + unsigned i, j, o, n, bits; + video_xr_quad_set_t set, one; + video_xr_pose_t p; + video_xr_quat_t tip; + video_xr_hinge_t h; + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + for (i = 0; i < 2; i++) + for (o = 0; o < 2; o++) + { + double ya = i ? -0.472 : -0.49; + memset(&set, 0, sizeof(set)); + for (j = 0; j < 2; j++) + { + bool a = j == o; + snap_add(&set, a ? 0 : 2, a ? -0.32 : 0.32, 0.0); + set.quads[j].pose.position.y = (float)((a ? ya : -0.41) + 0.2); + set.quads[j].pose.position.z = -1.8f; + set.quads[j].width = 0.6f; + } + t_gap = i ? 0.02f : 0.0f; + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.5, -1.8); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && (bits & VIDEO_XR_SNAP_GAP)); + CHECK(NEAR(h.point.y, ya - t_gap) && fabs(h.gap - t_gap) < 1e-5); + /* With screen 1 turned 3 degrees, inside the whole snap, its edge + * still wins over the parallel one. */ + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.5, -1.8); + snap_pose(&set.quads[o].pose, -0.32, ya + 0.2, -1.8, 3.0); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && (bits & VIDEO_XR_SNAP_GAP) + && h.slot == 0); + } + t_gap = 0.0f; + snap_set(&one); + for (n = 0; n < 4; n++) + for (o = 0; o < 2; o++) + { + double r = noise[n][0] * M_PI / 180.0; + snap_pose(&one.quads[0].pose, 0.0, 0.0, -2.0, noise[n][0]); + memset(&set, 0, sizeof(set)); + if (!o) + set.quads[set.num_quads++] = one.quads[0]; + snap_add(&set, 2, 2.15, noise[n][1]); + if (o) + set.quads[set.num_quads++] = one.quads[0]; + snap_pose(&p, 1.31, 0.0, -2.0, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.0f, 1.0f, &p, + &h) == GAP_ALL); + CHECK(h.edge == 3 && h.slot == 0 && h.on == 2 + && vec_is(&p.position, 1.30 * cos(r), 0.0, + -2.0 - 1.30 * sin(r))); + } + /* Screen 1 turned up to 4.9 degrees about its right edge, the small + * screen 10 cm out and parallel: both are in the turn's whole snap, + * so the nearer wins and the held screen turns to it, in either + * order, at gaps 0 and 2. */ + for (n = 0; n < 4; n++) + for (i = 0; i < 2; i++) + for (o = 0; o < 2; o++) + { + double g = i ? 0.02 : 0.0; + double r = turned[n] * M_PI / 180.0; + gap_nearer_set(&set, turned[n], g, o); + snap_pose(&p, 1.31 + g, 0.0, -2.0, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, (float)g, 1.0f, + &p, &h) == GAP_ALL); + CHECK(h.edge == 3 && h.slot == 0 && h.on == 2 + && fabs(snap_yaw(&p) - turned[n]) < 1e-3 + && vec_is(&p.position, 0.8 + (0.5 + g) * cos(r), 0.0, + -2.0 - (0.5 + g) * sin(r))); + } + /* Nearer is the real distance from where the click puts the edge: + * with 2 cm set, screen 1 touching is 2 cm off it and the small + * screen 2.5 cm out only 0.5, so the turn and the hinge both go to + * the small one, in either order. */ + snap_pose(&one.quads[0].pose, 0.0, 0.0, -2.0, 0.0); + for (o = 0; o < 2; o++) + { + memset(&set, 0, sizeof(set)); + if (!o) + set.quads[set.num_quads++] = one.quads[0]; + snap_add(&set, 2, 2.075, 0.0); + if (o) + set.quads[set.num_quads++] = one.quads[0]; + snap_pose(&p, 1.30, 0.0, -2.0, 0.0); + CHECK(video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, + &h) == GAP_ALL); + CHECK(h.edge == 2 && h.slot == 2 && h.on == 3 + && vec_is(&p.position, 1.305, 0.0, -2.0)); + } +} + +/* gap_nearer_set's screen 1 turned from 0 to 6 degrees, in either order + * and at gaps 0 and 2: through the turn's whole snap the held screen + * follows it, hinged with the gap, no step more than twice screen 1's; + * past it the small screen's whole turn wins, a switch as big as the + * turn between the two. Fading in at 3 degrees, it turns toward + * screen 1 alone, smoothly. */ +static void test_gap_nearer_sweep(void) +{ + unsigned i, j, o, bits; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + video_xr_hinge_t h; + for (j = 0; j < 2; j++) + for (o = 0; o < 2; o++) + { + double g = j ? 0.02 : 0.0; + for (i = 0; i < 1200; i++) + { + double th = 0.0025 + 0.005 * i; + gap_nearer_set(&set, th, g, o); + snap_pose(&p, 1.31 + g, 0.0, -2.0, 0.0); + bits = video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, (float)g, 1.0f, + &p, &h); + if (th < 5.0) + CHECK(bits == GAP_ALL && h.slot == 0 + && fabs(snap_yaw(&p) - th) < 1e-3); + else + CHECK(fabs(snap_yaw(&p)) < 1e-3); + if (i && (th < 5.0 || th > 5.005)) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 0.0101 + && pose_gap(&p, &last) <= 0.0005); + else if (i) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 5.01); + last = p; + } + for (i = 0; i <= 100; i++) + { + gap_nearer_set(&set, 3.0, g, o); + snap_pose(&p, 1.31 + g, 0.0, -2.0, 0.0); + video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, (float)g, 0.01f * i, + &p, &h); + CHECK(fabs(snap_yaw(&p) - 0.03 * i) < 1e-3); + if (i) + CHECK(pose_gap(&p, &last) <= 0.001); + last = p; + } + } +} + +/* Hinged to A at a free 4 to 9 cm (a 2 cm gap set) or touching (5 or + * 10 set), with a larger screen stacked 2 to 9 cm behind and turned 3 + * or 4.9 degrees, inside the turn's whole snap, or one turned 3 corner + * to corner 1 cm right, its edges off the held one's ends: A's edge is + * the nearer, so the held screen neither turns nor lets go of A, in + * either order. */ +static void test_gap_nearer_stacked(void) +{ + static const double gf[][2] = { + { 0.02, 0.04 }, { 0.02, 0.08 }, { 0.02, 0.09 }, { 0.05, 0.0 }, + { 0.10, 0.0 } + }; + static const double yaw[] = { 3.0, -3.0, 4.9 }; + unsigned i, j, k, o; + video_xr_quad_set_t set; + video_xr_pose_t p; + video_xr_hinge_t h; + for (i = 0; i < 5; i++) + for (j = 0; j < 3; j++) + for (o = 0; o < 2; o++) + for (k = 0; k <= 70; k++) + { + memset(&set, 0, sizeof(set)); + if (o) + snap_add(&set, 2, 0.05, yaw[j]); + snap_add(&set, 0, -0.5 - gf[i][1], 0.0); + if (!o) + snap_add(&set, 2, 0.05, yaw[j]); + set.quads[!o].pose.position.z = (float)(-2.02 - 0.001 * k); + set.quads[!o].width = 0.6f; + set.quads[!o].height = 0.5f; + snap_pose(&p, 0.0, 0.0, -2.0, 0.0); + CHECK((video_xr_snap_hinge(&set, 1, 0, 0.5f, 0.4f, (float)gf[i][0], + 1.0f, &p, &h) & VIDEO_XR_SNAP_HINGE) + && h.slot == 0 && fabs(h.gap - gf[i][1]) < 1e-5); + CHECK(fabs(snap_yaw(&p)) < 1e-3 && vec_is(&p.position, 0.0, 0.0, + -2.0)); + } + /* The corner's x line clicks the held screen 1 cm left to the gap. */ + for (i = 0; i < 4; i++) + for (j = 0; j < 2; j++) + for (o = 0; o < 2; o++) + { + double fr = (i & 1) ? 0.09 : 0.06; + memset(&set, 0, sizeof(set)); + if (o) + snap_add(&set, 2, 0.51, j ? 3.0 : -3.0); + snap_add(&set, 0, -0.5 - fr, 0.0); + if (!o) + snap_add(&set, 2, 0.51, j ? 3.0 : -3.0); + set.quads[!o].pose.position.y = (i & 2) ? 0.41f : 0.4f; + snap_pose(&p, 0.0, 0.0, -2.0, 0.0); + CHECK((video_xr_snap_hinge(&set, 1, 0, 0.5f, 0.4f, 0.02f, 1.0f, &p, + &h) & VIDEO_XR_SNAP_HINGE) + && h.slot == 0 && fabs(h.gap - (fr - 0.01)) < 1e-5); + CHECK(fabs(snap_yaw(&p)) < 1e-3 && vec_is(&p.position, -0.01, 0.0, + -2.0)); + } +} + +/* Square at A's corner, a 2 cm gap set, turned by hand from 4 degrees + * short of square to 5 past, with C on the side wall up to 2 mm short + * of its click and 0.5 to 2 degrees off square, or off where the hand + * is 4 past: the held screen stays square, hinged to A, in either + * order. */ +static void test_gap_nearer_corner(void) +{ + static const double gc[] = { 0.018, 0.019, 0.02 }; + static const double dc[] = { 0.5, 1.0, 2.0 }; + unsigned i, j, k, o, w; + video_xr_quad_set_t set; + video_xr_pose_t p, last; + video_xr_hinge_t h; + for (w = 0; w < 2; w++) + for (i = 0; i < 3; i++) + for (j = 0; j < 3; j++) + for (o = 0; o < 2; o++) + for (k = 0; k < 900; k++) + { + double at = w ? -94.0 : -90.0; + double c = at * M_PI / 180.0; + double e = (at + (w ? dc[j] : 0.0)) * M_PI / 180.0; + double psi = -86.0 - 0.01 * k; + double r = psi * M_PI / 180.0; + memset(&set, 0, sizeof(set)); + if (o) + snap_add(&set, 2, 0.0, at + dc[j]); + snap_add(&set, 0, 0.0, 0.0); + if (!o) + snap_add(&set, 2, 0.0, at + dc[j]); + /* Turned about its middle, or 4 past about its near edge. */ + set.quads[!o].pose.position.x = (float)(0.27 + + (0.5 + gc[i]) * cos(c) + 0.25 * cos(e)); + set.quads[!o].pose.position.z = (float)(-2.0 + - (0.5 + gc[i]) * sin(c) - 0.25 * sin(e)); + snap_pose(&p, 0.27 + 0.25 * cos(r), 0.0, -2.0 - 0.25 * sin(r), psi); + CHECK((video_xr_snap_hinge(&set, 1, 0, 0.5f, 0.4f, 0.02f, 1.0f, &p, + &h) & VIDEO_XR_SNAP_HINGE) && h.slot == 0); + CHECK(fabs(snap_yaw(&p) + 90.0) < 1e-3); + if (k) + CHECK(fabs(snap_yaw(&p) - snap_yaw(&last)) <= 0.101 + && pose_gap(&p, &last) <= 0.005); + last = p; + } +} + +/* A commit takes a gap the click holds to the set one, which it shows + * already, and leaves one in the click's fade or free where the hand + * has it: a 2 cm gap set, taken 2.6, 3.5 and 6 cm under at 30 degrees, + * shown at 2, 3 and 6 before and after. */ +static void test_gap_commit(void) +{ + static const double under[] = { 0.026, 0.035, 0.06 }; + static const double kept[] = { 0.02, 0.035, 0.06 }; + unsigned u; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, start, want, shown, now, hand; + video_xr_quat_t tip; + video_xr_hinge_t h; + t_gap = 0.02f; + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + video_xr_pose_identity(&aim); + for (u = 0; u < 3; u++) + { + hinge_grab(&set, &g, &start, false); + hinge_place(&want, &tip, 0.0, 0.48, 0.0, -0.48 - under[u], -1.8); + video_xr_grab_put(&g, &aim, &want); + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &shown, &h) + & VIDEO_XR_SNAP_HINGE); + video_xr_grab_put_line(&g, &aim, &h); + CHECK(video_xr_grab_hinge(&g, &aim, &set, 2u, &start, &now, NULL) + & VIDEO_XR_SNAP_HINGE); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &hand, NULL, NULL); + CHECK(pose_gap(&now, &shown) < 1e-5 + && fabs(hinge_off_line(&hand) - kept[u]) < 1e-5); + } + t_gap = 0.0f; +} + +/* Square, 8 cm out with a 2 cm gap set, the corner puts its edge in + * the other's plane and leaves the gap; with that gap, 88 degrees from + * flat hinges and 92 does not; 13.5 cm under at 30 degrees nothing + * moves it. */ +static void test_gap_limits(void) +{ + unsigned bits; + video_xr_quad_set_t set; + video_xr_pose_t p, was; + video_xr_quat_t tip; + video_xr_hinge_t h; + snap_set(&set); + snap_pose(&p, 0.88, 0.01, -1.49, 90.0); + bits = video_xr_snap_hinge(&set, 1, 0, 1.0f, 0.6f, 0.02f, 1.0f, &p, + &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && (bits & VIDEO_XR_SNAP_X) + && !(bits & (VIDEO_XR_SNAP_Z | VIDEO_XR_SNAP_GAP))); + CHECK(vec_is(&p.position, 0.88, 0.0, -1.5) && fabs(h.gap - 0.08) < 1e-5 + && angle_near((float)snap_yaw(&p), 90.0f)); + hinge_set(&set); + t_gap = 0.02f; + hinge_turn(&tip, 1.0, 0.0, 0.0, -88.0); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.5, -1.8); + bits = hinge_snap(&set, &p, 1.0f, &h); + CHECK((bits & VIDEO_XR_SNAP_HINGE) && (bits & VIDEO_XR_SNAP_GAP) + && fabs(h.angle + 88.0) < 0.01); + hinge_turn(&tip, 1.0, 0.0, 0.0, -92.0); + hinge_place(&p, &tip, 0.0, 0.48, 0.0, -0.5, -1.8); + CHECK(!(hinge_snap(&set, &p, 1.0f, NULL) & VIDEO_XR_SNAP_HINGE)); + hinge_turn(&tip, 1.0, 0.0, 0.0, -30.0); + hinge_place(&p, &tip, 0.0, 0.48, 0.05, -0.615, -1.8); + was = p; + CHECK(hinge_snap(&set, &p, 1.0f, NULL) == 0 && pose_same(&p, &was)); + t_gap = 0.0f; +} + +/* The hinge's sweeps again with a gap: no hop or slide, no creep, a + * commit never moves it (test_hinge_continuous, test_hinge_roll_band, + * test_hinge_put_still at 2, 5 and 8 cm). */ +static void test_gap_sweeps(void) +{ + static const float gaps[] = { 0.02f, 0.05f, 0.08f }; + unsigned i; + for (i = 0; i < 3; i++) + { + t_gap = gaps[i]; + test_hinge_continuous(); + test_hinge_roll_band(); + test_hinge_put_still(); + } + t_gap = 0.0f; +} + +/* In the grab: snapped after levelling, its settings from the snapped + * pose, no clamp for a snap, the clamp still winning; the XR thread's + * call shape (no settings, no snaps) gives the same pose. Taken 10.8 cm + * from where the hand now puts it, so the fade-in is whole. */ +static void test_grab_snap(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, bare, start; + video_screen_xr_t s; + unsigned snapped = 0; + snap_set(&set); + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&g.rel); + g.rel.position.z = -2.0f; + g.turn = g.rel.orientation; + g.base.z = -2.0f; + g.width = 1.0f; + g.height = 0.6f; + g.slot = 1; + g.state = VIDEO_XR_GRAB_HELD; + video_xr_pose_identity(&start); + start.position.x = 1.2f; + start.position.z = -2.0f; + video_xr_pose_identity(&aim); + aim.position.x = 1.308f; + aim.position.y = 0.01f; + aim.position.z = 0.02f; + CHECK(!video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, + &snapped)); + CHECK((snapped & VIDEO_XR_SNAP_ALL) == SNAP_ALL + && vec_is(&pose.position, 1.30, 0.0, -2.0)); + CHECK(NEAR(s.x, 1.30) && NEAR(s.y, 0.0) && NEAR(s.z, 0.0)); + CHECK(!video_xr_grab_pose(&g, &aim, &set, 0, &start, &bare, NULL, NULL)); + CHECK(!memcmp(&pose, &bare, sizeof(pose))); + CHECK(!video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, &snapped)); + CHECK(snapped == 0 && vec_is(&pose.position, 1.308, 0.01, -1.98)); + video_xr_grab_pose(&g, &aim, &set, 1u, &start, &pose, &s, &snapped); + CHECK(snapped == 0 && vec_is(&pose.position, 1.308, 0.01, -1.98)); + /* A snap past +10 m: the clamp stops it there, and says so. */ + set.quads[0].pose.position.x = 11.303f; + aim.position.x = 9.995f; + aim.position.y = 0.0f; + aim.position.z = 0.0f; + CHECK(video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, + &snapped)); + CHECK(NEAR(s.x, 10.0) && NEAR(pose.position.x, 10.0)); +} + +/* Turned by the stick into square at screen 1's right corner, a held + * screen snaps exactly square there and says so. */ +static void test_turn_snap(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, start; + video_screen_xr_t s; + unsigned snapped = 0; + snap_set(&set); + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + snap_pose(&g.rel, 0.773, 0.0, -1.5, 82.0); + g.base = g.rel.position; + g.turn = g.rel.orientation; + g.width = 1.0f; + g.height = 0.6f; + g.slot = 1; + /* Taken a metre away, so the snaps are whole. */ + snap_pose(&start, 0.8, 0.0, -0.5, 82.0); + video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, &snapped); + CHECK(!(snapped & VIDEO_XR_SNAP_TURN) + && (snapped & VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_TURN)) + && !angle_near((float)snap_yaw(&pose), 90.0f)); + /* Four degrees more by the stick: square, at the corner. */ + video_xr_grab_turn(&g, &aim, 1.0f, 0.0f, 4.0 / VIDEO_XR_EDIT_TURN_RATE); + video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, &snapped); + CHECK((snapped & VIDEO_XR_SNAP_ALL) == SNAP_ALL + && angle_near(s.yaw, 90.0f) + && vec_is(&pose.position, 0.8, 0.0, -1.5)); +} + +/* Tilted by the stick into parallel beside screen 1, a held screen + * snaps exactly parallel and says so, as a turn does. */ +static void test_tilt_snap(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, start; + video_screen_xr_t s; + unsigned snapped = 0; + snap_set(&set); + memset(&g, 0, sizeof(g)); + video_xr_pose_identity(&aim); + axis_turn(&g.rel, 0, 8.0); + g.rel.position.x = 1.3f; + g.rel.position.z = -2.0f; + g.base = g.rel.position; + g.turn = g.rel.orientation; + g.width = 1.0f; + g.height = 0.6f; + g.slot = 1; + /* Taken a metre away, so the snaps are whole. */ + start = g.rel; + start.position.z = -1.0f; + video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, &snapped); + CHECK(!(snapped & VIDEO_XR_SNAP_TURN) + && (snapped & VIDEO_XR_SNAP_PULLING(VIDEO_XR_SNAP_TURN)) + && !angle_near(s.pitch, 0.0f)); + /* Four degrees back by the stick: parallel. */ + video_xr_grab_turn(&g, &aim, 0.0f, 1.0f, 4.0 / VIDEO_XR_EDIT_TURN_RATE); + video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, &s, &snapped); + CHECK((snapped & VIDEO_XR_SNAP_TURN) + && turn_apart(&pose.orientation, &aim.orientation) < 0.01); +} + +/* The angle the free hand's stick has given, as steps every size + * degrees show it: within VIDEO_XR_EDIT_TURN_HOLD of a step, the step, + * and *held. Size 0: no steps. */ +static double step_shown(double deg, double size, int *held) +{ + double s; + *held = 0; + if (size <= 0.0) + return deg; + s = floor(deg / size + 0.5) * size; + *held = fabs(deg - s) <= VIDEO_XR_EDIT_TURN_HOLD; + return *held ? s : deg; +} + +/* Degrees between two angles, either side of +-180. */ +static double angle_off(double a, double b) +{ + double d = fmod(fabs(a - b), 360.0); + return (d > 180.0) ? 360.0 - d : d; +} + +/* g holding an item at x, y, z turned yaw then pitch degrees, taken + * with aim. */ +static void step_grab(video_xr_grab_t *g, const video_xr_pose_t *aim, + double x, double y, double z, double yaw, double pitch) +{ + video_xr_pose_t item, tip, inv; + memset(g, 0, sizeof(*g)); + snap_pose(&item, x, y, z, yaw); + axis_turn(&tip, 0, pitch); + video_xr_compose(&item, &tip, &item); + g->base = item.position; + video_xr_pose_invert(aim, &inv); + video_xr_compose(&inv, &item, &g->rel); + video_xr_grab_hold(g, aim, &item.orientation); +} + +/* The free hand's stick for 10 ms at x, y on g's item, then its steps + * every size degrees from facing head; *buzzes counts the steps taken + * with their buzz armed, as the main thread arms it in *armed. */ +static unsigned step_stick(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_vec3_t *head, float size, float *rest, float x, + float y, unsigned *armed, unsigned *buzzes) +{ + unsigned held; + *armed |= video_xr_grab_off_step(g, aim, head, size, rest); + held = video_xr_grab_step(g, aim, head, size, + video_xr_grab_turn(g, aim, x, y, 0.01), rest, NULL, 0, NULL, + NULL); + if (held & *armed) + (*buzzes)++; + *armed &= ~held; + return held; +} + +/* Turned by the free hand's stick, an item off to the side steps every + * size degrees from facing the head across the floor, from where the + * head is, not where the hand points: within VIDEO_XR_EDIT_TURN_HOLD + * of a step it holds there, and pushed on, it leaves it, its buzz armed + * again for the next step it comes to. Left, then right, and on round a + * whole turn. Size 0: never held. */ +static void test_step_turn_at(float size) +{ + unsigned k, held, bad = 0, holds = 0, faced = 0, armed = 0, buzzes = 0; + unsigned entered = 0; + int was_at = 0; + float rest[2]; + double zero, deg = 20.1; + video_xr_grab_t g; + video_xr_pose_t aim, pose; + video_xr_vec3_t head; + video_screen_xr_t s; + rest[0] = 0.0f; + rest[1] = 0.0f; + head.x = -0.3f; + head.y = 0.2f; + head.z = 0.4f; + /* The yaw that faces the head from 0.9, -1.8. */ + zero = atan2(-1.2, 2.2) * 180.0 / M_PI; + turn_aim(&aim, 30.0f, 10.0f, 0.0f); + step_grab(&g, &aim, 0.9, 0.0, -1.8, zero + deg, 0.0); + for (k = 0; k < 600; k++) + { + float x = (k < 100) ? -1.0f : 1.0f; + int at; + double want; + deg += 0.9 * x; + want = step_shown(deg, size, &at); + held = step_stick(&g, &aim, &head, size, rest, x, 0.0f, + &armed, &buzzes); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, &s, NULL); + if (held) + holds++; + if (at && !was_at) + entered++; + was_at = at; + if ( angle_off(s.yaw, zero + want) > 0.02 + || held != (at ? VIDEO_XR_STEP_TURN : 0u) + || !vec_is(&pose.position, 0.9, 0.0, -1.8)) + { + if (!bad++) + printf(" FAIL %s:%d: size %.0f frame %u at %.2f: yaw %.3f" + " held %u, want %.3f\n", __FILE__, __LINE__, size, k, + deg, s.yaw, held, zero + want); + } + if (size > 0.0f && want == 0.0) + { + /* At 0, its front looks at the head across the floor. */ + video_xr_vec3_t v, front; + v.x = 0.0f; + v.y = 0.0f; + v.z = 1.0f; + video_xr_rotate(&pose.orientation, &v, &front); + if ( fabs(front.x * (head.z - pose.position.z) + - front.z * (head.x - pose.position.x)) < 1e-3 + && front.x * (head.x - pose.position.x) + + front.z * (head.z - pose.position.z) > 0.0) + faced++; + else + bad++; + } + } + CHECK(bad == 0); + /* One buzz a step, about four frames each. */ + CHECK(buzzes == entered && holds >= 4 * entered && holds <= 5 * entered); + if (size > 0.0f) + CHECK(entered >= (unsigned)(500.0f / size) && faced >= 8); + else + CHECK(holds == 0 && buzzes == 0); +} + +static void test_step_turn(void) +{ + test_step_turn_at(15.0f); + test_step_turn_at(30.0f); + test_step_turn_at(45.0f); + test_step_turn_at(90.0f); + test_step_turn_at(0.0f); +} + +/* Tilted by the free hand's stick, an item steps every size degrees + * from upright about its own left-right line, holding within + * VIDEO_XR_EDIT_TURN_HOLD of each and buzzing once each time it comes + * to one, either way and on all the way round; carried or with Tilt + * held. Size 0: never held. */ +static void test_step_tilt_at(float size) +{ + unsigned tilt; + for (tilt = 0; tilt < 2; tilt++) + { + unsigned k, held, bad = 0, holds = 0, armed = 0, buzzes = 0; + unsigned entered = 0; + int was_at = 0; + float rest[2]; + double deg = -6.3; + video_xr_grab_t g; + video_xr_pose_t aim, pose, want_q, tip; + rest[0] = 0.0f; + rest[1] = 0.0f; + turn_aim(&aim, -20.0f, 15.0f, 5.0f); + step_grab(&g, &aim, 0.4, 0.1, -1.8, 25.0, deg); + g.tilt = tilt != 0; + for (k = 0; k < 480; k++) + { + /* Down 30 frames, then up 450: a whole turn and more. */ + float y = (k < 30) ? -1.0f : 1.0f; + int at; + double want; + deg -= 0.9 * y; + want = step_shown(deg, size, &at); + held = step_stick(&g, &aim, NULL, size, rest, 0.0f, y, &armed, + &buzzes); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + snap_pose(&want_q, 0.0, 0.0, 0.0, 25.0); + axis_turn(&tip, 0, want); + video_xr_compose(&want_q, &tip, &want_q); + if (held) + holds++; + if (at && !was_at) + entered++; + was_at = at; + if ( turn_apart(&pose.orientation, &want_q.orientation) > 0.02 + || held != (at ? VIDEO_XR_STEP_TILT : 0u) + || !vec_is(&pose.position, 0.4, 0.1, -1.8)) + { + if (!bad++) + printf(" FAIL %s:%d: size %.0f tilt %u frame %u at %.2f:" + " %.3f off, held %u\n", __FILE__, __LINE__, size, + tilt, k, deg, + turn_apart(&pose.orientation, &want_q.orientation), + held); + } + } + CHECK(bad == 0); + CHECK(buzzes == entered && holds >= 4 * entered + && holds <= 5 * entered); + if (size > 0.0f) + CHECK(entered >= (unsigned)(400.0f / size)); + else + CHECK(holds == 0 && buzzes == 0); + } +} + +static void test_step_tilt(void) +{ + test_step_tilt_at(15.0f); + test_step_tilt_at(30.0f); + test_step_tilt_at(45.0f); + test_step_tilt_at(90.0f); + test_step_tilt_at(0.0f); +} + +/* Lying flat, face up, an item still turns in steps from facing the + * head, its top away from it at 0, and never to NaN. Its heading there + * blends in a little of its front's, so a little off. */ +static void test_step_flat(void) +{ + unsigned k, held, bad = 0, armed = 0, buzzes = 0; + float rest[2]; + double deg = 0.0; + video_xr_grab_t g; + video_xr_pose_t aim, pose, want_q, tip; + video_xr_vec3_t head; + rest[0] = 0.0f; + rest[1] = 0.0f; + head.x = 0.0f; + head.y = 0.0f; + head.z = 0.0f; + video_xr_pose_identity(&aim); + step_grab(&g, &aim, 0.0, -0.5, -1.8, 0.0, -90.0); + for (k = 0; k < 50; k++) + { + int at; + double want; + deg += 0.9; + want = step_shown(deg, 15.0, &at); + held = step_stick(&g, &aim, &head, 15.0f, rest, 1.0f, 0.0f, &armed, + &buzzes); + video_xr_grab_pose(&g, &aim, NULL, 0, NULL, &pose, NULL, NULL); + snap_pose(&want_q, 0.0, 0.0, 0.0, want); + axis_turn(&tip, 0, -90.0); + video_xr_compose(&want_q, &tip, &want_q); + if ( !(turn_apart(&pose.orientation, &want_q.orientation) < 0.2) + || held != (at ? VIDEO_XR_STEP_TURN : 0u)) + { + if (!bad++) + printf(" FAIL %s:%d: frame %u at %.2f: %.3f off\n", __FILE__, + __LINE__, k, deg, + turn_apart(&pose.orientation, &want_q.orientation)); + } + } + CHECK(bad == 0); +} + +/* A snap to another screen that would engage beats a step: turned on + * by the stick from holding at a step 6.5 degrees short of parallel, + * the held screen snaps parallel once the stick has taken it within + * VIDEO_XR_SNAP_ANGLE, still inside the step's hold. */ +static void test_step_snap_first(void) +{ + unsigned k, held[7], snapped = 0; + float rest[2]; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, start; + video_xr_vec3_t head; + rest[0] = 0.0f; + rest[1] = 0.0f; + snap_set(&set); + video_xr_pose_identity(&aim); + /* Facing the head 23.5 degrees left of parallel: the 30 step is + * 6.5 right of it. */ + head.x = (float)(1.3 - 2.0 * tan(23.5 * M_PI / 180.0)); + head.y = 0.0f; + head.z = 0.0f; + step_grab(&g, &aim, 1.3, 0.0, -2.0, 10.0, 0.0); + g.width = 1.0f; + g.height = 0.6f; + g.slot = 1; + /* Taken a metre away, so the snaps are whole. */ + snap_pose(&start, 1.3, 0.0, -1.0, 10.0); + for (k = 1; k <= 6; k++) + { + held[k] = video_xr_grab_step(&g, &aim, &head, 15.0f, + video_xr_grab_turn(&g, &aim, -1.0f, 0.0f, 0.01), rest, &set, 0, + &start, NULL); + video_xr_grab_pose(&g, &aim, &set, 0, &start, &pose, NULL, &snapped); + } + /* 31.7 to 29.0 held at 30; at 28.1, 4.6 from parallel, the snap. */ + CHECK(held[1] == 0 && held[2] == VIDEO_XR_STEP_TURN + && held[5] == VIDEO_XR_STEP_TURN); + CHECK(held[6] == 0 && (snapped & VIDEO_XR_SNAP_TURN) + && turn_apart(&pose.orientation, &aim.orientation) < 0.01 + && rest[0] == 0.0f); +} + +/* g's item where aim puts it with no snap, and the point x, y of it, + * in its own frame, where that puts it. */ +static void lid_edge(const video_xr_grab_t *g, const video_xr_pose_t *aim, + float x, float y, video_xr_pose_t *pose, video_xr_vec3_t *edge) +{ + video_xr_vec3_t v; + v.x = x; + v.y = y; + v.z = 0.0f; + video_xr_grab_pose(g, aim, NULL, 0, NULL, pose, NULL, NULL); + video_xr_rotate(&pose->orientation, &v, edge); + edge->x += pose->position.x; + edge->y += pose->position.y; + edge->z += pose->position.z; +} + +/* Let go off a hinge on its top edge, an item tilted 1.7 degrees past + * the 90 step holds there about that edge, as a lid: the hold catching + * it, holding it again and letting it go leave that edge where it was. + * With no lid, or a lid on the turn, about its middle. Turned 1.7 past + * facing square to the head, one let go off its right edge holds about + * that edge. */ +static void test_step_lid(void) +{ + unsigned k, held, again, off; + float rest[2]; + video_xr_grab_t g; + video_xr_hinge_t lid; + video_xr_pose_t aim, pose, want_q, tip; + video_xr_vec3_t e0, e1, e2, e3, head; + turn_aim(&aim, -20.0f, 15.0f, 5.0f); + memset(&lid, 0, sizeof(lid)); + for (k = 0; k < 3; k++) + { + rest[0] = 0.0f; + rest[1] = 0.0f; + lid.edge = 0; + lid.axis = (k == 2) ? VIDEO_XR_STEP_TURN : VIDEO_XR_STEP_TILT; + step_grab(&g, &aim, 0.4, 0.1, -1.8, 25.0, -91.7); + g.width = 1.0f; + g.height = 0.6f; + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e0); + held = video_xr_grab_step(&g, &aim, NULL, 15.0f, VIDEO_XR_STEP_TILT, + rest, NULL, 0, NULL, k ? &lid : NULL); + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e1); + snap_pose(&want_q, 0.0, 0.0, 0.0, 25.0); + axis_turn(&tip, 0, -90.0); + video_xr_compose(&want_q, &tip, &want_q); + CHECK(held == VIDEO_XR_STEP_TILT && NEAR(rest[1], -1.7) + && turn_apart(&pose.orientation, &want_q.orientation) < 0.02); + /* 2 sin 0.85 of 0.3 m: the edge or the middle moves 8.9 mm. */ + if (k == 1) + CHECK(dist(&e0, &e1) < 1e-5 + && !vec_is(&pose.position, 0.4, 0.1, -1.8) + && fabs(sqrt(pow(pose.position.x - 0.4, 2.0) + + pow(pose.position.y - 0.1, 2.0) + + pow(pose.position.z + 1.8, 2.0)) - 0.0089) + < 1e-4); + else + CHECK(vec_is(&pose.position, 0.4, 0.1, -1.8) + && fabs(dist(&e0, &e1) - 0.0089) < 1e-4); + again = video_xr_grab_step(&g, &aim, NULL, 15.0f, VIDEO_XR_STEP_TILT, + rest, NULL, 0, NULL, k ? &lid : NULL); + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e2); + /* Carried 1.3 on past the hold, it lets go where the stick put it. */ + rest[1] -= 1.3f; + off = video_xr_grab_step(&g, &aim, NULL, 15.0f, VIDEO_XR_STEP_TILT, + rest, NULL, 0, NULL, k ? &lid : NULL); + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e3); + CHECK(again == VIDEO_XR_STEP_TILT && dist(&e1, &e2) < 1e-5 + && off == 0 && rest[1] == 0.0f); + if (k == 1) + CHECK(dist(&e0, &e3) < 1e-5); + else + CHECK(vec_is(&pose.position, 0.4, 0.1, -1.8)); + } + rest[0] = 0.0f; + rest[1] = 0.0f; + lid.edge = 2; + lid.axis = VIDEO_XR_STEP_TURN; + head.x = 0.4f; + head.y = 0.1f; + head.z = 0.0f; + step_grab(&g, &aim, 0.4, 0.1, -1.8, 91.7, 0.0); + g.width = 1.0f; + g.height = 0.6f; + lid_edge(&g, &aim, 0.5f, 0.0f, &pose, &e0); + held = video_xr_grab_step(&g, &aim, &head, 15.0f, VIDEO_XR_STEP_TURN, + rest, NULL, 0, NULL, &lid); + lid_edge(&g, &aim, 0.5f, 0.0f, &pose, &e1); + CHECK(held == VIDEO_XR_STEP_TURN && NEAR(fabs(rest[0]), 1.7) + && dist(&e0, &e1) < 1e-5 + && !vec_is(&pose.position, 0.4, 0.1, -1.8)); +} + +/* The stick on an item let go off a hinge on its top edge: its tilt + * keeps that edge, as video_xr_grab_step holds it, and its turn the + * middle; with no lid, as video_xr_grab_turn. */ +static void test_turn_lid(void) +{ + unsigned k, acted; + video_xr_grab_t g, plain; + video_xr_hinge_t lid; + video_xr_pose_t aim, pose, want; + video_xr_vec3_t e0, e1; + turn_aim(&aim, -20.0f, 15.0f, 5.0f); + memset(&lid, 0, sizeof(lid)); + lid.edge = 0; + lid.axis = VIDEO_XR_STEP_TILT; + for (k = 0; k < 3; k++) + { + float x = (k == 1) ? 1.0f : 0.0f; + float y = (k == 1) ? 0.0f : 1.0f; + step_grab(&g, &aim, 0.4, 0.1, -1.8, 25.0, -60.0); + g.width = 1.0f; + g.height = 0.6f; + plain = g; + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e0); + acted = video_xr_grab_turn_lid(&g, &aim, k < 2 ? &lid : NULL, x, y, + 0.1); + lid_edge(&g, &aim, 0.0f, 0.3f, &pose, &e1); + video_xr_grab_turn(&plain, &aim, x, y, 0.1); + video_xr_grab_pose(&plain, &aim, NULL, 0, NULL, &want, NULL, NULL); + CHECK(acted == (k == 1 ? VIDEO_XR_STEP_TURN : VIDEO_XR_STEP_TILT) + && turn_apart(&pose.orientation, &want.orientation) < 1e-4 + && vec_is(&want.position, 0.4, 0.1, -1.8)); + /* 9 degrees about the edge: 2 sin 4.5 of 0.3 m, 4.7 cm. */ + if (k == 0) + CHECK(dist(&e0, &e1) < 1e-5 + && fabs(dist(&pose.position, &want.position) - 0.0471) + < 1e-4); + else + CHECK(dist(&pose.position, &want.position) < 1e-6); + } +} + +/* An upright item's top edge hinged on the bottom edge of the screen + * above it, then carried: in reach within 3 cm of that screen's plane, + * from 3 cm over to 13 cm out from its edge and while the edges + * overlap; measured against where that screen is now, not with it + * gone. */ +static void test_near_hinge(void) +{ + /* x, the gap, off the plane, in reach */ + static const double at[][4] = { + { 0.0, 0.0, 0.0, 1 }, + { 0.0, 0.0, 0.029, 1 }, + { 0.0, 0.0, -0.029, 1 }, + { 0.0, 0.0, 0.031, 0 }, + { 0.0, 0.0, -0.031, 0 }, + { 0.0, 0.129, 0.0, 1 }, + { 0.0, 0.131, 0.0, 0 }, + { 0.0, -0.029, 0.0, 1 }, + { 0.0, -0.031, 0.0, 0 }, + { 0.99, 0.0, 0.0, 1 }, + { -1.01, 0.0, 0.0, 0 } + }; + unsigned k; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_hinge_t h; + video_xr_pose_t aim; + turn_aim(&aim, -20.0f, 15.0f, 5.0f); + memset(&set, 0, sizeof(set)); + snap_add(&set, 0, 0.0, 0.0); + set.quads[0].pose.position.y = 0.6f; + set.quads[0].pose.position.z = -1.8f; + set.quads[0].width = 1.0f; + set.quads[0].height = 0.6f; + memset(&h, 0, sizeof(h)); + h.edge = 0; + h.slot = 0; + h.on = 1; + h.axis = VIDEO_XR_STEP_TILT; + for (k = 0; k < sizeof(at) / sizeof(at[0]); k++) + { + step_grab(&g, &aim, at[k][0], -at[k][1], -1.8 + at[k][2], 0.0, + 0.0); + g.width = 1.0f; + g.height = 0.6f; + CHECK(video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_SNAP_DIST) == (at[k][3] != 0.0)); + } + step_grab(&g, &aim, 0.0, 0.0, -1.8, 0.0, 0.0); + g.width = 1.0f; + g.height = 0.6f; + set.quads[0].pose.position.z = -1.765f; + CHECK(!video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_SNAP_DIST)); + set.quads[0].pose.position.z = -1.79f; + CHECK(video_xr_grab_near_hinge(&g, &aim, &set, &h, VIDEO_XR_SNAP_DIST)); + h.slot = 2; + CHECK(!video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_SNAP_DIST)); + h.slot = 0; + CHECK(!video_xr_grab_near_hinge(&g, &aim, NULL, &h, + VIDEO_XR_SNAP_DIST)); + /* The hinge a hold was taken in reaches 4.5 cm off its plane. */ + CHECK(fabs(VIDEO_XR_HINGE_REACH - 0.045) < 1e-6); + set.quads[0].pose.position.z = -1.8f; + step_grab(&g, &aim, 0.0, 0.0, -1.8 + 0.044, 0.0, 0.0); + g.width = 1.0f; + g.height = 0.6f; + CHECK( video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_HINGE_REACH) + && !video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_SNAP_DIST)); + step_grab(&g, &aim, 0.0, 0.0, -1.8 - 0.046, 0.0, 0.0); + g.width = 1.0f; + g.height = 0.6f; + CHECK(!video_xr_grab_near_hinge(&g, &aim, &set, &h, + VIDEO_XR_HINGE_REACH)); +} + +#define SMOOTH_MAX 400 +static double t_smooth_raw[SMOOTH_MAX]; +static double t_smooth_out[SMOOTH_MAX]; + +/* Edit Smoothing at level, hz frames a second for secs, on a yaw that + * turns rate degrees a second with a shake of amp degrees at f Hz: each + * frame's raw and smoothed yaw into t_smooth_raw and t_smooth_out. + * Returns the frames. */ +static unsigned smooth_run(unsigned level, double hz, double secs, + double rate, double amp, double f) +{ + unsigned i; + video_xr_smooth_t s; + video_xr_pose_t raw, out; + unsigned n = (unsigned)(secs * hz) + 1; + memset(&s, 0, sizeof(s)); + for (i = 0; i < n && i < SMOOTH_MAX; i++) + { + double t = i / hz; + double yaw = rate * t + amp * sin(2.0 * M_PI * f * t); + snap_pose(&raw, 0.0, 0.0, 0.0, yaw); + video_xr_smooth_step(&s, level, &raw, i ? 1.0 / hz : 0.0, &out); + t_smooth_raw[i] = yaw; + t_smooth_out[i] = snap_yaw(&out); + } + return i; +} + +/* The root mean square of a run's second differences: what a shake + * adds and a steady turn does not. */ +static double smooth_jerk(const double *a, unsigned n) +{ + unsigned i; + double sum = 0.0; + for (i = 1; i + 1 < n; i++) + { + double d = a[i + 1] - 2.0 * a[i] + a[i - 1]; + sum += d * d; + } + return sqrt(sum / (n - 2)); +} + +/* Steady after a step of 2 cm and 5 degrees, Medium settles on the + * hand within two seconds, part way after one frame. */ +static void test_smooth_steady(void) +{ + unsigned i; + video_xr_smooth_t s; + video_xr_pose_t raw, out; + memset(&s, 0, sizeof(s)); + snap_pose(&raw, 0.0, 0.0, 0.0, 0.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, &raw, 0.0, + &out); + CHECK(s.on && pose_same(&out, &raw)); + snap_pose(&raw, 0.02, 0.0, 0.0, 5.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, &raw, + 1.0 / 90.0, &out); + CHECK(out.position.x > 0.0f && out.position.x < 0.02f + && snap_yaw(&out) > 0.0 && snap_yaw(&out) < 5.0); + for (i = 0; i < 180; i++) + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, &raw, + 1.0 / 90.0, &out); + CHECK(fabs(snap_yaw(&out) - 5.0) < 0.01 + && fabs(out.position.x - 0.02) < 0.0001); +} + +/* A 0.3 degree shake at 8 to 12 Hz, the hand still or turning 10 + * degrees a second: Medium cuts it by at least half (about a fifth). */ +static void test_smooth_tremor(void) +{ + unsigned k, m; + static const double f[3] = { 8.0, 10.0, 12.0 }; + for (k = 0; k < 3; k++) + for (m = 0; m < 2; m++) + { + unsigned n = smooth_run(VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, 90.0, + 3.0, m ? 10.0 : 0.0, 0.3, f[k]); + /* After a second's settling. */ + CHECK(smooth_jerk(t_smooth_out + 90, n - 90) + < 0.5 * smooth_jerk(t_smooth_raw + 90, n - 90)); + } +} + +/* A fast 90 degree sweep in 0.25 s, at 72, 90 and 120 Hz: 100 ms after + * it ends Medium is within a degree of the hand (about 0.5 at most). */ +static void test_smooth_sweep(void) +{ + unsigned k; + static const double hz[3] = { 72.0, 90.0, 120.0 }; + for (k = 0; k < 3; k++) + { + unsigned i; + video_xr_smooth_t s; + video_xr_pose_t raw, out; + unsigned end = (unsigned)(0.35 * hz[k] + 0.5); + memset(&s, 0, sizeof(s)); + for (i = 0; i <= end; i++) + { + double t = i / hz[k]; + snap_pose(&raw, 0.0, 0.0, 0.0, (t < 0.25 ? t / 0.25 : 1.0) * 90.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, &raw, + i ? 1.0 / hz[k] : 0.0, &out); + } + CHECK(fabs(snap_yaw(&out) - 90.0) < 1.0); + } +} + +/* The same move, 30 degrees and 10 cm over a second then still, at 72 + * and 120 Hz: at every 24th of a second the two agree within a degree + * (about 0.4 at most) and 3 mm. */ +static void test_smooth_rates(void) +{ + unsigned i, j; + static const double hz[2] = { 72.0, 120.0 }; + static const unsigned every[2] = { 3, 5 }; + double yaw[2][49]; + double x[2][49]; + for (j = 0; j < 2; j++) + { + video_xr_smooth_t s; + video_xr_pose_t raw, out; + memset(&s, 0, sizeof(s)); + for (i = 0; i <= 48 * every[j]; i++) + { + double t = i / hz[j]; + double k = (t < 1.0) ? t : 1.0; + snap_pose(&raw, 0.1 * k, 0.0, 0.0, 30.0 * k); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM, &raw, + i ? 1.0 / hz[j] : 0.0, &out); + if (i % every[j] == 0) + { + yaw[j][i / every[j]] = snap_yaw(&out); + x[j][i / every[j]] = out.position.x; + } + } + } + for (i = 0; i < 49; i++) + CHECK(fabs(yaw[0][i] - yaw[1][i]) < 1.0 + && fabs(x[0][i] - x[1][i]) < 0.003); +} + +/* Off is the raw aim and keeps nothing. A first step, a step of no + * time, a filter not on, and a jump of more than half a metre or 45 + * degrees in a step all start afresh from the raw aim; the next step + * smooths again. */ +static void test_smooth_resets(void) +{ + video_xr_smooth_t s; + video_xr_pose_t raw, out; + memset(&s, 0, sizeof(s)); + snap_pose(&raw, 0.0, 0.0, 0.0, 0.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_OFF, &raw, 0.0, + &out); + snap_pose(&raw, 0.01, 0.0, 0.0, 3.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_OFF, &raw, + 1.0 / 90.0, &out); + CHECK(!s.on && pose_same(&out, &raw)); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(s.on && pose_same(&out, &raw)); + snap_pose(&raw, 0.02, 0.0, 0.0, 4.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(!pose_same(&out, &raw)); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, 0.0, + &out); + CHECK(pose_same(&out, &raw)); + snap_pose(&raw, 0.62, 0.0, 0.0, 4.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(pose_same(&out, &raw)); + snap_pose(&raw, 0.62, 0.0, 0.0, 54.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(pose_same(&out, &raw)); + /* 0.4 m and 40 degrees in a step: smoothed. */ + snap_pose(&raw, 0.22, 0.0, 0.0, 14.0); + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(!pose_same(&out, &raw)); + s.on = false; + video_xr_smooth_step(&s, VIDEO_OPENXR_EDIT_SMOOTHING_HIGH, &raw, + 1.0 / 90.0, &out); + CHECK(pose_same(&out, &raw)); +} + +/* The 3DS layout: screen 1, 1.6 x 0.96 at 1.8 m; screen 2, 1.28 x 0.96, + * held by a hand at the origin from its spot 1.6 cm under screen 1, + * inside the click's 1-2 cm fade. */ +static void snap_3ds(video_xr_quad_set_t *set, video_xr_grab_t *g, + video_xr_pose_t *start) +{ + memset(set, 0, sizeof(*set)); + video_xr_pose_identity(&set->quads[0].pose); + set->quads[0].pose.position.z = -1.8f; + set->quads[0].width = 1.6f; + set->quads[0].height = 0.96f; + set->quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set->num_quads = 1; + memset(g, 0, sizeof(*g)); + video_xr_pose_identity(&g->rel); + g->turn = g->rel.orientation; + g->rel.position.y = -0.976f; + g->rel.position.z = -1.8f; + g->base.y = -0.976f; + g->base.z = -1.8f; + g->width = 1.28f; + g->height = 0.96f; + g->slot = 1; + g->state = VIDEO_XR_GRAB_HELD; + *start = g->rel; +} + +/* Grabbing a screen does not move it: a 3DS layout's screen 2, taken + * and held still, stays put and engages nothing, where a whole pull + * would hinge it and lift it 0.4 cm toward screen 1's bottom edge; the + * XR thread's call shape agrees. */ +static void test_snap_no_hop(void) +{ + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, start, away; + video_screen_xr_t s; + unsigned snapped = 1; + snap_3ds(&set, &g, &start); + video_xr_pose_identity(&aim); + CHECK(!video_xr_grab_pose(&g, &aim, &set, 2u, &start, &pose, &s, + &snapped)); + CHECK(snapped == 0 && pose_same(&pose, &start)); + CHECK(NEAR(s.x, 0.0) && NEAR(s.y, 0.0) && NEAR(s.z, 0.0)); + video_xr_grab_pose(&g, &aim, &set, 2u, &start, &pose, NULL, NULL); + CHECK(pose_same(&pose, &start)); + pose = start; + CHECK(video_xr_snap(&set, 1, 2u, 1.28f, 0.96f, 0.0f, &pose) == 0 + && !memcmp(&pose, &start, sizeof(pose))); + /* Had it been taken 1 m away, the pull would be whole. */ + away = start; + away.position.x = 1.0f; + video_xr_grab_pose(&g, &aim, &set, 2u, &away, &pose, NULL, NULL); + CHECK(vec_is(&pose.position, 0.0, -0.972, -1.8)); +} + +/* The fade-in: 0 where the screen was taken, rising evenly with travel + * or turn, whichever is further on, to 1 at 5 cm or 10 degrees; carried + * sideways in 0.5 mm steps, the 3DS screen 2 rises toward screen 1's + * edge as the fade grows, half way at 2.5 cm, no step moving it more + * than twice the hand's, and is whole by 5 cm. */ +static void test_snap_fade_in(void) +{ + unsigned i; + video_xr_quad_set_t set; + video_xr_grab_t g; + video_xr_pose_t aim, pose, start, last; + snap_3ds(&set, &g, &start); + pose = start; + CHECK(video_xr_snap_fade(&start, &pose) == 0.0f); + pose.position.x = 0.025f; + CHECK(NEAR(video_xr_snap_fade(&start, &pose), 0.5)); + pose.position.x = 0.05f; + CHECK(NEAR(video_xr_snap_fade(&start, &pose), 1.0)); + pose.position.x = 0.2f; + CHECK(video_xr_snap_fade(&start, &pose) == 1.0f); + /* 1 cm along and turned 5 degrees about its up: the turn counts. */ + pose.position.x = 0.01f; + pose.orientation.y = (float)sin(5.0 * M_PI / 360.0); + pose.orientation.w = (float)cos(5.0 * M_PI / 360.0); + CHECK(NEAR(video_xr_snap_fade(&start, &pose), 0.5)); + + video_xr_pose_identity(&aim); + last = start; + for (i = 0; i <= 160; i++) + { + aim.position.x = 0.0005f * i; + video_xr_grab_pose(&g, &aim, &set, 2u, &start, &pose, NULL, NULL); + CHECK(fabs(pose.position.x - last.position.x) <= 0.00101 + && fabs(pose.position.y - last.position.y) <= 0.00101); + if (i == 50) + CHECK(NEAR(pose.position.y, -0.974)); + last = pose; + } + CHECK(vec_is(&pose.position, 0.08, -0.972, -1.8)); +} + +/* An outline round an item: centred on it, facing as it does, its rim's + * inner edge with filtering's half texel 5 mm outside the item's; about + * 4 cm thick at the sides and 2 cm at the top and bottom of a + * 1.6 x 0.9 m screen, as before. */ +static void test_outline(void) +{ + video_xr_quad_t q, o; + const double f = (VIDEO_XR_OUTLINE_EDGE + 0.5) + / (double)VIDEO_XR_OUTLINE_DIM; + memset(&q, 0, sizeof(q)); + video_xr_pose_identity(&q.pose); + q.pose.position.x = 0.3f; + q.pose.position.z = -1.8f; + q.pose.orientation.y = 0.38268f; + q.pose.orientation.w = 0.92388f; + q.width = 1.6f; + q.height = 0.9f; + q.slot = 3; + q.kind = VIDEO_XR_QUAD_SCREEN; + video_xr_outline(&q, &o); + CHECK(NEAR(o.width * (1.0 - 2.0 * f), 1.61)); + CHECK(NEAR(o.height * (1.0 - 2.0 * f), 0.91)); + CHECK(fabs(o.width * VIDEO_XR_OUTLINE_EDGE / VIDEO_XR_OUTLINE_DIM - 0.04) + < 0.005); + CHECK(fabs(o.height * VIDEO_XR_OUTLINE_EDGE / VIDEO_XR_OUTLINE_DIM + - 0.0225) < 0.005); + CHECK(pose_same(&o.pose, &q.pose)); + CHECK(o.slot == 3 && o.kind == VIDEO_XR_QUAD_SCREEN); + /* A small item: still 5 mm clear. */ + q.width = 0.1f; + q.height = 0.05f; + video_xr_outline(&q, &o); + CHECK(NEAR(o.width * (1.0 - 2.0 * f), 0.11)); + CHECK(NEAR(o.height * (1.0 - 2.0 * f), 0.06)); +} + +/* An outline hiding an edge's rim: VIDEO_XR_OUTLINE_CROP texels cut + * on its x at that side, the rest where it was, so its other sides + * stay; a top or bottom turned a quarter about its face first, its + * sides swapped; turned to the head, left and right swap, so the same + * side of the item goes; opposite edges both go. 1 x 0.5 m: a cut is + * 3.125 cm across, 1.5625 cm turned. */ +static void test_outline_crop(void) +{ + unsigned x, w; + video_xr_quad_t o, c; + video_xr_vec3_t ax, unit_x; + const unsigned cut = VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP; + const unsigned both = VIDEO_XR_OUTLINE_DIM - 2 * VIDEO_XR_OUTLINE_CROP; + CHECK(VIDEO_XR_OUTLINE_CROP > VIDEO_XR_OUTLINE_EDGE); + memset(&o, 0, sizeof(o)); + video_xr_pose_identity(&o.pose); + o.pose.position.z = -1.8f; + o.width = 1.0f; + o.height = 0.5f; + unit_x.x = 1.0f; + unit_x.y = 0.0f; + unit_x.z = 0.0f; + c = o; + video_xr_outline_crop(&c, 0, false, &x, &w); + CHECK(x == 0 && w == VIDEO_XR_OUTLINE_DIM && pose_same(&c.pose, &o.pose) + && c.width == 1.0f && c.height == 0.5f); + /* Right: its high texels; its left side stays at x -0.5. */ + c = o; + video_xr_outline_crop(&c, 1u << 2, false, &x, &w); + CHECK(x == 0 && w == cut && fabs(c.width - 0.96875) < 1e-6 + && fabs(c.height - 0.5) < 1e-6 + && fabs(c.pose.position.x + 0.015625) < 1e-6 + && turn_apart(&c.pose.orientation, &o.pose.orientation) < 1e-3); + c = o; + video_xr_outline_crop(&c, 1u << 3, false, &x, &w); + CHECK(x == VIDEO_XR_OUTLINE_CROP && w == cut + && fabs(c.pose.position.x - 0.015625) < 1e-6); + /* Right, turned to the head: its low texels, the same side in the + * room; left, its high ones. */ + c = o; + video_xr_turn_back(&c.pose); + video_xr_outline_crop(&c, 1u << 2, true, &x, &w); + CHECK(x == VIDEO_XR_OUTLINE_CROP && w == cut + && fabs(c.width - 0.96875) < 1e-6 + && fabs(c.pose.position.x + 0.015625) < 1e-6); + c = o; + video_xr_turn_back(&c.pose); + video_xr_outline_crop(&c, 1u << 3, true, &x, &w); + CHECK(x == 0 && w == cut && fabs(c.width - 0.96875) < 1e-6 + && fabs(c.pose.position.x - 0.015625) < 1e-6); + /* Top: its x up the item, its high texels; its bottom stays at + * y -0.25. */ + c = o; + video_xr_outline_crop(&c, 1u << 0, false, &x, &w); + video_xr_rotate(&c.pose.orientation, &unit_x, &ax); + CHECK(x == 0 && w == cut && vec_is(&ax, 0.0, 1.0, 0.0) + && fabs(c.width - 0.484375) < 1e-6 && fabs(c.height - 1.0) < 1e-6 + && fabs(c.pose.position.y + 0.0078125) < 1e-6 + && fabs(c.pose.position.x) < 1e-6); + c = o; + video_xr_outline_crop(&c, 1u << 1, false, &x, &w); + CHECK(x == VIDEO_XR_OUTLINE_CROP + && fabs(c.pose.position.y - 0.0078125) < 1e-6); + /* Top, turned to the head: still its high texels; bottom, its low + * ones. */ + c = o; + video_xr_turn_back(&c.pose); + video_xr_outline_crop(&c, 1u << 0, true, &x, &w); + video_xr_rotate(&c.pose.orientation, &unit_x, &ax); + CHECK(x == 0 && vec_is(&ax, 0.0, 1.0, 0.0) + && fabs(c.pose.position.y + 0.0078125) < 1e-6); + c = o; + video_xr_turn_back(&c.pose); + video_xr_outline_crop(&c, 1u << 1, true, &x, &w); + video_xr_rotate(&c.pose.orientation, &unit_x, &ax); + CHECK(x == VIDEO_XR_OUTLINE_CROP && w == cut && vec_is(&ax, 0.0, 1.0, 0.0) + && fabs(c.pose.position.y - 0.0078125) < 1e-6); + /* Opposite edges: both sides, the middle kept. */ + c = o; + video_xr_outline_crop(&c, 3u, false, &x, &w); + CHECK(x == VIDEO_XR_OUTLINE_CROP && w == both + && fabs(c.width - 0.46875) < 1e-6 + && fabs(c.pose.position.y) < 1e-6); + c = o; + video_xr_outline_crop(&c, 12u, false, &x, &w); + CHECK(x == VIDEO_XR_OUTLINE_CROP && w == both + && fabs(c.width - 0.9375) < 1e-6 + && fabs(c.pose.position.x) < 1e-6); +} + +/* At full width an outline cut keeps its pose and size and shows its + * cut's cell: none, the whole square; from behind, a side is the room's + * same side, the other cell; a top or bottom wins over a side. */ +static void test_outline_cell(void) +{ + unsigned k; + for (k = 0; k < 2; k++) + { + CHECK(video_xr_outline_cell(0, false, k) == VIDEO_XR_OUTLINE_X(k)); + CHECK(video_xr_outline_cell(0, true, k) == VIDEO_XR_OUTLINE_X(k)); + CHECK(video_xr_outline_cell(1u, false, k) == VIDEO_XR_CUT_X(0, k)); + CHECK(video_xr_outline_cell(2u, false, k) == VIDEO_XR_CUT_X(1, k)); + CHECK(video_xr_outline_cell(4u, false, k) == VIDEO_XR_CUT_X(2, k)); + CHECK(video_xr_outline_cell(8u, false, k) == VIDEO_XR_CUT_X(3, k)); + CHECK(video_xr_outline_cell(3u, false, k) == VIDEO_XR_CUT_X(4, k)); + CHECK(video_xr_outline_cell(12u, false, k) == VIDEO_XR_CUT_X(5, k)); + CHECK(video_xr_outline_cell(1u, true, k) == VIDEO_XR_CUT_X(0, k)); + CHECK(video_xr_outline_cell(3u, true, k) == VIDEO_XR_CUT_X(4, k)); + CHECK(video_xr_outline_cell(4u, true, k) == VIDEO_XR_CUT_X(3, k)); + CHECK(video_xr_outline_cell(8u, true, k) == VIDEO_XR_CUT_X(2, k)); + CHECK(video_xr_outline_cell(12u, true, k) == VIDEO_XR_CUT_X(5, k)); + CHECK(video_xr_outline_cell(2u | 4u, false, k) == VIDEO_XR_CUT_X(1, k)); + } +} + static void test_compose(void) { unsigned i; @@ -1159,6 +7817,114 @@ static void test_request_rate(void) int main(void) { + test_screen_defaults(); + test_screen_transform(); + test_screen_visibility(); + test_screen_identity(); + test_screen_density(); + test_pose_invert(); + test_euler(); + test_layout(); + test_settings_identity(); + test_grab(); + test_grab_stick(); + test_grab_turn(); + test_grab_turn_pitched(); + test_grab_turn_lowered(); + test_grab_turn_tilt_ray(); + test_carry_heading(); + test_carry_full_turn(); + test_grab_tilt(); + test_grab_tilt_evenly(); + test_grab_turn_and_tilt(); + test_grab_turn_lock(); + test_grab_tilt_turned(); + test_tilt_full_turn(); + test_carry_vertical(); + test_carry_flip(); + test_carry_still(); + test_menu_handle(); + test_handle_first(); + test_handle_sticky(); + test_snap_lines(); + test_snap_pulling(); + test_snap_zones(); + test_snap_targets(); + test_snap_continuous(); + test_snap_ranked(); + test_snap_whole_line(); + test_snap_turn_tie(); + test_snap_turned(); + test_hinge_3ds(); + test_hinge_square_fold(); + test_hinge_side(); + test_hinge_overlap(); + test_hinge_range(); + test_hinge_flush_corner(); + test_hinge_pivot(); + test_hinge_ranked(); + test_hinge_turned_away(); + test_hinge_outranked(); + test_hinge_continuous(); + test_hinge_roll_band(); + test_hinge_whole(); + test_hinge_swing(); + test_hinge_other_axis(); + test_hinge_step_put(); + test_hinge_put_still(); + test_gap_click(); + test_hinge_line(); + test_hinge_place(); + test_gap_free(); + test_gap_fade_out(); + test_gap_stop(); + test_gap_stop_one(); + test_gap_stop_arm(); + test_gap_stop_arm_turned(); + test_gap_stop_turned(); + test_gap_swing(); + test_gap_nearer(); + test_gap_nearer_sweep(); + test_gap_nearer_stacked(); + test_gap_nearer_corner(); + test_gap_commit(); + test_gap_limits(); + test_gap_sweeps(); + test_grab_snap(); + test_turn_snap(); + test_tilt_snap(); + test_step_turn(); + test_step_tilt(); + test_step_flat(); + test_step_snap_first(); + test_step_lid(); + test_turn_lid(); + test_near_hinge(); + test_rest_hinges(); + test_rest_hinge(); + test_grab_kept(); + test_grab_through(); + test_grab_through_rest(); + test_rest_gap_one(); + test_rest_hinge_sides(); + test_rest_hinge_size(); + test_hinge_settle(); + test_hinge_settle_taken(); + test_hinge_fold_rest(); + test_grab_kept_turn(); + test_smooth_steady(); + test_smooth_tremor(); + test_smooth_sweep(); + test_smooth_rates(); + test_smooth_resets(); + test_snap_no_hop(); + test_snap_fade_in(); + test_live_edit(); + test_edit_fixed(); + test_frame_as_screen(); + test_menu_spot(); + test_edit_seq(); + test_keep_dims(); test_image_dims(); test_shrinks(); test_place_3ds(); @@ -1179,7 +7945,17 @@ int main(void) test_window_views_3ds(); test_window_rect_rotation(); test_cursor(); + test_beam(); test_cursor_pixels(); + test_tag_pixels(); + test_cut_pixels(); + test_back_pixels(); + test_hinge_pixels(); + test_back_pick(); + test_tag_pose(); + test_outline(); + test_outline_crop(); + test_outline_cell(); test_compose(); test_lazy_follow(); test_lazy_edges(); diff --git a/samples/gfx/vulkan_chain_alloc/vulkan_chain_alloc_test.c b/samples/gfx/vulkan_chain_alloc/vulkan_chain_alloc_test.c index d7854d4152..8b59d1a8b6 100644 --- a/samples/gfx/vulkan_chain_alloc/vulkan_chain_alloc_test.c +++ b/samples/gfx/vulkan_chain_alloc/vulkan_chain_alloc_test.c @@ -288,6 +288,212 @@ static void test_discard_clears(void) "any other pass's target is not"); } +/* ---- Replay ---- */ +MOCK_CREATE(mock_create_image, VkImageCreateInfo, VkImage) +MOCK_DESTROY(mock_destroy_image, VkImage) +MOCK_CREATE(mock_create_image_view, VkImageViewCreateInfo, VkImageView) +MOCK_DESTROY(mock_destroy_image_view, VkImageView) +MOCK_CREATE(mock_create_framebuffer, VkFramebufferCreateInfo, VkFramebuffer) +MOCK_DESTROY(mock_destroy_framebuffer, VkFramebuffer) +MOCK_DESTROY(mock_destroy_render_pass, VkRenderPass) + +static void VKAPI_CALL mock_image_requirements(VkDevice d, VkImage i, + VkMemoryRequirements *r) +{ + (void)d; (void)i; + r->size = 4096; + r->alignment = 256; + r->memoryTypeBits = 1; +} + +static VkResult VKAPI_CALL mock_bind_image_memory(VkDevice d, VkImage i, + VkDeviceMemory m, VkDeviceSize off) +{ (void)d; (void)i; (void)m; (void)off; return VK_SUCCESS; } + +/* The view each binding of the final pass's set was last written. */ +static VkImageView written_view[4]; +static void VKAPI_CALL mock_update_sets(VkDevice d, uint32_t n, + const VkWriteDescriptorSet *w, uint32_t cn, const VkCopyDescriptorSet *c) +{ + uint32_t i; + (void)d; (void)cn; (void)c; + for (i = 0; i < n; i++) + if (w[i].dstBinding < 4 && w[i].pImageInfo) + written_view[w[i].dstBinding] = w[i].pImageInfo[0].imageView; +} + +/* A replay of the last frame's final pass, in a later frame and at + * another size, reads what that pass read: the second pass's output as + * the frame left it - not the framebuffer the feedback swap gave it - + * that pass's feedback, the history the frame saw, and that frame's + * count; and swaps no feedback. */ +static void test_replay(struct vulkan_filter_chain *chain) +{ + struct slang_pass *last = chain->passes[PASSES - 1]; + struct slang_pass *prev = chain->passes[PASSES - 2]; + slang_texture_semantic_meta source_meta[1], feedback_meta[PASSES - 1]; + slang_texture_semantic_meta history_meta[2]; + slang_texture_semantic_meta unread[SLANG_NUM_TEXTURE_SEMANTICS]; + struct slang_framebuffer swapped, feedback; + unsigned i; + VkDescriptorSet sets[INDICES]; + uint32_t push[4]; + VkViewport vp; + float mvp[16]; + + vkUpdateDescriptorSets = mock_update_sets; + vkCmdPipelineBarrier = mock_barrier; + vkCmdBindPipeline = (PFN_vkCmdBindPipeline)mock_cmd_nop; + vkCmdBindDescriptorSets= (PFN_vkCmdBindDescriptorSets)mock_cmd_nop; + vkCmdBindVertexBuffers = (PFN_vkCmdBindVertexBuffers)mock_cmd_nop; + vkCmdSetViewport = (PFN_vkCmdSetViewport)mock_cmd_nop; + vkCmdSetScissor = (PFN_vkCmdSetScissor)mock_cmd_nop; + vkCmdDraw = (PFN_vkCmdDraw)mock_cmd_nop; + + CHECK(!vulkan_filter_chain_replay_ready(chain), + "a chain with no frame drawn is ready to replay"); + + memset(unread, 0, sizeof(unread)); + for (i = 0; i < SLANG_NUM_TEXTURE_SEMANTICS; i++) + { + last->reflection.semantic_textures[i].data = &unread[i]; + last->reflection.semantic_textures[i].size = 1; + } + memset(source_meta, 0, sizeof(source_meta)); + memset(feedback_meta, 0, sizeof(feedback_meta)); + memset(history_meta, 0, sizeof(history_meta)); + source_meta[0].texture = true; + source_meta[0].binding = 0; + feedback_meta[PASSES - 2].texture = true; + feedback_meta[PASSES - 2].binding = 1; + history_meta[1].texture = true; + history_meta[1].binding = 2; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_SOURCE].data = source_meta; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_SOURCE].size = 1; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_PASS_FEEDBACK].data = feedback_meta; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_PASS_FEEDBACK].size = PASSES - 1; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].data = history_meta; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].size = 2; + last->reflection.semantics[SLANG_SEMANTIC_FRAME_COUNT].push_constant = true; + last->reflection.semantics[SLANG_SEMANTIC_FRAME_COUNT].push_constant_offset = 0; + memset(push, 0, sizeof(push)); + memset(sets, 0, sizeof(sets)); + last->push.buffer = push; + last->push.stages = 0; + last->sets = sets; + last->final_pass = true; + last->framebuffer = NULL; + + /* As the last frame left the chain: the second pass's output was + * frame A, the feedback swap gave it the other framebuffer. */ + memset(&swapped, 0, sizeof(swapped)); + memset(&feedback, 0, sizeof(feedback)); + swapped.view = (VkImageView)(uintptr_t)0xB1; + swapped.size_dims = VIDEO_SCALE_PACK(64, 32); + feedback.view = (VkImageView)(uintptr_t)0xA1; + feedback.size_dims = VIDEO_SCALE_PACK(64, 32); + prev->framebuffer = &swapped; + prev->fb_feedback = &feedback; + texture_array_resize(&chain->common.pass_outputs, + &chain->common.num_pass_outputs, PASSES); + texture_array_resize(&chain->common.fb_feedback, + &chain->common.num_fb_feedback, PASSES - 1); + texture_array_resize(&chain->common.original_history, + &chain->common.num_original_history, 1); + chain->common.pass_outputs[PASSES - 2].texture.view = (VkImageView)(uintptr_t)0xA1; + chain->common.pass_outputs[PASSES - 2].texture.dims = VIDEO_SCALE_PACK(64, 32); + chain->common.fb_feedback[PASSES - 2].texture.view = (VkImageView)(uintptr_t)0xF1; + chain->common.fb_feedback[PASSES - 2].texture.dims = VIDEO_SCALE_PACK(64, 32); + chain->common.original_history[0].texture.view = (VkImageView)(uintptr_t)0xC1; + chain->common.original_history[0].texture.dims = VIDEO_SCALE_PACK(64, 32); + chain->input_texture.view = (VkImageView)(uintptr_t)0xD1; + chain->input_texture.dims = VIDEO_SCALE_PACK(64, 32); + chain->drawn = true; + chain->drawn_frame_count = 7; + last->frame_count = 9; + + CHECK(vulkan_filter_chain_replay_ready(chain), "no replay after a frame"); + memset(&vp, 0, sizeof(vp)); + vp.width = 80.0f; + vp.height = 40.0f; + build_identity_matrix(mvp); + memset(written_view, 0, sizeof(written_view)); + vulkan_filter_chain_build_viewport_pass_replay(chain, VK_NULL_HANDLE, + &vp, mvp); + CHECK(written_view[0] == (VkImageView)(uintptr_t)0xA1, + "replay Source: view %p, want the frame's output", + (void*)(uintptr_t)written_view[0]); + CHECK(written_view[1] == (VkImageView)(uintptr_t)0xF1, + "replay PassFeedback: view %p", (void*)(uintptr_t)written_view[1]); + CHECK(written_view[2] == (VkImageView)(uintptr_t)0xC1, + "replay OriginalHistory1: view %p", (void*)(uintptr_t)written_view[2]); + CHECK(push[0] == 7, "replay drew with count %u, want 7", push[0]); + CHECK(last->frame_count == 9, "replay left the count at %u", + (unsigned)last->frame_count); + CHECK(prev->framebuffer == &swapped && prev->fb_feedback == &feedback, + "replay swapped the feedback buffers"); + + prev->framebuffer = NULL; + prev->fb_feedback = NULL; + memset(&last->reflection, 0, sizeof(last->reflection)); + last->push.buffer = NULL; + last->sets = NULL; +} + +/* A chain made to replay keeps one history framebuffer more than its + * passes read, so the frame's input goes where the last frame did not + * read: a replay still finds that frame's history. */ +static void test_history_spare(struct vulkan_filter_chain *chain) +{ + slang_texture_semantic_meta history_meta[3]; + struct slang_pass *last = chain->passes[PASSES - 1]; + unsigned before; + + vkCreateImage = mock_create_image; + vkDestroyImage = mock_destroy_image; + vkCreateImageView = mock_create_image_view; + vkDestroyImageView = mock_destroy_image_view; + vkCreateFramebuffer = mock_create_framebuffer; + vkDestroyFramebuffer = mock_destroy_framebuffer; + vkCreateRenderPass = mock_create_render_pass; + vkDestroyRenderPass = mock_destroy_render_pass; + vkGetImageMemoryRequirements = mock_image_requirements; + vkBindImageMemory = mock_bind_image_memory; + + memset(history_meta, 0, sizeof(history_meta)); + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].data = history_meta; + last->reflection.semantic_textures[SLANG_TEXTURE_SEMANTIC_ORIGINAL_HISTORY].size = 3; + /* Any other chain keeps only what its passes read. */ + chain->replay = false; + CHECK(slang_chain_init_history(chain), "history not made"); + CHECK(chain->num_history == 2 && chain->common.num_original_history == 2, + "without replay: %u framebuffers for %u read", + (unsigned)chain->num_history, + (unsigned)chain->common.num_original_history); + chain->replay = true; + CHECK(slang_chain_init_history(chain), "history not made"); + CHECK(chain->num_history == 3 && chain->common.num_original_history == 2, + "history: %u framebuffers for %u read", + (unsigned)chain->num_history, + (unsigned)chain->common.num_original_history); + if (chain->num_history == 3) + { + chain->history_ring_index = 0; + slang_chain_update_history_info(chain); + before = chain->history_ring_index; + /* What the frame's end writes: the slot before the newest. */ + CHECK((before + chain->num_history - 1) % chain->num_history + != (before + 0) % chain->num_history + && (before + chain->num_history - 1) % chain->num_history + != (before + 1) % chain->num_history, + "the frame's end writes a slot the frame read"); + CHECK(chain->common.original_history[1].texture.image + == chain->original_history[1]->image, + "history 2 is not the ring's second slot"); + } + memset(&last->reflection, 0, sizeof(last->reflection)); +} + static bool chain_whole(const struct vulkan_filter_chain *chain) { return chain->pass_count == PASSES && chain->num_deferred == INDICES; @@ -350,6 +556,8 @@ int main(void) if (chain) { test_scanline_band(chain); + test_replay(chain); + test_history_spare(chain); slang_chain_free(chain); } diff --git a/samples/gfx/vulkan_views_target/.gitignore b/samples/gfx/vulkan_views_target/.gitignore new file mode 100644 index 0000000000..30bcfa4ed5 --- /dev/null +++ b/samples/gfx/vulkan_views_target/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/samples/gfx/vulkan_views_target/Makefile b/samples/gfx/vulkan_views_target/Makefile new file mode 100644 index 0000000000..2137db5499 --- /dev/null +++ b/samples/gfx/vulkan_views_target/Makefile @@ -0,0 +1,27 @@ +CC ?= cc +PYTHON ?= python3 +BUILD_DIR ?= .build +REPO_ROOT := ../../.. +TARGET := $(BUILD_DIR)/vulkan_views_target_test$(EXE) + +CFLAGS += -Wall -Wextra -pedantic -std=c89 -Werror=declaration-after-statement \ + -Werror=implicit-function-declaration -Wno-long-long \ + -I$(REPO_ROOT) -I$(REPO_ROOT)/libretro-common/include \ + -I$(REPO_ROOT)/gfx/include -I$(BUILD_DIR) + +all: $(TARGET) + +$(BUILD_DIR)/driver_code.inc: extract.py $(REPO_ROOT)/gfx/drivers/vulkan.c $(REPO_ROOT)/gfx/video_views_draw.h + @mkdir -p $(BUILD_DIR) + $(PYTHON) extract.py $@ + +$(TARGET): vulkan_views_target_test.c $(BUILD_DIR)/driver_code.inc + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) + +check: $(TARGET) + $(RUNNER) $(TARGET) + +clean: + rm -f $(BUILD_DIR)/driver_code.inc $(TARGET) + +.PHONY: all check clean diff --git a/samples/gfx/vulkan_views_target/extract.py b/samples/gfx/vulkan_views_target/extract.py new file mode 100644 index 0000000000..e055dc7f2d --- /dev/null +++ b/samples/gfx/vulkan_views_target/extract.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Exercise Vulkan's shipping target allocation, shared retry policy +and the XR draw: what it draws straight into the runtime's images, and +the slots it cannot draw.""" +from pathlib import Path +import re +import sys + +root = Path(__file__).resolve().parents[3] +driver = (root / 'gfx/drivers/vulkan.c').read_text() +shared = (root / 'gfx/video_views_draw.h').read_text() + + +def function(source, name): + masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"', + lambda m: ' ' * len(m[0]), source, flags=re.S) + match = re.search(r'static (?:bool|void) ' + re.escape(name) + + r'\([^;]*?\)\s*\{', masked) + if not match: + raise RuntimeError('function missing: ' + name) + end = masked.index('{', match.start()) + 1 + depth = 1 + while depth: + depth += (masked[end] == '{') - (masked[end] == '}') + end += 1 + return source[match.start():end] + + +parts = [function(driver, name) for name in + ('vulkan_views_retire', 'vulkan_views_make')] +for name in ('VV_FN', 'VV_DRV', 'VV_TARGET', 'VV_TARGET_MAKE', + 'VV_TARGET_RETIRE'): + match = re.search(r'^#define ' + name + r'(?:\(|\s).*$', driver, re.M) + if not match: + raise RuntimeError('binding missing: ' + name) + parts.append(match[0]) +parts.append(function(shared, 'VV_FN(target_ensure)')) +parts.append(function(driver, 'vulkan_xr_draw_screen')) +parts.append(function(driver, 'vulkan_xr_slot_ensure')) +parts.append(function(driver, 'vulkan_xr_draw')) +Path(sys.argv[1]).write_text('\n\n'.join(parts) + '\n') diff --git a/samples/gfx/vulkan_views_target/vulkan_views_target_test.c b/samples/gfx/vulkan_views_target/vulkan_views_target_test.c new file mode 100644 index 0000000000..d39d6958e8 --- /dev/null +++ b/samples/gfx/vulkan_views_target/vulkan_views_target_test.c @@ -0,0 +1,620 @@ +/* Vulkan retained-target failures use the real allocator, retirement and + * shared ensure policy. Only device allocation and logging are replaced. + * The XR draw and its slots run with the runtime's swapchains and the + * pass steps stubbed: what reaches the runtime's images, when a chain + * ends a frame, and when a repeat replays the last frame's final pass. */ +#include +#include +#include +#include "gfx/video_views.h" +#include "gfx/video_defines.h" +#include "gfx/video_xr.h" + +#define VULKAN_OPENXR_MAX_IMAGES 4 +#define VULKAN_DIRTY_DYNAMIC_BIT 0x0001 +#define VULKAN_IMAGE_LAYOUT_TRANSITION(cmd, img, a, b, c, d, e, f) \ + do { (void)(cmd); (void)(img); } while (0) + +struct vk_image +{ + VkImage image; + VkDeviceMemory memory; + VkFramebuffer framebuffer; +}; + +typedef struct +{ + struct { VkPhysicalDeviceLimits limits; } gpu_properties; + VkDevice device; + VkFormat swapchain_format; + struct vulkan_openxr *xr; +} test_context_t; + +typedef struct +{ + struct vk_image view_retired[2]; +} test_chain_t; + +typedef struct +{ + test_context_t *context; + test_chain_t *chain; + VkCommandBuffer cmd; + struct + { + VkImageView view; + VkSampler sampler; + VkPipeline pipeline; + unsigned dirty; + struct { float data[16]; } mvp; + } tracker; + struct + { + struct vk_image ui; + struct vk_image canvas; + VkFormat ui_format; + VkRenderPass ui_render_pass; + VkRenderPass render_pass; + } views; + struct vk_image cached[3]; + unsigned rotation_raw; + struct + { + struct + { + VkImage images[VULKAN_OPENXR_MAX_IMAGES][2]; + VkImageView views[VULKAN_OPENXR_MAX_IMAGES][2]; + VkFramebuffer framebuffers[VULKAN_OPENXR_MAX_IMAGES][2]; + unsigned num_images; + unsigned dims; + unsigned layers; + unsigned failed_dims; + unsigned index[2]; + bool drawn; + bool released; + } slots[VIDEO_XR_MAX_SLOTS]; + video_xr_quad_set_t set; + unsigned ui_dims; + struct { float data[16]; } mvp; + VkFormat format; + bool mutable_format; + bool proj_shown; + } xr; +} vk_t; + +typedef struct vulkan_filter_chain vulkan_filter_chain_t; +typedef struct vulkan_openxr vulkan_openxr_t; + +typedef struct +{ + video_views_map_t views; +} video_frame_info_t; + +static unsigned allocations, destructions, warnings, failures; +static unsigned fail_allocation; + +static void vulkan_init_render_target(struct vk_image *img, unsigned dims, + VkFormat format, VkRenderPass pass, test_context_t *context) +{ + (void)dims; + (void)format; + (void)pass; + (void)context; + allocations++; + img->image = (VkImage)(uintptr_t)allocations; + img->memory = fail_allocation == 1 ? VK_NULL_HANDLE + : (VkDeviceMemory)(uintptr_t)allocations; + img->framebuffer = fail_allocation == 2 ? VK_NULL_HANDLE + : (VkFramebuffer)(uintptr_t)allocations; +} + +static void vulkan_destroy_hdr_buffer(VkDevice device, struct vk_image *img) +{ + (void)device; + destructions++; + memset(img, 0, sizeof(*img)); +} + +static void RARCH_WARN(const char *format, ...) +{ + (void)format; + warnings++; +} + +static video_xr_quad_set_t planned; +static unsigned acquire_fails; + +static void vulkan_xr_plan(vk_t *vk, const video_frame_info_t *video_info, + bool views, bool eyes, unsigned frame_dims, unsigned src_dims, + unsigned ui_dims) +{ + (void)video_info; + (void)views; + (void)eyes; + (void)frame_dims; + (void)src_dims; + (void)ui_dims; + vk->xr.set = planned; +} + +bool video_xr_shrinks(unsigned image_dims, unsigned source_dims, + unsigned rotation) +{ + (void)image_dims; + (void)source_dims; + (void)rotation; + return false; +} + +static void vulkan_openxr_slot_forget(vulkan_openxr_t *xr, unsigned slot) +{ + (void)xr; + (void)slot; +} + + +/* What reached the runtime's images: offscreen runs, final passes, + * replays of the last frame's, the size of the last, the render passes + * into a swapchain framebuffer, and frames the chains ended. */ +static unsigned offscreen_passes, viewport_passes, replays; +static unsigned last_pass_w, last_pass_h; +static unsigned render_passes, frames_ended; +static bool chain_drawn = true; + +static void vulkan_filter_chain_build_offscreen_passes( + vulkan_filter_chain_t *chain, VkCommandBuffer cmd, const VkViewport *vp) +{ + (void)chain; + (void)cmd; + (void)vp; + offscreen_passes++; +} + +static bool vulkan_filter_chain_replay_ready( + const vulkan_filter_chain_t *chain) +{ + (void)chain; + return chain_drawn; +} + +static void vulkan_filter_chain_build_viewport_pass_replay( + vulkan_filter_chain_t *chain, VkCommandBuffer cmd, + const VkViewport *vp, const float *mvp) +{ + (void)chain; + (void)cmd; + (void)mvp; + replays++; + last_pass_w = (unsigned)vp->width; + last_pass_h = (unsigned)vp->height; +} + +static void vulkan_filter_chain_build_viewport_pass( + vulkan_filter_chain_t *chain, VkCommandBuffer cmd, + const VkViewport *vp, const float *mvp) +{ + (void)chain; + (void)cmd; + (void)mvp; + viewport_passes++; + last_pass_w = (unsigned)vp->width; + last_pass_h = (unsigned)vp->height; +} + +static void vulkan_filter_chain_end_frame(vulkan_filter_chain_t *chain, + VkCommandBuffer cmd) +{ + (void)chain; + (void)cmd; + frames_ended++; +} + +static void test_begin_render_pass(VkCommandBuffer cmd, + const VkRenderPassBeginInfo *rp, VkSubpassContents contents) +{ + (void)cmd; + (void)contents; + if (rp->framebuffer) + render_passes++; +} + +static void test_end_render_pass(VkCommandBuffer cmd) +{ + (void)cmd; +} + +#define vkCmdBeginRenderPass test_begin_render_pass +#define vkCmdEndRenderPass test_end_render_pass + +static unsigned slot_creates[VIDEO_XR_MAX_SLOTS]; +static unsigned slot_refuses; + +static void vulkan_wait_own_submissions(vk_t *vk) +{ + (void)vk; +} + +static void vulkan_xr_slot_free_views(vk_t *vk, unsigned s) +{ + memset(vk->xr.slots[s].views, 0, sizeof(vk->xr.slots[s].views)); + memset(vk->xr.slots[s].framebuffers, 0, + sizeof(vk->xr.slots[s].framebuffers)); +} + +static void vulkan_xr_slot_free(vk_t *vk, unsigned s) +{ + memset(&vk->xr.slots[s], 0, sizeof(vk->xr.slots[s])); +} + +/* The runtime's swapchains: a refused slot is left with none. */ +static bool vulkan_openxr_slot_create(vulkan_openxr_t *xr, unsigned slot, + VkFormat format, bool mutable_format, unsigned dims, unsigned layers, + VkImage (*images)[2], unsigned *num_images) +{ + (void)xr; + (void)format; + (void)mutable_format; + (void)dims; + (void)layers; + slot_creates[slot]++; + if (slot_refuses & (1u << slot)) + return false; + images[0][0] = (VkImage)(uintptr_t)1; + images[1][0] = (VkImage)(uintptr_t)2; + *num_images = 2; + return true; +} + +static VkResult test_create_image_view(VkDevice device, + const VkImageViewCreateInfo *info, const VkAllocationCallbacks *alloc, + VkImageView *view) +{ + (void)device; + (void)info; + (void)alloc; + *view = (VkImageView)(uintptr_t)1; + return VK_SUCCESS; +} + +static VkResult test_create_framebuffer(VkDevice device, + const VkFramebufferCreateInfo *info, const VkAllocationCallbacks *alloc, + VkFramebuffer *framebuffer) +{ + (void)device; + (void)info; + (void)alloc; + *framebuffer = (VkFramebuffer)(uintptr_t)1; + return VK_SUCCESS; +} + +#define vkCreateImageView test_create_image_view +#define vkCreateFramebuffer test_create_framebuffer + +static bool vulkan_openxr_slot_acquire(vulkan_openxr_t *xr, unsigned slot, + unsigned *index) +{ + (void)xr; + (void)index; + return !(acquire_fails & (1u << slot)); +} + +static void vulkan_xr_copy_ui(vk_t *vk, unsigned s, unsigned ui_dims) +{ + (void)vk; + (void)s; + (void)ui_dims; +} + +static void vulkan_xr_draw_projection(vk_t *vk, + const video_frame_info_t *video_info) +{ + (void)vk; + (void)video_info; +} + +#include "driver_code.inc" + +#define CHECK(cond) do { if (!(cond)) { \ + printf("FAIL line %u: %s\n", (unsigned)__LINE__, #cond); \ + failures++; } } while (0) + +static void init(vk_t *vk, test_context_t *context, test_chain_t *chain) +{ + memset(vk, 0, sizeof(*vk)); + memset(context, 0, sizeof(*context)); + memset(chain, 0, sizeof(*chain)); + context->gpu_properties.limits.maxImageDimension2D = 1024; + context->gpu_properties.limits.maxFramebufferWidth = 1024; + context->gpu_properties.limits.maxFramebufferHeight = 1024; + vk->context = context; + vk->chain = chain; + allocations = destructions = warnings = fail_allocation = 0; + offscreen_passes = viewport_passes = replays = 0; + last_pass_w = last_pass_h = 0; + render_passes = frames_ended = 0; + chain_drawn = true; + memset(slot_creates, 0, sizeof(slot_creates)); + slot_refuses = 0; +} + +static void test_desktop_failure(bool ui) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + video_views_target_t target; + struct vk_image *image; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + init(&vk, &context, &chain); + memset(&target, 0, sizeof(target)); + image = ui ? &vk.views.ui : &vk.views.canvas; + fail_allocation = 1; + CHECK(!vulkan_vv_target_ensure(&vk, &target, image, dims)); + CHECK(warnings == 1 && allocations == 1 && destructions == 1); + CHECK(!vulkan_vv_target_ensure(&vk, &target, image, dims)); + CHECK(warnings == 1 && allocations == 1); +} + +static void test_retirement_saturation(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + video_views_target_t targets[3]; + unsigned i; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 32); + init(&vk, &context, &chain); + memset(targets, 0, sizeof(targets)); + for (i = 0; i < 3; i++) + CHECK(vulkan_vv_target_ensure(&vk, &targets[i], &vk.cached[i], dims)); + CHECK(vulkan_vv_target_ensure(&vk, &targets[0], &vk.cached[0], changed)); + CHECK(vulkan_vv_target_ensure(&vk, &targets[1], &vk.cached[1], changed)); + CHECK(chain.view_retired[0].image && chain.view_retired[1].image); + CHECK(!vulkan_vv_target_ensure(&vk, &targets[2], &vk.cached[2], changed)); + CHECK(targets[2].dims == dims && targets[2].made); + CHECK(allocations == 5 && warnings == 0); + /* Model a completed frame slot freeing a retired image. */ + vulkan_destroy_hdr_buffer(context.device, &chain.view_retired[0]); + CHECK(vulkan_vv_target_ensure(&vk, &targets[2], &vk.cached[2], changed)); + CHECK(targets[2].dims == changed && targets[2].made); + CHECK(allocations == 6 && warnings == 0); +} + +/* @screens screens drawn whole, then the menu. */ +static void xr_plan(unsigned dims, unsigned screens) +{ + unsigned s; + memset(&planned, 0, sizeof(planned)); + for (s = 0; s < screens; s++) + { + planned.slots[s].dims = dims; + planned.slots[s].layers = 1; + planned.slots[s].view[0] = -1; + planned.quads[s].slot = s; + } + planned.slots[VIDEO_XR_MENU_SLOT].dims = dims; + planned.slots[VIDEO_XR_MENU_SLOT].layers = 1; + planned.quads[s].slot = VIDEO_XR_MENU_SLOT; + planned.num_quads = screens + 1; +} + +/* Any chain: the pass steps are stubbed. */ +#define XR_FRAME_CHAIN ((vulkan_filter_chain_t*)(uintptr_t)1) + +static void xr_draw_frame(vk_t *vk, unsigned src_dims, bool new_frame) +{ + video_frame_info_t info; + memset(&info, 0, sizeof(info)); + vulkan_xr_draw(vk, &info, new_frame, true, false, NULL, XR_FRAME_CHAIN, + NULL, src_dims, src_dims, src_dims, true); +} + +static void xr_draw(vk_t *vk, unsigned src_dims) +{ + xr_draw_frame(vk, src_dims, true); +} + +/* After the frame's submit, as vulkan_xr_release. */ +static void xr_release(vk_t *vk) +{ + unsigned s; + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + if (vk->xr.slots[s].drawn) + { + vk->xr.slots[s].drawn = false; + vk->xr.slots[s].released = true; + } +} + +/* A slot whose image isn't ready keeps its quads: the headset shows its + * last image, or the swapchains a resize keeps until the new ones have + * one. */ +static void test_xr_draw_unready(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + init(&vk, &context, &chain); + xr_plan(dims, 3); + acquire_fails = (1u << 0) | (1u << VIDEO_XR_MENU_SLOT); + xr_draw(&vk, dims); + CHECK(vk.xr.set.num_quads == 4); + CHECK(!vk.xr.slots[0].drawn && vk.xr.slots[1].drawn); + CHECK(!vk.xr.slots[VIDEO_XR_MENU_SLOT].drawn); + acquire_fails = 0; + xr_draw(&vk, dims); + CHECK(vk.xr.set.num_quads == 4); + CHECK(vk.xr.slots[0].drawn && vk.xr.slots[1].drawn); +} + +/* A screen is drawn straight into the runtime's image: one final pass + * per layer, no image of the driver's own between, and no allocation. + * A new frame ends the chain's frame; a repeat whose image the runtime + * has draws nothing. */ +static void test_xr_draw_direct(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + init(&vk, &context, &chain); + acquire_fails = 0; + xr_plan(dims, 2); + xr_draw(&vk, dims); + CHECK(vk.xr.slots[0].drawn && vk.xr.slots[1].drawn); + CHECK(offscreen_passes == 2 && viewport_passes == 2); + CHECK(render_passes == 2 && frames_ended == 2 && replays == 0); + CHECK(allocations == 0); + xr_release(&vk); + xr_draw_frame(&vk, dims, false); + CHECK(!vk.xr.slots[0].drawn && !vk.xr.slots[1].drawn); + CHECK(viewport_passes == 2 && frames_ended == 2); +} + +/* A new frame with no frame data - a movie frame the driver imported - + * reaches the headset, as a core frame does. */ +static void test_xr_draw_movie_frame(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + init(&vk, &context, &chain); + acquire_fails = 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + xr_release(&vk); + viewport_passes = frames_ended = 0; + xr_draw_frame(&vk, dims, true); + CHECK(vk.xr.slots[0].drawn); + CHECK(viewport_passes == 1 && frames_ended == 1 && replays == 0); +} + +/* Resized while paused: the last frame's final pass is replayed at the + * new size straight into the new image. No other pass runs and no frame + * ends, so shader feedback and history stay where that frame left them. */ +static void test_xr_draw_resize_repeat(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 40); + init(&vk, &context, &chain); + acquire_fails = 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + xr_release(&vk); + offscreen_passes = viewport_passes = render_passes = frames_ended = 0; + xr_plan(changed, 1); + xr_draw_frame(&vk, dims, false); + CHECK(vk.xr.slots[0].drawn && vk.xr.slots[0].dims == changed); + CHECK(replays == 1 && render_passes == 1); + CHECK(offscreen_passes == 0 && viewport_passes == 0); + CHECK(frames_ended == 0 && allocations == 0); + CHECK(last_pass_w == 80 && last_pass_h == 40); + CHECK(slot_creates[0] == 2); +} + +/* A chain with no frame drawn since it was built - a preset loaded while + * paused - has nothing to replay: its passes run, and no frame ends. */ +static void test_xr_draw_repeat_unready(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 40); + init(&vk, &context, &chain); + acquire_fails = 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + xr_release(&vk); + offscreen_passes = viewport_passes = frames_ended = 0; + chain_drawn = false; + xr_plan(changed, 1); + xr_draw_frame(&vk, dims, false); + CHECK(offscreen_passes == 1 && viewport_passes == 1); + CHECK(replays == 0 && frames_ended == 0); +} + +/* Model completed frame slots freeing the retire ring. */ +static void xr_retired(vk_t *vk) +{ + unsigned i; + for (i = 0; i < 2; i++) + if (vk->chain->view_retired[i].image) + vulkan_destroy_hdr_buffer(vk->context->device, + &vk->chain->view_retired[i]); +} + +/* A size the runtime refused is not tried again. A resize back to it + * keeps the quad: the slot still has the swapchain it had. */ +static void test_xr_draw_slot_refused(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + unsigned changed = VIDEO_SCALE_PACK(80, 32); + init(&vk, &context, &chain); + acquire_fails = 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + CHECK(vk.xr.set.num_quads == 2 && vk.xr.slots[0].drawn); + slot_refuses = 1u << 0; + xr_plan(changed, 1); + xr_draw(&vk, changed); + xr_retired(&vk); + CHECK(vk.xr.set.num_quads == 1); + CHECK(vk.xr.set.quads[0].slot == VIDEO_XR_MENU_SLOT); + CHECK(slot_creates[0] == 2); + slot_refuses = 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + xr_retired(&vk); + CHECK(vk.xr.set.num_quads == 2 && vk.xr.slots[0].drawn); + CHECK(slot_creates[0] == 3); + xr_plan(changed, 1); + xr_draw(&vk, changed); + xr_retired(&vk); + CHECK(vk.xr.set.num_quads == 2 && !vk.xr.slots[0].drawn); + CHECK(vk.xr.slots[0].dims == dims && slot_creates[0] == 3); +} + +/* A slot the runtime never made is left out, and not tried again every + * frame. */ +static void test_xr_draw_slot_unmade(void) +{ + vk_t vk; + test_context_t context; + test_chain_t chain; + unsigned dims = VIDEO_SCALE_PACK(64, 32); + init(&vk, &context, &chain); + acquire_fails = 0; + slot_refuses = 1u << 0; + xr_plan(dims, 1); + xr_draw(&vk, dims); + CHECK(vk.xr.set.num_quads == 1); + CHECK(vk.xr.set.quads[0].slot == VIDEO_XR_MENU_SLOT); + xr_draw(&vk, dims); + CHECK(vk.xr.set.num_quads == 1 && slot_creates[0] == 1); +} + +int main(void) +{ + test_desktop_failure(false); + test_desktop_failure(true); + test_retirement_saturation(); + test_xr_draw_unready(); + test_xr_draw_direct(); + test_xr_draw_movie_frame(); + test_xr_draw_resize_repeat(); + test_xr_draw_repeat_unready(); + test_xr_draw_slot_refused(); + test_xr_draw_slot_unmade(); + printf("%u failed (10 Vulkan target cases)\n", failures); + return failures ? 1 : 0; +} diff --git a/samples/input/openxr_haptics/openxr_haptics_test.c b/samples/input/openxr_haptics/openxr_haptics_test.c index 05888baada..77225466e9 100644 --- a/samples/input/openxr_haptics/openxr_haptics_test.c +++ b/samples/input/openxr_haptics/openxr_haptics_test.c @@ -45,10 +45,14 @@ bool openxr_session_ended(openxr_session_t *session) { (void)session; return false; } XrSwapchain openxr_frame_cursor(const openxr_frame_t *frame) { (void)frame; return test_frame ? (XrSwapchain)(uintptr_t)1 : XR_NULL_HANDLE; } +unsigned openxr_frame_cursor_w(const openxr_frame_t *frame) +{ (void)frame; return VIDEO_XR_CURSOR_W; } void openxr_frame_set_input_hooks(const openxr_input_hooks_t *hooks) { (void)hooks; } XrTime openxr_frame_predicted_time(openxr_frame_t *frame) { (void)frame; return 0; } +XrDuration openxr_frame_period(openxr_frame_t *frame) +{ (void)frame; return 0; } void openxr_frame_get_shown_quads(openxr_frame_t *frame, video_xr_quad_set_t *out, XrSpace *space) { (void)frame; memset(out, 0, sizeof(*out)); *space = XR_NULL_HANDLE; } @@ -57,6 +61,17 @@ bool video_driver_get_views_core(const video_views_map_t **map, unsigned *dims) { (void)map; (void)dims; return false; } bool video_driver_get_viewport_info(struct video_viewport *vp) { (void)vp; return false; } +void input_driver_platform_request(enum input_platform_request req) +{ (void)req; } +void runloop_msg_queue_push(const char *msg, size_t len, unsigned prio, + unsigned duration, bool flush, char *title, + enum message_queue_icon icon, enum message_queue_category category) +{ (void)msg; (void)len; (void)prio; (void)duration; (void)flush; + (void)title; (void)icon; (void)category; } +const char *msg_hash_to_str(enum msg_hash_enums msg) +{ (void)msg; return ""; } +void video_driver_headset_edit(bool editing, unsigned seq) +{ (void)editing; (void)seq; } bool input_autoconfigure_connect_ex(const char *name, const char *display_name, const char *phys, const char *driver, unsigned port, unsigned vid, unsigned pid, uint8_t flags) @@ -100,9 +115,208 @@ int video_xr_pick(const video_xr_quad_set_t *set, unsigned laser, bool menu, picked_laser = laser; picked_menu = menu; picks++; return -1; } +int video_xr_pick_edit(const video_xr_quad_set_t *set, bool stuck, + const video_xr_vec3_t *o, const video_xr_vec3_t *d, + float *u, float *v, float *t, bool *stick) +{ + (void)set; (void)stuck; (void)o; (void)d; (void)u; (void)v; (void)t; + if (stick) + *stick = false; + picked_laser = VIDEO_XR_LIVE_EDIT; picked_menu = false; picks++; + return -1; +} float video_xr_cursor(const video_xr_quad_t *q, float u, float v, float t, video_xr_pose_t *pose) { (void)q; (void)u; (void)v; (void)t; (void)pose; return 0.01f; } +void video_xr_beam(const video_xr_vec3_t *o, const video_xr_vec3_t *d, + float len, const video_xr_vec3_t *eye, video_xr_pose_t *pose) +{ (void)o; (void)d; (void)len; (void)eye; memset(pose, 0, sizeof(*pose)); } +void video_xr_tag(const video_xr_vec3_t *hand, const video_xr_vec3_t *eye, + video_xr_pose_t *pose) +{ (void)hand; (void)eye; memset(pose, 0, sizeof(*pose)); } +/* What edit mode reaches, never run here. */ +void openxr_frame_get_quads(openxr_frame_t *f, video_xr_quad_set_t *out) +{ (void)f; memset(out, 0, sizeof(*out)); } +void openxr_frame_get_screens(openxr_frame_t *f, video_xr_pose_t *pose, + XrSpace *space) +{ (void)f; memset(pose, 0, sizeof(*pose)); *space = XR_NULL_HANDLE; } +void config_get_video_screen_xr(unsigned screen, video_screen_xr_t *out) +{ (void)screen; memset(out, 0, sizeof(*out)); } +bool config_get_video_menu_xr(video_screen_xr_t *out) +{ memset(out, 0, sizeof(*out)); return false; } +void config_set_video_screen_xr(unsigned screen, + const video_screen_xr_t *values) +{ (void)screen; (void)values; } +void config_set_video_menu_xr(const video_screen_xr_t *values) +{ (void)values; } +void video_views_xr_reset(video_screen_xr_t *screens, unsigned count) +{ memset(screens, 0, count * sizeof(*screens)); } +void video_views_xr_normalize(video_screen_xr_t *screen) { (void)screen; } +void video_xr_compose(const video_xr_pose_t *a, const video_xr_pose_t *b, + video_xr_pose_t *out) +{ (void)a; *out = *b; } +void video_xr_pose_invert(const video_xr_pose_t *a, video_xr_pose_t *out) +{ *out = *a; } +void video_xr_grab_hold(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quat_t *q) +{ (void)aim; g->turn = *q; } +bool video_xr_grab_pose(const video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, video_xr_pose_t *pose, + video_screen_xr_t *out, unsigned *snapped) +{ + (void)g; (void)targets; (void)skip; (void)start; (void)out; + if (snapped) + *snapped = 0; + *pose = *aim; + return false; +} +void video_xr_grab_stick(video_xr_grab_t *g, float x, float y, double dt, + float *scale) +{ (void)g; (void)x; (void)y; (void)dt; (void)scale; } +unsigned video_xr_grab_off_step(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_vec3_t *head, float step, + const float *rest) +{ (void)g; (void)aim; (void)head; (void)step; (void)rest; return 0; } +unsigned video_xr_grab_step(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_vec3_t *head, float step, unsigned axes, float *rest, + const video_xr_quad_set_t *targets, unsigned skip, + const video_xr_pose_t *start, const video_xr_hinge_t *lid) +{ + (void)g; (void)aim; (void)head; (void)step; (void)axes; (void)rest; + (void)targets; (void)skip; (void)start; (void)lid; + return 0; +} +unsigned video_xr_grab_turn_lid(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *lid, float x, + float y, double dt) +{ (void)g; (void)aim; (void)lid; (void)x; (void)y; (void)dt; return 0; } +bool video_xr_grab_near_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h, float reach) +{ + (void)g; (void)aim; (void)targets; (void)h; (void)reach; + return false; +} +void video_xr_rest_take(const video_xr_quad_set_t *set, unsigned slot, + float width, float height, float gap, const video_xr_hinge_t *h, + video_xr_pose_t *pose) +{ + (void)set; (void)slot; (void)width; (void)height; (void)gap; (void)h; + (void)pose; +} +bool video_xr_grab_through(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_hinge_t *h) +{ (void)g; (void)aim; (void)targets; (void)h; return false; } +unsigned video_xr_grab_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start, video_xr_pose_t *pose, + video_xr_hinge_t *hinge) +{ + (void)g; (void)targets; (void)skip; (void)start; (void)hinge; + *pose = *aim; + return 0; +} +void video_xr_grab_put(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_pose_t *pose) +{ (void)g; (void)aim; (void)pose; } +void video_xr_grab_put_hinge(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h, const video_xr_pose_t *pose, bool turned) +{ (void)g; (void)aim; (void)h; (void)pose; (void)turned; } +void video_xr_grab_put_line(video_xr_grab_t *g, const video_xr_pose_t *aim, + const video_xr_hinge_t *h) +{ (void)g; (void)aim; (void)h; } +unsigned video_xr_grab_turn_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float x, + float y, double dt) +{ (void)g; (void)aim; (void)h; (void)x; (void)y; (void)dt; return 0; } +unsigned video_xr_grab_step_hinge(video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + unsigned axes, float *rest, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_pose_t *start) +{ + (void)g; (void)aim; (void)h; (void)size; (void)axes; (void)rest; + (void)targets; (void)skip; (void)start; + return 0; +} +unsigned video_xr_grab_off_hinge(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_hinge_t *h, float size, + const float *rest) +{ (void)g; (void)aim; (void)h; (void)size; (void)rest; return 0; } +void video_xr_turn_back(video_xr_pose_t *pose) { (void)pose; } +void video_xr_menu_handle(const video_xr_quad_t *menu, video_xr_quad_t *out) +{ *out = *menu; } +void video_xr_outline(const video_xr_quad_t *q, video_xr_quad_t *out) +{ *out = *q; } +void video_xr_outline_crop(video_xr_quad_t *o, unsigned hide, bool back, + unsigned *x, unsigned *w) +{ (void)o; (void)hide; (void)back; *x = 0; *w = VIDEO_XR_OUTLINE_DIM; } +unsigned video_xr_outline_cell(unsigned hide, bool back, unsigned lit) +{ (void)hide; (void)back; return VIDEO_XR_OUTLINE_X(lit); } +bool video_xr_hinge_line(const video_xr_hinge_t *h, float width, + float height, const video_xr_quad_t *target, video_xr_vec3_t *a, + video_xr_vec3_t *b) +{ + (void)h; (void)width; (void)height; (void)target; (void)a; (void)b; + return false; +} +void video_xr_hinge_place(video_xr_hinge_t *h, const video_xr_pose_t *pose, + float width, float height, const video_xr_quad_t *target, bool over) +{ + (void)h; (void)pose; (void)width; (void)height; (void)target; + (void)over; +} +unsigned video_xr_rest_hinges(const video_xr_quad_set_t *set, + unsigned skip, float gap, video_xr_rest_t *rest, unsigned max) +{ + (void)set; (void)skip; (void)gap; (void)rest; (void)max; + return 0; +} +bool video_xr_rest_hinge(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_pose_t *pose, video_xr_hinge_t *hinge) +{ + (void)set; (void)slot; (void)skip; (void)width; (void)height; + (void)gap; (void)pose; (void)hinge; + return false; +} +bool video_xr_hinge_settle(const video_xr_quad_set_t *set, unsigned slot, + unsigned skip, float width, float height, float gap, + const video_xr_hinge_t *h, const video_xr_pose_t *hand, + video_xr_pose_t *pose) +{ + (void)set; (void)slot; (void)skip; (void)width; (void)height; + (void)gap; (void)h; (void)hand; (void)pose; + return false; +} +bool video_xr_grab_settle(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + unsigned skip, const video_xr_hinge_t *h, video_xr_pose_t *pose) +{ + (void)g; (void)aim; (void)targets; (void)skip; (void)h; (void)pose; + return false; +} +bool video_xr_grab_kept(const video_xr_grab_t *g, + const video_xr_pose_t *aim, const video_xr_quad_set_t *targets, + const video_xr_pose_t *start, video_xr_hinge_t *kept) +{ + (void)g; (void)aim; (void)targets; (void)start; (void)kept; + return false; +} +void openxr_frame_publish_grabs(openxr_frame_t *f, const openxr_grabs_t *g) +{ (void)f; (void)g; } +void video_xr_smooth_step(video_xr_smooth_t *s, unsigned level, + const video_xr_pose_t *raw, double dt, video_xr_pose_t *out) +{ (void)s; (void)level; (void)dt; *out = *raw; } +void openxr_frame_publish_aims(openxr_frame_t *f, const openxr_aims_t *a) +{ (void)f; (void)a; } +void openxr_frame_get_aims(openxr_frame_t *f, openxr_aims_t *out) +{ (void)f; memset(out, 0, sizeof(*out)); } +int openxr_frame_recenters(openxr_frame_t *f) +{ (void)f; return 0; } +static video_xr_quad_set_t test_shown; +static unsigned test_under; static XrResult XRAPI_CALL locate(XrSpace space, XrSpace base, XrTime time, XrSpaceLocation *out) @@ -205,7 +419,8 @@ static void publish_pointer(unsigned laser, bool menu) openxr_frame_t *frame = input_openxr_st.frame; test_menu_open = menu; input_openxr_st.frame = NULL; - input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, laser, 0.5f); + input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, laser, + VIDEO_OPENXR_LASER_BEAM_EDIT, 0.5f); input_openxr_st.frame = frame; } static void replace_pointer(void) @@ -224,29 +439,40 @@ static void pointer_tests(void) { publish_pointer(laser, menu != 0); picks = 0; - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); if (laser == VIDEO_OPENXR_LASER_OFF) CHECK(picks == 0, "disabled pointer picked a quad"); else CHECK(picks == 2 && picked_laser == laser && picked_menu == (menu != 0), "pointer pair changed"); } + publish_pointer(VIDEO_OPENXR_LASER_LAST, false); + picks = 0; + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); + CHECK(picks == 2 && picked_laser == VIDEO_OPENXR_LASER_AUTO + && !picked_menu, "a laser past the choices was not Auto"); publish_pointer(VIDEO_OPENXR_LASER_AUTO, false); after_load = replace_pointer; picks = 0; - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picks == 2 && picked_laser == VIDEO_OPENXR_LASER_AUTO && !picked_menu, "frame mixed two published pointer configurations"); - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picked_laser == VIDEO_OPENXR_LASER_ALWAYS && picked_menu, "next frame did not observe replacement pointer pair"); input_openxr_st.yield = true; publish_pointer(VIDEO_OPENXR_LASER_ALWAYS, true); picks = 0; - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picks == 0, "yielding pointer remained visible"); publish_pointer(VIDEO_OPENXR_LASER_ALWAYS, false); - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picks == 2 && !picked_menu, "closed menu did not end pointer yield"); input_openxr_st.SyncActions = sync_actions; input_openxr_st.GetActionStateBoolean = read_bool; @@ -254,14 +480,16 @@ static void pointer_tests(void) input_openxr_st.GetActionStateVector2f = read_vector; test_navigation = test_menu_open = true; input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, - VIDEO_OPENXR_LASER_ALWAYS, 0.5f); + VIDEO_OPENXR_LASER_ALWAYS, VIDEO_OPENXR_LASER_BEAM_EDIT, 0.5f); picks = 0; - input_openxr_frame_layers(NULL, 1, layers, 2); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picks == 0, "post-navigation publication did not hide pointer"); test_navigation = test_menu_open = false; input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, - VIDEO_OPENXR_LASER_ALWAYS, 0.5f); - input_openxr_frame_layers(NULL, 1, layers, 2); + VIDEO_OPENXR_LASER_ALWAYS, VIDEO_OPENXR_LASER_BEAM_EDIT, 0.5f); + input_openxr_frame_layers(NULL, 1, &test_shown, XR_NULL_HANDLE, + layers, 2, &test_under); CHECK(picks == 2 && picked_laser == VIDEO_OPENXR_LASER_ALWAYS && !picked_menu, "completed poll published the wrong pointer pair"); } @@ -340,7 +568,8 @@ int main(void) service(false, false); CHECK(input_openxr_st.haptic[0] == 0.0f, "unfocused result cached as applied"); test_focused = false; - input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF, 0.5f); + input_openxr_poll(4, VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF, + VIDEO_OPENXR_LASER_BEAM_EDIT, 0.5f); test_focused = true; apply_result[0] = XR_SUCCESS; service(false, false); diff --git a/samples/input/openxr_input/openxr_input_test.c b/samples/input/openxr_input/openxr_input_test.c index 8803b89610..5e8faee697 100644 --- a/samples/input/openxr_input/openxr_input_test.c +++ b/samples/input/openxr_input/openxr_input_test.c @@ -122,14 +122,14 @@ static unsigned t_set_index(XrActionSet s) } /* Actions as "set/name", in the order they are made. */ -static char t_actions[128][XR_MAX_ACTION_SET_NAME_SIZE + XR_MAX_ACTION_NAME_SIZE]; -static unsigned t_action_set[128]; +static char t_actions[256][XR_MAX_ACTION_SET_NAME_SIZE + XR_MAX_ACTION_NAME_SIZE]; +static unsigned t_action_set[256]; static unsigned t_num_actions; static XRAPI_ATTR XrResult XRAPI_CALL t_create_action(XrActionSet s, const XrActionCreateInfo *ci, XrAction *a) { unsigned set = t_set_index(s); - if (t_num_actions >= 128 || set >= t_num_sets) + if (t_num_actions >= 256 || set >= t_num_sets) return XR_ERROR_HANDLE_INVALID; snprintf(t_actions[t_num_actions], sizeof(t_actions[0]), "%s/%s", t_set_names[set], ci->actionName); @@ -142,10 +142,14 @@ static unsigned t_action_index(XrAction a) return (unsigned)((uintptr_t)a - 0x201); } -/* Profiles suggested; Touch's bindings as action and path. */ -static unsigned t_suggested; -static char t_touch[128][2][XR_MAX_PATH_LENGTH]; -static unsigned t_num_touch; +/* Profiles suggested; each binding as profile, action and path. A + * runtime that refuses edit mode's bindings refuses any suggestion that + * holds one: for every profile, or for t_refuse_edit_for alone. */ +static unsigned t_suggested; +static bool t_refuse_edit; +static const char *t_refuse_edit_for; +static char t_bound[256][3][XR_MAX_PATH_LENGTH]; +static unsigned t_num_bound; /* A runtime whose profiles lack XR_EXT_palm_pose's path. */ static bool t_refuse_palm; static XRAPI_ATTR XrResult XRAPI_CALL t_suggest(XrInstance i, @@ -156,31 +160,44 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_suggest(XrInstance i, for (k = 0; k < sb->countSuggestedBindings; k++) if (strstr(t_path(sb->suggestedBindings[k].binding), "palm_ext")) return XR_ERROR_PATH_UNSUPPORTED; + if ( t_refuse_edit + || (t_refuse_edit_for + && !strcmp(t_path(sb->interactionProfile), t_refuse_edit_for))) + for (k = 0; k < sb->countSuggestedBindings; k++) + if (!strncmp(t_actions[t_action_index( + sb->suggestedBindings[k].action)], "edit/", 5)) + return XR_ERROR_PATH_UNSUPPORTED; t_suggested++; - if (strcmp(t_path(sb->interactionProfile), - "/interaction_profiles/oculus/touch_controller")) - return XR_SUCCESS; - for (k = 0; k < sb->countSuggestedBindings && t_num_touch < 128; k++) + for (k = 0; k < sb->countSuggestedBindings && t_num_bound < 256; k++) { - strlcpy(t_touch[t_num_touch][0], + strlcpy(t_bound[t_num_bound][0], t_path(sb->interactionProfile), + sizeof(t_bound[0][0])); + strlcpy(t_bound[t_num_bound][1], t_actions[t_action_index(sb->suggestedBindings[k].action)], - sizeof(t_touch[0][0])); - strlcpy(t_touch[t_num_touch][1], + sizeof(t_bound[0][1])); + strlcpy(t_bound[t_num_bound][2], t_path(sb->suggestedBindings[k].binding), - sizeof(t_touch[0][1])); - t_num_touch++; + sizeof(t_bound[0][2])); + t_num_bound++; } return XR_SUCCESS; } -static bool t_touch_binds(const char *action, const char *path) +static bool t_binds(const char *profile, const char *action, + const char *path) { unsigned k; - for (k = 0; k < t_num_touch; k++) - if ( !strcmp(t_touch[k][0], action) - && (!path || !strcmp(t_touch[k][1], path))) + for (k = 0; k < t_num_bound; k++) + if ( !strcmp(t_bound[k][0], profile) + && !strcmp(t_bound[k][1], action) + && (!path || !strcmp(t_bound[k][2], path))) return true; return false; } +static bool t_touch_binds(const char *action, const char *path) +{ + return t_binds("/interaction_profiles/oculus/touch_controller", + action, path); +} static unsigned t_attached; static XrResult t_attach_result = XR_SUCCESS; @@ -243,14 +260,19 @@ static void t_hold(const char *action, int hand, float x, float y) h->y = y; } static void t_let_go(void) { t_num_held = 0; } +/* Actions a binding leaves out, by their names' start ("edit/": a + * user's binding saved before edit mode's actions existed). */ +static const char *t_unbound; /* An action's held value for the hand asked, only while its set is - * synced. */ + * synced and it is bound. */ static const t_held_t *t_find(const XrActionStateGetInfo *gi, bool *active) { unsigned k; unsigned a = t_action_index(gi->action); int hand = t_hand(gi->subactionPath); - *active = a < t_num_actions && t_synced[t_action_set[a]]; + *active = a < t_num_actions && t_synced[t_action_set[a]] + && !(t_unbound && !strncmp(t_actions[a], t_unbound, + strlen(t_unbound))); if (!*active) return NULL; for (k = 0; k < t_num_held; k++) @@ -288,9 +310,13 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_get_vec2(XrSession s, return XR_SUCCESS; } -/* Each hand's aim, by hand, and the space the last locate was in. */ +/* Each hand's aim, by hand; the head, when a case tracks it; and the + * space the last locate was in. */ +#define T_VIEW ((XrSpace)(uintptr_t)0x42) static bool t_aim_tracked[3]; static XrPosef t_aim[3]; +static bool t_head_tracked; +static XrPosef t_head = { { 0.0f, 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, 0.0f } }; static XrSpace t_located_base; static XRAPI_ATTR XrResult XRAPI_CALL t_locate(XrSpace space, XrSpace base, XrTime time, XrSpaceLocation *loc) @@ -303,9 +329,10 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_locate(XrSpace space, XrSpace base, /* A runtime fills a chained velocity; none is known here. */ if (vel && vel->type == XR_TYPE_SPACE_VELOCITY) vel->velocityFlags = 0; - if (hand && t_aim_tracked[hand]) + if ( (hand && t_aim_tracked[hand]) + || (space == T_VIEW && t_head_tracked)) { - loc->pose = t_aim[hand]; + loc->pose = hand ? t_aim[hand] : t_head; loc->locationFlags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT; } @@ -331,12 +358,14 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_get_pose(XrSession s, st->isActive = active ? XR_TRUE : XR_FALSE; return XR_SUCCESS; } -/* Touch in both hands. */ +/* Each hand's controller's profile, by hand: Touch unless a case says. */ +static const char *t_current[3]; static XRAPI_ATTR XrResult XRAPI_CALL t_get_profile(XrSession s, XrPath user, XrInteractionProfileState *st) { - return t_string_to_path(T_INSTANCE, - "/interaction_profiles/oculus/touch_controller", + const char *profile = t_current[t_hand(user)]; + return t_string_to_path(T_INSTANCE, profile ? profile + : "/interaction_profiles/oculus/touch_controller", &st->interactionProfile); } @@ -415,13 +444,24 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_locate_joints(XrHandTrackerEXT t, return XR_SUCCESS; } -/* Each hand's haptics: the last amplitude, 0 once stopped. */ -static float t_amp[3]; +/* Each hand's haptics: the last amplitude, 0 once stopped, and length, + * how many were applied, and how many ran longer than a snap's tick, + * T_TICK_NS (INPUT_OPENXR_SNAP_TICK_NS, pinned in test_edit_hinge_buzz). */ +#define T_TICK_NS 20000000 +static float t_amp[3]; +static XrDuration t_dur[3]; +static unsigned t_applies[3]; +static unsigned t_longs[3]; static XRAPI_ATTR XrResult XRAPI_CALL t_apply(XrSession s, const XrHapticActionInfo *hi, const XrHapticBaseHeader *h) { - t_amp[t_hand(hi->subactionPath)] = - ((const XrHapticVibration*)h)->amplitude; + const XrHapticVibration *v = (const XrHapticVibration*)h; + unsigned hand = t_hand(hi->subactionPath); + t_amp[hand] = v->amplitude; + t_dur[hand] = v->duration; + t_applies[hand]++; + if (v->duration > T_TICK_NS) + t_longs[hand]++; return XR_SUCCESS; } static XRAPI_ATTR XrResult XRAPI_CALL t_stop(XrSession s, @@ -565,6 +605,109 @@ bool video_driver_get_viewport_info(struct video_viewport *vp) static bool t_menu_open; bool menu_driver_alive(void) { return t_menu_open; } +/* The video state, the frontend's pause and notices edit mode uses. A + * pause asked from inside a poll, which can be inside the core's frame, + * or before video was told, is counted. */ +static bool t_editing; +static unsigned t_edit_seq; +void video_driver_headset_edit(bool editing, unsigned seq) +{ + t_editing = editing; + t_edit_seq = seq; +} +/* The settings edit mode reads and writes, kept as configuration.c + * keeps them: a write normalised and marked modified, Visible left + * alone; NULL resets a screen as Reset Screen does, or puts the menu + * back in front of screen 1. */ +static video_screen_xr_t t_xr[RETRO_VIDEO_VIEWS_MAX]; +static video_screen_xr_t t_menu_xr; +static bool t_menu_placed; +static bool t_modified; +void config_get_video_screen_xr(unsigned screen, video_screen_xr_t *out) +{ + *out = t_xr[screen]; +} +bool config_get_video_menu_xr(video_screen_xr_t *out) +{ + *out = t_menu_xr; + return t_menu_placed; +} +void config_set_video_screen_xr(unsigned screen, + const video_screen_xr_t *values) +{ + video_screen_xr_t s; + if (screen >= RETRO_VIDEO_VIEWS_MAX) + return; + video_views_xr_reset(&s, 1); + if (values) + { + s = *values; + s.visible = t_xr[screen].visible; + video_views_xr_normalize(&s); + } + t_xr[screen] = s; + t_modified = true; +} +void config_set_video_menu_xr(const video_screen_xr_t *values) +{ + video_views_xr_reset(&t_menu_xr, 1); + t_menu_placed = values != NULL; + if (values) + { + t_menu_xr = *values; + video_views_xr_normalize(&t_menu_xr); + } + t_modified = true; +} +static bool t_in_poll; +static unsigned t_pauses, t_unpauses, t_bad_pauses; +/* A pause the frontend refuses (hardcore, netplay) when a case says. */ +static bool t_pause_refused; +void input_driver_platform_request(enum input_platform_request req) +{ + if (req == INPUT_PLATFORM_PAUSE) + { + t_pauses++; + if (t_in_poll || !t_editing) + t_bad_pauses++; + if (!t_pause_refused) + t_run_flags |= RUNLOOP_FLAG_PAUSED; + } + else if (req == INPUT_PLATFORM_UNPAUSE) + { + t_unpauses++; + t_run_flags &= ~RUNLOOP_FLAG_PAUSED; + } +} +static char t_notice[256]; +static unsigned t_notices; +void runloop_msg_queue_push(const char *msg, size_t len, + unsigned prio, unsigned duration, bool flush, char *title, + enum message_queue_icon icon, enum message_queue_category category) +{ + strlcpy(t_notice, msg, sizeof(t_notice)); + t_notices++; +} +const char *msg_hash_to_str(enum msg_hash_enums msg) +{ + switch (msg) + { + case MSG_OPENXR_EDIT_ON: + return "edit on"; + case MSG_OPENXR_EDIT_OFF: + return "edit off"; + case MSG_OPENXR_EDIT_UNSAVED: + return "edit unsaved"; + case MSG_OPENXR_EDIT_UNAVAILABLE: + return "edit unavailable"; + case MSG_OPENXR_EDIT_NO_CONTENT: + return "edit no content"; + default: + break; + } + return ""; +} + static openxr_session_t t_xr_session; static openxr_frame_t t_xr_frame; static bool t_focused = true; @@ -572,6 +715,7 @@ static bool t_ended; static XrTime t_time = 1000000000; static video_xr_quad_set_t t_quads; static openxr_input_hooks_t t_hooks; +static unsigned t_under; bool openxr_session_focused(openxr_session_t *s) { return s == &t_xr_session && t_focused; @@ -580,7 +724,16 @@ bool openxr_session_ended(openxr_session_t *s) { return s == &t_xr_session && t_ended; } -XrTime openxr_frame_predicted_time(openxr_frame_t *f) { return t_time; } +/* The frame the main thread locates at: a new one as t_clock moves, as + * the frame loop runs alongside. */ +XrTime openxr_frame_predicted_time(openxr_frame_t *f) +{ + return t_time + (XrTime)t_clock * 1000; +} +/* The headset's display period as published: 90 Hz unless a case sets + * it. */ +static XrDuration t_period = 11111111; +XrDuration openxr_frame_period(openxr_frame_t *f) { return t_period; } /* The quads as shown, and their space: LOCAL unless a case sets it. */ static XrSpace t_space; void openxr_frame_get_shown_quads(openxr_frame_t *f, @@ -589,9 +742,67 @@ void openxr_frame_get_shown_quads(openxr_frame_t *f, *out = t_quads; *space = t_space ? t_space : t_xr_session.local_space; } +/* The same quads from an identity anchor, planned at video's edit count + * or, while a case holds them back (the drawing thread behind), at the + * count it holds them at; and the screens' pose: the identity unless a + * case turns it, in the shown quads' space. */ +static video_xr_pose_t t_screens = { { 0.0f, 0.0f, 0.0f, 1.0f }, + { 0.0f, 0.0f, 0.0f } }; +static bool t_quads_behind; +static unsigned t_quads_seq; +void openxr_frame_get_quads(openxr_frame_t *f, video_xr_quad_set_t *out) +{ + unsigned s; + *out = t_quads; + for (s = 0; s < VIDEO_XR_MAX_SLOTS; s++) + out->slots[s].edit_seq = t_quads_behind ? t_quads_seq : t_edit_seq; +} +void openxr_frame_get_screens(openxr_frame_t *f, video_xr_pose_t *pose, + XrSpace *space) +{ + *pose = t_screens; + *space = t_space ? t_space : t_xr_session.local_space; +} +/* The dots' image: none unless a case gives one. */ +static bool t_cursor; XrSwapchain openxr_frame_cursor(const openxr_frame_t *f) { - return XR_NULL_HANDLE; + return t_cursor ? (XrSwapchain)(uintptr_t)0x51 : XR_NULL_HANDLE; +} +/* Its width: with the cut outlines, unless a case narrows it. */ +static unsigned t_cursor_w = VIDEO_XR_CURSOR_W; +unsigned openxr_frame_cursor_w(const openxr_frame_t *f) +{ + return t_cursor_w; +} +/* What the hands hold, as last given to the frame loop. */ +static openxr_grabs_t t_grabs; +static unsigned t_grabs_published; +void openxr_frame_publish_grabs(openxr_frame_t *f, const openxr_grabs_t *g) +{ + t_grabs = *g; + t_grabs_published++; +} +/* Edit Smoothing's aims as the frame loop last published them, and the + * recenters it has seen. */ +static openxr_aims_t t_aims; +static unsigned t_aims_published; +static int t_recenters; +void openxr_frame_publish_aims(openxr_frame_t *f, const openxr_aims_t *a) +{ + (void)f; + t_aims = *a; + t_aims_published++; +} +void openxr_frame_get_aims(openxr_frame_t *f, openxr_aims_t *out) +{ + (void)f; + *out = t_aims; +} +int openxr_frame_recenters(openxr_frame_t *f) +{ + (void)f; + return t_recenters; } void openxr_frame_set_input_hooks(const openxr_input_hooks_t *hooks) { @@ -612,7 +823,7 @@ static void t_session(bool frame_controller) memset(&t_xr_frame, 0, sizeof(t_xr_frame)); t_xr_session.session = T_SESSION; t_xr_session.local_space = (XrSpace)(uintptr_t)0x41; - t_xr_session.view_space = (XrSpace)(uintptr_t)0x42; + t_xr_session.view_space = T_VIEW; t_xr_frame.session = &t_xr_session; memset(&h, 0, sizeof(h)); h.frame = &t_xr_frame; @@ -721,9 +932,14 @@ static void t_forget_devices(void) * next. */ static bool t_hold_tasks; +/* The Laser Beam setting each poll passes. */ +static unsigned t_beam = VIDEO_OPENXR_LASER_BEAM_EDIT; + static void t_poll(unsigned controllers, unsigned laser) { - input_openxr_poll(4, controllers, laser, 0.5f); + t_in_poll = true; + input_openxr_poll(4, controllers, laser, t_beam, 0.5f); + t_in_poll = false; if (!t_hold_tasks) t_land(); } @@ -787,7 +1003,8 @@ static bool t_nothing_held(void) } return !t_btn(0, RARCH_MENU_TOGGLE) && !t_btn(0, RARCH_HEADSET_RECENTER) && !t_btn(0, RARCH_LASER_POINTER_TOGGLE) - && !t_btn(0, RARCH_HEADSET_VIEW_TOGGLE); + && !t_btn(0, RARCH_HEADSET_VIEW_TOGGLE) + && !t_btn(0, RARCH_HEADSET_EDIT_SCREENS); } /* ---- the tests ---- */ @@ -795,7 +1012,7 @@ static void test_before_session(void) { input_openxr_register(); CHECK(t_hooks.session_created && t_hooks.session_destroying - && t_hooks.frame_layers && t_hooks.locate_hands, + && t_hooks.frame_layers && t_hooks.locate_hands && t_hooks.aim, "the hooks were not registered"); t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); CHECK(t_nothing_held(), "something is held before there is a session"); @@ -806,13 +1023,13 @@ static void test_session_made(void) { t_reset_log(); t_session(false); - CHECK(t_num_sets == 3 && t_num_actions == 37, - "%u sets and %u actions made, want 3 and 37", t_num_sets, + CHECK(t_num_sets == 4 && t_num_actions == 47, + "%u sets and %u actions made, want 4 and 47", t_num_sets, t_num_actions); CHECK(t_suggested == 4, "%u profiles suggested without the Frame's extension, want 4", t_suggested); - CHECK(t_attached == 3 && t_spaces_made == 4, + CHECK(t_attached == 4 && t_spaces_made == 4, "%u sets attached and %u aim and grip spaces", t_attached, t_spaces_made); CHECK(t_touch_binds("pointer/grip", "/user/hand/right/input/grip/pose") @@ -822,6 +1039,18 @@ static void test_session_made(void) "/user/hand/left/input/menu/click") && !t_touch_binds("combined/start", NULL), "Touch's menu button is not Start and the menu"); + CHECK(t_touch_binds("edit/grab", "/user/hand/left/input/squeeze/value") + && t_touch_binds("edit/grab", "/user/hand/right/input/squeeze/value") + && t_touch_binds("edit/tilt", "/user/hand/right/input/trigger/value") + && t_touch_binds("edit/stick", "/user/hand/left/input/thumbstick") + && t_touch_binds("edit/reset", "/user/hand/left/input/x/click") + && t_touch_binds("edit/reset", "/user/hand/right/input/a/click") + && t_touch_binds("edit/menu", "/user/hand/left/input/menu/click"), + "Touch's edit bindings are not grip, trigger, stick, X/A, menu"); + CHECK(!t_touch_binds("combined/edit_screens", NULL) + && !t_touch_binds("separate/edit_screens", NULL) + && !t_touch_binds("edit/edit_screens", NULL), + "Edit Screens suggested: the user binds it"); CHECK(input_openxr_can_rumble(), "rumble does not reach ready controllers"); CHECK(t_logged("[OpenXR] Headset controllers ready.\n") == 1, "log: %s", t_log); @@ -912,15 +1141,18 @@ static void test_hotkeys(void) t_hold("combined/laser", 0, 1.0f, 0.0f); t_hold("combined/view", 0, 1.0f, 0.0f); t_hold("combined/menu", 0, 1.0f, 0.0f); + t_hold("combined/edit_screens", 0, 1.0f, 0.0f); t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); CHECK(t_btn(3, RARCH_HEADSET_RECENTER) && t_btn(3, RARCH_LASER_POINTER_TOGGLE) && t_btn(3, RARCH_HEADSET_VIEW_TOGGLE) - && t_btn(3, RARCH_MENU_TOGGLE), - "the hotkeys do not answer on any port: %d %d %d %d", + && t_btn(3, RARCH_MENU_TOGGLE) + && t_btn(3, RARCH_HEADSET_EDIT_SCREENS), + "the hotkeys do not answer on any port: %d %d %d %d %d", t_btn(3, RARCH_HEADSET_RECENTER), t_btn(3, RARCH_LASER_POINTER_TOGGLE), - t_btn(3, RARCH_HEADSET_VIEW_TOGGLE), t_btn(3, RARCH_MENU_TOGGLE)); + t_btn(3, RARCH_HEADSET_VIEW_TOGGLE), t_btn(3, RARCH_MENU_TOGGLE), + t_btn(3, RARCH_HEADSET_EDIT_SCREENS)); t_let_go(); t_hold("separate/view", 0, 1.0f, 0.0f); @@ -1015,6 +1247,43 @@ static void test_start_menu(void) t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); } +/* Touch's left menu button, which opens the menu from edit mode, held + * on as edit mode ends: spent, it is no Start and toggles no menu again + * until let go; held anew it opens the menu as before. */ +static void test_start_menu_after_edit(void) +{ + t_menu_open = false; + t_let_go(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + input_openxr_edit_begin(false, false, 0, 0, 0); + t_hold("edit/menu", 0, 1.0f, 0.0f); + t_hold("combined/start_menu", 0, 1.0f, 0.0f); + t_clock = 70000000; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_btn(3, RARCH_MENU_TOGGLE), "edit mode's menu press opens no menu"); + t_menu_open = true; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(!t_editing, "edit mode went on with the menu open"); + t_clock += 1500000; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK( !t_btn(0, RARCH_MENU_TOGGLE) + && !t_btn(0, RETRO_DEVICE_ID_JOYPAD_START), + "the press held from edit mode: menu %d, Start %d", + t_btn(0, RARCH_MENU_TOGGLE), + t_btn(0, RETRO_DEVICE_ID_JOYPAD_START)); + t_let_go(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_hold("combined/start_menu", 0, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_btn(0, RETRO_DEVICE_ID_JOYPAD_START), "a new press is no Start"); + t_clock += 1000000; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_btn(0, RARCH_MENU_TOGGLE), "a new second's hold opens no menu"); + t_let_go(); + t_menu_open = false; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); +} + static void test_laser(void) { int16_t x = 0; @@ -1044,6 +1313,11 @@ static void test_laser(void) CHECK(!input_openxr_pointer(0, RETRO_DEVICE_LIGHTGUN, 0, RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X, &x), "Auto took a whole frame's gun"); + /* So does a config's value past the choices: Auto, not edit mode's. */ + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_LAST); + CHECK(!input_openxr_pointer(0, RETRO_DEVICE_LIGHTGUN, 0, + RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X, &x), + "a Laser Pointer past the choices took a whole frame's gun"); /* Always: the gun at the middle, aimed in LOCAL. Off the middle * first, so a read that writes nothing fails. */ @@ -1263,7 +1537,7 @@ static void test_session_gone(void) t_hold("combined/a", 0, 1.0f, 0.0f); t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); t_hooks.session_destroying(t_hooks.user); - CHECK(t_sets_destroyed == 3 && t_spaces_destroyed == 4, + CHECK(t_sets_destroyed == 4 && t_spaces_destroyed == 4, "%u sets and %u spaces destroyed", t_sets_destroyed, t_spaces_destroyed); CHECK(!input_openxr_can_rumble(), "rumble with the session gone"); @@ -1291,8 +1565,8 @@ static void test_session_failed(void) t_session(false); t_attach_result = XR_SUCCESS; CHECK(!input_openxr_active(), "active after a failed setup"); - CHECK( t_num_sets - sets == 3 - && t_sets_destroyed - sets_gone == 3 + CHECK( t_num_sets - sets == 4 + && t_sets_destroyed - sets_gone == 4 && t_spaces_destroyed - gone == t_spaces_made - spaces, "a failed setup left %u of %u sets and %u spaces", (t_num_sets - sets) - (t_sets_destroyed - sets_gone), @@ -1313,7 +1587,7 @@ static void t_fresh_session(void) { t_num_sets = 0; t_num_actions = 0; - t_num_touch = 0; + t_num_bound = 0; t_spaces_made = 0; t_session(false); } @@ -1798,6 +2072,8080 @@ static void test_joypad_wrapped(void) t_forget_devices(); } +/* Snap to Screens, as edit mode starts; off unless a case says. */ +static bool t_snap; +static unsigned t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +/* Hinge Gap, as edit mode starts: touching unless a case says. */ +static unsigned t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +static unsigned t_smooth; + +/* As the frontend's command does it, between frames, with the menu + * closed. */ +static void t_edit_on(bool pause) +{ + if (input_openxr_edit_toggle()) + input_openxr_edit_begin(pause, t_snap, t_angles, t_gap, t_smooth); +} + +/* Edit Screens: the frontend's command starts it at once, between + * frames, and the headset's action only raises that command; its own + * set replaces the pads' and their buttons reach nothing; the laser + * points at every item whatever its setting; the menu, the command + * again, a video reinit and the session's end stop it. Without content + * or with the session ended, nothing starts. */ +static void test_edit_mode(void) +{ + int config; + const XrCompositionLayerBaseHeader *layers[4]; + t_forget_devices(); + t_fresh_session(); + t_menu_open = false; + t_reset_log(); + t_notices = 0; + t_let_go(); + t_hold("combined/edit_screens", 0, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_btn(3, RARCH_HEADSET_EDIT_SCREENS) && !t_editing, + "the action is no hotkey, or started edit mode in the poll"); + t_let_go(); + CHECK(input_openxr_edit_toggle(), "a start was refused"); + CHECK(!t_editing, "the toggle alone started it"); + input_openxr_edit_begin(false, false, 0, 0, 0); + CHECK(t_editing, "did not start at once"); + t_hold("combined/a", 0, 1.0f, 0.0f); + t_hold("edit/edit_screens", 0, 1.0f, 0.0f); + t_hold("edit/menu", 0, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing && t_synced[3] && t_synced[2] && !t_synced[0] + && !t_synced[1], "synced %d %d %d %d, want the edit and pointer sets", + t_synced[0], t_synced[1], t_synced[2], t_synced[3]); + CHECK(!t_btn(0, RETRO_DEVICE_ID_JOYPAD_A), + "a pad button reached the core while editing"); + CHECK(t_btn(3, RARCH_HEADSET_EDIT_SCREENS), + "the edit set's Edit Screens is no hotkey"); + CHECK(t_btn(3, RARCH_MENU_TOGGLE), + "the edit set's menu button opens no menu"); + config = retro_atomic_load_acquire_int(&input_openxr_pointer_config); + CHECK((config & INPUT_OPENXR_LASER_MASK) == VIDEO_XR_LIVE_EDIT, + "the laser is %d while editing", config); + CHECK(!strcmp(t_notice, "edit on") + && t_logged("[OpenXR] Edit Screens on.\n") == 1, + "notice %s, log: %s", t_notice, t_log); + /* A whole frame, which Off leaves alone: a dot from each hand. */ + memset(&t_quads, 0, sizeof(t_quads)); + t_quads.num_quads = 1; + video_xr_pose_identity(&t_quads.quads[0].pose); + t_quads.quads[0].pose.position.z = -1.8f; + t_quads.quads[0].width = 1.6f; + t_quads.quads[0].height = 0.96f; + t_quads.quads[0].kind = VIDEO_XR_QUAD_FRAME; + memset(t_aim, 0, sizeof(t_aim)); + t_aim[1].orientation.w = 1.0f; + t_aim[2].orientation.w = 1.0f; + t_aim_tracked[1] = true; + t_aim_tracked[2] = true; + t_cursor = true; + CHECK(t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, + t_xr_session.local_space, layers, 4, &t_under) == 3, + "both hands do not point while editing, after its outline"); + + /* The menu opens: off, and the laser is the setting's again. */ + t_let_go(); + t_menu_open = true; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + config = retro_atomic_load_acquire_int(&input_openxr_pointer_config); + CHECK(!t_editing && t_synced[0] && !t_synced[3], + "still editing with the menu open"); + CHECK((config & INPUT_OPENXR_LASER_MASK) == VIDEO_OPENXR_LASER_OFF, + "the laser is %d after editing", config); + CHECK(!strcmp(t_notice, "edit off"), "notice %s", t_notice); + CHECK(t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, + t_xr_session.local_space, layers, 4, &t_under) == 0, + "the laser points after editing with the setting Off"); + t_cursor = false; + memset(&t_quads, 0, sizeof(t_quads)); + t_aim_tracked[1] = false; + t_aim_tracked[2] = false; + t_menu_open = false; + + /* The menu would not close, which only happens without content: the + * desktop is told, and nothing starts. */ + t_menu_open = true; + t_notices = 0; + CHECK(input_openxr_edit_toggle(), "a start was refused with the menu open"); + input_openxr_edit_begin(false, false, 0, 0, 0); + CHECK(!t_editing && t_notices == 1 && !strcmp(t_notice, "edit no content"), + "%u notices, the last %s, editing %d", t_notices, t_notice, + (int)t_editing); + t_menu_open = false; + + /* An ended session cannot start it. */ + t_ended = true; + t_notices = 0; + CHECK(!input_openxr_edit_toggle() && !t_editing && t_notices == 1 + && !strcmp(t_notice, "edit unavailable"), + "an ended session: %u notices, the last %s", t_notices, t_notice); + t_ended = false; + + /* The command while on: off. */ + t_edit_on(false); + CHECK(t_editing && !input_openxr_edit_toggle() && !t_editing, + "the command did not stop it"); + + /* A video reinit ends it. */ + t_edit_on(false); + input_openxr_edit_end(); + CHECK(!t_editing, "a video reinit left it on"); + + /* So does the session's end. */ + t_edit_on(false); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing, "fixture: not editing"); + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(!t_editing, "the session's end left it on"); + t_forget_devices(); +} + +/* The pads' triggers go unread while editing: one held as the menu opens + * and ends it is no pull on the menu. */ +static void test_edit_trigger_held(void) +{ + float u = 0.0f; + float v = 0.0f; + bool pressed = true; + t_forget_devices(); + t_fresh_session(); + t_menu_open = false; + memset(&t_quads, 0, sizeof(t_quads)); + t_quads.num_quads = 1; + video_xr_pose_identity(&t_quads.quads[0].pose); + t_quads.quads[0].pose.position.z = -1.7f; + t_quads.quads[0].width = 1.6f; + t_quads.quads[0].height = 0.96f; + t_quads.quads[0].kind = VIDEO_XR_QUAD_MENU; + memset(t_aim, 0, sizeof(t_aim)); + t_aim[2].orientation.w = 1.0f; + t_aim_tracked[2] = true; + t_let_go(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); + t_edit_on(false); + t_hold("combined/r2", 0, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); + CHECK(t_editing, "fixture: not editing"); + t_menu_open = true; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); + CHECK(!t_editing && input_openxr_menu_pointer(&u, &v, &pressed) + && !pressed, "a trigger held through editing pressed the menu"); + t_let_go(); + t_menu_open = false; + memset(&t_quads, 0, sizeof(t_quads)); + t_aim_tracked[2] = false; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); + t_forget_devices(); +} + +/* A runtime that refuses edit mode's bindings keeps the rest of each + * profile; edit mode is then unavailable: told every time, logged once. */ +static void test_edit_unavailable(void) +{ + t_forget_devices(); + t_reset_log(); + t_refuse_edit = true; + t_fresh_session(); + t_refuse_edit = false; + CHECK(t_touch_binds("combined/a", NULL) + && !t_touch_binds("edit/grab", NULL), + "Touch lost its game bindings or kept edit mode's"); + CHECK(t_logged("[OpenXR] Edit Screens bindings refused for ") == 4, + "log: %s", t_log); + t_reset_log(); + t_notices = 0; + CHECK(!input_openxr_edit_toggle() && !input_openxr_edit_toggle(), + "edit mode started without its bindings"); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(!t_editing && !t_synced[3], "the edit set was synced"); + CHECK(t_notices == 2 && !strcmp(t_notice, "edit unavailable"), + "%u notices, the last %s", t_notices, t_notice); + CHECK(t_logged("[OpenXR] Edit Screens unavailable") == 1, + "log: %s", t_log); + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* Refused for Touch alone: a hand on a Touch controller cannot start + * edit mode, which it could not leave; Index controllers can. */ +static void test_edit_profile_refused(void) +{ + t_forget_devices(); + t_reset_log(); + t_refuse_edit_for = "/interaction_profiles/oculus/touch_controller"; + t_fresh_session(); + t_refuse_edit_for = NULL; + CHECK(t_logged("[OpenXR] Edit Screens bindings refused for ") == 1 + && t_binds("/interaction_profiles/valve/index_controller", + "edit/grab", NULL), + "log: %s", t_log); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_current[1] = "/interaction_profiles/valve/index_controller"; + t_current[2] = "/interaction_profiles/oculus/touch_controller"; + t_notices = 0; + CHECK(!input_openxr_edit_toggle() && !t_editing && t_notices == 1 + && !strcmp(t_notice, "edit unavailable"), + "a Touch controller: editing %d, %u notices, the last %s", + (int)t_editing, t_notices, t_notice); + t_current[2] = "/interaction_profiles/valve/index_controller"; + t_edit_on(false); + CHECK(t_editing, "Index controllers could not start it"); + input_openxr_edit_toggle(); + t_current[1] = NULL; + t_current[2] = NULL; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* A pause the frontend refuses (hardcore allows one only so often): + * edit mode runs on and claims no pause, so leaving it resumes nothing + * the user pauses meanwhile. */ +static void test_edit_pause_refused(void) +{ + t_forget_devices(); + t_fresh_session(); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_pauses = 0; + t_unpauses = 0; + t_pause_refused = true; + t_edit_on(true); + t_pause_refused = false; + CHECK(t_editing && t_pauses == 1 && !(t_run_flags & RUNLOOP_FLAG_PAUSED) + && !input_openxr_edit.paused, + "a refused pause: editing %d, %u pauses, claimed %d", + (int)t_editing, t_pauses, (int)input_openxr_edit.paused); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_run_flags |= RUNLOOP_FLAG_PAUSED; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + input_openxr_edit_toggle(); + CHECK(!t_editing && t_unpauses == 0 && (t_run_flags & RUNLOOP_FLAG_PAUSED), + "the user's pause undone: %u resumes", t_unpauses); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* Edit mode pauses; the user resumes, then pauses again: that pause is + * the user's, and leaving edit mode keeps it. */ +static void test_edit_repause(void) +{ + t_forget_devices(); + t_fresh_session(); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_pauses = 0; + t_unpauses = 0; + t_edit_on(true); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing && t_pauses == 1 && input_openxr_edit.paused, + "fixture: edit mode did not pause"); + t_run_flags &= ~RUNLOOP_FLAG_PAUSED; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_run_flags |= RUNLOOP_FLAG_PAUSED; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + input_openxr_edit_toggle(); + CHECK(!t_editing && t_unpauses == 0 && (t_run_flags & RUNLOOP_FLAG_PAUSED), + "the user's pause undone: %u resumes", t_unpauses); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* A runtime that takes edit mode's bindings and binds none of them, as + * for a binding the user saved before they existed: nothing could grab + * or leave. A few polls in a row with neither grab nor the menu bound + * end edit mode, resume what it paused and say it is unavailable, + * logged once; fewer do not, nor does the menu bound alone. */ +static void test_edit_unbound(void) +{ + unsigned i; + t_forget_devices(); + t_fresh_session(); + t_reset_log(); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_pauses = 0; + t_unpauses = 0; + t_edit_on(true); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing && (t_run_flags & RUNLOOP_FLAG_PAUSED), + "fixture: not editing, paused"); + t_unbound = "edit/grab"; + for (i = 0; i <= INPUT_OPENXR_EDIT_UNBOUND; i++) + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing, "ended with its menu bound"); + t_unbound = "edit/"; + for (i = 1; i < INPUT_OPENXR_EDIT_UNBOUND; i++) + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_unbound = NULL; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_unbound = "edit/"; + for (i = 1; i < INPUT_OPENXR_EDIT_UNBOUND; i++) + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(t_editing, "ended before %u unbound polls in a row", + INPUT_OPENXR_EDIT_UNBOUND); + t_notices = 0; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(!t_editing && t_unpauses == 1 + && !(t_run_flags & RUNLOOP_FLAG_PAUSED), + "unbound: editing %d, %u resumes", (int)t_editing, t_unpauses); + CHECK(t_notices && !strcmp(t_notice, "edit unavailable"), + "%u notices, the last %s", t_notices, t_notice); + CHECK((retro_atomic_load_acquire_int(&input_openxr_pointer_config) + & INPUT_OPENXR_LASER_MASK) == VIDEO_OPENXR_LASER_OFF, + "the laser kept edit mode's"); + /* Again: told, not logged again. */ + t_edit_on(false); + for (i = 0; i < INPUT_OPENXR_EDIT_UNBOUND; i++) + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + CHECK(!t_editing && !strcmp(t_notice, "edit unavailable"), + "a second start: editing %d, notice %s", (int)t_editing, t_notice); + CHECK(t_logged("[OpenXR] Edit Screens unavailable") == 1, + "log: %s", t_log); + t_unbound = NULL; + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* A profile with more bindings than one suggestion holds keeps its + * game's, edit mode's refused for it: a hand on it cannot start it. */ +static void test_edit_oversized(void) +{ + static input_openxr_bind_t game[INPUT_OPENXR_MAX_BINDS - 1]; + static const input_openxr_bind_t edit[] = { + IXB_BOTH(IXA_E_GRAB, "squeeze/value") }; + input_openxr_profile_t p; + unsigned i; + const char *path = "/interaction_profiles/test/oversized"; + t_forget_devices(); + t_fresh_session(); + for (i = 0; i < ARRAY_SIZE(game); i++) + { + game[i].action = IXA_C_A; + game[i].path = "/user/hand/right/input/a/click"; + } + memset(&p, 0, sizeof(p)); + p.path = path; + p.binds = game; + p.edit = edit; + p.count = ARRAY_SIZE(game); + p.edit_count = ARRAY_SIZE(edit); + t_reset_log(); + CHECK(!input_openxr_suggest(&input_openxr_st, &p), + "edit mode's bindings taken past the limit"); + CHECK(t_binds(path, "combined/a", NULL) && !t_binds(path, "edit/grab", + NULL), "the game's not suggested, or edit mode's were"); + CHECK(t_logged("[OpenXR] Edit Screens bindings refused for " + "/interaction_profiles/test/oversized") == 1 + && t_logged("[OpenXR] Bindings suggested for " + "/interaction_profiles/test/oversized") == 1, "log: %s", t_log); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_current[2] = path; + CHECK(!input_openxr_edit_toggle() && !t_editing, + "a hand on it started edit mode"); + t_current[2] = NULL; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* While Editing Screens: Pause pauses the content, once video knows and + * never from inside a poll, which can be inside the core's frame; it + * resumes only what it paused. */ +static void test_edit_pause(void) +{ + t_forget_devices(); + t_fresh_session(); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_pauses = 0; + t_unpauses = 0; + t_bad_pauses = 0; + t_edit_on(true); + CHECK(t_editing && t_pauses == 1 && (t_run_flags & RUNLOOP_FLAG_PAUSED), + "entering did not pause: %u", t_pauses); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + input_openxr_edit_toggle(); + CHECK(!t_editing && t_unpauses == 1 + && !(t_run_flags & RUNLOOP_FLAG_PAUSED), "leaving did not resume"); + /* Paused already: left paused. */ + t_run_flags |= RUNLOOP_FLAG_PAUSED; + t_edit_on(true); + input_openxr_edit_toggle(); + CHECK(t_pauses == 1 && t_unpauses == 1 + && (t_run_flags & RUNLOOP_FLAG_PAUSED), + "a pause edit mode did not make was undone"); + /* Resumed by hand while editing: nothing to resume. */ + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_edit_on(true); + t_run_flags &= ~RUNLOOP_FLAG_PAUSED; + input_openxr_edit_toggle(); + CHECK(t_pauses == 2 && t_unpauses == 1, "%u pauses %u resumes", + t_pauses, t_unpauses); + /* Ended in a poll, by the menu opening: resumed. */ + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_edit_on(true); + t_menu_open = true; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_menu_open = false; + CHECK(!t_editing && t_pauses == 3 && t_unpauses == 2 + && !(t_run_flags & RUNLOOP_FLAG_PAUSED), + "the menu ended it: %u pauses %u resumes", t_pauses, t_unpauses); + /* Keep Running: never paused. */ + t_edit_on(false); + CHECK(t_pauses == 3 && !(t_run_flags & RUNLOOP_FLAG_PAUSED), + "Keep Running paused"); + input_openxr_edit_toggle(); + /* The headset's action held through polls only raises the command. */ + t_let_go(); + t_hold("combined/edit_screens", 0, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_let_go(); + CHECK(!t_editing && t_pauses == 3, "the action started it in a poll"); + CHECK(t_bad_pauses == 0, + "%u pauses from inside a poll or before video knew", t_bad_pauses); + t_run_flags = RUNLOOP_FLAG_CORE_RUNNING; + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +static bool t_near(float a, float b) +{ + return fabs((double)a - (double)b) < 1e-3; +} + +/* An outline's size round an item s metres across. */ +static float t_outline(float s) +{ + return (s + 2.0f * VIDEO_XR_OUTLINE_GAP) / (1.0f - 2.0f + * (VIDEO_XR_OUTLINE_EDGE + 0.5f) / VIDEO_XR_OUTLINE_DIM); +} + +/* The menu's handle bar's middle, for a menu h metres tall centred at + * y 0: 5 mm below its outline. */ +static float t_handle_y(float h) +{ + return -(t_outline(h) + VIDEO_XR_HANDLE_HEIGHT) * 0.5f - 0.005f; +} + +/* Screen 1 1.8 m ahead and the menu placed 2 m to its right, from an + * identity anchor; both hands at the eyes, looking ahead. */ +static void t_edit_fixture(void) +{ + memset(&t_quads, 0, sizeof(t_quads)); + t_quads.num_quads = 2; + video_xr_pose_identity(&t_quads.quads[0].pose); + t_quads.quads[0].pose.position.z = -1.8f; + t_quads.quads[0].width = 1.6f; + t_quads.quads[0].height = 0.96f; + t_quads.quads[0].kind = VIDEO_XR_QUAD_SCREEN; + t_quads.slots[0].base.z = -1.8f; + video_xr_pose_identity(&t_quads.quads[1].pose); + t_quads.quads[1].pose.position.x = 2.0f; + t_quads.quads[1].pose.position.z = -1.7f; + t_quads.quads[1].width = 1.0f; + t_quads.quads[1].height = 0.6f; + t_quads.quads[1].kind = VIDEO_XR_QUAD_MENU; + t_quads.quads[1].slot = VIDEO_XR_MENU_SLOT; + t_quads.slots[VIDEO_XR_MENU_SLOT].base.z = -1.7f; + memset(t_aim, 0, sizeof(t_aim)); + t_aim[1].orientation.w = 1.0f; + t_aim[2].orientation.w = 1.0f; + t_aim_tracked[1] = true; + t_aim_tracked[2] = true; + video_views_xr_reset(t_xr, RETRO_VIDEO_VIEWS_MAX); + video_views_xr_reset(&t_menu_xr, 1); + t_menu_xr.x = 2.0f; + t_menu_placed = true; + t_modified = false; + video_xr_pose_identity(&t_screens); + t_space = XR_NULL_HANDLE; + t_menu_open = false; + t_let_go(); +} + +/* t_clock's step past a hinge's buzz (INPUT_OPENXR_HINGE_BUZZ_NS), us: + * a tick a case means after an attach lands outside it, and no case + * starts inside the last one's. */ +#define T_BUZZ_CLEAR 200000 + +/* A fresh session in edit mode, started as the frontend's command + * starts it. */ +static void t_edit_start(void) +{ + t_clock += T_BUZZ_CLEAR; + t_forget_devices(); + t_fresh_session(); + t_edit_fixture(); + t_edit_on(false); + CHECK(t_editing, "fixture: not editing"); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); +} + +static void t_edit_end(void) +{ + t_let_go(); + if (t_editing) + input_openxr_edit_toggle(); + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + memset(&t_quads, 0, sizeof(t_quads)); + t_aim_tracked[1] = t_aim_tracked[2] = false; + video_xr_pose_identity(&t_screens); + t_space = XR_NULL_HANDLE; + t_forget_devices(); +} + +static void t_edit_poll(void) +{ + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); +} + +/* With Edit Smoothing on, the frame at t_clock: the right hand's aim + * stepped for it and published as the frame loop does, into *a. */ +static bool t_frame_aim(video_xr_pose_t *a) +{ + return t_hooks.aim(t_hooks.user, 1, t_xr_session.local_space, + openxr_frame_predicted_time(&t_xr_frame), a); +} + +/* A grip on a screen grabs it; it follows the hand, level; the trigger + * lets its roll follow; the stick pushes and resizes, by how far past + * its deadzone; letting go leaves it there, counted for the frame + * snapshot. */ +static void test_edit_grab(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + unsigned seq; + t_edit_start(); + seq = t_edit_seq; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == 0, + "the grip grabbed %u in state %u", g->slot, g->state); + CHECK(t_near(t_xr[0].x, 0.0f) && t_near(t_xr[0].z, 0.0f), + "a grab moved the screen"); + + t_aim[2].position.x = 0.2f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.2f) && t_near(t_xr[0].y, 0.0f) + && t_near(t_xr[0].z, 0.0f) && t_near(t_xr[0].yaw, 0.0f), + "moved 20 cm: %.3f %.3f %.3f", t_xr[0].x, t_xr[0].y, t_xr[0].z); + + /* A 30 degree roll of the wrist: level. */ + t_aim[2].orientation.z = (float)sin(M_PI / 12.0); + t_aim[2].orientation.w = (float)cos(M_PI / 12.0); + t_edit_poll(); + CHECK(t_near(t_xr[0].roll, 0.0f), "rolled %.2f while level", + t_xr[0].roll); + /* The trigger: no jump, then the roll follows. */ + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_near(t_xr[0].roll, 0.0f), "the trigger rolled it at once"); + t_aim[2].orientation.z = 0.5f; + t_aim[2].orientation.w = 0.8660254f; + t_edit_poll(); + CHECK(t_near(t_xr[0].roll, 30.0f), "rolled %.2f tilting", t_xr[0].roll); + /* Let go of the trigger: the roll stays. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_aim[2].orientation.z = 0.0f; + t_aim[2].orientation.w = 1.0f; + t_edit_poll(); + CHECK(t_near(t_xr[0].roll, 30.0f), "the roll went back to %.2f", + t_xr[0].roll); + + /* The stick for 0.1 s: up pushes 15 cm, right grows 2^0.1 times. */ + t_hold("edit/stick", 2, 0.0f, 1.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].z, -0.15f), "pushed to %.3f", t_xr[0].z); + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 1.0f, 0.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].scale, 1.07177f), "scaled to %.4f", t_xr[0].scale); + CHECK(t_near(g->width, 1.6f * 1.07177f), "width %.3f", g->width); + /* Inside the deadzone, nothing; half past it, half the rate. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 0.15f, 0.15f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].z, -0.15f) && t_near(t_xr[0].scale, 1.07177f), + "the deadzone moved it to %.3f, %.4f", t_xr[0].z, t_xr[0].scale); + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 0.0f, 0.6f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].z, -0.225f), "half past the deadzone: %.3f", + t_xr[0].z); + /* A stalled poll steps no more than 0.1 s. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 0.0f, 1.0f); + t_clock += 2000000; + t_edit_poll(); + CHECK(t_near(t_xr[0].z, -0.375f), "a 2 s stall pushed to %.3f", + t_xr[0].z); + + /* Let go: left there, the change counted. */ + t_let_go(); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_RELEASED && g->release_seq == seq + 1 + && t_edit_seq == seq + 1, "state %u seq %u/%u", g->state, + g->release_seq, t_edit_seq); + CHECK(t_near(t_xr[0].x, 0.2f), "letting go moved it"); + CHECK(t_modified, "not marked modified"); + input_openxr_edit_toggle(); + CHECK(!strcmp(t_notice, "edit unsaved"), "notice %s", t_notice); + t_edit_end(); +} + +/* A carried screen keeps its pitch as the ray rises; the trigger lets + * the pitch follow from where it is, and letting go keeps it. The free + * hand's stick turns what the other hand holds, about its middle, as + * fast as it is pushed; the holding hand's own stick still resizes; + * with both hands holding, each stick works its own item. */ +static void test_edit_turn(void) +{ + const video_xr_grab_t *r = &input_openxr_edit.hand[1].g; + const video_xr_grab_t *l = &input_openxr_edit.hand[0].g; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state == VIDEO_XR_GRAB_HELD && r->slot == 0, + "took slot %u in state %u", r->slot, r->state); + /* The ray 20 degrees up: pitch held. */ + t_aim[2].orientation.x = (float)sin(M_PI / 18.0); + t_aim[2].orientation.w = (float)cos(M_PI / 18.0); + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 0.0f) && t_xr[0].y > 0.5f, + "carried up to %.3f, pitched %.2f", t_xr[0].y, t_xr[0].pitch); + /* The trigger, then 10 degrees more: it follows from there. */ + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 0.0f), "the trigger pitched it at once"); + t_aim[2].orientation.x = (float)sin(M_PI / 12.0); + t_aim[2].orientation.w = (float)cos(M_PI / 12.0); + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 10.0f), "pitched %.2f tilting", + t_xr[0].pitch); + /* Let go of the trigger, the ray level: the pitch stays. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_aim[2].orientation.x = 0.0f; + t_aim[2].orientation.w = 1.0f; + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 10.0f), "the pitch went back to %.2f", + t_xr[0].pitch); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* The left stick right for 0.1 s turns screen 1 nine degrees, in + * place. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_hold("edit/stick", 1, 1.0f, 0.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].yaw, 9.0f) && t_near(t_xr[0].x, 0.0f) + && t_near(t_xr[0].z, 0.0f) && t_near(t_xr[0].pitch, 0.0f), + "turned to %.2f at %.3f,%.3f", t_xr[0].yaw, t_xr[0].x, + t_xr[0].z); + /* The holding hand's own stick right resizes it and turns nothing. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 1.0f, 0.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].yaw, 9.0f) && t_near(t_xr[0].scale, 1.07177f), + "own stick: yaw %.2f, scale %.4f", t_xr[0].yaw, t_xr[0].scale); + /* The left hand takes the menu by its bar: its stick is the menu's. */ + t_let_go(); + t_aim[1].position.x = 2.0f; + t_aim[1].position.y = t_handle_y(0.6f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == VIDEO_XR_MENU_SLOT, + "the left took slot %u", l->slot); + t_hold("edit/stick", 1, 1.0f, 0.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].yaw, 9.0f) && t_near(t_menu_xr.scale, 1.07177f), + "both holding: screen 1 yaw %.2f, menu scale %.4f", t_xr[0].yaw, + t_menu_xr.scale); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* Taken pitched 15 degrees, the ray then 20 up: the grab took that + * pitch, and it keeps it. */ + t_edit_start(); + t_quads.quads[0].pose.orientation.x = (float)sin(M_PI / 24.0); + t_quads.quads[0].pose.orientation.w = (float)cos(M_PI / 24.0); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.2f; + t_aim[2].orientation.x = (float)sin(M_PI / 18.0); + t_aim[2].orientation.w = (float)cos(M_PI / 18.0); + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 15.0f), "taken at 15, carried at %.2f", + t_xr[0].pitch); + t_let_go(); + t_edit_poll(); + t_edit_end(); +} + +/* The free hand's stick up tips what the other hand holds back, its + * top away, about its own left-right line through its middle, as fast + * as it turns it, the trigger held or not; left and right turn it at + * once. The holding hand's own stick up still pushes and tips nothing. */ +static void test_edit_tilt_stick(void) +{ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + /* The left stick up for 0.1 s: nine degrees back, in place. */ + t_hold("edit/stick", 1, 0.0f, 1.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, -9.0f) && t_near(t_xr[0].yaw, 0.0f) + && t_near(t_xr[0].roll, 0.0f) && t_near(t_xr[0].x, 0.0f) + && t_near(t_xr[0].y, 0.0f) && t_near(t_xr[0].z, 0.0f), + "tilted to pitch %.2f yaw %.2f roll %.2f at %.3f,%.3f,%.3f", + t_xr[0].pitch, t_xr[0].yaw, t_xr[0].roll, t_xr[0].x, t_xr[0].y, + t_xr[0].z); + /* The holding hand's own stick up pushes it and tips nothing. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, 0.0f, 1.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, -9.0f) && t_near(t_xr[0].z, -0.15f), + "own stick: pitch %.2f, pushed to %.3f", t_xr[0].pitch, + t_xr[0].z); + /* The trigger held, the left stick down and right: tipped back up + * and turned, in place. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, 1.0f, -1.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[0].pitch, 0.0f) && t_near(t_xr[0].yaw, 9.0f) + && t_near(t_xr[0].roll, 0.0f) && t_near(t_xr[0].x, 0.0f) + && t_near(t_xr[0].z, -0.15f), + "tilting: pitch %.2f yaw %.2f roll %.2f at %.3f,%.3f", + t_xr[0].pitch, t_xr[0].yaw, t_xr[0].roll, t_xr[0].x, t_xr[0].z); + t_let_go(); + t_edit_poll(); + t_edit_end(); +} + +/* Degrees in 0.1 s from a stick axis at v. */ +static float t_stick_deg(float v) +{ + double a = (fabs(v) - VIDEO_XR_EDIT_DEADZONE) + / (1.0 - VIDEO_XR_EDIT_DEADZONE); + return (a > 0.0) ? (float)(a * VIDEO_XR_EDIT_TURN_RATE * 0.1) : 0.0f; +} + +/* The free hand's stick near left or right only turns what the other + * hand holds, near up or down only tilts it, and on a diagonal does + * both. The holding hand's own stick 15 degrees off right still pushes + * as it resizes. */ +static void test_edit_stick_lock(void) +{ + float c = (float)cos(M_PI / 12.0); + float s = (float)sin(M_PI / 12.0); + float d = (float)cos(M_PI / 4.0); + float yaw, pitch; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + /* The left stick 15 degrees off right for 0.1 s: turned only. */ + t_hold("edit/stick", 1, c, s); + t_clock += 100000; + t_edit_poll(); + yaw = t_stick_deg(c); + CHECK(fabs(t_xr[0].yaw - yaw) < 0.01 && t_near(t_xr[0].pitch, 0.0f) + && t_near(t_xr[0].roll, 0.0f), + "15 off right: yaw %.2f pitch %.2f roll %.2f, want yaw %.2f", + t_xr[0].yaw, t_xr[0].pitch, t_xr[0].roll, yaw); + /* 15 degrees off down: tilted only. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, -s, -c); + t_clock += 100000; + t_edit_poll(); + pitch = t_stick_deg(c); + CHECK(fabs(t_xr[0].yaw - yaw) < 0.01 + && fabs(t_xr[0].pitch - pitch) < 0.01 + && t_near(t_xr[0].roll, 0.0f), + "15 off down: yaw %.2f pitch %.2f roll %.2f, want %.2f %.2f", + t_xr[0].yaw, t_xr[0].pitch, t_xr[0].roll, yaw, pitch); + /* Up and right together: both. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, d, d); + t_clock += 100000; + t_edit_poll(); + yaw += t_stick_deg(d); + pitch -= t_stick_deg(d); + CHECK(fabs(t_xr[0].yaw - yaw) < 0.01 + && fabs(t_xr[0].pitch - pitch) < 0.01 + && t_near(t_xr[0].roll, 0.0f) && t_near(t_xr[0].z, 0.0f), + "diagonal: yaw %.2f pitch %.2f roll %.2f, want %.2f %.2f", + t_xr[0].yaw, t_xr[0].pitch, t_xr[0].roll, yaw, pitch); + /* The holding hand's own stick 15 degrees off right. */ + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 2, c, s); + t_clock += 100000; + t_edit_poll(); + CHECK(fabs(t_xr[0].z + t_stick_deg(s) / 60.0f) < 1e-3 + && fabs(t_xr[0].scale - pow(2.0, t_stick_deg(c) / 90.0)) < 1e-3 + && fabs(t_xr[0].yaw - yaw) < 0.01, + "own stick: pushed to %.4f, scale %.4f, yaw %.2f", t_xr[0].z, + t_xr[0].scale, t_xr[0].yaw); + t_let_go(); + t_edit_poll(); + t_edit_end(); +} + +/* n polls 10 ms apart, the right hand gripping and the left stick at + * x, y: 0.9 degrees each at full travel. */ +static void t_step_polls(unsigned n, float x, float y) +{ + unsigned i; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, x, y); + for (i = 0; i < n; i++) + { + t_clock += 10000; + t_edit_poll(); + } +} + +static bool t_deg_near(float a, double b) +{ + return fabs((double)a - b) < 0.01; +} + +/* The free hand's stick turns and tilts what the other hand holds in + * 15 degree steps from facing the head and from upright: within 2 of + * one it holds there, the holding hand buzzing once as it takes hold, + * and again only once it has left that step. Taken at a step, no buzz + * there. */ +static void test_edit_step_stick(void) +{ + unsigned n; + t_head_tracked = true; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + /* Right from facing the head: held at 0 to 1.8, then on. */ + t_step_polls(2, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 0.0) && t_applies[2] == n, + "at 1.8: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(12, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 12.6) && t_applies[2] == n, + "at 12.6: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + /* 13.5 to 16.2 at 15, one buzz; 17.1 is past it. */ + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 15.0) && t_applies[2] == n + 1, + "at 13.5: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(3, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 15.0) && t_applies[2] == n + 1, + "at 16.2: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 17.1) && t_applies[2] == n + 1, + "at 17.1: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + /* On to 31.5, back to 28.8, all within 30's hold: one buzz. */ + t_step_polls(16, 1.0f, 0.0f); + t_step_polls(3, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 30.0) && t_applies[2] == n + 2, + "at 28.8: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + /* Out of it at 27.9 and back in at 28.8: again. */ + t_step_polls(1, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 27.9) && t_applies[2] == n + 2, + "at 27.9: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 30.0) && t_applies[2] == n + 3, + "back at 28.8: yaw %.3f, %u buzzes", t_xr[0].yaw, + t_applies[2] - n); + /* Up from upright: -13.5 holds at -15 with a buzz, in place. */ + t_step_polls(15, 0.0f, 1.0f); + CHECK(t_deg_near(t_xr[0].pitch, -15.0) && t_deg_near(t_xr[0].yaw, 30.0) + && t_near(t_xr[0].x, 0.0f) && t_near(t_xr[0].z, 0.0f) + && t_applies[2] == n + 4, + "tilted to -13.5: pitch %.3f yaw %.3f, %u buzzes", t_xr[0].pitch, + t_xr[0].yaw, t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* Carried, nothing steps: turned to 12.6, then the hand 4.4 cm right + * puts it 14 from facing the head, and it stays at yaw 12.6. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_step_polls(14, 1.0f, 0.0f); + n = t_applies[2]; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_aim[2].position.x = (float)(1.8 * tan(1.4 * M_PI / 180.0)); + t_edit_poll(); + CHECK(t_deg_near(t_xr[0].yaw, 12.6) && t_applies[2] == n, + "carried to 14 from facing: yaw %.3f, %u buzzes", t_xr[0].yaw, + t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* Taken at 0, carried out of its hold and into 15's with the stick + * idle: the first push holds at 15 with a buzz. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_aim[2].position.x = (float)(1.8 * tan(8.0 * M_PI / 180.0)); + t_edit_poll(); + t_aim[2].position.x = (float)(1.8 * tan(14.0 * M_PI / 180.0)); + t_edit_poll(); + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 1.0) && t_applies[2] == n + 1, + "carried to 14, pushed: yaw %.3f, %u buzzes", t_xr[0].yaw, + t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* Tilted by the trigger to 14, it stays there. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_aim[2].orientation.x = (float)sin(7.0 * M_PI / 180.0); + t_aim[2].orientation.w = (float)cos(7.0 * M_PI / 180.0); + t_edit_poll(); + CHECK(t_deg_near(t_xr[0].pitch, 14.0) && t_applies[2] == n, + "tilted by the trigger: pitch %.3f, %u buzzes", t_xr[0].pitch, + t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_head_tracked = false; +} + +/* What the stick gave past a step is kept while the stick is out of + * its deadzone, though a wavering push drops its axis now and then, and + * let go of as the stick is: held at 0, the stick let go, then pushed + * back, it leaves after 2. */ +static void test_edit_step_waver(void) +{ + unsigned i; + double on = 26.0 * M_PI / 180.0; + double off = 24.0 * M_PI / 180.0; + t_head_tracked = true; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_step_polls(2, 1.0f, 0.0f); + t_step_polls(1, 0.0f, 0.0f); + t_step_polls(3, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, -2.7), "let go at 1.8, back 2.7: yaw %.3f", + t_xr[0].yaw); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* About the lock line, the tilt acts every other poll. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + for (i = 0; i < 10; i++) + { + t_step_polls(1, (float)cos(on), (float)sin(on)); + t_step_polls(1, (float)cos(off), (float)sin(off)); + } + CHECK(t_deg_near(t_xr[0].pitch, -9.0 * (sin(on) - 0.2) / 0.8), + "lock line: pitch %.3f", t_xr[0].pitch); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + /* The turn about its deadzone, a little up, too little to tilt, + * keeping the stick out of it. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + for (i = 0; i < 20; i++) + { + t_step_polls(1, 0.3f, 0.15f); + t_step_polls(1, 0.19f, 0.15f); + } + CHECK(t_deg_near(t_xr[0].yaw, 18.0 * 0.1 / 0.8) && t_near(t_xr[0].pitch, + 0.0f), "deadzone: yaw %.3f pitch %.3f", t_xr[0].yaw, + t_xr[0].pitch); + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_head_tracked = false; +} + +/* Snap to Angles sets the step. At 45 the stick turns on past 15 and + * 30, holds within 2 of 45 with one buzz, leaves it, buzzes again back + * in its hold, and again at 90. Off, it neither holds nor buzzes, + * turning or tilting. A config's value past 90 steps at 15. */ +static void test_edit_step_sizes(void) +{ + unsigned n; + t_head_tracked = true; + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_45; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_step_polls(15, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 13.5) && t_applies[2] == n, + "45, at 13.5: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(17, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 28.8) && t_applies[2] == n, + "45, at 28.8: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(16, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 45.0) && t_applies[2] == n + 1, + "45, at 43.2: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(4, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 45.0) && t_applies[2] == n + 1, + "45, at 46.8: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 47.7) && t_applies[2] == n + 1, + "45, at 47.7: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_step_polls(1, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 45.0) && t_applies[2] == n + 2, + "45, back at 46.8: yaw %.3f, %u buzzes", t_xr[0].yaw, + t_applies[2] - n); + t_step_polls(46, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 90.0) && t_applies[2] == n + 3, + "45, at 88.2: yaw %.3f, %u buzzes", t_xr[0].yaw, t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_step_polls(2, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 1.8), "off, at 1.8: yaw %.3f", + t_xr[0].yaw); + t_step_polls(13, 1.0f, 0.0f); + t_step_polls(15, 0.0f, 1.0f); + CHECK(t_deg_near(t_xr[0].yaw, 13.5) && t_deg_near(t_xr[0].pitch, -13.5) + && t_applies[2] == n, "off, at 13.5 and -13.5: yaw %.3f pitch %.3f," + " %u buzzes", t_xr[0].yaw, t_xr[0].pitch, t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_LAST; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_step_polls(15, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[0].yaw, 15.0) && t_applies[2] == n + 1, + "past 90, at 13.5: yaw %.3f, %u buzzes", t_xr[0].yaw, + t_applies[2] - n); + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; + t_head_tracked = false; +} + +/* A grab with the trigger already held tilts from the start: the roll + * follows at the next poll. */ +static void test_edit_tilt_from_grab(void) +{ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].orientation.z = (float)sin(M_PI / 12.0); + t_aim[2].orientation.w = (float)cos(M_PI / 12.0); + t_edit_poll(); + CHECK(t_near(t_xr[0].roll, 30.0f), "grabbed tilting, rolled %.2f", + t_xr[0].roll); + t_edit_end(); +} + +/* Offsets stop at 10 m and resume from there; a hand out of tracking + * holds its item still; an item gone from the map ends the grab. */ +static void test_edit_limits(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + unsigned seq; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 10.3f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 10.0f), "past the limit: %.3f", t_xr[0].x); + t_aim[2].position.x = 10.2f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 9.9f), "back from the limit: %.3f", t_xr[0].x); + t_aim_tracked[2] = false; + t_aim[2].position.x = 5.0f; + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && t_near(t_xr[0].x, 9.9f), + "an untracked hand moved it"); + t_aim_tracked[2] = true; + t_aim[2].position.x = 10.1f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 9.8f), "tracked again: %.3f", t_xr[0].x); + seq = t_edit_seq; + t_quads.num_quads = 0; + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_NONE && t_edit_seq == seq, + "a screen gone: state %u, seq %u/%u", g->state, t_edit_seq, seq); + t_edit_end(); +} + +/* The core's layout moves the held screen's spot under the hand: the + * grab ends there, as for a screen gone from the map, and writes + * nothing from the old spot. */ +static void test_edit_spot_moved(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + unsigned seq; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.2f; + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && t_near(t_xr[0].x, 0.2f), + "fixture: not held at 0.2"); + seq = t_edit_seq; + t_quads.slots[0].base.y = -0.5f; + t_quads.quads[0].pose.position.y = -0.5f; + t_aim[2].position.x = 0.4f; + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_NONE && t_edit_seq == seq, + "a moved spot: state %u, seq %u/%u", g->state, t_edit_seq, seq); + CHECK(t_near(t_xr[0].x, 0.2f) && t_near(t_xr[0].y, 0.0f), + "written from the old spot: %.3f %.3f", t_xr[0].x, t_xr[0].y); + t_edit_end(); +} + +/* One item per hand: the second hand on the same item does nothing; + * on another item it holds that one. The menu, by its handle, has a + * place of its own. */ +static void test_edit_two_hands(void) +{ + const video_xr_grab_t *l = &input_openxr_edit.hand[0].g; + const video_xr_grab_t *r = &input_openxr_edit.hand[1].g; + t_edit_start(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == 0 + && r->state != VIDEO_XR_GRAB_HELD, + "the second hand took screen 1 too"); + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = t_handle_y(0.6f); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state == VIDEO_XR_GRAB_HELD && r->slot == VIDEO_XR_MENU_SLOT, + "the right hand did not take the menu"); + /* A hand holds one item: the left on the menu's handle takes + * nothing more. */ + t_aim[1].position.x = 2.0f; + t_aim[1].position.y = t_handle_y(0.6f); + t_aim[2].position.x = 2.3f; + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == 0 + && t_menu_placed && t_near(t_menu_xr.x, 2.3f) + && t_near(t_xr[0].x, 2.0f), + "menu at %.3f placed %d, screen 1 at %.3f", t_menu_xr.x, + (int)t_menu_placed, t_xr[0].x); + t_edit_end(); +} + +/* The menu is taken by its handle alone: through the menu, the laser + * takes screen 1 behind it. A menu that follows screen 1 is placed + * where and as large as it shows. */ +static void test_edit_menu_handle(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + /* 10 cm in front of a screen 1 pulled 0.9 m nearer: 0.8 m away, and + * sized by that from its 1.7 m spot. */ + float k = 0.8f / 1.7f; + t_edit_start(); + t_quads.quads[0].pose.position.z = -0.9f; + t_xr[0].z = 0.9f; + t_quads.quads[1].pose.position.x = 0.0f; + t_quads.quads[1].pose.position.z = -0.8f; + t_quads.quads[1].width = 1.0f * k; + t_quads.quads[1].height = 0.6f * k; + video_views_xr_reset(&t_menu_xr, 1); + t_menu_placed = false; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == 0, + "through the menu: slot %u state %u", g->slot, g->state); + t_let_go(); + t_edit_poll(); + t_aim[2].position.y = t_handle_y(0.6f * k); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == VIDEO_XR_MENU_SLOT, + "its handle: slot %u state %u", g->slot, g->state); + t_edit_poll(); + CHECK(t_menu_placed && t_near(t_menu_xr.x, 0.0f) + && t_near(t_menu_xr.y, 0.0f) && t_near(t_menu_xr.z, 0.9f) + && t_near(t_menu_xr.scale, k) && t_near(g->width, k), + "placed %d at %.3f %.3f %.3f, scale %.4f, width %.4f", + (int)t_menu_placed, t_menu_xr.x, t_menu_xr.y, t_menu_xr.z, + t_menu_xr.scale, g->width); + t_edit_end(); +} + +/* The main button on a free item resets it as Reset Screen does, and + * puts the menu back in front of screen 1; on a held item, nothing. */ +static void test_edit_reset(void) +{ + t_edit_start(); + t_xr[0].x = 0.5f; + t_xr[0].scale = 2.0f; + t_xr[0].visible = false; + t_quads.quads[0].pose.position.x = 0.5f; + t_aim[2].position.x = 0.5f; + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_xr[0].x == 0.0f && t_xr[0].scale == 1.0f && t_xr[0].visible + && t_modified, "Reset left screen 1 at %.2f", t_xr[0].x); + /* Held on to the menu's handle: no new press, no reset. */ + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = t_handle_y(0.6f); + t_edit_poll(); + CHECK(t_menu_placed, "a held button reset the menu too"); + t_let_go(); + t_edit_poll(); + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = t_handle_y(0.6f); + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(!t_menu_placed && t_menu_xr.x == 0.0f, "Reset left the menu placed"); + t_let_go(); + t_edit_poll(); + /* Held by the other hand: no reset. */ + t_xr[0].x = 0.5f; + t_aim[1].position.x = 0.5f; + t_aim[2].position.x = 0.5f; + t_aim[2].position.y = 0.0f; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.5f), "a held screen was reset"); + t_edit_end(); +} + +/* A button held as edit mode starts (the menu's OK on A, which is + * Reset) counts once released and pressed again; a grip too. */ +static void test_edit_held_at_start(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + t_forget_devices(); + t_fresh_session(); + t_edit_fixture(); + t_xr[0].x = 0.5f; + t_quads.quads[0].pose.position.x = 0.5f; + t_aim[2].position.x = 0.5f; + t_hold("combined/a", 0, 1.0f, 0.0f); + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_edit_on(false); + CHECK(t_editing, "fixture: not editing"); + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.5f) && !t_modified, + "Reset held from the start reset screen 1 to %.2f", t_xr[0].x); + t_let_go(); + t_edit_poll(); + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_xr[0].x == 0.0f && t_modified, + "Reset pressed again left screen 1 at %.2f", t_xr[0].x); + t_edit_end(); + + t_forget_devices(); + t_fresh_session(); + t_edit_fixture(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_on(false); + CHECK(t_editing, "fixture: not editing"); + t_edit_poll(); + CHECK(g->state != VIDEO_XR_GRAB_HELD, + "a grip held from the start grabbed %u", g->slot); + t_let_go(); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == 0, + "a grip pressed again grabbed %u in state %u", g->slot, g->state); + t_edit_end(); +} + +/* A recenter lets go first; edit mode stays on, and a grip still held + * grabs nothing until pressed again. */ +static void test_edit_recenter(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + unsigned seq; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + seq = t_edit_seq; + input_openxr_edit_release(); + CHECK(g->state == VIDEO_XR_GRAB_RELEASED && t_edit_seq == seq + 1 + && t_editing, "a recenter did not let go: %u", g->state); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_RELEASED, "a held grip grabbed again"); + t_let_go(); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD, "a new grip did not grab"); + /* Leaving edit mode lets go too. */ + seq = t_edit_seq; + input_openxr_edit_toggle(); + CHECK(g->state == VIDEO_XR_GRAB_RELEASED && g->release_seq == seq + 1 + && !t_editing, "leaving held on: state %u", g->state); + t_edit_end(); +} + +/* A hand points at items where they show: one another hand holds is + * where that hand put it, not where the last quads had it. Screen 1 + * lifted 2 m, the laser through its old place takes screen 2 behind. */ +static void test_edit_held_moves(void) +{ + const video_xr_grab_t *l = &input_openxr_edit.hand[0].g; + const video_xr_grab_t *r = &input_openxr_edit.hand[1].g; + t_edit_start(); + t_quads.num_quads = 3; + t_quads.quads[2] = t_quads.quads[0]; + t_quads.quads[2].pose.position.z = -3.0f; + t_quads.quads[2].screen = 1; + t_quads.quads[2].slot = 1; + t_quads.slots[1].base.z = -3.0f; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + t_aim[1].position.y = 2.0f; + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == 0 + && t_near(t_xr[0].y, 2.0f), "the left hand did not lift screen 1"); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state == VIDEO_XR_GRAB_HELD && r->slot == 1, + "the right hand took slot %u in state %u", r->slot, r->state); + t_edit_end(); +} + +/* A stereo screen is one item: a grab moves both eyes' quads, and a + * second hand on the pair is refused. */ +static void test_edit_stereo(void) +{ + const video_xr_grab_t *l = &input_openxr_edit.hand[0].g; + const video_xr_grab_t *r = &input_openxr_edit.hand[1].g; + const video_xr_quad_t *q = input_openxr_st.quads.quads; + t_edit_start(); + t_quads.num_quads = 3; + t_quads.quads[2] = t_quads.quads[1]; + t_quads.quads[1] = t_quads.quads[0]; + t_quads.quads[0].eye = VIDEO_XR_EYE_LEFT; + t_quads.quads[1].eye = VIDEO_XR_EYE_RIGHT; + t_quads.quads[1].layer = 1; + t_quads.slots[0].layers = 2; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + t_aim[1].position.x = 0.3f; + t_edit_poll(); + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == 0 + && t_near(t_xr[0].x, 0.3f) && t_near(q[0].pose.position.x, 0.3f) + && t_near(q[1].pose.position.x, 0.3f), + "the pair: setting %.3f, eyes at %.3f and %.3f", t_xr[0].x, + q[0].pose.position.x, q[1].pose.position.x); + t_aim[2].position.x = 0.3f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state != VIDEO_XR_GRAB_HELD, "a second hand took the pair"); + t_edit_end(); +} + +/* A views core's frame shown whole without the screens' settings is no + * item: a grip and the main button on it do nothing and write nothing, + * no dot shows on it, and a screen held as it turns into one is let go + * as gone from the map. */ +static void test_edit_fallback_frame(void) +{ + const XrCompositionLayerBaseHeader *layers[4]; + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + video_xr_quad_set_t frame; + unsigned seq; + t_edit_start(); + t_quads.quads[0].kind = VIDEO_XR_QUAD_FRAME; + t_quads.slots[0].fixed = true; + t_xr[0].x = 0.5f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.2f; + t_edit_poll(); + CHECK(g->state != VIDEO_XR_GRAB_HELD && !t_modified + && t_xr[0].x == 0.5f, "the fallback frame grabbed: state %u, " + "screen 1 at %.3f", g->state, t_xr[0].x); + t_let_go(); + t_edit_poll(); + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(!t_modified && t_xr[0].x == 0.5f, + "the main button on the fallback frame reset screen 1"); + t_let_go(); + t_cursor = true; + frame = t_quads; + frame.num_quads = 1; + CHECK(t_hooks.frame_layers(t_hooks.user, t_time, &frame, + t_xr_session.local_space, layers, 4, &t_under) == 0, + "a dot or an outline on the fallback frame"); + t_cursor = false; + t_quads.quads[0].kind = VIDEO_XR_QUAD_SCREEN; + t_quads.slots[0].fixed = false; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD, "fixture: screen 1 not held"); + seq = t_edit_seq; + t_quads.quads[0].kind = VIDEO_XR_QUAD_FRAME; + t_quads.slots[0].fixed = true; + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_NONE && t_edit_seq == seq, + "held into the fallback: state %u, seq %u/%u", g->state, + t_edit_seq, seq); + t_edit_end(); +} + +/* What the hands hold reaches the frame loop each poll, let go with its + * count; a hand that lets go and takes another item at its next poll + * still hands the first over, where it let go of it; leaving edit mode + * lets go for the frame loop too; a new session starts with nothing + * held. */ +static void test_edit_hold_record(void) +{ + unsigned seq, n; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_grabs.hand[1].state == VIDEO_XR_GRAB_HELD + && t_grabs.hand[1].slot == 0 + && t_near(t_grabs.hand[1].width, 1.6f) + && t_near(t_grabs.hand[1].base.z, -1.8f), + "held: state %u slot %u", t_grabs.hand[1].state, + t_grabs.hand[1].slot); + t_aim[2].position.x = 0.3f; + t_edit_poll(); + CHECK(t_near(t_grabs.hand[1].last.position.x, 0.3f), + "the hold's place not given: %.3f", + t_grabs.hand[1].last.position.x); + t_let_go(); + t_edit_poll(); + seq = t_edit_seq; + CHECK(t_grabs.hand[1].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.hand[1].release_seq == seq + && t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.let_go[0].release_seq == seq + && t_near(t_grabs.let_go[0].last.position.x, 0.3f), + "let go: state %u seq %u/%u, slot 0's %u at %.3f", + t_grabs.hand[1].state, t_grabs.hand[1].release_seq, seq, + t_grabs.let_go[0].state, t_grabs.let_go[0].last.position.x); + /* The same hand on the menu's handle at its next poll. */ + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = t_handle_y(0.6f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_grabs.hand[1].state == VIDEO_XR_GRAB_HELD + && t_grabs.hand[1].slot == VIDEO_XR_MENU_SLOT + && t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.let_go[0].release_seq == seq + && t_near(t_grabs.let_go[0].last.position.x, 0.3f) + && t_near(t_grabs.let_go[0].width, 1.6f), + "screen 1's hand-over lost with its hold: slot 0's %u seq %u", + t_grabs.let_go[0].state, t_grabs.let_go[0].release_seq); + input_openxr_edit_toggle(); + CHECK(!t_editing && t_grabs.hand[1].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.let_go[VIDEO_XR_MENU_SLOT].state + == VIDEO_XR_GRAB_RELEASED + && t_grabs.let_go[VIDEO_XR_MENU_SLOT].release_seq == t_edit_seq + && t_grabs.let_go[0].release_seq == seq, + "leaving edit mode: the frame loop still sees state %u", + t_grabs.hand[1].state); + t_edit_end(); + /* A session's start is the frame's to clear; a recenter with nothing + * held outside edit mode publishes nothing either. */ + n = t_grabs_published; + t_fresh_session(); + CHECK(t_grabs_published == n, "the session's start published a record"); + input_openxr_edit_release(); + CHECK(t_grabs_published == n, + "a recenter outside edit mode published a record"); + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* The XR thread's aim: each hand's own, in the space asked; nothing + * for a hand that is not tracked or does not exist. */ +static void test_edit_frame_aim(void) +{ + video_xr_pose_t pose; + t_edit_start(); + t_aim[1].position.x = -0.25f; + CHECK( t_hooks.aim(t_hooks.user, 0, t_xr_session.view_space, t_time, + &pose) + && t_near(pose.position.x, -0.25f) + && t_located_base == t_xr_session.view_space, + "the left hand's aim: %.3f", pose.position.x); + t_aim_tracked[1] = false; + CHECK(!t_hooks.aim(t_hooks.user, 0, t_xr_session.local_space, t_time, + &pose), "an untracked hand aimed"); + CHECK(!t_hooks.aim(t_hooks.user, 2, t_xr_session.local_space, t_time, + &pose), "a third hand aimed"); + t_edit_end(); +} + +/* Where l's front faces, along z. */ +static float t_front_z(const XrCompositionLayerQuad *l) +{ + video_xr_quat_t o; + video_xr_vec3_t z; + o.x = l->pose.orientation.x; + o.y = l->pose.orientation.y; + o.z = l->pose.orientation.z; + o.w = l->pose.orientation.w; + z.x = 0.0f; + z.y = 0.0f; + z.z = 1.0f; + video_xr_rotate(&o, &z, &z); + return z.z; +} + +/* Items the frame shows by their backs: their outlines, the menu's + * handle and the dot on one face the head, just in front of the back; + * a hand behind a screen points at it and grabs it by its back. Out of + * edit mode the dot is on the back too. */ +static void test_edit_backs(void) +{ + unsigned i, n; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + XrSpace local = XR_NULL_HANDLE; + bool facing = true; + t_edit_start(); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_cursor = true; + shown = t_quads; + /* The right hand 1.2 m behind screen 1, aiming back at it. */ + t_aim[2].position.x = 0.4f; + t_aim[2].position.z = -3.0f; + t_aim[2].orientation.y = 1.0f; + t_aim[2].orientation.w = 0.0f; + t_xr_frame.backs = 3u; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + for (i = 0; i < n; i++) + facing &= t_near(t_front_z((const XrCompositionLayerQuad*)layers[i]), + -1.0f); + CHECK(n == 4 && facing, "%u layers from behind, all facing it %d", n, + (int)facing); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && t_near(q->pose.position.z, -1.8f - VIDEO_XR_CURSOR_LIFT), + "screen 1's outline from behind: offset %d at z %.4f", + (int)q->subImage.imageRect.offset.x, q->pose.position.z); + q = (const XrCompositionLayerQuad*)layers[2]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_HANDLE_X(0) + && t_near(q->pose.position.x, 2.0f) + && t_near(q->pose.position.y, t_handle_y(0.6f)) + && t_near(q->pose.position.z, -1.7f - VIDEO_XR_CURSOR_LIFT), + "the menu's handle from behind at %.3f,%.3f,%.4f", + q->pose.position.x, q->pose.position.y, q->pose.position.z); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK( q->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM + && t_near(q->pose.position.x, 0.4f) + && t_near(q->pose.position.z, -1.8f - VIDEO_XR_CURSOR_LIFT), + "the dot on screen 1's back at %.3f,%.4f", q->pose.position.x, + q->pose.position.z); + /* Shown by their fronts, all face the screens' front. */ + t_xr_frame.backs = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK(n == 4 && t_near(t_front_z(q), 1.0f) + && t_near(q->pose.position.x, 0.4f) + && t_near(q->pose.position.z, -1.8f + VIDEO_XR_CURSOR_LIFT), + "fronts: the dot at %.3f,%.4f facing %.2f", q->pose.position.x, + q->pose.position.z, t_front_z(q)); + + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == 0, + "from behind the grip grabbed %u in state %u", g->slot, g->state); + + /* Out of edit mode, Laser Pointer Always: a hand in front of screen + * 1 with the head behind it puts the dot on its back. */ + t_let_go(); + input_openxr_edit_toggle(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_ALWAYS); + t_aim[2].position.z = 0.0f; + t_aim[2].orientation.y = 0.0f; + t_aim[2].orientation.w = 1.0f; + t_xr_frame.backs = 1u; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 1 && t_near(t_front_z(q), -1.0f) + && t_near(q->pose.position.x, 0.4f) + && t_near(q->pose.position.z, -1.8f - VIDEO_XR_CURSOR_LIFT), + "Laser Pointer Always: %u layers, the dot at %.3f,%.4f facing " + "%.2f", n, q->pose.position.x, q->pose.position.z, t_front_z(q)); + t_xr_frame.backs = 0; + t_edit_end(); +} + +/* Edit mode's layers: a bright outline round what a hand points at, a + * faint one round every other item, the menu's handle bar after its + * outline, then the dot; each where the frame shows its item; outlines + * only in the layers the dots leave; none round a fixed slot. The + * screens' outlines go first, under the screens; the menu's and its + * handle over them. */ +static void test_edit_outlines(void) +{ + unsigned n; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local = XR_NULL_HANDLE; + t_edit_start(); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_cursor = true; + shown = t_quads; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4, "%u layers, want two outlines, a handle and a dot", n); + CHECK(t_under == 1, "%u layers under the screens, want screen 1's " + "outline alone", t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && q->subImage.imageRect.offset.y == 0 + && q->subImage.imageRect.extent.width == VIDEO_XR_OUTLINE_DIM + && q->subImage.imageRect.extent.height == VIDEO_XR_OUTLINE_DIM + && q->subImage.swapchain == (XrSwapchain)(uintptr_t)0x51 + && q->space == local + && (q->layerFlags + & XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT) + && t_near(q->size.width, t_outline(1.6f)) + && t_near(q->size.height, t_outline(0.96f)) + && t_near(q->pose.position.z, -1.8f + VIDEO_XR_CURSOR_LIFT), + "screen 1's outline: offset %d, %.3f x %.3f at z %.4f", + (int)q->subImage.imageRect.offset.x, q->size.width, + q->size.height, q->pose.position.z); + q = (const XrCompositionLayerQuad*)layers[1]; + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && t_near(q->pose.position.x, 2.0f) + && t_near(q->size.width, t_outline(1.0f)), + "the menu's outline is not the faint one round it"); + q = (const XrCompositionLayerQuad*)layers[2]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_HANDLE_X(0) + && q->subImage.imageRect.offset.y == VIDEO_XR_HANDLE_Y + && q->subImage.imageRect.extent.width == VIDEO_XR_HANDLE_COLS + && q->subImage.imageRect.extent.height == VIDEO_XR_HANDLE_ROWS + && t_near(q->size.width, 1.0f * VIDEO_XR_HANDLE_WIDTH) + && t_near(q->size.height, VIDEO_XR_HANDLE_HEIGHT) + && t_near(q->pose.position.x, 2.0f) + && t_near(q->pose.position.y, t_handle_y(0.6f)), + "the menu's handle: offset %d, %.3f x %.3f at %.3f,%.3f", + (int)q->subImage.imageRect.offset.x, q->size.width, q->size.height, + q->pose.position.x, q->pose.position.y); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK(q->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM + && q->subImage.imageRect.offset.x == 0 + && q->subImage.imageRect.offset.y == VIDEO_XR_CURSOR_Y, + "the dot is not last, or not its part of the image"); + + /* The frame shows screen 1 elsewhere: its outline goes there. */ + shown.quads[0].pose.position.x = 0.5f; + t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(t_near(q->pose.position.x, 0.5f), + "the outline at %.3f, not where the frame shows screen 1", + q->pose.position.x); + shown = t_quads; + + /* A laser on the handle lights the menu's outline and handle. */ + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = t_handle_y(0.6f); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4 + && ((const XrCompositionLayerQuad*)layers[0]) + ->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && ((const XrCompositionLayerQuad*)layers[1]) + ->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && ((const XrCompositionLayerQuad*)layers[2]) + ->subImage.imageRect.offset.x == VIDEO_XR_HANDLE_X(1), + "the handle under the laser: %u layers", n); + t_aim[2].position.x = 0.0f; + t_aim[2].position.y = 0.0f; + + /* The menu first among the quads: its outline still over. */ + shown.quads[0] = t_quads.quads[1]; + shown.quads[1] = t_quads.quads[0]; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4 && t_under == 1 + && ((const XrCompositionLayerQuad*)layers[0]) + ->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && t_near(((const XrCompositionLayerQuad*)layers[1]) + ->pose.position.x, 2.0f) + && ((const XrCompositionLayerQuad*)layers[2]) + ->subImage.imageRect.offset.x == VIDEO_XR_HANDLE_X(0), + "the menu first: %u layers, %u under", n, t_under); + shown = t_quads; + + /* With the dots' two layers alone: the dot, no outline. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 1 && t_under == 0 + && q->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM + && q->subImage.imageRect.offset.x == 0, + "with two layers: %u, the dot dropped or an outline kept", n); + /* One more: screen 1's outline first. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 1, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 2 && t_under == 1 + && q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1), + "with three layers: %u", n); + + /* A frame shown whole without the screens' settings: no outline. */ + shown.slots[0].fixed = true; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 2 && t_under == 0 + && q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && t_near(q->pose.position.x, 2.0f), + "%u layers round a fixed slot and the menu", n); + + /* A stereo screen is one item: one outline round its pair. */ + shown = t_quads; + shown.num_quads = 3; + shown.quads[2] = shown.quads[1]; + shown.quads[1] = shown.quads[0]; + shown.quads[0].eye = VIDEO_XR_EYE_LEFT; + shown.quads[1].eye = VIDEO_XR_EYE_RIGHT; + shown.quads[1].layer = 1; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4 && t_under == 1, + "%u layers round a stereo screen and the menu", n); + + /* Out of edit mode, Always: the dot alone. */ + input_openxr_edit_toggle(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_ALWAYS); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 1 && t_under == 0 && q->subImage.imageRect.offset.x == 0, + "%u layers out of edit mode", n); + t_cursor = false; + t_edit_end(); +} + +/* Screen 2, 0.8 x 0.5, 20 cm nearer than the menu and under its + * handle bar, its top edge 7 mm above the bar's middle. */ +static void t_screen_under_handle(void) +{ + video_xr_quad_t *q = &t_quads.quads[t_quads.num_quads++]; + memset(q, 0, sizeof(*q)); + video_xr_pose_identity(&q->pose); + q->pose.position.x = 2.0f; + q->pose.position.y = t_handle_y(0.6f) + 0.007f - 0.25f; + q->pose.position.z = -1.5f; + q->width = 0.8f; + q->height = 0.5f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->screen = 1; + q->slot = 1; + t_quads.slots[1].base = q->pose.position; +} + +/* Whether the bright outlines in the frame loop's layers are only one + * w wide (the menu's 1.0, screen 2's 0.8). */ +static bool t_lit_only(const XrCompositionLayerBaseHeader **layers, + unsigned n, float w) +{ + unsigned i; + unsigned lit = 0; + for (i = 0; i < n; i++) + { + const XrCompositionLayerQuad *q = + (const XrCompositionLayerQuad*)layers[i]; + if ( q->subImage.imageRect.offset.x != VIDEO_XR_OUTLINE_X(1) + || q->subImage.imageRect.extent.width != VIDEO_XR_OUTLINE_DIM) + continue; + if (!t_near(q->size.width, t_outline(w))) + return false; + lit++; + } + return lit == 1; +} + +/* The menu's handle sticks to a laser that was on its bar: in the 3 cm + * margin over screen 2 the grip still takes the menu; arriving there + * from screen 2, screen 2. Losing tracking and edit mode's end let go; + * the frame loop is told, and lights what is stuck. */ +static void test_edit_handle_sticky(void) +{ + unsigned n; + video_xr_quad_set_t shown; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local = XR_NULL_HANDLE; + const float hy = t_handle_y(0.6f); + input_openxr_edit_t *e = &input_openxr_edit; + t_edit_start(); + t_screen_under_handle(); + t_aim_tracked[1] = false; + t_aim[2].position.x = 2.0f; + t_aim[2].position.y = hy; + t_edit_poll(); + CHECK(e->stuck == 2u && t_grabs.stuck == 2u + && input_openxr_st.hit[1] == 1, + "on the bar: stuck %#x, published %#x, hit %d", e->stuck, + t_grabs.stuck, input_openxr_st.hit[1]); + t_aim[2].position.y = hy - 0.03f; + t_edit_poll(); + CHECK(e->stuck == 2u && input_openxr_st.hit[1] == 1, + "in the margin over screen 2: stuck %#x, hit %d", e->stuck, + input_openxr_st.hit[1]); + /* The frame loop lights the menu while stuck, screen 2 when not. */ + local = t_xr_session.local_space; + t_cursor = true; + shown = t_quads; + t_xr_frame.grabs = t_grabs; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit_only(layers, n, 1.0f), "stuck: the menu not alone lit"); + t_xr_frame.grabs.stuck = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit_only(layers, n, 0.8f), "unstuck: screen 2 not alone lit"); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + /* The grip takes the menu. */ + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(e->hand[1].g.state == VIDEO_XR_GRAB_HELD + && e->hand[1].g.slot == VIDEO_XR_MENU_SLOT, + "stuck, the grip took slot %u", e->hand[1].g.slot); + t_let_go(); + t_edit_poll(); + /* From screen 2 past the margin into it: screen 2's. */ + t_aim[2].position.y = hy - 0.06f; + t_edit_poll(); + CHECK(!e->stuck && input_openxr_st.hit[1] == 2, + "past the margin: stuck %#x, hit %d", e->stuck, + input_openxr_st.hit[1]); + t_aim[2].position.y = hy - 0.03f; + t_edit_poll(); + CHECK(!e->stuck && input_openxr_st.hit[1] == 2, + "arriving from screen 2: stuck %#x, hit %d", e->stuck, + input_openxr_st.hit[1]); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(e->hand[1].g.slot == 1, "not stuck, the grip took slot %u", + e->hand[1].g.slot); + t_let_go(); + t_edit_poll(); + /* Untracked, it lets go; edit mode's end clears it. */ + t_aim[2].position.y = hy; + t_edit_poll(); + t_aim_tracked[2] = false; + t_edit_poll(); + CHECK(!e->stuck && !t_grabs.stuck, "untracked, still stuck %#x", + e->stuck); + t_aim_tracked[2] = true; + t_edit_poll(); + CHECK(e->stuck == 2u, "back on the bar: %#x", e->stuck); + /* Per hand: the left hand in the margin, only the right stuck. */ + t_aim_tracked[1] = true; + t_aim[1].position.x = 2.0f; + t_aim[1].position.y = hy - 0.03f; + t_edit_poll(); + CHECK(e->stuck == 2u && input_openxr_st.hit[0] == 2, + "the left hand in the margin: stuck %#x, hit %d", e->stuck, + input_openxr_st.hit[0]); + t_aim_tracked[2] = false; + t_cursor = true; + shown = t_quads; + t_xr_frame.grabs.stuck = 2u; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit_only(layers, n, 0.8f), "the left hand lit the menu"); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_edit_end(); + CHECK(!e->stuck, "edit mode ended, still stuck %#x", e->stuck); +} + +/* A layer's own axis (x, y, z), in its space. */ +static void t_layer_axis(const XrCompositionLayerQuad *l, float x, float y, + float z, video_xr_vec3_t *out) +{ + video_xr_quat_t o; + video_xr_vec3_t v; + o.x = l->pose.orientation.x; + o.y = l->pose.orientation.y; + o.z = l->pose.orientation.z; + o.w = l->pose.orientation.w; + v.x = x; + v.y = y; + v.z = z; + video_xr_rotate(&o, &v, out); +} + +/* The beams: one a tracked hand, from its aim to its dot, or + * VIDEO_XR_BEAM_REACH into nothing; along the ray, facing the head; + * after the outlines and before the dots, and the first to go when the + * layers run short. Off: none; Edit Mode Only: in edit mode alone; + * Always: wherever the laser can point. No head, no beam. */ +static void test_edit_beams(void) +{ + unsigned n; + video_xr_vec3_t ax, az; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local = XR_NULL_HANDLE; + t_edit_start(); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_aim[2].position.x = 0.2f; + t_aim[2].position.y = -0.3f; + t_head_tracked = true; + t_cursor = true; + + /* Straight ahead onto screen 1, 1.8 m away. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 5, "%u layers, want three outlines, a beam and a dot", n); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_BEAM_X(0) + && q->subImage.imageRect.offset.y == VIDEO_XR_BEAM_Y + && q->subImage.imageRect.extent.width == VIDEO_XR_BEAM_LEN + && q->subImage.imageRect.extent.height == VIDEO_XR_BEAM_ROWS + && q->subImage.swapchain == (XrSwapchain)(uintptr_t)0x51 + && q->space == local + && (q->layerFlags + & XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT) + && t_near(q->size.width, 1.8f) + && t_near(q->size.height, VIDEO_XR_BEAM_WIDTH) + && t_near(q->pose.position.x, 0.2f) + && t_near(q->pose.position.y, -0.3f) + && t_near(q->pose.position.z, -0.9f), + "the beam: offset %d, %.3f x %.4f at %.3f,%.3f,%.3f", + (int)q->subImage.imageRect.offset.x, q->size.width, q->size.height, + q->pose.position.x, q->pose.position.y, q->pose.position.z); + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + t_layer_axis(q, 0.0f, 0.0f, 1.0f, &az); + CHECK(t_near(ax.z, -1.0f), "the beam's x is not down the ray"); + CHECK(t_near(az.x, -0.5547f) && t_near(az.y, 0.8321f) && t_near(az.z, 0.0f), + "the beam faces %.3f,%.3f,%.3f, not the head", az.x, az.y, az.z); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK(q->subImage.imageRect.offset.x == 0 + && q->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM, + "the dot is not last"); + + /* Straight up, into nothing: 1.5 m, the strip that fades out. */ + t_aim[2].orientation.x = 0.70711f; + t_aim[2].orientation.w = 0.70711f; + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK(n == 4 && q->subImage.imageRect.offset.x == VIDEO_XR_BEAM_X(1) + && t_near(q->size.width, VIDEO_XR_BEAM_REACH) + && t_near(q->pose.position.y, -0.3f + 0.5f * VIDEO_XR_BEAM_REACH) + && t_near(q->pose.position.z, 0.0f), + "%u layers; the missing beam: offset %d, %.3f long at y %.3f", n, + (int)q->subImage.imageRect.offset.x, q->size.width, + q->pose.position.y); + t_layer_axis(q, 0.0f, 0.0f, 1.0f, &az); + CHECK(t_near(az.x, -1.0f), "the missing beam does not face the head"); + t_aim[2].orientation.x = 0.0f; + t_aim[2].orientation.w = 1.0f; + + /* Short of layers: the beam goes before any outline; the dot stays. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 3, &t_under); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK(n == 4 && q->subImage.imageRect.offset.x == 0, + "with room for the outlines and dots: %u layers", n); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 2, &t_under); + CHECK(n == 3, "with room for two outlines: %u layers", n); + /* Two hands on screen 1, one more layer's room: the left's tag. */ + t_aim_tracked[1] = true; + t_aim[1].position.x = -0.2f; + t_aim[1].position.y = -0.3f; + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 4, &t_under); + q = (const XrCompositionLayerQuad*)layers[3]; + CHECK(n == 6 && q->subImage.imageRect.offset.x == VIDEO_XR_TAG_X(0), + "two hands, one more layer's room: the tag, beams dropped first: " + "%u layers", n); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 5, &t_under); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK(n == 7 && q->subImage.imageRect.offset.x == VIDEO_XR_TAG_X(0), + "two hands, two more layers' room: %u layers, want 3 + 1 + 1 + 2", + n); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 8, "two hands with room: %u layers, want 3 + 2 + 1 + 2", n); + t_aim_tracked[1] = false; + + /* No head: no beam. */ + t_head_tracked = false; + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4, "%u layers with the head lost", n); + t_head_tracked = true; + + /* Off: none. A value past the choices is Edit Mode Only. */ + t_beam = VIDEO_OPENXR_LASER_BEAM_OFF; + t_edit_poll(); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 4, "%u layers with Laser Beam Off", n); + t_beam = 7; + t_edit_poll(); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 5, "%u layers with Laser Beam 7", n); + + /* Out of edit mode, Laser Pointer Always: Edit Mode Only draws the + * dot alone, Always the beam too; Laser Pointer Off neither. */ + t_beam = VIDEO_OPENXR_LASER_BEAM_EDIT; + input_openxr_edit_toggle(); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_ALWAYS); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 1, "%u layers out of edit mode, Edit Mode Only", n); + t_beam = VIDEO_OPENXR_LASER_BEAM_ALWAYS; + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_ALWAYS); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 2 && q->subImage.imageRect.offset.x == VIDEO_XR_BEAM_X(0), + "%u layers out of edit mode, Always", n); + /* Auto with the menu closed: screen 1 is not the laser's, so there is + * nothing to point at and no beam. */ + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_AUTO); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 0, "%u layers with nothing the laser can point at", n); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + n = t_hooks.frame_layers(t_hooks.user, t_time, &t_quads, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 0, "%u layers with Laser Pointer Off", n); + + t_beam = VIDEO_OPENXR_LASER_BEAM_EDIT; + t_head_tracked = false; + t_cursor = false; + t_aim[2].position.x = 0.0f; + t_aim[2].position.y = 0.0f; + t_edit_end(); +} + +/* Screen 2 hidden behind screen 1: 1.2 x 0.9 at 2.1 m, its spot. */ +static void t_edit_buried(void) +{ + video_xr_quad_t *q = &t_quads.quads[t_quads.num_quads++]; + memset(q, 0, sizeof(*q)); + video_xr_pose_identity(&q->pose); + q->pose.position.z = -2.1f; + q->width = 1.2f; + q->height = 0.9f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->screen = 1; + q->slot = 1; + t_quads.slots[1].base.z = -2.1f; +} + +/* Select Next (step 1) or Select Previous (-1) pressed and let go; + * whatever else was held is let go too. */ +static void t_edit_select(int step) +{ + t_hold(step > 0 ? "edit/select_next" : "edit/select_previous", 0, + 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); +} + +/* The slots whose outline is bright in the frame loop's layers: a + * bright outline shows the image's VIDEO_XR_OUTLINE_X(1) square, as + * test_edit_outlines tells it (handles, beams and dots show other + * parts); its item told by where it sits: the menu at x 2, screen 2 + * beyond 2 m, screen 1 else. */ +static unsigned t_lit(const XrCompositionLayerBaseHeader **layers, + unsigned n) +{ + unsigned i; + unsigned lit = 0; + for (i = 0; i < n; i++) + { + const XrCompositionLayerQuad *q = + (const XrCompositionLayerQuad*)layers[i]; + if ( q->subImage.imageRect.offset.x != VIDEO_XR_OUTLINE_X(1) + || q->subImage.imageRect.extent.width != VIDEO_XR_OUTLINE_DIM) + continue; + if (t_near(q->pose.position.x, 2.0f)) + lit |= 1u << VIDEO_XR_MENU_SLOT; + else if (q->pose.position.z < -2.0f) + lit |= 1u << 1; + else + lit |= 1u; + } + return lit; +} + +/* Select Next and Select Previous: suggested per profile, the Frame's + * on the left D-pad beside its Reset, the Vive's next alone, the simple + * controller's none. */ +static void test_edit_select_bindings(void) +{ + const char *index = "/interaction_profiles/valve/index_controller"; + const char *frame = "/interaction_profiles/valve/frame_controller_valve"; + const char *vive = "/interaction_profiles/htc/vive_controller"; + const char *simple = "/interaction_profiles/khr/simple_controller"; + t_forget_devices(); + t_num_sets = 0; + t_num_actions = 0; + t_num_bound = 0; + t_spaces_made = 0; + t_session(true); + CHECK(t_binds(frame, "edit/select_next", + "/user/hand/left/input/dpad_right/click") + && t_binds(frame, "edit/select_previous", + "/user/hand/left/input/dpad_left/click") + && t_binds(frame, "edit/reset", + "/user/hand/left/input/dpad_down/click"), + "the Frame's choice is not the left D-pad's right and left"); + CHECK(t_touch_binds("edit/select_next", "/user/hand/right/input/b/click") + && t_touch_binds("edit/select_previous", + "/user/hand/left/input/y/click"), + "Touch's choice is not B and Y"); + CHECK(t_binds(index, "edit/select_next", "/user/hand/right/input/b/click") + && t_binds(index, "edit/select_previous", + "/user/hand/left/input/b/click"), + "Index's choice is not the right and the left B"); + CHECK(t_binds(vive, "edit/select_next", "/user/hand/right/input/menu/click") + && !t_binds(vive, "edit/select_previous", NULL), + "the Vive's choice is not the right menu button alone"); + CHECK(!t_binds(simple, "edit/select_next", NULL) + && !t_binds(simple, "edit/select_previous", NULL), + "the simple controller has a choice"); + t_hooks.session_destroying(t_hooks.user); + t_poll(VIDEO_OPENXR_CONTROLLERS_COMBINED, VIDEO_OPENXR_LASER_OFF); + t_forget_devices(); +} + +/* Select Next steps Laser, the shown screens by number (a stereo pair + * one, a hidden or fixed screen none), the menu, Laser again; Select + * Previous the other way; a held button steps once. Without the menu's + * quad (gl) there is no menu step. */ +static void test_edit_select_cycle(void) +{ + input_openxr_edit_t *e = &input_openxr_edit; + t_edit_start(); + CHECK(e->chosen == -1, "edit mode starts on %d, not the laser", + e->chosen); + /* Screen 1 a stereo pair; screen 2 hidden (no quad in slot 1); + * screen 3 shown (slot 2); screen 4 fixed (slot 3). */ + t_quads.quads[0].eye = VIDEO_XR_EYE_LEFT; + t_quads.quads[2] = t_quads.quads[0]; + t_quads.quads[2].eye = VIDEO_XR_EYE_RIGHT; + t_quads.quads[3] = t_quads.quads[0]; + t_quads.quads[3].eye = VIDEO_XR_EYE_BOTH; + t_quads.quads[3].slot = 2; + t_quads.quads[3].pose.position.x = -2.0f; + t_quads.quads[4] = t_quads.quads[3]; + t_quads.quads[4].slot = 3; + t_quads.quads[4].pose.position.x = -4.0f; + t_quads.slots[3].fixed = true; + t_quads.num_quads = 5; + t_edit_select(1); + CHECK(e->chosen == 0, "next from the laser: %d", e->chosen); + t_edit_select(1); + CHECK(e->chosen == 2, "next from screen 1: %d", e->chosen); + t_edit_select(1); + CHECK(e->chosen == VIDEO_XR_MENU_SLOT, "next from screen 3: %d", + e->chosen); + t_edit_select(1); + CHECK(e->chosen == -1, "next from the menu: %d", e->chosen); + t_edit_select(-1); + CHECK(e->chosen == VIDEO_XR_MENU_SLOT, "previous from the laser: %d", + e->chosen); + t_edit_select(-1); + CHECK(e->chosen == 2, "previous from the menu: %d", e->chosen); + t_edit_select(-1); + CHECK(e->chosen == 0, "previous from screen 3: %d", e->chosen); + t_edit_select(-1); + CHECK(e->chosen == -1, "previous from screen 1: %d", e->chosen); + t_hold("edit/select_next", 0, 1.0f, 0.0f); + t_edit_poll(); + t_edit_poll(); + CHECK(e->chosen == 0, "a held Select Next stepped to %d", e->chosen); + t_let_go(); + t_edit_poll(); + /* No menu quad: screen 3 steps back to the laser. */ + t_quads.quads[1] = t_quads.quads[2]; + t_edit_select(1); + CHECK(e->chosen == 2, "next from screen 1 without a menu: %d", + e->chosen); + t_edit_select(1); + CHECK(e->chosen == -1, "next from screen 3 without a menu: %d", + e->chosen); + t_edit_end(); +} + +/* A chosen item gone from the cycle falls back to the laser; edit mode + * ending forgets the choice, for the frame loop too. */ +static void test_edit_select_fallback(void) +{ + input_openxr_edit_t *e = &input_openxr_edit; + t_edit_start(); + t_edit_buried(); + t_edit_select(1); + t_edit_select(1); + CHECK(e->chosen == 1 && t_grabs.chosen == 2, + "screen 2 not chosen: %d, published %u", e->chosen, + t_grabs.chosen); + t_quads.num_quads--; + t_edit_poll(); + CHECK(e->chosen == -1 && t_grabs.chosen == 0, + "hidden, screen 2 stayed chosen"); + t_edit_buried(); + t_edit_select(-1); + CHECK(e->chosen == VIDEO_XR_MENU_SLOT, "previous chose %d", e->chosen); + input_openxr_edit_toggle(); + CHECK(!t_editing && t_grabs.chosen == 0, + "edit mode ended with %u published", t_grabs.chosen); + t_edit_on(false); + t_edit_poll(); + CHECK(e->chosen == -1, "a new edit mode starts on %d", e->chosen); + t_edit_end(); +} + +/* Screen 2 chosen behind screen 1: the grip takes it from where the + * hand is and moves it as on a stick; the other hand takes what its + * laser is on meanwhile; let go, it stays chosen; Reset resets it, not + * what the laser is on. */ +static void test_edit_select_grab(void) +{ + input_openxr_edit_t *e = &input_openxr_edit; + const video_xr_grab_t *l = &e->hand[0].g; + const video_xr_grab_t *r = &e->hand[1].g; + t_edit_start(); + t_edit_buried(); + t_edit_select(1); + t_edit_select(1); + /* The right hand 0.5 right, 0.2 up, turned 30 degrees left: its + * laser on screen 1. */ + t_aim[2].position.x = 0.5f; + t_aim[2].position.y = 0.2f; + t_aim[2].orientation.y = (float)sin(M_PI / 12.0); + t_aim[2].orientation.w = (float)cos(M_PI / 12.0); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state == VIDEO_XR_GRAB_HELD && r->slot == 1, + "the grip took slot %u in state %u", r->slot, r->state); + CHECK(!t_modified, "the grab itself moved something"); + t_aim[2].position.x = 0.6f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, 0.1f) && t_near(t_xr[1].y, 0.0f) + && t_near(t_xr[1].z, 0.0f) && t_near(t_xr[0].x, 0.0f), + "screen 2 at %.3f %.3f %.3f, screen 1 at %.3f", t_xr[1].x, + t_xr[1].y, t_xr[1].z, t_xr[0].x); + /* The hand turns back straight: screen 2 swings round it. */ + t_aim[2].orientation.y = 0.0f; + t_aim[2].orientation.w = 1.0f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, 1.2169873f) && t_near(t_xr[1].z, 0.0313467f) + && t_near(t_xr[1].yaw, -30.0f), + "turned with the hand to %.4f, %.4f, yaw %.2f", t_xr[1].x, + t_xr[1].z, t_xr[1].yaw); + /* The left hand, its laser on screen 1, takes screen 1. */ + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_edit_poll(); + CHECK(l->state == VIDEO_XR_GRAB_HELD && l->slot == 0, + "the free hand took slot %u in state %u", l->slot, l->state); + t_let_go(); + t_edit_poll(); + CHECK(e->chosen == 1, "letting go forgot the choice: %d", e->chosen); + /* Reset with the right hand's laser on screen 1. */ + t_xr[0].x = 0.4f; + t_hold("edit/reset", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_near(t_xr[1].x, 0.0f) && t_near(t_xr[1].yaw, 0.0f) + && t_near(t_xr[0].x, 0.4f), + "Reset: screen 2 at %.3f, screen 1 at %.3f", t_xr[1].x, + t_xr[0].x); + t_edit_end(); +} + +/* Screen 2 chosen: an untracked hand's grip takes nothing; both grips + * in one poll, the left hand's first takes it and the right then takes + * what its laser is on. */ +static void test_edit_select_grips(void) +{ + input_openxr_edit_t *e = &input_openxr_edit; + const video_xr_grab_t *l = &e->hand[0].g; + const video_xr_grab_t *r = &e->hand[1].g; + t_edit_start(); + t_edit_buried(); + t_edit_select(1); + t_edit_select(1); + t_aim_tracked[2] = false; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(r->state != VIDEO_XR_GRAB_HELD && !t_modified, + "untracked, the grip took slot %u in state %u", r->slot, r->state); + t_let_go(); + t_edit_poll(); + t_aim_tracked[2] = true; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK( l->state == VIDEO_XR_GRAB_HELD && l->slot == 1 + && r->state == VIDEO_XR_GRAB_HELD && r->slot == 0, + "both grips: the left took slot %u (%u), the right %u (%u)", + l->slot, l->state, r->slot, r->state); + CHECK(e->chosen == 1, "the grips changed the choice to %d", e->chosen); + t_edit_end(); +} + +/* A held item is bright wherever the lasers are: screen 1 held with the + * laser turned off it, as a snap can slide it; with screen 2 chosen, + * screen 1 held by the other hand too. */ +static void test_edit_held_lit(void) +{ + unsigned n; + video_xr_quad_set_t shown; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local; + t_edit_start(); + t_edit_buried(); + local = t_xr_session.local_space; + t_cursor = true; + t_aim_tracked[1] = false; + shown = t_quads; + t_xr_frame.grabs = t_grabs; + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[1].slot = 0; + /* The right hand's laser turned up, off everything. */ + t_aim[2].orientation.x = (float)sin(M_PI / 4.0); + t_aim[2].orientation.w = (float)cos(M_PI / 4.0); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 1u, "held off its laser, %#x lit", + t_lit(layers, n)); + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_NONE; + t_edit_select(1); + t_edit_select(1); + t_xr_frame.grabs = t_grabs; + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[0].slot = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 3u, "screen 2 chosen, screen 1 held, %#x lit", + t_lit(layers, n)); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_edit_end(); +} + +/* The frame loop's outlines: with an item chosen it alone is bright, + * wherever the lasers are; while one hand holds it, what the other + * points at is bright too; on the laser, what the lasers are on. */ +static void test_edit_select_lit(void) +{ + unsigned n; + video_xr_quad_set_t shown; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local; + t_edit_start(); + t_edit_buried(); + local = t_xr_session.local_space; + t_cursor = true; + t_aim_tracked[1] = false; + shown = t_quads; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 1u, "the laser on screen 1 lit %#x", + t_lit(layers, n)); + t_edit_select(1); + t_edit_select(1); + t_xr_frame.grabs = t_grabs; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 2u, "screen 2 chosen, %#x lit", + t_lit(layers, n)); + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[0].slot = 1; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 3u, "the left holding it, %#x lit", + t_lit(layers, n)); + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_NONE; + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[1].slot = 1; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(t_lit(layers, n) == 2u, "the right holding it, %#x lit", + t_lit(layers, n)); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_edit_end(); +} + +/* The tag among layers, and where: the one showing a tag cell. */ +static const XrCompositionLayerQuad *t_tag_layer( + const XrCompositionLayerBaseHeader **layers, unsigned n, + unsigned *at) +{ + unsigned i; + for (i = 0; i < n; i++) + { + const XrCompositionLayerQuad *q = + (const XrCompositionLayerQuad*)layers[i]; + if ( q->subImage.imageRect.extent.width == VIDEO_XR_TAG_COLS + && q->subImage.imageRect.extent.height == VIDEO_XR_TAG_ROWS) + { + *at = i; + return q; + } + } + return NULL; +} + +/* Edit mode's tag: over the hand that last pressed Select Next or + * Previous, the left at each start, showing the chosen item's cell; after + * the outlines and beams, before the dots; only while that hand and + * the head are tracked. */ +static void test_edit_tag(void) +{ + unsigned n, at, k, beams; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local = XR_NULL_HANDLE; + t_edit_start(); + local = t_xr_session.local_space; + t_cursor = true; + t_head_tracked = true; + shown = t_quads; + t_aim[1].position.x = -0.2f; + t_aim[1].position.y = -0.3f; + t_edit_poll(); + CHECK(t_grabs.tag == 0, "the tag starts on hand %u", t_grabs.tag); + t_xr_frame.grabs = t_grabs; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = t_tag_layer(layers, n, &at); + CHECK(q && q->subImage.imageRect.offset.x == VIDEO_XR_TAG_X(0) + && q->subImage.imageRect.offset.y == VIDEO_XR_TAG_Y + && q->subImage.swapchain == (XrSwapchain)(uintptr_t)0x51 + && t_near(q->pose.position.x, -0.2f) + && t_near(q->pose.position.y, -0.3f + VIDEO_XR_TAG_LIFT) + && t_near(q->size.height, VIDEO_XR_TAG_HEIGHT) + && t_near(q->size.width, VIDEO_XR_TAG_HEIGHT * VIDEO_XR_TAG_COLS + / VIDEO_XR_TAG_ROWS) + && at == n - 3, + "the laser's tag: layer %u of %u", q ? at : 99u, n); + /* The right hand's Select Next: Screen 1's, over the right. */ + t_hold("edit/select_next", 2, 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); + CHECK(t_grabs.tag == 1 && t_grabs.chosen == 1, "tag %u chosen %u", + t_grabs.tag, t_grabs.chosen); + t_xr_frame.grabs = t_grabs; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = t_tag_layer(layers, n, &at); + CHECK(q && q->subImage.imageRect.offset.x == VIDEO_XR_TAG_X(1) + && t_near(q->pose.position.x, 0.0f) + && t_near(q->pose.position.y, VIDEO_XR_TAG_LIFT), + "screen 1's tag over the right hand"); + /* The right hand lost, or the head: no tag. */ + t_aim_tracked[2] = false; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(!t_tag_layer(layers, n, &at), "a tag over an untracked hand"); + t_aim_tracked[2] = true; + t_head_tracked = false; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(!t_tag_layer(layers, n, &at), "a tag with the head lost"); + t_head_tracked = true; + /* Nothing to edit, as when the screens go at the end: no tag. */ + memset(&shown, 0, sizeof(shown)); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(!t_tag_layer(layers, n, &at), "a tag with nothing to edit"); + shown = t_quads; + /* An image too narrow for the tags: no tag; the beams and dots. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, + layers, OPENXR_MAX_EXTRA_LAYERS, &t_under); + t_cursor_w = VIDEO_XR_TAG_X(0) - 1; + at = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, + layers, OPENXR_MAX_EXTRA_LAYERS, &t_under); + t_cursor_w = VIDEO_XR_CURSOR_W; + for (k = 0, beams = 0; k < at; k++) + { + int32_t x = ((const XrCompositionLayerQuad*)layers[k]) + ->subImage.imageRect.offset.x; + beams += x == VIDEO_XR_BEAM_X(0) || x == VIDEO_XR_BEAM_X(1); + } + CHECK( at == n - 1 && !t_tag_layer(layers, at, &k) && beams + && ((const XrCompositionLayerQuad*)layers[at - 1]) + ->subImage.imageRect.offset.x == 0, + "a narrow image: %u layers of %u, %u beams, a tag or no dot", at, + n, beams); + /* The left's Select Previous: back to the laser, over the left. */ + t_hold("edit/select_previous", 1, 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); + CHECK(t_grabs.tag == 0 && t_grabs.chosen == 0, "tag %u chosen %u", + t_grabs.tag, t_grabs.chosen); + /* Both hands at once: the left's. */ + t_hold("edit/select_next", 2, 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); + t_hold("edit/select_previous", 1, 1.0f, 0.0f); + t_hold("edit/select_previous", 2, 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); + CHECK(t_grabs.tag == 0 && t_grabs.chosen == 0, + "both hands: tag %u chosen %u", t_grabs.tag, t_grabs.chosen); + /* The right's, then edit mode again: the left's from the first poll, + * whose frames read what the stop published. */ + t_hold("edit/select_next", 2, 1.0f, 0.0f); + t_edit_poll(); + t_let_go(); + t_edit_poll(); + input_openxr_edit_toggle(); + t_edit_on(false); + CHECK(t_editing && t_grabs.tag == 0, + "edit mode again: the tag on hand %u", t_grabs.tag); + t_edit_poll(); + CHECK(t_grabs.tag == 0, "its first poll: the tag on hand %u", + t_grabs.tag); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_head_tracked = false; + t_edit_end(); +} + +/* A tenth of a millimetre in front of screen 1's plane: no screen rests + * in a hinge there, so cases about carrying one into its hinge do not + * take it hinged. */ +#define T_APART 0.0001f + +/* Screen 2, 0.8 x 0.6, at its spot 1.245 m right of screen 1's middle, + * 4.5 cm off standing flush beside it: a free gap, past the edge's + * click, a hair off screen 1's plane (T_APART). */ +static void t_snap_beside(void) +{ + video_xr_quad_t *q = &t_quads.quads[t_quads.num_quads++]; + memset(q, 0, sizeof(*q)); + video_xr_pose_identity(&q->pose); + q->pose.position.x = 1.245f; + q->pose.position.z = -1.8f + T_APART; + q->width = 0.8f; + q->height = 0.6f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->screen = 1; + q->slot = 1; + t_quads.slots[1].base.x = 1.245f; + t_quads.slots[1].base.z = -1.8f + T_APART; +} + +/* Snap to Screens on the main thread: a screen taken near a line + * stays put; carried 5 cm or more it snaps to one no hand holds and + * its settings say so; the hand buzzes once as a snap takes hold, not + * while it holds, and again as one takes hold anew; the frame loop is + * told the setting and where the screen was taken. Off, nothing snaps + * or buzzes. */ +static void test_edit_snap(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + unsigned n; + t_snap = true; + t_edit_start(); + CHECK(t_grabs.snap, "the grabs record does not carry Snap to Screens"); + t_snap_beside(); + t_aim[2].position.x = 1.245f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && g->slot == 1, + "took slot %u in state %u", g->slot, g->state); + CHECK(t_near(t_grabs.start[1].position.x, 1.245f) + && t_near(t_grabs.start[1].position.z, -1.8f), + "the grabs record has it taken at %.4f %.4f", + t_grabs.start[1].position.x, t_grabs.start[1].position.z); + n = t_applies[2]; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, 0.0f) && t_applies[2] == n, + "taken and held still, it hopped to %.4f, %u buzzes", t_xr[1].x, + t_applies[2] - n); + /* 5.3 cm from where it was taken: whole. */ + t_aim[2].position.x = 1.205f; + t_aim[2].position.y = 0.025f; + t_aim[2].position.z = 0.025f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, -0.045f) && t_near(t_xr[1].y, 0.0f) + && t_near(t_xr[1].z, 0.0f), + "snapped to %.4f %.4f %.4f", t_xr[1].x, t_xr[1].y, t_xr[1].z); + CHECK(t_applies[2] == n + 1 + && t_near(t_amp[2], INPUT_OPENXR_HINGE_BUZZ_AMP) + && t_dur[2] == INPUT_OPENXR_HINGE_BUZZ_NS, + "%u buzzes as it hinged, at %.2f", t_applies[2] - n, t_amp[2]); + t_aim[2].position.x = 1.20f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, -0.045f) && t_applies[2] == n + 1, + "held on the line: at %.4f, %u buzzes", t_xr[1].x, + t_applies[2] - n); + t_aim[2].position.x = 1.30f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, 0.055f) && t_near(t_xr[1].y, 0.0f) + && t_applies[2] == n + 1, + "off the edge: at %.4f, %u buzzes", t_xr[1].x, t_applies[2] - n); + t_aim[2].position.x = 1.205f; + t_clock += T_BUZZ_CLEAR; + t_edit_poll(); + CHECK(t_applies[2] == n + 2, "back on the edge: %u buzzes", + t_applies[2] - n); + t_edit_end(); + + t_snap = false; + t_edit_start(); + CHECK(!t_grabs.snap, "the grabs record says snap with it off"); + t_snap_beside(); + t_aim[2].position.x = 1.245f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + n = t_applies[2]; + t_aim[2].position.x = 1.205f; + t_aim[2].position.y = 0.025f; + t_aim[2].position.z = 0.025f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, -0.04f) && t_near(t_xr[1].y, 0.025f) + && t_near(t_xr[1].z, 0.025f) && t_applies[2] == n, + "off: at %.4f %.4f %.4f, %u buzzes", t_xr[1].x, t_xr[1].y, + t_xr[1].z, t_applies[2] - n); + t_edit_end(); +} + +/* Snapping on, each hand holding a screen: neither snaps to the other, + * whichever hand carries its screen to it; the frame loop is told both + * holds. */ +static void test_edit_snap_two_hands(void) +{ + const video_xr_grab_t *l = &input_openxr_edit.hand[0].g; + const video_xr_grab_t *r = &input_openxr_edit.hand[1].g; + t_snap = true; + t_edit_start(); + t_snap_beside(); + t_aim[2].position.x = 1.245f; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK( l->state == VIDEO_XR_GRAB_HELD && l->slot == 0 + && r->state == VIDEO_XR_GRAB_HELD && r->slot == 1 + && t_grabs.snap && t_grabs.hand[0].state == VIDEO_XR_GRAB_HELD + && t_grabs.hand[1].state == VIDEO_XR_GRAB_HELD, + "took slots %u and %u", l->slot, r->slot); + /* Screen 2 carried 5.3 cm, to 4 cm off flush beside screen 1. */ + t_aim[2].position.x = 1.205f; + t_aim[2].position.y = 0.025f; + t_aim[2].position.z = 0.025f; + t_edit_poll(); + CHECK(t_near(t_xr[1].x, -0.04f) && t_near(t_xr[1].y, 0.025f) + && t_near(t_xr[1].z, 0.025f), + "screen 2 snapped to a held screen 1: %.4f %.4f %.4f", t_xr[1].x, + t_xr[1].y, t_xr[1].z); + /* Screen 1 carried 5.2 cm, 2.5 cm into screen 2's edge. */ + t_aim[1].position.x = 0.03f; + t_aim[1].position.y = 0.03f; + t_aim[1].position.z = 0.03f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.03f) && t_near(t_xr[0].y, 0.03f) + && t_near(t_xr[0].z, 0.03f), + "screen 1 snapped to a held screen 2: %.4f %.4f %.4f", t_xr[0].x, + t_xr[0].y, t_xr[0].z); + t_edit_end(); + t_snap = false; +} + +/* Snapping on, a held screen 8 degrees short of parallel, turned into + * the window by the free hand's stick, snaps while the stick turns it, + * and the holding hand buzzes. */ +static void test_edit_turn_snap(void) +{ + unsigned n; + video_xr_quad_t *q; + t_snap = true; + t_edit_start(); + t_snap_beside(); + q = &t_quads.quads[t_quads.num_quads - 1]; + q->pose.orientation.y = (float)-sin(M_PI / 45.0); + q->pose.orientation.w = (float)cos(M_PI / 45.0); + t_aim[2].position.x = 1.245f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + /* Carried 5.3 cm, so the snaps are whole. */ + t_aim[2].position.x = 1.205f; + t_aim[2].position.y = 0.025f; + t_aim[2].position.z = 0.025f; + t_edit_poll(); + CHECK(!t_near(t_xr[1].yaw, 0.0f), "parallel 8 degrees out"); + n = t_applies[2]; + t_hold("edit/stick", 1, 1.0f, 0.0f); + t_clock += 100000; + t_edit_poll(); + CHECK(t_near(t_xr[1].yaw, 0.0f) && t_applies[2] == n + 1, + "turned in by the stick: yaw %.2f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + t_edit_end(); + t_snap = false; +} + +/* A snap to another screen beats a step. Screen 2 beside screen 1, + * 10 degrees past parallel, the head where the 30 step is 6.5 past it: + * turned back by the stick it holds at that step with one buzz, and at + * 4.6 from parallel, still in the step's hold, snaps parallel with the + * snap's buzz. */ +static void test_edit_step_snap(void) +{ + unsigned i, n; + video_xr_quad_t *q; + t_snap = true; + t_head_tracked = true; + t_edit_start(); + t_snap_beside(); + q = &t_quads.quads[t_quads.num_quads - 1]; + q->pose.orientation.y = (float)sin(5.0 * M_PI / 180.0); + q->pose.orientation.w = (float)cos(5.0 * M_PI / 180.0); + t_aim[2].position.x = 1.245f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + /* Carried 5.3 cm, so the snaps are whole. */ + t_aim[2].position.x = 1.205f; + t_aim[2].position.y = 0.025f; + t_aim[2].position.z = 0.025f; + t_head.position.x = (float)(1.205 - 1.775 * tan(23.5 * M_PI / 180.0)); + t_edit_poll(); + n = t_applies[2]; + CHECK(t_deg_near(t_xr[1].yaw, 10.0), "taken at %.3f", t_xr[1].yaw); + t_step_polls(2, -1.0f, 0.0f); + /* Held 6.5 past parallel, the snap's pull shows it at 3. */ + CHECK(t_deg_near(t_xr[1].yaw, 3.0) && t_applies[2] == n + 1, + "at 31.7 from facing: yaw %.3f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + t_step_polls(3, -1.0f, 0.0f); + CHECK(t_applies[2] == n + 1, "held at 30: %u buzzes", t_applies[2] - n); + t_step_polls(1, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[1].yaw, 0.0) && t_applies[2] == n + 2, + "at 28.1 from facing: yaw %.3f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + /* Out of the snap and back to 30, never out of its hold: no step + * buzz, however often. */ + for (i = 0; i < 2; i++) + { + n = t_applies[2]; + t_step_polls(1, 1.0f, 0.0f); + CHECK(t_deg_near(t_xr[1].yaw, 3.0) && t_applies[2] == n, + "back at 29.0 from facing: yaw %.3f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + t_step_polls(1, -1.0f, 0.0f); + CHECK(t_deg_near(t_xr[1].yaw, 0.0), "at 28.1 again: yaw %.3f", + t_xr[1].yaw); + } + t_head.position.x = 0.0f; + t_head_tracked = false; + t_edit_end(); + t_snap = false; +} + +/* Carried in, each snap buzzes as it takes hold, one still fading + * staying armed. Resting at the edge of its whole pull, where it takes + * hold and lets go poll by poll, no more; again only once that axis has + * stopped pulling at all, or after a new grab. */ +static void test_edit_snap_edge(void) +{ + unsigned i, n; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_snap_beside(); + /* Screen 2 10 cm right of flush, past the edge's click: taken there. */ + t_quads.quads[t_quads.num_quads - 1].pose.position.x = 1.30f; + t_quads.slots[1].base.x = 1.30f; + t_aim[2].position.x = 1.30f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(hold->g.state == VIDEO_XR_GRAB_HELD && hold->g.slot == 1 + && hold->armed == (VIDEO_XR_SNAP_ALL | VIDEO_XR_SNAP_GAP), + "took slot %u armed %#x", hold->g.slot, hold->armed); + n = t_applies[2]; + /* 1.5 cm off flush, 8.5 cm carried: the turn, Y and Z whole, one + * buzz; the edge and its gap fade, still armed. */ + t_aim[2].position.x = 1.215f; + t_edit_poll(); + CHECK( t_applies[2] == n + 1 + && hold->armed == (VIDEO_XR_SNAP_X | VIDEO_XR_SNAP_GAP), + "%u buzzes in the edge's fade, armed %#x", t_applies[2] - n, + hold->armed); + /* 0.9 cm off: the edge whole, one more. */ + t_aim[2].position.x = 1.209f; + t_clock += T_BUZZ_CLEAR; + t_edit_poll(); + CHECK(t_applies[2] == n + 2, "%u buzzes taking hold", + t_applies[2] - n); + /* 1.1 cm off and back again: the edge's whole pull comes and goes. */ + for (i = 0; i < 3; i++) + { + t_aim[2].position.x = 1.211f; + t_edit_poll(); + t_aim[2].position.x = 1.209f; + t_edit_poll(); + } + CHECK(t_applies[2] == n + 2, "%u buzzes resting at the edge", + t_applies[2] - n); + /* 7 cm off the edge lets go; back on it, one more. */ + t_aim[2].position.x = 1.27f; + t_edit_poll(); + CHECK(hold->armed & VIDEO_XR_SNAP_X, "the edge not re-armed: %#x", + hold->armed); + t_aim[2].position.x = 1.209f; + t_edit_poll(); + CHECK(t_applies[2] == n + 3, "%u buzzes back on the edge", + t_applies[2] - n); + /* A new grab re-arms them all. */ + t_let_go(); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(hold->armed == (VIDEO_XR_SNAP_ALL | VIDEO_XR_SNAP_GAP), + "armed %#x after a new grab", hold->armed); + t_edit_end(); + t_snap = false; +} + +/* Screen 2, 0.8 x 0.6, 18 degrees short of square to screen 1 (yaw + * -72), its middle 1.4 m ahead of a right hand at x 0.82: square, it + * would stand 2 cm right of screen 1's right corner, a free gap; + * turned 14.4 in about its middle, its edge is 0.5 cm over the corner, + * which clicks it there. */ +static void t_snap_square(void) +{ + video_xr_quad_t *q = &t_quads.quads[t_quads.num_quads++]; + memset(q, 0, sizeof(*q)); + q->pose.orientation.y = (float)-sin(0.2 * M_PI); + q->pose.orientation.w = (float)cos(0.2 * M_PI); + q->pose.position.x = 0.82f; + q->pose.position.z = -1.4f; + q->width = 0.8f; + q->height = 0.6f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->screen = 1; + q->slot = 1; + t_quads.slots[1].base.x = 0.82f; + t_quads.slots[1].base.z = -1.4f; + t_aim[2].position.x = 0.82f; +} + +/* n polls us apart, the right hand gripping, its trigger held if tilt, + * and the left stick at x. */ +static void t_turn_polls(unsigned n, float x, retro_time_t us, bool tilt) +{ + unsigned i; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + if (tilt) + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, x, 0.0f); + for (i = 0; i < n; i++) + { + t_clock += us; + t_edit_poll(); + } +} + +/* Edit mode with Snap to Screens as snap, screen 2 taken by the right + * hand and carried 7 cm up, past the lines across screen 1's middle, + * so its snaps are whole. */ +static void t_take_square(bool snap, bool tilt) +{ + t_snap = snap; + t_edit_start(); + t_snap_square(); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK( input_openxr_edit.hand[1].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[1].g.slot == 1 + && input_openxr_edit.hand[1].g.tilt == tilt, + "took slot %u", input_openxr_edit.hand[1].g.slot); + t_aim[2].position.y = 0.07f; + t_turn_polls(1, 0.0f, 10000, tilt); +} + +static void t_take_end(void) +{ + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_snap = false; +} + +/* The right hand turned deg about the vertical, its ray still through + * where screen 2's middle was taken. */ +static void t_point(double deg) +{ + double r = deg * M_PI / 180.0; + t_aim[2].orientation.y = (float)sin(r / 2.0); + t_aim[2].orientation.w = (float)cos(r / 2.0); + t_aim[2].position.x = (float)(0.82 + 1.4 * sin(r)); + t_aim[2].position.z = (float)(-1.4 + 1.4 * cos(r)); +} + +/* Screen 2's hinged edge at pose: metres off screen 1's edge line, at + * the farther of its ends. Its near edge, its own -X side 0.4 m from + * its middle, off screen 1's right edge line, x 0.8, z -1.8; or under, + * its top edge, 0.3 m up, off screen 1's bottom edge line, y -0.48, + * z -1.8. */ +static double t_edge_off(const video_xr_pose_t *pose, bool under) +{ + unsigned i; + double worst = 0.0; + video_xr_vec3_t m, w; + for (i = 0; i < 2; i++) + { + double d[3]; + m.x = (under && i) ? 0.4f : -0.4f; + m.y = under ? 0.3f : (i ? 0.3f : -0.3f); + m.z = 0.0f; + video_xr_rotate(&pose->orientation, &m, &w); + d[0] = under ? 0.0 : (double)pose->position.x + w.x - 0.8; + d[1] = under ? (double)pose->position.y + w.y + 0.48 : 0.0; + d[2] = (double)pose->position.z + w.z + 1.8; + if (sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]) > worst) + worst = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + } + return worst; +} + +/* Screen 2's near edge off the line as edit mode last wrote it. */ +static double t_hinge_off(void) +{ + video_xr_pose_t pose; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + return t_edge_off(&pose, false); +} + +/* Screen 2's top edge off the line under screen 1 as edit mode last + * wrote it. */ +static double t_under_off(void) +{ + video_xr_pose_t pose; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + return t_edge_off(&pose, true); +} + +/* Where the right hand holds screen 2, with no snap. */ +static void t_held_pose(video_xr_pose_t *pose) +{ + video_xr_pose_t aim; + aim.orientation.x = t_aim[2].orientation.x; + aim.orientation.y = t_aim[2].orientation.y; + aim.orientation.z = t_aim[2].orientation.z; + aim.orientation.w = t_aim[2].orientation.w; + aim.position.x = t_aim[2].position.x; + aim.position.y = t_aim[2].position.y; + aim.position.z = t_aim[2].position.z; + video_xr_grab_pose(&input_openxr_edit.hand[1].g, &aim, NULL, 0, NULL, + pose, NULL, NULL); +} + +/* Screen 2's hinged edge off the line where the right hand holds it, + * with no snap. */ +static double t_held_off(bool under) +{ + video_xr_pose_t pose; + t_held_pose(&pose); + return t_edge_off(&pose, under); +} + +/* Screen 2's hinged edge, as t_edge_off takes it, at pose against at + * from: metres moved across the line, at the farther of its ends. */ +static double t_edge_moved(const video_xr_pose_t *pose, + const video_xr_pose_t *from, bool under) +{ + unsigned i; + double worst = 0.0; + video_xr_vec3_t m, w, v; + for (i = 0; i < 2; i++) + { + double d[3]; + m.x = (under && i) ? 0.4f : -0.4f; + m.y = under ? 0.3f : (i ? 0.3f : -0.3f); + m.z = 0.0f; + video_xr_rotate(&pose->orientation, &m, &w); + video_xr_rotate(&from->orientation, &m, &v); + d[0] = under ? 0.0 : (double)pose->position.x + w.x + - from->position.x - v.x; + d[1] = under ? (double)pose->position.y + w.y + - from->position.y - v.y : 0.0; + d[2] = (double)pose->position.z + w.z - from->position.z - v.z; + if (sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]) > worst) + worst = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + } + return worst; +} + +/* An engaged screen snap's turn sticks as the free hand's stick lets + * go or starts a push, carried or tilting: turned on, the screen moves + * on from the angle it showed, and its place is still the hand's. Where + * the pull only fades nothing sticks: let go, it stays where it shows, + * and pushed away, it never hops toward the snap. Carrying alone keeps + * the hand's angle. */ +static void t_snap_commit(bool tilt) +{ + const char *how = tilt ? "tilting" : "carried"; + /* Turned 14.4 in, 3.6 short: square, at the corner. */ + t_take_square(true, tilt); + t_turn_polls(16, -1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -90.0) && t_near(t_xr[1].x, -0.02f) + && t_near(t_xr[1].y, 0.07f), + "%s, turned in: yaw %.3f at %.4f,%.4f", how, t_xr[1].yaw, + t_xr[1].x, t_xr[1].y); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -90.0), "%s, let go: yaw %.3f", how, + t_xr[1].yaw); + /* Out 9 from square, where the pull leaves 1; then 18, unpulled, at + * the hand's place. */ + t_turn_polls(1, 1.0f, 100000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -82.0), "%s, pushed out: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 1.0f, 100000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -72.0) && t_near(t_xr[1].x, 0.1036f) + && t_hinge_off() < 1e-3 && t_near(t_xr[1].y, 0.07f), + "%s, pushed on: yaw %.3f at %.4f,%.4f", how, t_xr[1].yaw, + t_xr[1].x, t_xr[1].y); + t_take_end(); + + /* Turned 10.8 in, 7.2 short, the pull shows it at 4.4: let go, it + * stays there; pushed out, it goes on from its own 7.2. */ + t_take_square(true, tilt); + t_turn_polls(12, -1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -85.6), "%s, in the fade: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -85.6), "%s, let go in the fade: yaw %.3f", + how, t_xr[1].yaw); + t_turn_polls(1, 1.0f, 100000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -73.8), + "%s, pushed out of the fade: yaw %.3f", how, t_xr[1].yaw); + t_take_end(); + + /* Turned 9 in, 9 short, shown at 8: let go, it stays; pushed slowly + * away, it goes on from there. */ + t_take_square(true, tilt); + t_turn_polls(10, -1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -82.0), "%s, at 9: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -82.0), "%s, let go at 9: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -80.2), "%s, pushed on from 9: yaw %.3f", + how, t_xr[1].yaw); + t_take_end(); + + /* Pointed 11 round, 7 short, shown at 4: pushed slowly away, it + * leaves the snap from there, a step a poll. */ + t_take_square(true, tilt); + t_point(-11.0); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -86.0), "%s, pointed to 7: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -84.2), "%s, pointed, pushed: yaw %.3f", + how, t_xr[1].yaw); + t_turn_polls(1, 1.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -82.4), "%s, pointed, pushed on: yaw %.3f", + how, t_xr[1].yaw); + t_take_end(); + + /* Pointed 14 round, 4 short: square; the stick then pushes on from + * square. */ + t_take_square(true, tilt); + t_point(-14.0); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -90.0), "%s, pointed in: yaw %.3f", how, + t_xr[1].yaw); + t_turn_polls(1, 1.0f, 100000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -82.0), "%s, pointed, pushed: yaw %.3f", + how, t_xr[1].yaw); + t_take_end(); + + /* Pointed in, then out again with the stick never touched: at the + * hand's angle. */ + t_take_square(true, tilt); + t_point(-14.0); + t_turn_polls(1, 0.0f, 10000, tilt); + t_point(4.0); + t_turn_polls(1, 0.0f, 10000, tilt); + CHECK(t_deg_near(t_xr[1].yaw, -68.0), "%s, pointed out: yaw %.3f", how, + t_xr[1].yaw); + t_take_end(); +} + +static void test_edit_snap_commit(void) +{ + float back; + t_snap_commit(false); + t_snap_commit(true); + /* In the fade, the grip let go with the stick still pushed: left + * where it showed 80 ms before. */ + t_take_square(true, false); + t_turn_polls(4, -1.0f, 10000, false); + back = t_xr[1].yaw; + t_turn_polls(8, -1.0f, 10000, false); + t_let_go(); + t_hold("edit/stick", 1, -1.0f, 0.0f); + t_edit_poll(); + CHECK( input_openxr_edit.hand[1].g.state != VIDEO_XR_GRAB_HELD + && t_deg_near(t_xr[1].yaw, -75.6) && t_deg_near(back, -75.6), + "the grip let go in the fade: yaw %.3f, shown %.3f 80 ms before", + t_xr[1].yaw, back); + t_take_end(); + /* Snap to Screens off: nothing snaps, nothing sticks. */ + t_take_square(false, false); + t_turn_polls(15, -1.0f, 10000, false); + t_turn_polls(1, 0.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -85.5) && t_near(t_xr[1].x, 0.0f), + "off, let go: yaw %.3f at %.4f", t_xr[1].yaw, t_xr[1].x); + t_turn_polls(1, 1.0f, 100000, false); + CHECK(t_deg_near(t_xr[1].yaw, -76.5), "off, pushed: yaw %.3f", + t_xr[1].yaw); + t_take_end(); +} + +/* Square at screen 1's right corner, screen 2 is hinged 90 degrees from + * flat: the turn stick swings it about the shared edge, which stays on + * the line all the way to flat beside screen 1, carried or tilting. + * The hinge takes hold as it comes square, with its long buzz, and the + * swing keeps it with no other. Snap to Angles holds it at 75 with no + * tick, and at 60 with one; Off, it swings through with none. */ +static void t_hinge_sweep(bool tilt, unsigned angles) +{ + unsigned i, n, l; + double worst = 0.0; + unsigned held75 = 0; + t_angles = angles; + t_take_square(true, tilt); + l = t_longs[2]; + t_turn_polls(16, -1.0f, 10000, tilt); + CHECK( t_deg_near(t_xr[1].yaw, -90.0) && t_hinge_off() < 1e-3 + && input_openxr_edit.hand[1].hinged && t_longs[2] == l + 1, + "square: yaw %.3f, %.4f off the line, hinged %d, %u long buzzes", + t_xr[1].yaw, t_hinge_off(), (int)input_openxr_edit.hand[1].hinged, + t_longs[2] - l); + n = t_applies[2]; + l = t_longs[2]; + for (i = 0; i < 84; i++) + { + t_turn_polls(1, 1.0f, 10000, tilt); + if (t_hinge_off() > worst) + worst = t_hinge_off(); + if (t_deg_near(t_xr[1].yaw, -75.0)) + held75++; + if (i == 30) + CHECK( t_applies[2] - n - (t_longs[2] - l) == (angles ? 1u : 0u) + && t_longs[2] == l, + "%u step ticks and %u long buzzes by 60", + t_applies[2] - n - (t_longs[2] - l), t_longs[2] - l); + } + CHECK(worst < 1e-3, "%.4f off the line in the sweep", worst); + CHECK(angles ? held75 >= 3 : held75 <= 1, "held at 75 for %u polls", + held75); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +static void test_edit_hinge_swing(void) +{ + t_hinge_sweep(false, VIDEO_OPENXR_EDIT_SNAP_ANGLES_15); + t_hinge_sweep(true, VIDEO_OPENXR_EDIT_SNAP_ANGLES_15); + t_hinge_sweep(false, VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF); +} + +/* Swung to 67.5 from flat and let go, screen 2 stays there, on the line, + * and stays so carried 1 cm up along the edge. The tilt stick, the + * other axis, pulls it off the line: 10.8 degrees of it leave the edge + * twisted out of the hinge's reach. Snap to Screens off: no hinge, the + * turn stick turns it about its own middle. */ +static void test_edit_hinge_hold(void) +{ + unsigned i; + t_take_square(true, false); + t_turn_polls(16, -1.0f, 10000, false); + t_turn_polls(21, 1.0f, 10000, false); + t_turn_polls(1, 0.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -67.5) && t_hinge_off() < 1e-3, + "let go at yaw %.3f, %.4f off", t_xr[1].yaw, t_hinge_off()); + t_aim[2].position.y += 0.01f; + t_turn_polls(1, 0.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -67.5) && t_hinge_off() < 1e-3, + "carried up: yaw %.3f, %.4f off", t_xr[1].yaw, t_hinge_off()); + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, 0.0f, 1.0f); + for (i = 0; i < 12; i++) + { + t_clock += 10000; + t_edit_poll(); + } + CHECK(t_hinge_off() > 0.005, "tilted 10.8: %.4f off", t_hinge_off()); + t_take_end(); + t_take_square(false, false); + t_turn_polls(10, -1.0f, 10000, false); + CHECK(t_near(t_xr[1].x, 0.0f), "snap off: x %.4f", t_xr[1].x); + t_take_end(); +} + +/* Hinged at square and swung toward flat, the step at 75 buzzes once: + * a wiggle inside its hold buzzes no more, whatever the head sees; out + * of the hold and back, it buzzes again. */ +static void test_edit_hinge_rearm(void) +{ + unsigned n; + t_head_tracked = true; + t_head.position.x = 0.3f; + t_take_square(true, false); + t_turn_polls(15, -1.0f, 10000, false); + t_turn_polls(1, 0.0f, 10000, false); + n = t_applies[2]; + t_turn_polls(15, 1.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -75.0) && t_applies[2] == n + 1, + "at 76.5: yaw %.3f, %u buzzes", t_xr[1].yaw, t_applies[2] - n); + t_turn_polls(1, -1.0f, 5000, false); + t_turn_polls(1, 1.0f, 5000, false); + CHECK(t_deg_near(t_xr[1].yaw, -75.0) && t_applies[2] == n + 1, + "wiggled in the hold: yaw %.3f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + t_turn_polls(3, -1.0f, 10000, false); + t_turn_polls(3, 1.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -75.0) && t_applies[2] == n + 2, + "out of the hold and back: yaw %.3f, %u buzzes", t_xr[1].yaw, + t_applies[2] - n); + t_take_end(); + t_head.position.x = 0.0f; + t_head_tracked = false; +} + +/* Held in the 75 step's hold, the stick still pushed, then carried + * 4 cm off the line so the hinge lets go: the turn goes on from 75 by + * one poll's worth, without the hold's carry. */ +static void test_edit_hinge_drop(void) +{ + t_take_square(true, false); + t_turn_polls(15, -1.0f, 10000, false); + t_turn_polls(1, 0.0f, 10000, false); + t_turn_polls(15, 1.0f, 10000, false); + CHECK(t_deg_near(t_xr[1].yaw, -75.0), "held at 75: yaw %.3f", + t_xr[1].yaw); + t_aim[2].position.z += 0.04f; + t_turn_polls(1, 1.0f, 10000, false); + CHECK(!input_openxr_edit.hand[1].hinged + && t_deg_near(t_xr[1].yaw, -74.1), + "carried off: yaw %.3f, hinged %d", t_xr[1].yaw, + input_openxr_edit.hand[1].hinged); + t_take_end(); +} + +/* Screen 2 under screen 1, its top edge on screen 1's bottom edge, + * folded fold degrees from flat toward the viewer: the 3DS layout, + * apart metres in front of screen 1's plane. */ +static void t_under_at(double fold, float apart) +{ + double r = fold * M_PI / 180.0; + video_xr_quad_t *q = &t_quads.quads[t_quads.num_quads++]; + memset(q, 0, sizeof(*q)); + q->pose.orientation.x = (float)-sin(r / 2.0); + q->pose.orientation.w = (float)cos(r / 2.0); + q->pose.position.y = (float)(-0.48 - 0.3 * cos(r)); + q->pose.position.z = (float)(-1.8 + 0.3 * sin(r)) + apart; + q->width = 0.8f; + q->height = 0.6f; + q->kind = VIDEO_XR_QUAD_SCREEN; + q->screen = 1; + q->slot = 1; + t_quads.slots[1].base.y = q->pose.position.y; + t_quads.slots[1].base.z = q->pose.position.z; + t_aim[2].position.y = q->pose.position.y; +} + +/* Screen 2 in the 3DS layout a hair off its hinge (T_APART). */ +static void t_snap_under(double fold) +{ + t_under_at(fold, T_APART); +} + +/* Screen 2 in the 3DS layout resting in its hinge on screen 1. */ +static void t_rest_under(double fold) +{ + t_under_at(fold, 0.0f); +} + +/* A hinge line from a to b as its layer shows it, each end + * VIDEO_XR_HINGE_LIFT nearer the head at the origin: its middle and + * length. */ +static void t_lifted(float ax, float ay, float az, float bx, float by, + float bz, video_xr_vec3_t *mid, float *len) +{ + double sa = 1.0 - VIDEO_XR_HINGE_LIFT + / sqrt((double)ax * ax + (double)ay * ay + (double)az * az); + double sb = 1.0 - VIDEO_XR_HINGE_LIFT + / sqrt((double)bx * bx + (double)by * by + (double)bz * bz); + double dx = bx * sb - ax * sa; + double dy = by * sb - ay * sa; + double dz = bz * sb - az * sa; + mid->x = (float)((ax * sa + bx * sb) / 2.0); + mid->y = (float)((ay * sa + by * sb) / 2.0); + mid->z = (float)((az * sa + bz * sb) / 2.0); + *len = (float)sqrt(dx * dx + dy * dy + dz * dz); +} + +/* openxr_frame_hold from t_grabs into set, each hand at main's last + * pose, the screens unturned. */ +static void t_follow(video_xr_quad_set_t *set) +{ + unsigned h, i; + unsigned holding = 0; + t_xr_frame.grabs = t_grabs; + t_xr_frame.hinged = 0; + for (h = 0; h < 2; h++) + if ( t_grabs.hand[h].state == VIDEO_XR_GRAB_HELD + && t_grabs.hand[h].slot < VIDEO_XR_MAX_SLOTS) + holding |= 1u << t_grabs.hand[h].slot; + for (h = 0; h < 2; h++) + { + const video_xr_grab_t *g = &t_grabs.hand[h]; + if (g->state != VIDEO_XR_GRAB_HELD || g->slot >= VIDEO_XR_MAX_SLOTS) + continue; + for (i = 0; i < set->num_quads; i++) + if (set->quads[i].slot == g->slot && !set->slots[g->slot].fixed) + { + set->quads[i].pose = g->last; + set->quads[i].width = g->width; + set->quads[i].height = g->height; + } + if (video_xr_grab_marked(g, &g->last, set, holding, + &t_xr_frame.hinges[h])) + t_xr_frame.hinged |= 1u << h; + } +} + +/* What an edit mode layer shows: 1 an outline, 2 the menu's handle, 3 a + * hinge slot, 4 a beam, 5 the tag, 6 a dot; in *cut what a mark may + * change, an outline's image (its cut's cell, below 0, or its offset + * from its column and width) or a slot's first row; never its light. */ +static unsigned t_kind(const XrCompositionLayerBaseHeader *l, int32_t *cut) +{ + const XrCompositionLayerQuad *q = (const XrCompositionLayerQuad*)l; + int32_t x = q->subImage.imageRect.offset.x; + *cut = 0; + if (q->subImage.imageRect.extent.height == VIDEO_XR_OUTLINE_DIM) + { + if (x >= VIDEO_XR_CUT_X(0, 0)) + *cut = -1 - (x - VIDEO_XR_CUT_X(0, 0)) + / (VIDEO_XR_OUTLINE_DIM + 1) % VIDEO_XR_OUTLINE_CUTS; + else + *cut = (x - ((x >= VIDEO_XR_OUTLINE_X(1)) + ? VIDEO_XR_OUTLINE_X(1) : VIDEO_XR_OUTLINE_X(0))) * 1024 + + q->subImage.imageRect.extent.width; + return 1; + } + if (x == VIDEO_XR_HANDLE_X(0) || x == VIDEO_XR_HANDLE_X(1)) + return 2; + if (x == VIDEO_XR_HINGE_X + 1) + { + *cut = q->subImage.imageRect.offset.y; + return 3; + } + if (x == VIDEO_XR_BEAM_X(0) || x == VIDEO_XR_BEAM_X(1)) + return 4; + if (q->subImage.imageRect.extent.height == VIDEO_XR_TAG_ROWS) + return 5; + return 6; +} + +/* Whether q is a hinge slot's layer from a to b, hidden or shown. */ +static bool t_slot_is(const XrCompositionLayerQuad *q, bool hidden, + float ax, float ay, float az, float bx, float by, float bz) +{ + video_xr_vec3_t m; + float len; + t_lifted(ax, ay, az, bx, by, bz, &m, &len); + return q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && q->subImage.imageRect.offset.y + == (hidden ? VIDEO_XR_HINGE_HIDDEN_Y : VIDEO_XR_HINGE_Y) + && q->subImage.imageRect.extent.width == VIDEO_XR_HINGE_COLS - 2 + && q->subImage.imageRect.extent.height == VIDEO_XR_HINGE_ROWS + && t_near(q->size.height, VIDEO_XR_HINGE_WIDTH) + && t_near(q->size.width, len) && t_near(q->pose.position.x, m.x) + && t_near(q->pose.position.y, m.y) && t_near(q->pose.position.z, m.z); +} + +/* A frame's edit mode layers: how many, each one's kind and cut; the + * frames with an outline not its item's whole one. */ +typedef struct +{ + unsigned n; + unsigned kind[OPENXR_MAX_EXTRA_LAYERS]; + int32_t cut[OPENXR_MAX_EXTRA_LAYERS]; + unsigned bent; +} t_layout_t; + +/* Whether every outline in layers' first n has the pose, lifted toward + * the head, and the size of an item's whole outline in shown. */ +static bool t_outlines_whole(const video_xr_quad_set_t *shown, + const XrCompositionLayerBaseHeader **layers, unsigned n) +{ + unsigned i, j; + for (i = 0; i < n; i++) + { + const XrCompositionLayerQuad *l = (const XrCompositionLayerQuad*) + layers[i]; + bool whole = false; + if (l->subImage.imageRect.extent.height != VIDEO_XR_OUTLINE_DIM) + continue; + for (j = 0; j < shown->num_quads && !whole; j++) + { + video_xr_quad_t o; + double dx, dy, dz, dot; + video_xr_outline(&shown->quads[j], &o); + dx = l->pose.position.x - o.pose.position.x; + dy = l->pose.position.y - o.pose.position.y; + dz = l->pose.position.z - o.pose.position.z; + dot = l->pose.orientation.x * o.pose.orientation.x + + l->pose.orientation.y * o.pose.orientation.y + + l->pose.orientation.z * o.pose.orientation.z + + l->pose.orientation.w * o.pose.orientation.w; + whole = t_near(l->size.width, o.width) + && t_near(l->size.height, o.height) + && sqrt(dx * dx + dy * dy + dz * dz) + < VIDEO_XR_CURSOR_LIFT + 1e-3 + && fabs(fabs(dot) - 1.0) < 1e-5; + } + if (!whole) + return false; + } + return true; +} + +/* Three frames between two polls, from t_quads and t_grabs through + * t_follow, the head tracked, against s, the first frame and the last + * one's cuts (s->n 0: this is the first): adds to *counts the frames + * whose count is not the first's, to *moved those whose kinds by index + * are not; returns those whose cuts are not the last's. */ +static unsigned t_frames(t_layout_t *s, unsigned *counts, unsigned *moved) +{ + unsigned f, i; + video_xr_quad_set_t shown; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + unsigned changed = 0; + bool head = t_head_tracked; + bool cursor = t_cursor; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + t_follow(&shown); + for (f = 0; f < 3; f++) + { + bool order = false; + bool cut = false; + unsigned n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, + t_xr_session.local_space, layers, OPENXR_MAX_EXTRA_LAYERS, + &t_under); + s->bent += t_outlines_whole(&shown, layers, n) ? 0u : 1u; + if (!s->n) + { + s->n = n; + for (i = 0; i < n; i++) + s->kind[i] = t_kind(layers[i], &s->cut[i]); + continue; + } + if (n != s->n) + { + (*counts)++; + continue; + } + for (i = 0; i < n; i++) + { + int32_t c; + order = order || t_kind(layers[i], &c) != s->kind[i]; + cut = cut || c != s->cut[i]; + s->cut[i] = c; + } + *moved += order ? 1u : 0u; + changed += cut ? 1u : 0u; + } + t_head_tracked = head; + t_cursor = cursor; + return changed; +} + +/* The frame loop's hinges: the held screen's paired edge and its + * target's lose their rim, the rest of each outline as before; a green + * line along each hinge where the edges overlap, midway across the + * gap, facing the head, after the outlines and before the beams. Short + * of layers the beams go first, then the line; the rims go whatever the + * room. Two hands, two lines, the first hand's first; on a target's + * opposite edges both rims go, on adjacent ones the first hand's. */ +static void test_edit_hinge_layers(void) +{ + static const float gaps[] = { 0.0f, 0.001f, 0.002f, 0.004f, 0.006f, + 0.01f }; + unsigned i, n; + video_xr_vec3_t ax, az; + video_xr_vec3_t m; + float len; + video_xr_quad_set_t shown; + video_xr_hinge_t *k; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + const float cut = (float)(VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP) + / VIDEO_XR_OUTLINE_DIM; + const float both = (float)(VIDEO_XR_OUTLINE_DIM + - 2 * VIDEO_XR_OUTLINE_CROP) / VIDEO_XR_OUTLINE_DIM; + const float half = (float)VIDEO_XR_OUTLINE_CROP + / (2.0f * VIDEO_XR_OUTLINE_DIM); + XrSpace local = XR_NULL_HANDLE; + t_edit_start(); + t_snap_under(0.0); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + /* Screen 2 held 2 cm under screen 1, the right hand on it. */ + shown.quads[2].pose.position.y = -0.80f; + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.grabs.hand[1].slot = 1; + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.hinged = 2u; + k = &t_xr_frame.hinges[1]; + memset(k, 0, sizeof(*k)); + k->point.y = -0.50f; + k->point.z = -1.8f; + k->dir.x = 1.0f; + k->flat.y = -1.0f; + k->gap = 0.02f; + k->edge = 0; + k->slot = 0; + k->on = 1; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 7 && t_under == 2, "%u layers, %u under: want the screens' " + "outlines under, the menu's and its handle, a line, a beam and a " + "dot", n, t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP + && q->subImage.imageRect.offset.y == 0 + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && q->subImage.imageRect.extent.height == VIDEO_XR_OUTLINE_DIM + && t_near(ax.y, 1.0f) + && t_near(q->size.width, t_outline(0.96f) * cut) + && t_near(q->size.height, t_outline(1.6f)) + && t_near(q->pose.position.y, t_outline(0.96f) * half) + && t_near(q->pose.position.z, -1.8f + VIDEO_XR_CURSOR_LIFT), + "screen 1's outline without its bottom rim: offset %d, %.4f x " + "%.4f at y %.4f", (int)q->subImage.imageRect.offset.x, + q->size.width, q->size.height, q->pose.position.y); + q = (const XrCompositionLayerQuad*)layers[1]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.y, 1.0f) + && t_near(q->size.width, t_outline(0.6f) * cut) + && t_near(q->pose.position.y, -0.80f - t_outline(0.6f) * half), + "screen 2's outline without its top rim: offset %d at y %.4f", + (int)q->subImage.imageRect.offset.x, q->pose.position.y); + q = (const XrCompositionLayerQuad*)layers[2]; + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width == VIDEO_XR_OUTLINE_DIM, + "the menu's outline was cut"); + q = (const XrCompositionLayerQuad*)layers[4]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + t_layer_axis(q, 0.0f, 0.0f, 1.0f, &az); + /* Each end a centimetre toward the head, so the line shows over both + * screens whatever order the runtime draws them in. */ + t_lifted(-0.4f, -0.49f, -1.8f, 0.4f, -0.49f, -1.8f, &m, &len); + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && q->subImage.imageRect.offset.y == VIDEO_XR_HINGE_Y + && q->subImage.imageRect.extent.width == VIDEO_XR_HINGE_COLS - 2 + && q->subImage.imageRect.extent.height == VIDEO_XR_HINGE_ROWS + && q->subImage.swapchain == (XrSwapchain)(uintptr_t)0x51 + && q->space == local + && (q->layerFlags + & XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT) + && t_near(q->size.width, len) + && t_near(q->size.height, VIDEO_XR_HINGE_WIDTH) + && t_near(q->pose.position.x, m.x) + && t_near(q->pose.position.y, m.y) + && t_near(q->pose.position.z, m.z) + && t_near(ax.x, 1.0f) + && t_near(az.y, 0.2627f) && t_near(az.z, 0.9649f), + "the line: offset %d, %.4f x %.4f at %.4f,%.4f,%.4f facing " + "%.3f,%.3f,%.3f; want %.4f long at %.4f,%.4f,%.4f", + (int)q->subImage.imageRect.offset.x, q->size.width, + q->size.height, q->pose.position.x, q->pose.position.y, + q->pose.position.z, az.x, az.y, az.z, len, m.x, m.y, m.z); + q = (const XrCompositionLayerQuad*)layers[5]; + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_BEAM_X(0), + "the beam is not after the line"); + /* 6 mm wide at every gap. */ + for (i = 0; i < 6; i++) + { + k->gap = gaps[i]; + k->point.y = -0.48f - gaps[i]; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK( n == 7 + && q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && fabs(q->size.height - VIDEO_XR_HINGE_WIDTH) < 1e-6, + "a %.3f gap: the line %.4f wide, want %.4f", gaps[i], + q->size.height, VIDEO_XR_HINGE_WIDTH); + } + k->gap = 0.02f; + k->point.y = -0.50f; + /* Short of layers: the beam goes, then the line; the rims stay cut. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 5, &t_under); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK(n == 6 && q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1, + "one layer past the outlines: %u layers, the line not kept", n); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 4, &t_under); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK(n == 5 && q->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM, + "room for the outlines alone: %u layers", n); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 2, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 3 && q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP, + "room for two outlines: %u layers, screen 1's offset %d", n, + (int)q->subImage.imageRect.offset.x); + /* No head: no line, nor beam; the rims still go. */ + t_head_tracked = false; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK( n == 5 && t_under == 2 + && q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP + && ((const XrCompositionLayerQuad*)layers[1]) + ->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && ((const XrCompositionLayerQuad*)layers[4]) + ->subImage.imageRect.extent.width == VIDEO_XR_CURSOR_DIM, + "no head: %u layers, screen 1's offset %d", n, + (int)q->subImage.imageRect.offset.x); + t_head_tracked = true; + + /* The left hand holds screen 3, on screen 1's top edge: both rims of + * screen 1 go, two lines, the left hand's first. */ + shown.quads[3] = shown.quads[2]; + shown.quads[3].pose.position.y = 0.78f; + shown.quads[3].screen = 2; + shown.quads[3].slot = 2; + shown.num_quads = 4; + t_xr_frame.grabs.hand[0].slot = 2; + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.hinged = 3u; + k = &t_xr_frame.hinges[0]; + memset(k, 0, sizeof(*k)); + k->point.y = 0.48f; + k->point.z = -1.8f; + k->dir.x = 1.0f; + k->flat.y = 1.0f; + k->edge = 1; + k->slot = 0; + k->on = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK( n == 9 && t_under == 3 + && q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - 2 * VIDEO_XR_OUTLINE_CROP + && t_near(q->size.width, t_outline(0.96f) * both) + && t_near(q->pose.position.y, 0.0f), + "two hands, opposite edges: %u layers, screen 1's offset %d " + "width %d", n, (int)q->subImage.imageRect.offset.x, + (int)q->subImage.imageRect.extent.width); + q = (const XrCompositionLayerQuad*)layers[5]; + t_lifted(-0.4f, 0.48f, -1.8f, 0.4f, 0.48f, -1.8f, &m, &len); + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.y, m.y) + && t_near(q->pose.position.z, m.z), + "the left hand's line is not first"); + q = (const XrCompositionLayerQuad*)layers[6]; + t_lifted(-0.4f, -0.49f, -1.8f, 0.4f, -0.49f, -1.8f, &m, &len); + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.y, m.y) + && t_near(q->pose.position.z, m.z), + "the right hand's line is not second"); + + /* Screen 3 on screen 1's right edge instead, adjacent to its bottom: + * the left hand's rim alone goes there; both lines show. Screen 3 + * seen from behind loses the same side's rim, its left. */ + shown.quads[3].pose.position.x = 1.2f; + shown.quads[3].pose.position.y = 0.0f; + t_xr_frame.backs = 1u << 3; + memset(k, 0, sizeof(*k)); + k->point.x = 0.8f; + k->point.z = -1.8f; + k->dir.y = 1.0f; + k->flat.x = 1.0f; + k->edge = 3; + k->slot = 0; + k->on = 2; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( n == 9 && q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.x, 1.0f) + && t_near(q->pose.position.x, -t_outline(1.6f) * half), + "adjacent edges: %u layers, screen 1's offset %d at x %.4f", n, + (int)q->subImage.imageRect.offset.x, q->pose.position.x); + q = (const XrCompositionLayerQuad*)layers[2]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.x, -1.0f) + && t_near(q->pose.position.x, 1.2f + t_outline(0.8f) * half), + "screen 3 from behind: offset %d at x %.4f", + (int)q->subImage.imageRect.offset.x, q->pose.position.x); + q = (const XrCompositionLayerQuad*)layers[5]; + t_lifted(0.8f, -0.3f, -1.8f, 0.8f, 0.3f, -1.8f, &m, &len); + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.x, m.x) + && t_near(q->size.width, len), + "the side line: at x %.4f, %.4f long; want %.4f, %.4f", + q->pose.position.x, q->size.width, m.x, len); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_xr_frame.backs = 0; + t_head_tracked = false; + t_edit_end(); +} + +/* n polls 10 ms apart, the right hand gripping, its trigger held if + * tilt, and the left stick at y. */ +static void t_tilt_polls(unsigned n, float y, bool tilt) +{ + unsigned i; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + if (tilt) + t_hold("edit/tilt", 2, 1.0f, 0.0f); + t_hold("edit/stick", 1, 0.0f, y); + for (i = 0; i < n; i++) + { + video_xr_pose_t a; + t_clock += 10000; + if (t_smooth) + t_frame_aim(&a); + t_edit_poll(); + } +} + +/* Edit mode with Snap to Screens on, screen 2 taken under screen 1 + * folded fold degrees and carried 7 cm along the edge, so its snaps + * are whole. */ +static void t_take_under(double fold, bool tilt) +{ + t_snap = true; + t_edit_start(); + t_snap_under(fold); + t_tilt_polls(1, 0.0f, tilt); + CHECK( input_openxr_edit.hand[1].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[1].g.slot == 1, + "took slot %u under", input_openxr_edit.hand[1].g.slot); + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, tilt); +} + +/* Screens resting in a hinge, as the main thread counts them for the + * frame loop. Let go hinged, screen 2 rests on screen 1's bottom edge, + * in the record that lets it go. So the frame that first sees the + * release draws the line and hides both rims, as every frame after; + * a hand holding screen 2 again makes it give way; short of room the + * line goes, the rims stay cut. Nothing is counted again in a poll + * where nothing changed, and a moved screen is. A layout loaded + * hinged counts once edit mode starts, again when it starts again; + * edit mode's end and Snap to Screens off count none. */ +static void test_edit_rest(void) +{ + unsigned i, n; + float len, x; + video_xr_vec3_t m, ax; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local; + + t_take_under(0.0, false); + CHECK( input_openxr_edit.hand[1].hinged && t_grabs.num_rests == 0, + "carried hinged: hinged %d, %u at rest", + (int)input_openxr_edit.hand[1].hinged, t_grabs.num_rests); + t_tilt_polls(8, 0.0f, false); + t_let_go(); + t_edit_poll(); + CHECK( t_grabs.hand[1].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.num_rests == 1 + && t_grabs.rests[0].slot == 0 && t_grabs.rests[0].edge == 1 + && t_grabs.rests[0].target == 1 && t_grabs.rests[0].on == 0, + "let go: state %u, %u at rest, the first %u's edge %u on %u's %u", + t_grabs.hand[1].state, t_grabs.num_rests, t_grabs.rests[0].slot, + t_grabs.rests[0].edge, t_grabs.rests[0].target, + t_grabs.rests[0].on); + + /* The frame loop with that record, the screen let go where it is. */ + local = t_xr_session.local_space; + t_xr_frame.grabs = t_grabs; + t_xr_frame.hinged = 0; + t_aim_tracked[1] = false; + t_aim_tracked[2] = false; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + shown.quads[2].pose = t_grabs.let_go[1].last; + x = shown.quads[2].pose.position.x; + t_lifted(x - 0.4f, -0.48f, -1.8f, x + 0.4f, -0.48f, -1.8f, &m, &len); + for (i = 0; i < 2; i++) + { + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( n == 5 && t_under == 2 + && q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.y, 1.0f), + "frame %u at rest: %u layers, %u under, screen 1's offset %d", + i, n, t_under, (int)q->subImage.imageRect.offset.x); + q = (const XrCompositionLayerQuad*)layers[1]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP, + "frame %u at rest: screen 2's top rim not cut", i); + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->size.width, len) + && t_near(q->pose.position.x, m.x) + && t_near(q->pose.position.y, m.y) + && t_near(q->pose.position.z, m.z), + "frame %u at rest: the line %.4f long at %.4f,%.4f,%.4f, want " + "%.4f at %.4f,%.4f,%.4f", i, q->size.width, q->pose.position.x, + q->pose.position.y, q->pose.position.z, len, m.x, m.y, m.z); + } + /* Short of room: the line goes, the rims stay cut. */ + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 4, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK(n == 4 && q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP, + "room for the outlines alone: %u layers, screen 1's offset %d", n, + (int)q->subImage.imageRect.offset.x); + /* Held again, its pair gives way, hidden in its slot after the + * hand's own. */ + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + CHECK( n == 6 + && q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width == VIDEO_XR_OUTLINE_DIM + && t_slot_is((const XrCompositionLayerQuad*)layers[4], true, + x, shown.quads[2].pose.position.y, -1.8f, + x + VIDEO_XR_HINGE_WIDTH, shown.quads[2].pose.position.y, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[5], true, + 0.0f, 0.0f, -1.8f, VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f), + "held again: %u layers, screen 1's offset %d width %d", n, + (int)q->subImage.imageRect.offset.x, + (int)q->subImage.imageRect.extent.width); + + /* The drawing thread plans it there; then nothing changes. */ + t_quads.quads[2].pose = t_grabs.let_go[1].last; + t_edit_poll(); + input_openxr_edit.grabs.num_rests = 0; + t_edit_poll(); + CHECK(t_grabs.num_rests == 0, "counted again with nothing changed"); + t_quads.quads[0].pose.position.x += 0.001f; + t_edit_poll(); + CHECK(t_grabs.num_rests == 1, "screen 1 moved: %u at rest", + t_grabs.num_rests); + t_take_end(); + + /* Carried off in the grip's last 80 ms, it is let go hinged, resting + * in the record that lets it go. */ + t_take_under(0.0, false); + t_tilt_polls(8, 0.0f, false); + t_aim[2].position.z += 0.1f; + t_tilt_polls(1, 0.0f, false); + CHECK( !input_openxr_edit.hand[1].hinged && t_grabs.num_rests == 0, + "carried off: hinged %d, %u at rest", + (int)input_openxr_edit.hand[1].hinged, t_grabs.num_rests); + t_let_go(); + t_edit_poll(); + CHECK( t_grabs.hand[1].state == VIDEO_XR_GRAB_RELEASED + && t_grabs.num_rests == 1, + "let go hinged from 80 ms before: state %u, %u at rest", + t_grabs.hand[1].state, t_grabs.num_rests); + t_take_end(); + + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_edit_poll(); + CHECK(t_grabs.num_rests == 1, "loaded hinged: %u at rest", + t_grabs.num_rests); + input_openxr_edit_toggle(); + t_edit_on(false); + t_edit_poll(); + CHECK(t_grabs.num_rests == 1, "on again, nothing moved: %u at rest", + t_grabs.num_rests); + t_edit_end(); + CHECK(t_grabs.num_rests == 0, "edit mode's end left %u at rest", + t_grabs.num_rests); + t_snap = false; + t_edit_start(); + t_rest_under(0.0); + t_edit_poll(); + CHECK(t_grabs.num_rests == 0, "Snap to Screens off: %u at rest", + t_grabs.num_rests); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_head_tracked = false; + t_edit_end(); +} + +/* At rest on two adjacent edges, screen 1 loses only the first one's + * rim, each neighbour its own; both lines show, in the list's order, + * the first kept when there is room for one. */ +static void test_edit_rest_corner(void) +{ + unsigned n; + float len; + video_xr_vec3_t m, ax; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local; + t_edit_start(); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_aim_tracked[2] = false; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + /* Screen 2 right of screen 1, screen 3 under it, all touching. */ + shown.quads[2] = shown.quads[0]; + shown.quads[2].pose.position.x = 1.6f; + shown.quads[2].slot = 1; + shown.quads[2].screen = 1; + shown.quads[3] = shown.quads[0]; + shown.quads[3].pose.position.y = -0.96f; + shown.quads[3].slot = 2; + shown.quads[3].screen = 2; + shown.num_quads = 4; + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_xr_frame.grabs.num_rests = 2; + t_xr_frame.grabs.rests[0].slot = 0; + t_xr_frame.grabs.rests[0].edge = 2; + t_xr_frame.grabs.rests[0].target = 1; + t_xr_frame.grabs.rests[0].on = 3; + t_xr_frame.grabs.rests[1].slot = 0; + t_xr_frame.grabs.rests[1].edge = 1; + t_xr_frame.grabs.rests[1].target = 2; + t_xr_frame.grabs.rests[1].on = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK(n == 7 && t_under == 3, "%u layers, %u under: want three " + "outlines under, the menu's and its handle, two lines", n, t_under); + q = (const XrCompositionLayerQuad*)layers[0]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.x, 1.0f), + "screen 1: its right rim alone cut: offset %d width %d", + (int)q->subImage.imageRect.offset.x, + (int)q->subImage.imageRect.extent.width); + q = (const XrCompositionLayerQuad*)layers[1]; + CHECK( q->subImage.imageRect.offset.x + == VIDEO_XR_OUTLINE_X(0) + VIDEO_XR_OUTLINE_CROP + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP, + "screen 2: its left rim not cut"); + q = (const XrCompositionLayerQuad*)layers[2]; + t_layer_axis(q, 1.0f, 0.0f, 0.0f, &ax); + CHECK( q->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(0) + && q->subImage.imageRect.extent.width + == VIDEO_XR_OUTLINE_DIM - VIDEO_XR_OUTLINE_CROP + && t_near(ax.y, 1.0f), + "screen 3: its top rim not cut"); + q = (const XrCompositionLayerQuad*)layers[5]; + t_lifted(0.8f, -0.48f, -1.8f, 0.8f, 0.48f, -1.8f, &m, &len); + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.x, m.x) && t_near(q->size.width, len), + "the first line not on screen 1's right edge"); + q = (const XrCompositionLayerQuad*)layers[6]; + t_lifted(-0.8f, -0.48f, -1.8f, 0.8f, -0.48f, -1.8f, &m, &len); + CHECK(q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.y, m.y) && t_near(q->size.width, len), + "the second line not on screen 1's bottom edge"); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_KEPT_EXTRA_LAYERS + 6, &t_under); + q = (const XrCompositionLayerQuad*)layers[5]; + t_lifted(0.8f, -0.48f, -1.8f, 0.8f, 0.48f, -1.8f, &m, &len); + CHECK(n == 6 && q->subImage.imageRect.offset.x == VIDEO_XR_HINGE_X + 1 + && t_near(q->pose.position.x, m.x), + "room for one line: %u layers, not the first", n); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_cursor = false; + t_head_tracked = false; + t_edit_end(); +} + +/* Outlines of shown as t_xr_frame has them, each against the same + * frame with nothing hinged or resting on them: its pose and size the same, its + * image the whole square, or for want's (in the order the outlines + * come, a VIDEO_XR_CUT_X cut, -1 none) the cut's cell. Returns how many + * are not, or are not as many as want. */ +static unsigned t_cells_wrong(const video_xr_quad_set_t *shown, + XrSpace local, const int *want, unsigned outlines) +{ + static XrCompositionLayerQuad cut[OPENXR_MAX_EXTRA_LAYERS]; + static openxr_grabs_t grabs; + unsigned i, n, m; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + unsigned o = 0; + unsigned wrong = 0; + unsigned hinged = t_xr_frame.hinged; + n = t_hooks.frame_layers(t_hooks.user, t_time, shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + for (i = 0; i < n; i++) + cut[i] = *(const XrCompositionLayerQuad*)layers[i]; + /* Rests on the menu's slot hinge nothing but keep their slots. */ + grabs = t_xr_frame.grabs; + t_xr_frame.hinged = 0; + for (i = 0; i < grabs.num_rests; i++) + t_xr_frame.grabs.rests[i].target = VIDEO_XR_MENU_SLOT; + m = t_hooks.frame_layers(t_hooks.user, t_time, shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + t_xr_frame.hinged = hinged; + t_xr_frame.grabs = grabs; + if (m != n) + return outlines + 1; + for (i = 0; i < n; i++) + { + const XrCompositionLayerQuad *w = (const XrCompositionLayerQuad*) + layers[i]; + const XrCompositionLayerQuad *c = &cut[i]; + int32_t lit, x; + if (w->subImage.imageRect.extent.height != VIDEO_XR_OUTLINE_DIM) + continue; + lit = w->subImage.imageRect.offset.x == VIDEO_XR_OUTLINE_X(1); + x = (o < outlines && want[o] >= 0) + ? VIDEO_XR_CUT_X(want[o], lit) : VIDEO_XR_OUTLINE_X(lit); + if ( memcmp(&c->pose, &w->pose, sizeof(c->pose)) + || memcmp(&c->size, &w->size, sizeof(c->size)) + || c->subImage.imageRect.offset.x != x + || c->subImage.imageRect.offset.y != 0 + || c->subImage.imageRect.extent.width != VIDEO_XR_OUTLINE_DIM + || c->subImage.imageRect.extent.height != VIDEO_XR_OUTLINE_DIM + || w->subImage.imageRect.extent.width != VIDEO_XR_OUTLINE_DIM) + wrong++; + o++; + } + return wrong + (o != outlines ? 1u : 0u); +} + +/* At full width, every cut a hinge or a rest makes, top, bottom, top + * and bottom, right, left, right and left, a side from behind, a + * corner's one rim: each + * outline keeps its whole pose and size, only its image changes. */ +static void test_edit_hinge_cells(void) +{ + /* Outlines: screens 1, 2 and 3, then the menu's. */ + static const int under[] = { 1, 0, -1 }; + static const int both[] = { 4, 0, 1, -1 }; + static const int side[] = { 2, -1, 3, -1 }; + static const int behind[] = { 2, -1, 2, -1 }; + static const int sides[] = { 5, 2, 3, -1 }; + static const int corner[] = { 2, 3, 0, -1 }; + video_xr_quad_set_t shown; + video_xr_hinge_t *k; + unsigned wrong; + XrSpace local; + t_edit_start(); + t_snap_under(0.0); + local = t_xr_session.local_space; + t_aim_tracked[1] = false; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + /* Screen 2 held under screen 1, hinged by the right hand. */ + shown.quads[2].pose.position.y = -0.80f; + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.grabs.hand[1].slot = 1; + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.hinged = 2u; + k = &t_xr_frame.hinges[1]; + memset(k, 0, sizeof(*k)); + k->point.y = -0.50f; + k->point.z = -1.8f; + k->dir.x = 1.0f; + k->flat.y = -1.0f; + k->gap = 0.02f; + k->edge = 0; + k->slot = 0; + k->on = 1; + wrong = t_cells_wrong(&shown, local, under, 3); + CHECK(!wrong, "under: %u outlines moved or not their cut's cell", wrong); + /* Screen 3 held on screen 1's top by the left hand. */ + shown.quads[3] = shown.quads[2]; + shown.quads[3].pose.position.y = 0.78f; + shown.quads[3].screen = 2; + shown.quads[3].slot = 2; + shown.num_quads = 4; + t_xr_frame.grabs.hand[0].slot = 2; + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.hinged = 3u; + k = &t_xr_frame.hinges[0]; + memset(k, 0, sizeof(*k)); + k->point.y = 0.48f; + k->point.z = -1.8f; + k->dir.x = 1.0f; + k->flat.y = 1.0f; + k->edge = 1; + k->slot = 0; + k->on = 0; + wrong = t_cells_wrong(&shown, local, both, 4); + CHECK(!wrong, "top and bottom: %u outlines wrong", wrong); + /* Screen 3 on screen 1's right edge, the left hand's hinge alone; + * then screen 3 seen from behind. */ + shown.quads[3].pose.position.x = 1.2f; + shown.quads[3].pose.position.y = 0.0f; + t_xr_frame.hinged = 1u; + memset(k, 0, sizeof(*k)); + k->point.x = 0.8f; + k->point.z = -1.8f; + k->dir.y = 1.0f; + k->flat.x = 1.0f; + k->edge = 3; + k->slot = 0; + k->on = 2; + wrong = t_cells_wrong(&shown, local, side, 4); + CHECK(!wrong, "right and left: %u outlines wrong", wrong); + t_xr_frame.backs = 1u << 3; + wrong = t_cells_wrong(&shown, local, behind, 4); + CHECK(!wrong, "from behind: %u outlines wrong", wrong); + t_xr_frame.backs = 0; + /* Screen 2 on screen 1's left edge too: screen 1 loses both sides. */ + shown.quads[2].pose.position.x = -1.2f; + shown.quads[2].pose.position.y = 0.0f; + t_xr_frame.hinged = 3u; + k = &t_xr_frame.hinges[1]; + memset(k, 0, sizeof(*k)); + k->point.x = -0.8f; + k->point.z = -1.8f; + k->dir.y = 1.0f; + k->flat.x = -1.0f; + k->edge = 2; + k->slot = 0; + k->on = 3; + wrong = t_cells_wrong(&shown, local, sides, 4); + CHECK(!wrong, "both sides: %u outlines wrong", wrong); + /* Let go, screen 2 rests under screen 1. */ + shown.quads[2].pose.position.x = 0.0f; + shown.quads[2].pose.position.y = -0.80f; + shown.num_quads = 3; + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_xr_frame.grabs.num_rests = 1; + t_xr_frame.grabs.rests[0].slot = 1; + t_xr_frame.grabs.rests[0].edge = 0; + t_xr_frame.grabs.rests[0].target = 0; + t_xr_frame.grabs.rests[0].on = 1; + wrong = t_cells_wrong(&shown, local, under, 3); + CHECK(!wrong, "resting under: %u outlines wrong", wrong); + /* Screen 1 resting on screen 2's left edge and screen 3's top, as + * test_edit_rest_corner: one rim kept, its right. */ + shown.quads[2] = shown.quads[0]; + shown.quads[2].pose.position.x = 1.6f; + shown.quads[2].slot = 1; + shown.quads[2].screen = 1; + shown.quads[3] = shown.quads[0]; + shown.quads[3].pose.position.y = -0.96f; + shown.quads[3].slot = 2; + shown.quads[3].screen = 2; + shown.num_quads = 4; + t_xr_frame.grabs.num_rests = 2; + t_xr_frame.grabs.rests[0].slot = 0; + t_xr_frame.grabs.rests[0].edge = 2; + t_xr_frame.grabs.rests[0].target = 1; + t_xr_frame.grabs.rests[0].on = 3; + t_xr_frame.grabs.rests[1].slot = 0; + t_xr_frame.grabs.rests[1].edge = 1; + t_xr_frame.grabs.rests[1].target = 2; + t_xr_frame.grabs.rests[1].on = 0; + wrong = t_cells_wrong(&shown, local, corner, 4); + CHECK(!wrong, "resting in a corner: %u outlines wrong", wrong); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_head_tracked = false; + t_cursor = false; + t_edit_end(); +} + +/* Narrower than the cut outlines, with the tags or without: a cut + * outline as before, cut to its crop, turned for a top or bottom. */ +static void test_edit_hinge_crops(void) +{ + static const unsigned widths[2] = { VIDEO_XR_CURSOR_TAGGED_W, + VIDEO_XR_CURSOR_UNTAGGED_W }; + unsigned i; + for (i = 0; i < 2; i++) + { + t_cursor_w = widths[i]; + test_edit_hinge_layers(); + test_edit_rest(); + test_edit_rest_corner(); + } + t_cursor_w = VIDEO_XR_CURSOR_W; +} + +/* Taken where it rests hinged, screen 2 carries that hinge, its lid and + * buzz seeded at the grab, its mark in the grab's first publish, and is + * hinged from the first poll that moves it: no long buzz, no tick, + * nothing moves. Pulled 6 cm off screen 1's plane it lets go, its mark + * 150 ms later; carried back 7 cm along, it hinges with the long buzz. */ +static void test_edit_rest_take(void) +{ + unsigned n, l; + video_xr_pose_t at; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + at = t_quads.quads[2].pose; + n = t_applies[2]; + l = t_longs[2]; + /* The grip's poll takes it; the next is the carry's first. */ + t_tilt_polls(1, 0.0f, false); + CHECK( hold->g.state == VIDEO_XR_GRAB_HELD && hold->g.slot == 1 + && hold->g.rest_slot == 1 && hold->g.rest_on == 1 + && hold->g.rest_edge == 0 && hold->lidded && hold->buzzed + && !hold->armed && t_grabs.hand[1].rest_slot == 1 + && t_grabs.hand[1].mark_slot == 1 && t_grabs.hand[1].mark_on == 1 + && t_grabs.hand[1].mark_edge == 0, + "taken resting hinged: kept %u, lidded %d, buzzed %d, armed %#x, " + "published mark %u/%u/%u", (unsigned)hold->g.rest_slot, + (int)hold->lidded, (int)hold->buzzed, hold->armed, + (unsigned)t_grabs.hand[1].mark_slot, + (unsigned)t_grabs.hand[1].mark_on, + (unsigned)t_grabs.hand[1].mark_edge); + t_tilt_polls(1, 0.0f, false); + CHECK( hold->hinged && hold->g.rest_slot == 1 + && hold->lid.slot == 0 && hold->lid.on == 1 && hold->lid.edge == 0 + && t_applies[2] == n + && fabs(hold->g.last.position.x - at.position.x) < 1e-5 + && fabs(hold->g.last.position.y - at.position.y) < 1e-5 + && fabs(hold->g.last.position.z - at.position.z) < 1e-5, + "first carried poll: hinged %d, kept %u, lid %u/%u/%u, %u " + "buzzes, at %.5f,%.5f,%.5f", (int)hold->hinged, + (unsigned)hold->g.rest_slot, hold->lid.slot, hold->lid.on, + hold->lid.edge, t_applies[2] - n, hold->g.last.position.x, + hold->g.last.position.y, hold->g.last.position.z); + t_tilt_polls(10, 0.0f, false); + CHECK(hold->hinged && t_applies[2] == n, + "held still: hinged %d, %u buzzes", (int)hold->hinged, + t_applies[2] - n); + t_aim[2].position.z += 0.06f; + t_tilt_polls(1, 0.0f, false); + CHECK(!hold->hinged && !hold->g.rest_slot && hold->buzzed, + "6 cm off: hinged %d, kept %u, buzzed %d", (int)hold->hinged, + (unsigned)hold->g.rest_slot, (int)hold->buzzed); + t_tilt_polls(15, 0.0f, false); + CHECK(!hold->buzzed, "6 cm off 150 ms: buzzed %d", (int)hold->buzzed); + t_aim[2].position.z -= 0.06f; + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + CHECK(hold->hinged && t_longs[2] == l + 1, + "carried back: hinged %d, %u long buzzes", (int)hold->hinged, + t_longs[2] - l); + t_take_end(); +} + +/* Taken resting hinged, the free hand's stick swings screen 2 about the + * shared edge at once, the edge staying on screen 1's bottom edge. */ +static void test_edit_rest_swing(void) +{ + video_xr_vec3_t mid, edge; + video_xr_pose_t at; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + at = t_quads.quads[2].pose; + t_tilt_polls(20, 0.5f, false); + mid.x = 0.0f; + mid.y = 0.3f; + mid.z = 0.0f; + video_xr_rotate(&hold->g.last.orientation, &mid, &edge); + edge.x += hold->g.last.position.x; + edge.y += hold->g.last.position.y; + edge.z += hold->g.last.position.z; + CHECK( hold->hinged + && fabs(hold->g.last.position.z - at.position.z) > 0.005 + && t_near(edge.y, -0.48f) && t_near(edge.z, -1.8f), + "swung at once: hinged %d, middle z %.4f from %.4f, top edge at " + "%.4f,%.4f", (int)hold->hinged, hold->g.last.position.z, + at.position.z, edge.y, edge.z); + t_take_end(); +} + +/* Resting hinged to screen 1 above and screen 3 4 cm under, screen 2 is + * taken on the hinge the snap ranks best: screen 1's, touching. */ +static void test_edit_rest_take_two(void) +{ + video_xr_quad_t *q; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + q = &t_quads.quads[t_quads.num_quads++]; + *q = t_quads.quads[2]; + q->pose.position.y = -0.48f - 0.6f - 0.04f - 0.3f; + q->slot = 2; + q->screen = 2; + t_quads.slots[2].base.y = q->pose.position.y; + t_quads.slots[2].base.z = -1.8f; + t_tilt_polls(2, 0.0f, false); + CHECK( hold->g.slot == 1 && hold->hinged && hold->g.rest_slot == 1 + && hold->lid.slot == 0 && hold->lid.on == 1 && hold->lid.edge == 0, + "two neighbours: hinged %d, kept %u, lid on slot %u's edge %u", + (int)hold->hinged, (unsigned)hold->g.rest_slot, hold->lid.slot, + hold->lid.on); + t_take_end(); +} + +/* With the left hand holding screen 1, screen 2 is taken as any screen: + * not hinged, no lid. */ +static void test_edit_rest_take_other(void) +{ + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 + && hold->g.state == VIDEO_XR_GRAB_HELD && hold->g.slot == 1 + && !hold->hinged && !hold->g.rest_slot && !hold->lidded, + "the other hand on screen 1: hinged %d, kept %u, lidded %d", + (int)hold->hinged, (unsigned)hold->g.rest_slot, + (int)hold->lidded); + t_take_end(); +} + +/* Screen 2 taken resting hinged 2 cm under screen 1 at Hinge Gap 2; + * the left hand then takes screen 1, which ends the hinge kept: it is + * no longer hinged, nor kept, its buzz spent no more. Screen 1 let go + * 5 mm higher, screen 2 stays where the hand holds it, as the grab's + * fade of none has it, not hopping into the hinge; carried 7 cm along, + * it hinges anew with the long buzz. */ +static void test_edit_rest_take_target(void) +{ + unsigned l; + video_xr_pose_t at; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_2; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_quads.quads[2].pose.position.y -= 0.02f; + t_quads.slots[1].base.y -= 0.02f; + t_aim[2].position.y -= 0.02f; + t_tilt_polls(2, 0.0f, false); + at = hold->g.last; + l = t_longs[2]; + CHECK(hold->hinged && hold->g.rest_slot == 1, + "taken resting hinged: hinged %d, kept %u", (int)hold->hinged, + (unsigned)hold->g.rest_slot); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 && !hold->hinged + && !hold->g.rest_slot && !hold->buzzed && !hold->lidded + && !t_grabs.hand[1].rest_slot, + "screen 1 taken: hinged %d, kept %u, buzzed %d, lidded %d, " + "published %u", (int)hold->hinged, (unsigned)hold->g.rest_slot, + (int)hold->buzzed, (int)hold->lidded, + (unsigned)t_grabs.hand[1].rest_slot); + t_aim[1].position.y += 0.005f; + t_quads.quads[0].pose.position.y += 0.005f; + t_clock += 10000; + t_edit_poll(); + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state != VIDEO_XR_GRAB_HELD + && !hold->hinged && !hold->g.rest_slot && t_longs[2] == l + && fabs(hold->g.last.position.x - at.position.x) < 1e-5 + && fabs(hold->g.last.position.y - at.position.y) < 1e-5 + && fabs(hold->g.last.position.z - at.position.z) < 1e-5, + "screen 1 let go 5 mm up: hinged %d, kept %u, %u long buzzes, " + "at %.5f,%.5f,%.5f", (int)hold->hinged, + (unsigned)hold->g.rest_slot, t_longs[2] - l, + hold->g.last.position.x, hold->g.last.position.y, + hold->g.last.position.z); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, false); + CHECK(hold->hinged && t_longs[2] == l + 1, + "carried 7 cm along: hinged %d, %u long buzzes", + (int)hold->hinged, t_longs[2] - l); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 taken resting hinged on screen 1 at Hinge Gap 0, a third + * screen level 10 cm under it. Carried 6 cm down, still in reach of + * screen 1's edge, the third's 4 cm free gap is nearer: its hinge takes + * hold, which ends the one kept, with the long buzz once; held there, + * the hinge stays and nothing moves. Back 3 cm up, screen 1 pulls only + * as the fade has it, not whole. */ +static void test_edit_rest_take_over(void) +{ + unsigned l; + video_xr_pose_t at; + video_xr_quad_t *q; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + q = &t_quads.quads[t_quads.num_quads++]; + *q = t_quads.quads[2]; + q->pose.position.y = -0.48f - 0.6f - 0.1f - 0.3f; + q->slot = 2; + q->screen = 2; + t_quads.slots[2].base.y = q->pose.position.y; + t_quads.slots[2].base.z = -1.8f; + t_tilt_polls(2, 0.0f, false); + l = t_longs[2]; + CHECK(hold->hinged && hold->g.rest_slot == 1 && hold->lid.slot == 0, + "taken resting hinged: hinged %d, kept %u, lid on slot %u", + (int)hold->hinged, (unsigned)hold->g.rest_slot, hold->lid.slot); + t_aim[2].position.y -= 0.06f; + t_tilt_polls(1, 0.0f, false); + at = hold->g.last; + CHECK( hold->hinged && hold->lid.slot == 2 && hold->lid.on == 0 + && hold->lid.edge == 1 && !hold->g.rest_slot + && t_longs[2] == l + 1, + "carried 6 cm down: hinged %d, lid %u/%u/%u, kept %u, %u long " + "buzzes, at %.5f", (int)hold->hinged, hold->lid.slot, + hold->lid.on, hold->lid.edge, (unsigned)hold->g.rest_slot, + t_longs[2] - l, hold->g.last.position.y); + t_tilt_polls(3, 0.0f, false); + CHECK( hold->hinged && hold->lid.slot == 2 && t_longs[2] == l + 1 + && fabs(hold->g.last.position.x - at.position.x) < 1e-5 + && fabs(hold->g.last.position.y - at.position.y) < 1e-5 + && fabs(hold->g.last.position.z - at.position.z) < 1e-5, + "held there: hinged %d, lid on slot %u, %u long buzzes, at " + "%.5f,%.5f,%.5f from %.5f,%.5f,%.5f", (int)hold->hinged, + hold->lid.slot, t_longs[2] - l, hold->g.last.position.x, + hold->g.last.position.y, hold->g.last.position.z, at.position.x, + at.position.y, at.position.z); + t_aim[2].position.y += 0.03f; + t_tilt_polls(1, 0.0f, false); + CHECK( !(hold->hinged && hold->lid.slot == 0) + && t_longs[2] == l + 1, + "back 3 cm up: hinged %d, lid on slot %u, %u long buzzes", + (int)hold->hinged, hold->lid.slot, t_longs[2] - l); + t_take_end(); +} + +/* Screen 2 taken resting hinged on screen 1, then pulled straight off + * screen 1's plane 2 mm a poll to 4.8 cm and back: no poll moves it + * more than twice the hand, though the hinge's reach ends with the + * grab's fade short of whole; its mark shows out to 4.8 cm, within 150 + * ms of leaving its range. Back in the plane, the fade still short, it + * hinges only anew, + * with the long buzz once. Taken again and pulled 7 cm off, the hinge + * kept lasts past the reach until the fade is whole. */ +static void test_edit_rest_take_out(void) +{ + unsigned i, l; + float z; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_tilt_polls(2, 0.0f, false); + l = t_longs[2]; + z = hold->g.last.position.z; + CHECK(hold->hinged && hold->g.rest_slot == 1, + "taken resting hinged: hinged %d, kept %u", (int)hold->hinged, + (unsigned)hold->g.rest_slot); + for (i = 1; i <= 48; i++) + { + unsigned k = (i <= 24) ? i : 48 - i; + t_aim[2].position.z = 0.002f * k; + t_tilt_polls(1, 0.0f, false); + CHECK(fabs(hold->g.last.position.z - z) <= 0.00401, + "%s %.1f cm off: moved %.2f mm", (i <= 24) ? "out" : "back", + 0.2 * k, fabs(hold->g.last.position.z - z) * 1e3); + if (i <= 24) + CHECK(hold->buzzed, + "out %.1f cm off: buzzed %d", 0.2 * k, (int)hold->buzzed); + CHECK(!hold->hinged || t_longs[2] == l + (i <= 24 ? 0u : 1u), + "%s %.1f cm off: hinged %d, %u long buzzes", + (i <= 24) ? "out" : "back", 0.2 * k, (int)hold->hinged, + t_longs[2] - l); + z = hold->g.last.position.z; + } + CHECK(hold->hinged && t_longs[2] == l + 1, + "back in the plane: hinged %d, %u long buzzes", (int)hold->hinged, + t_longs[2] - l); + t_take_end(); + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_tilt_polls(2, 0.0f, false); + z = hold->g.last.position.z; + for (i = 1; i <= 35; i++) + { + t_aim[2].position.z = 0.002f * i; + t_tilt_polls(1, 0.0f, false); + CHECK(fabs(hold->g.last.position.z - z) <= 0.00401, + "%.1f cm off: moved %.2f mm", 0.2 * i, + fabs(hold->g.last.position.z - z) * 1e3); + if (i == 24 || i >= 26) + CHECK( (hold->g.rest_slot != 0) == (i == 24) + && t_grabs.hand[1].rest_slot == hold->g.rest_slot, + "%.1f cm off: kept %u, published %u", 0.2 * i, + (unsigned)hold->g.rest_slot, + (unsigned)t_grabs.hand[1].rest_slot); + z = hold->g.last.position.z; + } + t_take_end(); +} + +/* Flat at Hinge Gap 0, the pair shown at rest (screen 1 carried) is + * taken hinged from screen 1's side too, by the quads' own sizes as the + * list tests them: hinged on screen 2's top edge from the first poll + * that moves it, no buzz, unmoved. */ +static void test_edit_rest_take_sides(void) +{ + unsigned n; + video_xr_pose_t at; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[0]; + t_snap = true; + t_edit_start(); + t_rest_under(0.0); + t_edit_poll(); + CHECK( t_grabs.num_rests == 1 && t_grabs.rests[0].slot == 0 + && t_grabs.rests[0].edge == 1 && t_grabs.rests[0].target == 1 + && t_grabs.rests[0].on == 0, + "at rest: %u, the first %u's edge %u on %u's %u", + t_grabs.num_rests, t_grabs.rests[0].slot, t_grabs.rests[0].edge, + t_grabs.rests[0].target, t_grabs.rests[0].on); + at = t_quads.quads[0].pose; + n = t_applies[1]; + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( hold->g.state == VIDEO_XR_GRAB_HELD && hold->g.slot == 0 + && hold->g.rest_slot == 2 && hold->g.rest_on == 0 + && hold->g.rest_edge == 1 && hold->lidded && hold->buzzed, + "screen 1 taken resting hinged: kept %u/%u/%u, lidded %d", + (unsigned)hold->g.rest_slot, (unsigned)hold->g.rest_on, + (unsigned)hold->g.rest_edge, (int)hold->lidded); + t_clock += 10000; + t_edit_poll(); + CHECK( hold->hinged && hold->lid.slot == 1 && hold->lid.on == 0 + && hold->lid.edge == 1 && t_applies[1] == n + && fabs(hold->g.last.position.x - at.position.x) < 1e-5 + && fabs(hold->g.last.position.y - at.position.y) < 1e-5 + && fabs(hold->g.last.position.z - at.position.z) < 1e-5, + "screen 1 carried: hinged %d, lid %u/%u/%u, %u buzzes, at " + "%.5f,%.5f,%.5f", (int)hold->hinged, hold->lid.slot, + hold->lid.on, hold->lid.edge, t_applies[1] - n, + hold->g.last.position.x, hold->g.last.position.y, + hold->g.last.position.z); + t_take_end(); +} + +/* Flat under screen 1, screen 2 is hinged on screen 1's bottom edge: + * the tilt stick swings it about that edge, which stays on the line, + * carried or tilting, with a buzz at 15 and at 30. */ +static void t_hinge_under(bool tilt) +{ + unsigned i, n; + double worst = 0.0; + const char *how = tilt ? "tilting" : "carried"; + t_take_under(0.0, tilt); + n = t_applies[2]; + for (i = 0; i < 42; i++) + { + t_tilt_polls(1, 1.0f, tilt); + if (t_under_off() > worst) + worst = t_under_off(); + } + CHECK( worst < 1e-3 && t_applies[2] - n == 2 + && t_xr[1].pitch < -30.0f && t_deg_near(t_xr[1].yaw, 0.0), + "%s under: %.4f off the line, %u buzzes, pitch %.3f yaw %.3f", + how, worst, t_applies[2] - n, t_xr[1].pitch, t_xr[1].yaw); + t_take_end(); +} + +static void test_edit_hinge_under(void) +{ + t_hinge_under(false); + t_hinge_under(true); +} + +/* Screen 2's hinged edge as edit mode last wrote it: metres off the line + * (from NULL), else moved across it from where it was at from. */ +static double t_flick_off(bool under, const video_xr_pose_t *from) +{ + video_xr_pose_t pose; + if (!from) + return under ? t_under_off() : t_hinge_off(); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + return t_edge_moved(&pose, from, under); +} + +/* Flicked past where the hinge lets go and back, over and over with no + * let-go, screen 2 keeps its edge on the line: back each time, the + * hinge holds it whole, and swung on from there the edge stays on the + * line. At screen 1's right corner past square; under it past 90. At + * Hinge Gap gap cm, where it hinged is a free gap, and stays put. */ +static void t_hinge_flick(bool under, unsigned angles, unsigned gap) +{ + unsigned i, n = under ? 18 : 6; + double off, worst = 0.0; + video_xr_pose_t from; + const char *how = !under ? "corner" : angles ? "under" : "under, Off"; + float in = under ? 1.0f : -1.0f; + t_angles = angles; + if (under) + t_take_under(80.0, false); + else + t_take_square(true, false); + input_openxr_edit.hand[1].g.gap = (unsigned char)gap; + if (!under) + t_turn_polls(23, in, 10000, false); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &from); + for (i = 0; i < 8; i++) + { + if (under) + { + t_tilt_polls(n, in, false); + t_tilt_polls(n, -in, false); + } + else + { + t_turn_polls(n, in, 10000, false); + t_turn_polls(n, -in, 10000, false); + } + off = t_flick_off(under, gap ? &from : NULL); + CHECK( input_openxr_edit.hand[1].hinged && off < 1e-3 + && t_held_off(under) <= VIDEO_XR_SNAP_DIST, + "%s, gap %u, flick %u: hinged %d, %.4f off, held %.4f off", + how, gap, i + 1, input_openxr_edit.hand[1].hinged, off, + t_held_off(under)); + } + for (i = 0; i < 20; i++) + { + if (under) + t_tilt_polls(1, -in, false); + else + t_turn_polls(1, -in, 10000, false); + off = t_flick_off(under, gap ? &from : NULL); + if (off > worst) + worst = off; + } + CHECK(worst < 1e-3, "%s, gap %u, swung on: %.4f off", how, gap, worst); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +static void test_edit_hinge_flick(void) +{ + /* Touching, 2 cm is as far as the click's fade reaches. */ + static const unsigned gaps[] = { 0, 2, 5 }; + unsigned k; + for (k = 0; k < 3; k++) + { + t_hinge_flick(false, VIDEO_OPENXR_EDIT_SNAP_ANGLES_15, gaps[k]); + t_hinge_flick(true, VIDEO_OPENXR_EDIT_SNAP_ANGLES_15, gaps[k]); + t_hinge_flick(true, VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF, gaps[k]); + } +} + +/* Let go off its hinge past 90 and carried 11.5 cm down, apart in the + * hinge's fade, so the hinge cannot take it back: pushed back, screen 2 + * is held at the 90 step with the stick still pushing, and its edge + * stays still. */ +static void test_edit_hinge_glide(void) +{ + unsigned i, k, held = 0; + double worst[2] = { 0.0, 0.0 }; + video_xr_pose_t from[2], pose[2]; + t_take_under(80.0, false); + t_tilt_polls(16, 1.0f, false); + t_aim[2].position.y -= 0.115f; + t_tilt_polls(1, 0.0f, false); + for (i = 0; i < 12; i++) + { + t_tilt_polls(1, -1.0f, false); + if (input_openxr_edit.hand[1].rest[1] == 0.0f) + continue; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose[0]); + t_held_pose(&pose[1]); + for (k = 0; k < 2; k++) + { + if (!held) + from[k] = pose[k]; + if (t_edge_moved(&pose[k], &from[k], true) > worst[k]) + worst[k] = t_edge_moved(&pose[k], &from[k], true); + } + held++; + } + /* Shown, the settings' pitch at 90 is good to about 0.02 degrees. */ + CHECK( held >= 3 && worst[0] < 1e-3 && worst[1] < 1e-4 + && !input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "held %u polls: edge %.4f shown, %.4f held moved, hinged %d," + " lidded %d", held, worst[0], worst[1], + input_openxr_edit.hand[1].hinged, input_openxr_edit.hand[1].lidded); + t_take_end(); +} + +/* With the stick at v for 4 polls, tilting screen 2 hinged under screen + * 1, else turning it hinged on its right edge, screen 2 as the right + * hand holds it turns about the hinged edge if lid, else its middle; + * and the hold is lidded as lid. */ +static void t_lid_swing(float v, bool under, bool lid, const char *how) +{ + unsigned i; + double edge = 0.0, mid = 0.0; + video_xr_pose_t from, pose; + if (under) + t_tilt_polls(1, 0.0f, false); + else + t_turn_polls(1, 0.0f, 10000, false); + t_held_pose(&from); + for (i = 0; i < 4; i++) + { + double x[3]; + if (under) + t_tilt_polls(1, v, false); + else + t_turn_polls(1, v, 10000, false); + t_held_pose(&pose); + x[0] = (double)pose.position.x - from.position.x; + x[1] = (double)pose.position.y - from.position.y; + x[2] = (double)pose.position.z - from.position.z; + if (t_edge_moved(&pose, &from, under) > edge) + edge = t_edge_moved(&pose, &from, under); + if (sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]) > mid) + mid = sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]); + } + CHECK( (lid ? edge : mid) < 1e-4 && (lid ? mid : edge) > 0.01 + && !input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded == lid, + "%s: edge %.4f, middle %.4f moved, hinged %d, lidded %d", how, + edge, mid, input_openxr_edit.hand[1].hinged, + input_openxr_edit.hand[1].lidded); +} + +/* Let go off its hinge past 90, screen 2 swings on about that edge while + * the edge is within 3 cm of screen 1's plane. Carried 3.5 cm off, it + * turns about its middle for the rest of the hold, even back within + * reach; hinged again, the new hinge holds it as the first did. */ +static void test_edit_hinge_lid_far(void) +{ + video_xr_pose_t pose; + video_xr_vec3_t m, w; + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_take_under(80.0, false); + t_tilt_polls(16, 1.0f, false); + t_aim[2].position.z += 0.025f; + t_lid_swing(1.0f, true, true, "2.5 cm off"); + t_lid_swing(-1.0f, true, true, "2.5 cm off, back"); + t_aim[2].position.z += 0.01f; + t_lid_swing(1.0f, true, false, "3.5 cm off"); + t_aim[2].position.z -= 0.01f; + t_lid_swing(-1.0f, true, false, "2.5 cm off again"); + /* Turned inside 90 about its middle, then carried onto the line. */ + t_tilt_polls(6, -1.0f, false); + t_held_pose(&pose); + m.x = 0.0f; + m.y = 0.3f; + m.z = 0.0f; + video_xr_rotate(&pose.orientation, &m, &w); + t_aim[2].position.y -= pose.position.y + w.y + 0.48f; + t_aim[2].position.z -= pose.position.z + w.z + 1.8f; + t_tilt_polls(1, 0.0f, false); + CHECK( input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "hinged again: hinged %d, lidded %d", + input_openxr_edit.hand[1].hinged, + input_openxr_edit.hand[1].lidded); + t_tilt_polls(8, 1.0f, false); + t_lid_swing(1.0f, true, true, "let go again"); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* Screen 2 taken flush right of screen 1 and carried 7 cm up, so the + * hinge on screen 1's right edge holds it, with Snap to Angles Off. */ +static void t_take_flush(void) +{ + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_snap = true; + t_edit_start(); + t_snap_beside(); + t_quads.quads[t_quads.num_quads - 1].pose.position.x = 1.20f; + t_quads.slots[1].base.x = 1.20f; + t_aim[2].position.x = 1.20f; + t_turn_polls(1, 0.0f, 10000, false); + t_aim[2].position.y = 0.07f; + t_turn_polls(1, 0.0f, 10000, false); + CHECK( input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "flush: hinged %d, lidded %d", input_openxr_edit.hand[1].hinged, + input_openxr_edit.hand[1].lidded); +} + +/* Hinged flush beside screen 1, then carried 1.5 m along its plane and + * 0.4 m up, out of that hinge's reach, its mark gone: the stick turns + * screen 2 about its middle, not as a door on that edge. */ +static void test_edit_hinge_door(void) +{ + t_take_flush(); + t_aim[2].position.x += 1.5f; + t_aim[2].position.y += 0.4f; + t_turn_polls(15, 0.0f, 10000, false); + t_lid_swing(1.0f, false, false, "carried 1.5 m along"); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* Hinged flush beside screen 1 and carried off it, its mark gone, + * screen 2 turns about that edge while the hinge could take it again: + * 12.5 cm out, in the hinge's fade, or 1.5 cm still alongside screen + * 1's edge. 13.5 cm out, or 1.5 cm past that edge's end, about its + * middle. */ +static void test_edit_hinge_lid_reach(void) +{ + t_take_flush(); + t_aim[2].position.x += 0.125f; + t_turn_polls(16, 0.0f, 10000, false); + CHECK(!input_openxr_edit.hand[1].buzzed, "12.5 cm out 150 ms: buzzed"); + t_lid_swing(1.0f, false, true, "12.5 cm out"); + t_lid_swing(-1.0f, false, true, "12.5 cm out, back"); + t_aim[2].position.x += 0.01f; + t_turn_polls(15, 0.0f, 10000, false); + t_lid_swing(1.0f, false, false, "13.5 cm out"); + t_take_end(); + t_take_flush(); + t_aim[2].position.y = 0.765f; + t_turn_polls(16, 0.0f, 10000, false); + CHECK(!input_openxr_edit.hand[1].buzzed, + "1.5 cm alongside 150 ms: buzzed"); + t_lid_swing(1.0f, false, true, "1.5 cm alongside"); + t_lid_swing(-1.0f, false, true, "1.5 cm alongside, back"); + t_aim[2].position.y = 0.795f; + t_turn_polls(15, 0.0f, 10000, false); + t_lid_swing(1.0f, false, false, "1.5 cm past its end"); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* A poll 10 ms on, both hands gripping and no stick. */ +static void t_both_polls(unsigned n) +{ + unsigned i; + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + for (i = 0; i < n; i++) + { + t_clock += 10000; + t_edit_poll(); + } +} + +/* Let go off its hinge past 90, screen 2's lid ends as the left hand + * takes screen 1: carried 1 cm toward the viewer, then 3.5 cm, screen 2 + * turns about its middle. */ +static void test_edit_hinge_lid_moved(void) +{ + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_take_under(80.0, false); + t_tilt_polls(16, 1.0f, false); + t_both_polls(1); + CHECK(input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0, + "the left hand took slot %u", input_openxr_edit.hand[0].g.slot); + t_aim[1].position.z += 0.01f; + t_both_polls(2); + CHECK( !input_openxr_edit.hand[1].hinged + && !input_openxr_edit.hand[1].lidded, + "screen 1 1 cm on: hinged %d, lidded %d", + input_openxr_edit.hand[1].hinged, + input_openxr_edit.hand[1].lidded); + t_aim[1].position.z += 0.025f; + t_both_polls(2); + CHECK(!input_openxr_edit.hand[1].lidded, + "screen 1 3.5 cm on: lidded %d", input_openxr_edit.hand[1].lidded); + t_lid_swing(1.0f, true, false, "screen 1 moved"); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* A new hold starts with no lid: let go off its hinge past 90, then + * dropped and taken again folded past 90, so never hinged though its + * edge is still on the line, screen 2 turns about its middle. */ +static void test_edit_hinge_lid_new_hold(void) +{ + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_take_under(80.0, false); + t_tilt_polls(16, 1.0f, false); + CHECK(input_openxr_edit.hand[1].lidded, "let go past 90: lidded %d", + input_openxr_edit.hand[1].lidded); + t_take_end(); + t_take_under(95.0, false); + t_lid_swing(1.0f, true, false, "taken again past 90"); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* Let go off its hinge past 90, then dropped and taken again with no + * snap, so never hinged, screen 2 tilts to the 90 step about its + * middle, not the edge that hinge held. */ +static void test_edit_hinge_lid_ends(void) +{ + unsigned i; + double worst = 0.0; + video_xr_pose_t first, pose; + t_take_under(80.0, false); + t_tilt_polls(16, 1.0f, false); + CHECK( !input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "let go past 90: hinged %d, lidded %d", + input_openxr_edit.hand[1].hinged, input_openxr_edit.hand[1].lidded); + t_take_end(); + t_edit_start(); + t_snap_under(80.0); + t_tilt_polls(1, 0.0f, false); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &first); + for (i = 0; i < 12; i++) + { + double x, y, z; + t_tilt_polls(1, 1.0f, false); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + x = (double)pose.position.x - first.position.x; + y = (double)pose.position.y - first.position.y; + z = (double)pose.position.z - first.position.z; + if (sqrt(x * x + y * y + z * z) > worst) + worst = sqrt(x * x + y * y + z * z); + } + /* 10.8 tilted, held at 90 with 0.8 past it. */ + CHECK( worst < 1e-4 + && fabs(fabs(input_openxr_edit.hand[1].rest[1]) - 0.8) < 1e-3 + && !input_openxr_edit.hand[1].lidded, + "taken again: middle %.4f off, rest %.3f, lidded %d", worst, + input_openxr_edit.hand[1].rest[1], + input_openxr_edit.hand[1].lidded); + t_take_end(); +} + +/* Swung to 75, then the stick pushed back and forth on a diagonal past + * the axis lock, with no let-go: the tilt takes screen 2 off the hinge + * and back each time, and it comes back level and on the line, the + * hand's own tilt never traded for the pull's. */ +static void test_edit_hinge_diagonal(void) +{ + unsigned i, j, k; + for (k = 0; k < 2; k++) + { + float y = k ? -1.0f : 1.0f; + t_take_square(true, false); + t_turn_polls(16, -1.0f, 10000, false); + t_turn_polls(11, 1.0f, 10000, false); + for (i = 0; i < 6; i++) + { + for (j = 0; j < 24; j++) + t_step_polls(1, j < 12 ? 1.0f : -1.0f, j < 12 ? y : -y); + CHECK( input_openxr_edit.hand[1].hinged + && t_deg_near(t_xr[1].pitch, 0.0) && t_hinge_off() < 1e-3 + && t_held_off(false) <= VIDEO_XR_SNAP_DIST, + "diagonal %u, %u: hinged %d, pitch %.3f, %.4f off, held %.4f" + " off", + k, i + 1, input_openxr_edit.hand[1].hinged, t_xr[1].pitch, + t_hinge_off(), t_held_off(false)); + } + t_take_end(); + } +} + +/* Under screen 1 at 30 from flat, taken 4 cm left of the centres' + * level and carried 3.5 to 5.5 cm right of it, where its pull fades: + * pushed, swung and let go, screen 2 never moves along the edge, + * carried or tilting. */ +static void test_edit_hinge_level(void) +{ + static const float xs[] = { 0.035f, 0.045f, 0.055f }; + unsigned i, j, k; + for (k = 0; k < 2; k++) + { + for (j = 0; j < 3; j++) + { + float x0, pitch0; + double worst = 0.0; + t_snap = true; + t_edit_start(); + t_snap_under(30.0); + t_quads.quads[t_quads.num_quads - 1].pose.position.x = -0.04f; + t_quads.slots[1].base.x = -0.04f; + t_aim[2].position.x = -0.04f; + t_tilt_polls(1, 0.0f, k != 0); + t_aim[2].position.x = xs[j]; + t_tilt_polls(3, 0.0f, k != 0); + x0 = t_xr[1].x; + pitch0 = t_xr[1].pitch; + for (i = 0; i < 26; i++) + { + t_tilt_polls(1, (i >= 3 && i < 23) ? 1.0f : 0.0f, k != 0); + if (fabs(t_xr[1].x - x0) > worst) + worst = fabs(t_xr[1].x - x0); + } + CHECK( input_openxr_edit.hand[1].hinged && worst < 1e-4 + && t_under_off() < 1e-3 + && fabs(t_xr[1].pitch - pitch0) > 5.0f, + "%s %.3f along: hinged %d, %.4f along, %.4f off, pitch %.3f" + " from %.3f", k ? "tilting" : "carried", xs[j], + input_openxr_edit.hand[1].hinged, worst, t_under_off(), + t_xr[1].pitch, pitch0); + t_take_end(); + } + } +} + +/* Screen 2's hinged edge's middle as edit mode last wrote it: its place + * along the line, x under screen 1, y beside it. */ +static double t_edge_along(bool under) +{ + video_xr_pose_t pose; + video_xr_vec3_t m, w; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + m.x = under ? 0.0f : -0.4f; + m.y = under ? 0.3f : 0.0f; + m.z = 0.0f; + video_xr_rotate(&pose.orientation, &m, &w); + return under ? (double)pose.position.x + w.x + : (double)pose.position.y + w.y; +} + +/* Screen 2, as t_snap_under placed it, and the hand on it, d metres + * further under screen 1, across its bottom edge in its plane. */ +static void t_gap_shift(double d) +{ + t_quads.quads[t_quads.num_quads - 1].pose.position.y -= (float)d; + t_quads.slots[1].base.y -= (float)d; + t_aim[2].position.y -= (float)d; +} + +/* Screen 2's top edge's middle as edit mode last wrote it: how far + * under screen 1's bottom edge in screen 1's plane, and in *plane how + * far out of that plane. */ +static double t_under_gap(double *plane) +{ + video_xr_pose_t pose; + video_xr_vec3_t m, w; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &pose); + m.x = 0.0f; + m.y = 0.3f; + m.z = 0.0f; + video_xr_rotate(&pose.orientation, &m, &w); + *plane = (double)pose.position.z + w.z + 1.8; + return -0.48 - ((double)pose.position.y + w.y); +} + +static void t_qmul(const XrQuaternionf *a, const XrQuaternionf *b, + XrQuaternionf *o) +{ + XrQuaternionf r; + r.w = a->w * b->w - a->x * b->x - a->y * b->y - a->z * b->z; + r.x = a->w * b->x + a->x * b->w + a->y * b->z - a->z * b->y; + r.y = a->w * b->y - a->x * b->z + a->y * b->w + a->z * b->x; + r.z = a->w * b->z + a->x * b->y - a->y * b->x + a->z * b->w; + *o = r; +} + +/* The right hand's aim turned pitch, yaw and roll degrees about its own + * x, y and z, in that order. */ +static void t_turn_aim(double pitch, double yaw, double roll) +{ + unsigned i; + double deg[3]; + deg[0] = pitch; + deg[1] = yaw; + deg[2] = roll; + for (i = 0; i < 3; i++) + { + XrQuaternionf d; + double half = deg[i] * M_PI / 360.0; + memset(&d, 0, sizeof(d)); + d.w = (float)cos(half); + if (i == 0) + d.x = (float)sin(half); + else if (i == 1) + d.y = (float)sin(half); + else + d.z = (float)sin(half); + t_qmul(&t_aim[2].orientation, &d, &t_aim[2].orientation); + } +} + +/* Edit mode at Hinge Gap gap, screen 2 taken under screen 1 folded fold + * degrees, apart metres further under and x along, and carried 7 cm on + * along the edge, polled until it hinges. */ +static void t_hinged_along(unsigned gap, double fold, double apart, + double x) +{ + unsigned i; + t_gap = gap; + t_snap = true; + t_edit_start(); + t_snap_under(fold); + t_gap_shift(apart); + t_quads.quads[t_quads.num_quads - 1].pose.position.x = (float)x; + t_aim[2].position.x = (float)x; + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = (float)(x + 0.07); + for (i = 0; i < 10 && !input_openxr_edit.hand[1].hinged; i++) + t_tilt_polls(1, 0.0f, false); +} + +static void t_hinged_at(unsigned gap, double fold, double apart) +{ + t_hinged_along(gap, fold, apart, 0.0); +} + +/* Tilting with the hand rolled 1 to 4 degrees, the stick swings screen + * 2 from 12 degrees through flat's fade band under screen 1, or from + * about 12 short through square's at its corner: its hinged edge never + * slides along the line. */ +static void test_edit_hinge_rolled(void) +{ + static const double rolls[] = { 1.0, 2.0, 4.0 }; + unsigned i, j, k; + for (k = 0; k < 2; k++) + { + for (j = 0; j < 3; j++) + { + double r = rolls[j] * M_PI / 180.0; + double last, worst = 0.0; + if (k) + { + t_take_square(true, true); + t_turn_polls(16, -1.0f, 10000, true); + t_turn_polls(13, 1.0f, 10000, true); + t_turn_polls(1, 0.0f, 10000, true); + } + else + t_take_under(12.0, true); + t_aim[2].orientation.z = (float)sin(r / 2.0); + t_aim[2].orientation.w = (float)cos(r / 2.0); + if (k) + t_turn_polls(2, 0.0f, 10000, true); + else + t_tilt_polls(2, 0.0f, true); + last = t_edge_along(!k); + for (i = 0; i < 16; i++) + { + if (k) + t_turn_polls(1, -1.0f, 10000, true); + else + t_tilt_polls(1, -1.0f, true); + if (fabs(t_edge_along(!k) - last) > worst) + worst = fabs(t_edge_along(!k) - last); + last = t_edge_along(!k); + } + CHECK( input_openxr_edit.hand[1].hinged && worst < 1e-4 + && (k ? t_deg_near(t_xr[1].yaw, -90.0) + : fabs(t_xr[1].pitch) < 1.0f), + "%s rolled %.0f: hinged %d, %.4f along a poll, yaw %.3f" + " pitch %.3f", k ? "square" : "flat", rolls[j], + input_openxr_edit.hand[1].hinged, worst, t_xr[1].yaw, + t_xr[1].pitch); + t_take_end(); + } + } +} + +/* Hinge Gap 2 cm, screen 2 taken under screen 1 at 30 degrees from + * flat, its top edge under by d, carried 7 cm along so its snaps are + * whole. */ +static void t_take_gap(double d, bool tilt) +{ + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_2; + t_snap = true; + t_edit_start(); + t_snap_under(30.0); + t_gap_shift(d); + t_tilt_polls(1, 0.0f, tilt); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, tilt); +} + +/* Snap to Screens as snap, Hinge Gap gap, screen 2 taken touching + * under screen 1 at 30 degrees from flat and carried 7 cm along, so + * its snaps are whole; the hand then c metres higher, pushing it past + * touching. Returns the hand's height touching. */ +static double t_take_stop(bool snap, unsigned gap, double c) +{ + double y; + t_gap = gap; + t_snap = snap; + t_edit_start(); + t_snap_under(30.0); + y = t_aim[2].position.y; + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_aim[2].position.y = (float)(y + c); + t_tilt_polls(3, 0.0f, false); + return y; +} + +/* Hinged, pushed 0.5 to 2.9 cm past touching, screen 2 shows touching + * at Hinge Gap 0, 2 and 10; shaken 3 mm every way 5 mm or 2.5 cm past, + * and shaken in depth and along held exactly touching, it stays + * touching; let go there, it is saved touching. 4 cm past it is partly + * stopped, 5.5 cm past not at all; 3 mm apart it shows as the gap + * leaves it. Snap to Screens off, 2 cm past it overlaps. */ +static void test_edit_gap_stop(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_2, VIDEO_OPENXR_EDIT_HINGE_GAP_10 }; + static const unsigned cm[] = { 0, 2, 10 }; + static const double apart[] = { 0.0, 0.006, 0.003 }; + static const double past[] = { 0.005, 0.01, 0.025, 0.029 }; + unsigned i, j, k; + double plane, gap, y, z; + for (i = 0; i < 3; i++) + { + for (j = 0; j < 4; j++) + { + double worst = 0.0; + y = t_take_stop(true, gaps[i], past[j]); + z = t_aim[2].position.z; + gap = t_under_gap(&plane); + CHECK( input_openxr_edit.hand[1].hinged && fabs(gap) < 1e-5 + && fabs(plane) < 1e-5, + "Hinge Gap %u cm, %.3f past: hinged %d, gap %.5f, plane %.5f", + cm[i], past[j], input_openxr_edit.hand[1].hinged, gap, + plane); + for (k = 0; k < 80 && !(j & 1); k++) + { + double c = (k < 40) ? past[j] + 0.003 * sin(k * 2.1) : 0.0; + t_aim[2].position.x = (float)(0.07 + 0.003 * sin(k * 1.3)); + t_aim[2].position.y = (float)(y + c); + t_aim[2].position.z = (float)(z + 0.003 * sin(k * 0.7)); + t_tilt_polls(1, 0.0f, false); + gap = t_under_gap(&plane); + if (fabs(gap) > worst) + worst = fabs(gap); + } + t_tilt_polls(8, 0.0f, false); + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + CHECK(worst < 1e-5 && fabs(gap) < 1e-5 && fabs(plane) < 1e-5, + "Hinge Gap %u cm, %.3f past: shaken %.5f off, saved %.5f", + cm[i], past[j], worst, gap); + t_take_end(); + } + t_take_stop(true, gaps[i], 0.04); + gap = t_under_gap(&plane); + CHECK(gap < -0.001 && gap > -0.0399, + "Hinge Gap %u cm, 4 cm past: gap %.5f", cm[i], gap); + t_take_end(); + t_take_stop(true, gaps[i], 0.055); + gap = t_under_gap(&plane); + CHECK(fabs(gap + 0.055) < 1e-4, + "Hinge Gap %u cm, 5.5 cm past: gap %.5f", cm[i], gap); + t_take_end(); + t_take_stop(true, gaps[i], -0.003); + gap = t_under_gap(&plane); + CHECK( input_openxr_edit.hand[1].hinged + && fabs(gap - apart[i]) < 1e-5, + "Hinge Gap %u cm, 3 mm apart: hinged %d, gap %.5f, want %.3f", + cm[i], input_openxr_edit.hand[1].hinged, gap, apart[i]); + t_take_end(); + } + t_take_stop(false, VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.02); + gap = t_under_gap(&plane); + CHECK(fabs(gap + 0.02) < 1e-4, "Snap to Screens off, 2 cm past: gap " + "%.5f", gap); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Hinge Gap 1 cm: its click's fade shows touching 5 mm past, so + * screen 2 shows touching from there to 35 mm past, 0.5 to 3.4 cm + * past; shaken 3 mm every way about 1 or 3 cm past at 8 Hz it shows + * touching on every poll, and let go there it is saved touching. */ +static void test_edit_gap_stop_one(void) +{ + static const double past[] = { 0.005, 0.01, 0.014, 0.025, 0.034 }; + unsigned i, j, k; + double plane, gap, y, z; + for (j = 0; j < 5; j++) + { + t_take_stop(true, VIDEO_OPENXR_EDIT_HINGE_GAP_1, past[j]); + gap = t_under_gap(&plane); + CHECK( input_openxr_edit.hand[1].hinged && fabs(gap) < 1e-5 + && fabs(plane) < 1e-5, + "Hinge Gap 1 cm, %.3f past: hinged %d, gap %.5f, plane %.5f", + past[j], input_openxr_edit.hand[1].hinged, gap, plane); + t_take_end(); + } + for (i = 0; i < 2; i++) + { + double worst = 0.0; + y = t_take_stop(true, VIDEO_OPENXR_EDIT_HINGE_GAP_1, i ? 0.03 : 0.01); + z = t_aim[2].position.z; + for (k = 0; k < 450; k++) + { + t_aim[2].position.x = (float)(0.07 + 0.003 * sin(k * 1.3)); + t_aim[2].position.y = (float)(y + (i ? 0.03 : 0.01) + + 0.003 * sin(k * 0.5)); + t_aim[2].position.z = (float)(z + 0.003 * sin(k * 0.7)); + t_tilt_polls(1, 0.0f, false); + gap = t_under_gap(&plane); + if (fabs(gap) > worst) + worst = fabs(gap); + } + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + CHECK(worst < 1e-5 && fabs(gap) < 1e-5 && fabs(plane) < 1e-5, + "Hinge Gap 1 cm, shaken about %u cm past: %.5f off, saved " + "%.5f", i ? 3u : 1u, worst, gap); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 carried into its hinge under screen 1 and pushed on 1 mm a + * poll to 5 cm past touching and back, at Hinge Gap 0, 1 and 2: hinged + * and lidded to VIDEO_XR_SNAP_STOP past where its stop starts, touching + * from there, then neither, the lid once its mark has gone 150 ms on, + * the push marking it through; back, neither until + * VIDEO_XR_SNAP_STOP_ARM short of there, then hinged touching, no + * longer through. No poll moves it more than the fade's steepest, 3.8 + * times the hand at Hinge Gap 1. */ +static void test_edit_gap_push(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_1, VIDEO_OPENXR_EDIT_HINGE_GAP_2 }; + static const unsigned cm[] = { 0, 1, 2 }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, k; + for (i = 0; i < 3; i++) + { + unsigned from = (cm[i] == 1) ? 5u : 0u; + double plane, gap, last; + double y = t_take_stop(true, gaps[i], 0.0); + last = t_under_gap(&plane); + for (k = 1; k <= 100; k++) + { + unsigned x = (k <= 50) ? k : 100 - k; + bool want = x <= from + ((k <= 50) ? 30u : 25u); + bool lid = want || (k <= 50 && x <= from + 45u); + t_aim[2].position.y = (float)(y + 0.001 * x); + t_tilt_polls(1, 0.0f, false); + gap = t_under_gap(&plane); + CHECK(fabs(gap - last) <= 0.00381, + "Hinge Gap %u cm, %s %u mm past: moved %.2f mm", cm[i], + (k <= 50) ? "in" : "back", x, fabs(gap - last) * 1e3); + CHECK( hold->hinged == want && hold->lidded == lid + && (hold->g.through != 0) == !want + && (!want || x < from || fabs(gap) < 1e-5), + "Hinge Gap %u cm, %s %u mm past: hinged %d, lidded %d, " + "through %u, gap %.5f", cm[i], (k <= 50) ? "in" : "back", x, + (int)hold->hinged, (int)hold->lidded, + (unsigned)hold->g.through, gap); + last = gap; + } + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 hinged under screen 1 at Hinge Gap 0, 2 and 1, shaken 3 mm + * every way at 8 Hz for 2 s about where the push lets its hinge go (3 + * cm past touching, 3.5 at Gap 1): it ends once and stays ended, its + * mark and lid kept, as the hand comes back within the stop's hold + * inside 150 ms. Then shaken about VIDEO_XR_SNAP_STOP_ARM short of + * there: it takes hold once and stays hinged. */ +static void test_edit_gap_push_shaken(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_2, VIDEO_OPENXR_EDIT_HINGE_GAP_1 }; + static const unsigned cm[] = { 0, 2, 1 }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, k; + for (i = 0; i < 3; i++) + { + unsigned ends = 0; + unsigned takes = 0; + double from = (cm[i] == 1) ? 0.005 : 0.0; + double y = t_take_stop(true, gaps[i], from + 0.02); + double z = t_aim[2].position.z; + bool was = hold->hinged; + for (k = 0; k < 400; k++) + { + double c = from + ((k < 200) ? 0.03 : 0.025); + t_aim[2].position.x = (float)(0.07 + 0.003 * sin(k * 1.3)); + t_aim[2].position.y = (float)(y + c + 0.003 * sin(k * 0.5)); + t_aim[2].position.z = (float)(z + 0.003 * sin(k * 0.7)); + t_tilt_polls(1, 0.0f, false); + ends += (was && !hold->hinged) ? 1u : 0u; + takes += (!was && hold->hinged) ? 1u : 0u; + was = hold->hinged; + if (k == 199) + CHECK( ends == 1 && takes == 0 && hold->lidded + && hold->buzzed, + "Hinge Gap %u cm, shaken about its end: ended %u, " + "taken %u, lidded %d, buzzed %d", cm[i], ends, takes, + (int)hold->lidded, (int)hold->buzzed); + } + CHECK( ends == 1 && takes == 1 && hold->hinged && hold->lidded + && !hold->g.through, + "Hinge Gap %u cm, shaken 5 mm short of its end: ended %u, " + "taken %u, hinged %d, through %u", cm[i], ends, takes, + (int)hold->hinged, (unsigned)hold->g.through); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 pushed through its hinge to 5 cm past touching at Hinge Gap + * 0, 2 and 1 gives no long buzz; back to 1 mm short of where it takes + * hold again (2.5 cm past, 3 at Gap 1), still none; shaken 3 mm at 8 + * Hz for 2 s about there, one; carried back to touching, still one. */ +static void test_edit_gap_push_buzz(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_2, VIDEO_OPENXR_EDIT_HINGE_GAP_1 }; + static const unsigned cm[] = { 0, 2, 1 }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, k; + for (i = 0; i < 3; i++) + { + unsigned from = (cm[i] == 1) ? 5u : 0u; + double y = t_take_stop(true, gaps[i], 0.0); + unsigned l = t_longs[2]; + for (k = 1; k <= 74 - from; k++) + { + unsigned x = (k <= 50) ? k : 100 - k; + t_aim[2].position.y = (float)(y + 0.001 * x); + t_tilt_polls(1, 0.0f, false); + } + CHECK(!hold->hinged && t_longs[2] == l, + "Hinge Gap %u cm, through and back to %u mm: hinged %d, %u " + "long buzzes", cm[i], from + 26, (int)hold->hinged, + t_longs[2] - l); + for (k = 0; k < 200; k++) + { + t_aim[2].position.y = (float)(y + 0.001 * from + 0.025 + + 0.003 * cos(k * 0.5)); + t_tilt_polls(1, 0.0f, false); + } + CHECK(hold->hinged && t_longs[2] == l + 1, + "Hinge Gap %u cm, shaken about %u mm: hinged %d, %u long " + "buzzes", cm[i], from + 25, (int)hold->hinged, t_longs[2] - l); + for (k = from + 25; k-- > 0;) + { + t_aim[2].position.y = (float)(y + 0.001 * k); + t_tilt_polls(1, 0.0f, false); + } + CHECK(hold->hinged && t_longs[2] == l + 1, + "Hinge Gap %u cm, back to touching: hinged %d, %u long buzzes", + cm[i], (int)hold->hinged, t_longs[2] - l); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 hinged under screen 1 flat at Hinge Gap 0, carried 3.6 cm + * toward the user, its lid and mark kept 150 ms; pushed 3.2 cm past + * touching there, the push ends the hinge, so back in screen 1's plane, + * overlapping, the published mark goes 150 ms after it left. */ +static void test_edit_gap_push_off(void) +{ + double y; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; + t_snap = true; + t_edit_start(); + t_snap_under(0.0); + y = t_aim[2].position.y; + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + CHECK(hold->hinged, "carried in: hinged %d", (int)hold->hinged); + t_aim[2].position.z += 0.036f; + t_tilt_polls(1, 0.0f, false); + CHECK(hold->lidded && hold->buzzed && !hold->g.through, + "3.6 cm off: lidded %d, buzzed %d, through %u", + (int)hold->lidded, (int)hold->buzzed, (unsigned)hold->g.through); + t_aim[2].position.y = (float)(y + 0.032); + t_tilt_polls(1, 0.0f, false); + CHECK(hold->g.through, "pushed 3.2 cm past, off: through %u", + (unsigned)hold->g.through); + t_aim[2].position.z -= 0.036f; + t_tilt_polls(2, 0.0f, false); + CHECK( !hold->hinged && hold->g.through + && input_openxr_edit.grabs.hand[1].through + && hold->buzzed && input_openxr_edit.grabs.hand[1].mark_slot == 1, + "back in the plane: hinged %d, through %u, published %u, " + "buzzed %d, published mark %u", (int)hold->hinged, + (unsigned)hold->g.through, + (unsigned)input_openxr_edit.grabs.hand[1].through, + (int)hold->buzzed, + (unsigned)input_openxr_edit.grabs.hand[1].mark_slot); + t_tilt_polls(12, 0.0f, false); + CHECK( !hold->hinged && !hold->buzzed + && !input_openxr_edit.grabs.hand[1].mark_slot, + "back in the plane, 150 ms out: hinged %d, buzzed %d, published " + "mark %u", (int)hold->hinged, (int)hold->buzzed, + (unsigned)input_openxr_edit.grabs.hand[1].mark_slot); + t_take_end(); +} + +/* Screen 2 hinged under screen 1 at Hinge Gap 0, 1 and 2, pushed 1 mm a + * poll past touching and let go: the pair shows at rest exactly when + * its hinge was marked as it was let go, settled touching, and taken + * again it is hinged exactly then. So at Gap 0 and 2, 1, 2 and 2.9 cm + * past it is, and 3.2, inside the mark's 150 ms past the stop's end; + * 4 and 6 cm past it is not, nor back at 2.8 or 2.6 cm from 4 cm; back + * at 2.3 cm it is again. At Gap 1, whose stop starts 5 mm past, let go + * touching from there to 4 cm past it rests hinged too. */ +static void test_edit_gap_push_rest(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_1, VIDEO_OPENXR_EDIT_HINGE_GAP_2 }; + static const unsigned cm[] = { 0, 1, 2 }; + static const unsigned peak[] = { 10, 20, 29, 32, 40, 60, 40, 40, 40 }; + static const unsigned at[] = { 10, 20, 29, 32, 40, 60, 28, 26, 23 }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, j, k; + for (i = 0; i < 3; i++) + { + unsigned from = (cm[i] == 1) ? 5u : 0u; + for (j = 0; j < 9; j++) + { + bool was, marked; + unsigned rests; + double plane, before, after; + bool want = at[j] <= from + ((peak[j] <= from + 30u) ? 30u : 25u); + /* Held 8 polls where it stopped, its mark kept up to 15 polls + * past the stop's end (test_edit_gap_push). */ + bool mark_want = want + || (peak[j] == at[j] && at[j] + 8u <= from + 45u); + double y = t_take_stop(true, gaps[i], 0.0); + for (k = 1; k <= peak[j]; k++) + { + t_aim[2].position.y = (float)(y + 0.001 * k); + t_tilt_polls(1, 0.0f, false); + } + for (k = peak[j]; k-- > at[j];) + { + t_aim[2].position.y = (float)(y + 0.001 * k); + t_tilt_polls(1, 0.0f, false); + } + t_tilt_polls(8, 0.0f, false); + was = hold->hinged; + marked = hold->buzzed; + before = t_under_gap(&plane); + CHECK(was == want && marked == mark_want, + "Hinge Gap %u cm, %u mm past from %u: hinged %d, marked %d", + cm[i], at[j], peak[j], (int)was, (int)marked); + t_let_go(); + t_clock += 10000; + t_edit_poll(); + rests = t_grabs.num_rests; + after = t_under_gap(&plane); + /* Shown where it was saved, as the driver publishes it. */ + for (k = 0; k < t_quads.num_quads; k++) + if (t_quads.quads[k].slot == 1) + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], + &t_quads.quads[k].pose); + t_tilt_polls(2, 0.0f, false); + CHECK( (rests == 1) == mark_want + && (hold->g.rest_slot != 0) == mark_want + && hold->hinged == mark_want + && fabs(after - ((mark_want && !was) ? 0.0 : before)) < 1e-5 + && (!mark_want + || fabs(t_under_gap(&plane) - (was ? before : 0.0)) + < 1e-5), + "Hinge Gap %u cm, let go %u mm past from %u, hinged %d, " + "mark wanted %d: %u at rest, let go at %.5f, taken again kept " + "%u, hinged %d, gap %.5f from %.5f", cm[i], at[j], peak[j], + (int)was, (int)mark_want, rests, after, + (unsigned)hold->g.rest_slot, (int)hold->hinged, + t_under_gap(&plane), before); + t_take_end(); + } + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Snap ticks on the right hand while screen 2, under screen 1 stacked + * flat or folded 30 degrees and carried 7 cm along, is shaken 2 mm at + * 8 Hz for 5 s about touching, at each Hinge Gap: pushed there inside + * the hinge's long buzz, its tick is skipped and none follows; pushed + * from clearly apart (past the click's reach at 0 to 2 cm, past + * VIDEO_XR_SNAP_STOP_ARM and short of the click from 3) one tick, the + * click's or the stop's, and one again each time it parts so and is + * pushed again. */ +static void test_edit_gap_tick(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_0, + VIDEO_OPENXR_EDIT_HINGE_GAP_1, VIDEO_OPENXR_EDIT_HINGE_GAP_2, + VIDEO_OPENXR_EDIT_HINGE_GAP_3, VIDEO_OPENXR_EDIT_HINGE_GAP_5, + VIDEO_OPENXR_EDIT_HINGE_GAP_10 }; + static const unsigned cm[] = { 0, 1, 2, 3, 5, 10 }; + unsigned i, k, push; + for (i = 0; i < 12; i++) + { + double apart = (cm[i % 6] < 3) ? 0.01 * cm[i % 6] + 0.025 : 0.008; + double y; + unsigned n; + t_gap = gaps[i % 6]; + t_snap = true; + t_edit_start(); + t_snap_under((i < 6) ? 0.0 : 30.0); + y = t_aim[2].position.y; + t_tilt_polls(1, 0.0f, false); + for (push = 0; push < 3; push++) + { + n = t_applies[2] - t_longs[2]; + if (push) + { + t_aim[2].position.y = (float)(y - apart); + t_tilt_polls(20, 0.0f, false); + n = t_applies[2] - t_longs[2]; + for (k = 1; k <= 10; k++) + { + t_aim[2].position.y = (float)(y - apart + + (apart + 0.001) * k / 10.0); + t_tilt_polls(1, 0.0f, false); + } + } + t_aim[2].position.x = 0.07f; + for (k = 0; k < 500; k++) + { + t_aim[2].position.y = (float)(y + 0.002 * sin(k * 0.5)); + t_tilt_polls(1, 0.0f, false); + } + CHECK( input_openxr_edit.hand[1].hinged + && t_applies[2] - t_longs[2] == n + (push ? 1 : 0), + "%s, Hinge Gap %u cm, push %u: hinged %d, %u ticks", + (i < 6) ? "flat" : "folded 30", cm[i % 6], push, + input_openxr_edit.hand[1].hinged, + t_applies[2] - t_longs[2] - n); + } + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 taken at screen 1's right corner, pushed 5 mm past touching + * and turned 5 degrees off square, short or past, at Hinge Gap 2, 3 and + * 10 cm; the hand's turn then shaken 0.2 degrees at 8 Hz for 4.5 s + * about that edge of the turn's whole snap, moving it nothing like + * clearly apart: no tick repeats. */ +static void test_edit_gap_tick_turned(void) +{ + static const unsigned gaps[] = { VIDEO_OPENXR_EDIT_HINGE_GAP_2, + VIDEO_OPENXR_EDIT_HINGE_GAP_3, VIDEO_OPENXR_EDIT_HINGE_GAP_10 }; + static const unsigned cm[] = { 2, 3, 10 }; + unsigned i, k, side; + for (i = 0; i < 3; i++) + for (side = 0; side < 2; side++) + { + double turn = side ? -23.0 : -13.0; + /* Turned about its middle, its edge's middle sits 0.4 m along. */ + float push = (float)(0.4 * cos((turn - 72.0) * M_PI / 180.0) + - 0.025); + unsigned n; + t_gap = gaps[i]; + t_take_square(true, false); + t_point(turn); + t_aim[2].position.x += push; + t_turn_polls(20, 0.0f, 10000, false); + n = t_applies[2] - t_longs[2]; + for (k = 0; k < 450; k++) + { + t_point(turn + (side ? -0.2 : 0.2) * sin(k * 0.5)); + t_aim[2].position.x += push; + t_turn_polls(1, 0.0f, 10000, false); + } + CHECK( input_openxr_edit.hand[1].hinged + && t_applies[2] - t_longs[2] <= n + 1, + "%s square, Hinge Gap %u cm: hinged %d, %u ticks", + side ? "past" : "short of", cm[i], + input_openxr_edit.hand[1].hinged, + t_applies[2] - t_longs[2] - n); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* The stick swings screen 2 from 30 degrees, carried or tilting: taken + * 2.6 cm under it shows 2 (the click), 3.5 under 3 (its fade), 6 under + * 6 (free); the gap holds through the swing. Let go, it is saved so, + * but from the fade settled at 2. */ +static void test_edit_gap_swing(void) +{ + static const double under[] = { 0.026, 0.035, 0.06 }; + static const double shown[] = { 0.02, 0.03, 0.06 }; + static const double settled[] = { 0.02, 0.02, 0.06 }; + unsigned i, k, u; + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + for (u = 0; u < 3; u++) + for (k = 0; k < 2; k++) + { + double plane, gap, worst = 0.0; + float pitch0; + t_take_gap(under[u], k != 0); + gap = t_under_gap(&plane); + CHECK( input_openxr_edit.hand[1].hinged + && fabs(gap - shown[u]) < 1e-4 && fabs(plane) < 1e-4, + "taken %.3f under: hinged %d, gap %.4f, plane %.4f", + under[u], input_openxr_edit.hand[1].hinged, gap, plane); + pitch0 = t_xr[1].pitch; + for (i = 0; i < 20; i++) + { + t_tilt_polls(1, 1.0f, k != 0); + gap = t_under_gap(&plane); + if (fabs(gap - shown[u]) > worst) + worst = fabs(gap - shown[u]); + if (fabs(plane) > worst) + worst = fabs(plane); + } + t_tilt_polls(3, 0.0f, k != 0); + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + CHECK( worst < 1e-4 && fabs(gap - settled[u]) < 1e-4 + && fabs(t_xr[1].pitch - pitch0) > 5.0f, + "%s from %.3f under: worst %.5f, saved gap %.4f, pitch %.3f" + " from %.3f", k ? "tilting" : "carried", under[u], worst, gap, + t_xr[1].pitch, pitch0); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* Snap to Angles 15 with a 2 cm gap: the swing stops on a step from + * flat, the gap kept. */ +static void test_edit_gap_steps(void) +{ + unsigned i; + double plane, gap, off; + t_take_gap(0.026, false); + for (i = 0; i < 16; i++) + t_tilt_polls(1, 1.0f, false); + t_tilt_polls(3, 0.0f, false); + gap = t_under_gap(&plane); + off = fmod(fabs(t_xr[1].pitch) + 0.001, 15.0); + CHECK( input_openxr_edit.hand[1].hinged && off < 0.011 + && fabs(gap - 0.02) < 1e-4 && fabs(plane) < 1e-4, + "pitch %.3f (%.3f off a step), gap %.4f, plane %.4f", + t_xr[1].pitch, off, gap, plane); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* The menu's value reaches the snap: 5 cm clicks from 5.6; a value + * past the list is 2 cm; Snap to Screens off, no gap and no hinge. */ +static void test_edit_gap_menu(void) +{ + double plane, gap; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_5; + t_snap = true; + t_edit_start(); + t_snap_under(30.0); + t_gap_shift(0.056); + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, false); + gap = t_under_gap(&plane); + CHECK(fabs(gap - 0.05) < 1e-4, "Hinge Gap 5: gap %.4f", gap); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_LAST; + t_snap = true; + t_edit_start(); + t_snap_under(30.0); + t_gap_shift(0.026); + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, false); + gap = t_under_gap(&plane); + CHECK(fabs(gap - 0.02) < 1e-4, "past the list: gap %.4f", gap); + t_take_end(); + t_snap = false; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_2; + t_edit_start(); + t_snap_under(30.0); + t_gap_shift(0.026); + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, false); + gap = t_under_gap(&plane); + CHECK( !input_openxr_edit.hand[1].hinged + && fabs(gap - 0.026) < 1e-4, + "Snap to Screens off: hinged %d, gap %.4f", + input_openxr_edit.hand[1].hinged, gap); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Carried to 2.5 cm under, a 2 cm gap set, from 3.5 or from a free 6, + * where the plane holds its axes already and only the gap's own snap + * can buzz: the click takes hold with one buzz on the holding hand. */ +static void test_edit_gap_buzz(void) +{ + static const double from[] = { 0.035, 0.06 }; + unsigned i, n; + for (i = 0; i < 2; i++) + { + t_take_gap(from[i], false); + t_clock += T_BUZZ_CLEAR; + n = t_applies[2]; + t_aim[2].position.y += (float)(from[i] - 0.025); + t_tilt_polls(3, 0.0f, false); + CHECK(t_applies[2] == n + 1, + "%u buzzes as the gap clicked from %.3f under, want 1", + t_applies[2] - n, from[i]); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* A hinge taking hold buzzes once on the holding hand, longer and + * stronger than a tick; held there, no more. */ +static void test_edit_hinge_buzz(void) +{ + unsigned n, l, left; + CHECK( T_TICK_NS == INPUT_OPENXR_SNAP_TICK_NS + && (retro_time_t)T_BUZZ_CLEAR * 1000 > INPUT_OPENXR_HINGE_BUZZ_NS, + "the fake's tick or the harness's wait is off"); + t_snap = true; + t_edit_start(); + t_snap_under(0.0); + t_tilt_polls(1, 0.0f, false); + CHECK(!input_openxr_edit.hand[1].hinged, "hinged as it was taken"); + n = t_applies[2]; + l = t_longs[2]; + left = t_applies[1]; + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + CHECK( input_openxr_edit.hand[1].hinged && t_applies[2] == n + 1 + && t_longs[2] == l + 1 && t_dur[2] == INPUT_OPENXR_HINGE_BUZZ_NS + && t_near(t_amp[2], INPUT_OPENXR_HINGE_BUZZ_AMP) + && t_applies[1] == left, + "hinged %d: %u buzzes, %u long, the last %lld ns at %.2f", + (int)input_openxr_edit.hand[1].hinged, t_applies[2] - n, + t_longs[2] - l, (long long)t_dur[2], t_amp[2]); + t_tilt_polls(20, 0.0f, false); + CHECK(t_applies[2] == n + 1, "held on the hinge: %u buzzes", + t_applies[2] - n); + t_take_end(); +} + +/* Ticks due while a hinge's buzz plays are skipped, never replayed: + * taken 6 cm under, a free gap, screen 2 hinges with the long buzz; + * carried into the 2 cm click 30 ms on, it clicks with no tick, the + * click spent, and none once the buzz is over; out of the click's pull + * and back, past the buzz, the click ticks. */ +static void test_edit_hinge_buzz_skip(void) +{ + unsigned n = t_applies[2]; + unsigned l = t_longs[2]; + t_take_gap(0.06, false); + CHECK( input_openxr_edit.hand[1].hinged && t_applies[2] == n + 1 + && t_longs[2] == l + 1, + "taken 6 cm under: hinged %d, %u buzzes, %u long", + (int)input_openxr_edit.hand[1].hinged, t_applies[2] - n, + t_longs[2] - l); + t_aim[2].position.y += 0.035f; + t_tilt_polls(3, 0.0f, false); + CHECK( t_applies[2] == n + 1 + && !(input_openxr_edit.hand[1].armed & VIDEO_XR_SNAP_GAP), + "clicked inside the buzz: %u buzzes, armed %#x", t_applies[2] - n, + input_openxr_edit.hand[1].armed); + t_tilt_polls(20, 0.0f, false); + CHECK(t_applies[2] == n + 1, "after the buzz: %u buzzes", + t_applies[2] - n); + t_aim[2].position.y -= 0.035f; + t_tilt_polls(3, 0.0f, false); + t_aim[2].position.y += 0.035f; + t_tilt_polls(3, 0.0f, false); + CHECK( t_applies[2] == n + 2 && t_longs[2] == l + 1 + && t_dur[2] == INPUT_OPENXR_SNAP_TICK_NS, + "out of the click and back: %u buzzes, %u long", + t_applies[2] - n, t_longs[2] - l); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Swung past 90 and back, its lid kept: the hinge does not buzz again. */ +static void test_edit_hinge_buzz_flick(void) +{ + unsigned l; + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF; + t_take_under(80.0, false); + CHECK(input_openxr_edit.hand[1].hinged, "not hinged at 80"); + l = t_longs[2]; + t_tilt_polls(16, 1.0f, false); + CHECK( !input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "past 90: hinged %d, lidded %d", + (int)input_openxr_edit.hand[1].hinged, + (int)input_openxr_edit.hand[1].lidded); + t_tilt_polls(16, -1.0f, false); + CHECK(input_openxr_edit.hand[1].hinged && t_longs[2] == l, + "back from past 90: hinged %d, %u long buzzes", + (int)input_openxr_edit.hand[1].hinged, t_longs[2] - l); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; +} + +/* Carried 4 cm off screen 1's plane, out of the stick's 3 cm reach, + * its lid and mark kept for 150 ms: back on, no buzz. 5 cm off for 150 + * ms, its mark gone and the lid with it: back on, the hinge buzzes + * again. */ +static void test_edit_hinge_buzz_again(void) +{ + unsigned l = t_longs[2]; + t_take_under(0.0, false); + CHECK(input_openxr_edit.hand[1].hinged && t_longs[2] == l + 1, + "taken: hinged %d, %u long buzzes", + (int)input_openxr_edit.hand[1].hinged, t_longs[2] - l); + l = t_longs[2]; + t_aim[2].position.z += 0.04f; + t_tilt_polls(1, 0.0f, false); + CHECK( !input_openxr_edit.hand[1].hinged + && input_openxr_edit.hand[1].lidded, + "4 cm off: hinged %d, lidded %d", + (int)input_openxr_edit.hand[1].hinged, + (int)input_openxr_edit.hand[1].lidded); + t_aim[2].position.z -= 0.04f; + t_tilt_polls(1, 0.0f, false); + CHECK(input_openxr_edit.hand[1].hinged && t_longs[2] == l, + "back from 4 cm: hinged %d, %u long buzzes", + (int)input_openxr_edit.hand[1].hinged, t_longs[2] - l); + t_aim[2].position.z += 0.05f; + t_tilt_polls(16, 0.0f, false); + CHECK( !input_openxr_edit.hand[1].lidded + && !input_openxr_edit.hand[1].buzzed, + "5 cm off 150 ms: lidded %d, buzzed %d", + (int)input_openxr_edit.hand[1].lidded, + (int)input_openxr_edit.hand[1].buzzed); + t_aim[2].position.z -= 0.05f; + t_tilt_polls(1, 0.0f, false); + CHECK( input_openxr_edit.hand[1].hinged && t_longs[2] == l + 1 + && t_dur[2] == INPUT_OPENXR_HINGE_BUZZ_NS, + "back from 5 cm: hinged %d, %u long buzzes", + (int)input_openxr_edit.hand[1].hinged, t_longs[2] - l); + t_take_end(); +} + +/* Screen 2 carried into its hinge on screen 1; the left hand then takes + * screen 1, which ends the hinge: no lid, its buzz spent no more. + * Screen 1 let go 5 mm higher, screen 2 hinges on it anew, with the + * long buzz once. */ +static void test_edit_hinge_take_target(void) +{ + unsigned i, l; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_take_under(0.0, false); + l = t_longs[2]; + CHECK(hold->hinged && hold->lidded && hold->buzzed, + "carried in: hinged %d, lidded %d, buzzed %d", (int)hold->hinged, + (int)hold->lidded, (int)hold->buzzed); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 && !hold->hinged + && !hold->lidded && !hold->buzzed, + "screen 1 taken: hinged %d, lidded %d, buzzed %d", + (int)hold->hinged, (int)hold->lidded, (int)hold->buzzed); + t_aim[1].position.y += 0.005f; + t_quads.quads[0].pose.position.y += 0.005f; + t_clock += 10000; + t_edit_poll(); + for (i = 0; i < 3; i++) + { + t_tilt_polls(1, 0.0f, false); + CHECK(hold->hinged && t_longs[2] == l + 1, + "screen 1 let go 5 mm up, poll %u: hinged %d, %u long buzzes", + i, (int)hold->hinged, t_longs[2] - l); + } + t_aim[2].position.x = 0.0f; + t_tilt_polls(3, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(3, 0.0f, false); + CHECK(hold->hinged && t_longs[2] == l + 1, + "carried along again: hinged %d, %u long buzzes", + (int)hold->hinged, t_longs[2] - l); + t_take_end(); +} + +/* Screen 2 carried in its hinge under screen 1: flat at Hinge Gap 0 + * and folded 30 at Gap 2, each pushed 2.5 cm past touching; flat at + * Gap 0 pushed through to 4 cm; taken up where it rests touching flat + * at Gap 1; swung square at the corner and pushed 3 mm on. + * The left hand takes screen 1: + * screen 2 stays where it shows, to the snap's ties, that poll and the + * next, then carried on 1 cm it moves as the hand. */ +static void test_edit_hinge_take_hop(void) +{ + unsigned i, k; + for (i = 0; i < 5; i++) + { + video_screen_xr_t was; + double hop = 0.0; + double turn = 0.0; + t_gap = (i == 1) ? VIDEO_OPENXR_EDIT_HINGE_GAP_2 + : (i == 3) ? VIDEO_OPENXR_EDIT_HINGE_GAP_1 + : VIDEO_OPENXR_EDIT_HINGE_GAP_0; + t_snap = true; + if (i < 4) + t_edit_start(); + if (i == 4) + { + t_take_square(true, false); + t_turn_polls(16, -1.0f, 10000, false); + t_turn_polls(1, 0.0f, 10000, false); + t_aim[2].position.x -= 0.003f; + t_turn_polls(2, 0.0f, 10000, false); + } + else if (i == 3) + { + t_rest_under(0.0); + t_tilt_polls(2, 0.0f, false); + } + else + { + double y; + t_snap_under((i == 1) ? 30.0 : 0.0); + y = t_aim[2].position.y; + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.y = (float)(y + ((i == 2) ? 0.04 : 0.025)); + t_tilt_polls(2, 0.0f, false); + } + CHECK(input_openxr_edit.hand[1].hinged == (i != 2), + "case %u, carried: hinged %d", i, + (int)input_openxr_edit.hand[1].hinged); + was = t_xr[1]; + for (k = 0; k < 2; k++) + { + double d[6]; + unsigned j; + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + d[0] = t_xr[1].x - was.x; + d[1] = t_xr[1].y - was.y; + d[2] = t_xr[1].z - was.z; + d[3] = t_xr[1].yaw - was.yaw; + d[4] = t_xr[1].pitch - was.pitch; + d[5] = t_xr[1].roll - was.roll; + for (j = 0; j < 6; j++) + { + double *w = (j < 3) ? &hop : &turn; + if (fabs(d[j]) > *w) + *w = fabs(d[j]); + } + } + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 + && !input_openxr_edit.hand[1].hinged + && hop <= 1e-5 && turn <= 1e-3, + "case %u, screen 1 taken: slot %u, hinged %d, hopped %.3f mm, " + "turned %.4f deg", i, input_openxr_edit.hand[0].g.slot, + (int)input_openxr_edit.hand[1].hinged, hop * 1e3, turn); + was = t_xr[1]; + t_aim[2].position.x += 0.01f; + t_let_go(); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( fabs(t_xr[1].x - was.x - 0.01) < 1e-5 + && fabs(t_xr[1].y - was.y) < 1e-5 + && fabs(t_xr[1].z - was.z) < 1e-5, + "case %u, carried on 1 cm: moved %.5f, %.5f, %.5f", i, + t_xr[1].x - was.x, t_xr[1].y - was.y, t_xr[1].z - was.z); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Carried back into screen 1's plane from 5 cm in front at 1 cm/s, the + * hand shaking 3 mm either way: one long buzz as it hinges, however + * often the edge wobbles across the 3 cm where the hinge takes hold. */ +static void test_edit_hinge_buzz_depth(void) +{ + static const double hz[3] = { 4.3, 7.1, 10.9 }; + unsigned i, j, l; + float z; + t_take_under(0.0, false); + t_aim[2].position.z += 0.05f; + t_tilt_polls(1, 0.0f, false); + CHECK(!input_openxr_edit.hand[1].hinged, "5 cm off: hinged"); + l = t_longs[2]; + z = t_aim[2].position.z; + for (i = 0; i < 500; i++) + { + double shake = 0.0; + for (j = 0; j < 3; j++) + shake += sin(2.0 * M_PI * hz[j] * i * 0.01 + 5.1 * (j + 1)); + t_aim[2].position.z = (float)(z - 0.0001 * i + 0.003 * shake / 1.7); + t_tilt_polls(1, 0.0f, false); + } + t_aim[2].position.z = z - 0.05f; + t_tilt_polls(1, 0.0f, false); + CHECK(input_openxr_edit.hand[1].hinged && t_longs[2] == l + 1, + "carried in: hinged %d, %u long buzzes", + (int)input_openxr_edit.hand[1].hinged, t_longs[2] - l); + t_take_end(); +} + +/* A hinge is a new attach unless the hold's mark is that hinge, its + * buzz spent: the same target, its edge and the held screen's. */ +static void test_edit_hinge_attach(void) +{ + input_openxr_hold_t hold; + video_xr_hinge_t k; + memset(&hold, 0, sizeof(hold)); + memset(&k, 0, sizeof(k)); + k.slot = 0; + k.on = 0; + k.edge = 0; + CHECK(input_openxr_edit_attach(&hold, &k), "no mark: no attach"); + hold.mark = k; + hold.buzzed = true; + CHECK(!input_openxr_edit_attach(&hold, &k), "its own mark: an attach"); + k.slot = 2; + CHECK(input_openxr_edit_attach(&hold, &k), "another screen: no attach"); + k.slot = 0; + k.on = 2; + CHECK(input_openxr_edit_attach(&hold, &k), "another edge: no attach"); + k.on = 0; + k.edge = 3; + CHECK(input_openxr_edit_attach(&hold, &k), + "another held edge: no attach"); +} + +/* Edit Smoothing: the frame loop steps each hand's aim once a frame, + * starting from the raw aim, whichever hook asks first, and publishes + * it; losing the hand, a recenter or a jump start it afresh. The main + * thread grabs and lets go from the published aim, so a screen is saved + * where the frame loop last showed it. Off: the raw aim, nothing + * published. */ +static void test_edit_smoothing(void) +{ + unsigned i, n; + video_xr_pose_t a, b, want; + video_xr_grab_t held; + video_screen_xr_t s; + video_xr_quad_set_t set; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local = XR_NULL_HANDLE; + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + t_edit_start(); + local = t_xr_session.local_space; + t_time += 11111111; + CHECK(t_hooks.aim(t_hooks.user, 1, local, t_time, &a) + && a.position.x == 0.0f, "the first frame's aim at %.4f", + a.position.x); + /* The hand 1 cm right: the next frame's aim goes part way, and the + * layers in that frame get the same, not a second step. */ + t_aim[2].position.x = 0.01f; + t_time += 11111111; + CHECK(t_hooks.aim(t_hooks.user, 1, local, t_time, &a) + && a.position.x > 0.0005f && a.position.x < 0.009f, + "smoothed to %.4f", a.position.x); + n = t_aims_published; + set = t_quads; + t_hooks.frame_layers(t_hooks.user, t_time, &set, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + t_hooks.aim(t_hooks.user, 1, local, t_time, &b); + CHECK(b.position.x == a.position.x && t_aims_published == n + 1 + && (t_aims.valid & 3u) == 3u + && t_aims.pose[1].position.x == a.position.x, + "stepped again to %.4f, %u publishes, valid %#x", + b.position.x, t_aims_published - n, t_aims.valid); + /* The main thread grabs from the published aim. */ + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD + && t_near(g->rel.position.x, -a.position.x), + "taken from %.4f", -g->rel.position.x); + /* The hand moves on and holds there 90 ms: what the frame loop + * shows is what is let go. */ + t_aim[2].position.x = 0.05f; + t_time += 11111111; + t_hooks.aim(t_hooks.user, 1, local, t_time, &a); + for (i = 0; i < 9; i++) + { + t_clock += 10000; + t_edit_poll(); + } + held = *g; + video_xr_grab_pose(&held, &a, NULL, 0, NULL, &want, &s, NULL); + t_let_go(); + t_edit_poll(); + CHECK(t_near(t_xr[0].x, s.x) && t_xr[0].x < 0.045f, + "let go at %.4f, shown at %.4f", t_xr[0].x, s.x); + /* Lost, then back 10 cm on: the raw aim at once. */ + t_aim_tracked[2] = false; + t_time += 11111111; + CHECK(!t_hooks.aim(t_hooks.user, 1, local, t_time, &a) + && !(t_aims.valid & 2u), "untracked, still published"); + t_aim_tracked[2] = true; + t_aim[2].position.x = 0.15f; + t_time += 11111111; + t_hooks.aim(t_hooks.user, 1, local, t_time, &a); + CHECK(a.position.x == 0.15f, "back at %.4f", a.position.x); + /* A recenter: afresh. */ + t_aim[2].position.x = 0.16f; + t_recenters++; + t_time += 11111111; + t_hooks.aim(t_hooks.user, 1, local, t_time, &a); + CHECK(a.position.x == 0.16f, "after a recenter at %.4f", + a.position.x); + /* 0.6 m in a frame: afresh. */ + t_aim[2].position.x = 0.76f; + t_time += 11111111; + t_hooks.aim(t_hooks.user, 1, local, t_time, &a); + CHECK(a.position.x == 0.76f, "after a jump at %.4f", a.position.x); + t_edit_end(); + + /* Past High, as a config may hold: Medium. */ + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_LAST; + t_edit_start(); + t_time += 11111111; + t_hooks.aim(t_hooks.user, 1, local, t_time, &a); + t_aim[2].position.x = 0.01f; + t_time += 11111111; + CHECK(t_hooks.aim(t_hooks.user, 1, local, t_time, &a) + && a.position.x > 0.0005f && a.position.x < 0.009f + && (t_aims.valid & 2u), "past High: at %.4f, valid %#x", + a.position.x, t_aims.valid); + t_edit_end(); + + /* Off: the raw aim, nothing published. */ + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_OFF; + t_edit_start(); + n = t_aims_published; + t_aim[2].position.x = 0.01f; + t_time += 11111111; + CHECK(t_hooks.aim(t_hooks.user, 1, local, t_time, &a) + && a.position.x == 0.01f && t_aims_published == n, + "off: at %.4f, %u publishes", a.position.x, t_aims_published - n); + t_edit_end(); +} + +/* Let go with the hand still moving: the release poll's own aim is not + * used, so with no time passing it stays where the poll before put it; + * the stick moves nothing then. Untracked at the release, it stays + * where the last poll put it. */ +static void test_edit_release_moving(void) +{ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.3f; + t_edit_poll(); + t_let_go(); + t_hold("edit/stick", 2, 1.0f, 1.0f); + t_aim[2].position.x = 0.5f; + t_edit_poll(); + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && t_near(t_grabs.let_go[0].last.position.x, 0.3f) + && t_near(t_grabs.let_go[0].last.position.z, -1.8f) + && t_near(t_grabs.let_go[0].width, 1.6f) + && t_near(t_xr[0].x, 0.3f) && t_near(t_xr[0].z, 0.0f) + && t_near(t_xr[0].scale, 1.0f), + "let go moving: at %.3f, saved x %.3f z %.3f scale %.3f", + t_grabs.let_go[0].last.position.x, t_xr[0].x, t_xr[0].z, + t_xr[0].scale); + t_let_go(); + t_aim[2].position.x = 0.0f; + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.4f; + t_edit_poll(); + t_let_go(); + t_aim_tracked[2] = false; + t_aim[2].position.x = 0.6f; + t_edit_poll(); + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && t_near(t_grabs.let_go[0].last.position.x, 0.4f) + && t_near(t_xr[0].x, 0.4f), + "let go untracked: at %.3f, saved %.3f", + t_grabs.let_go[0].last.position.x, t_xr[0].x); + t_edit_end(); +} + +/* a and b the same place to 0.1 mm, and turn to 1e-4 of a quaternion. */ +static bool t_pose_close(const video_xr_pose_t *a, const video_xr_pose_t *b) +{ + const video_xr_quat_t *p = &a->orientation; + const video_xr_quat_t *q = &b->orientation; + double s = ((double)p->x * q->x + (double)p->y * q->y + + (double)p->z * q->z + (double)p->w * q->w) < 0.0 ? -1.0 : 1.0; + return fabs((double)a->position.x - b->position.x) < 1e-4 + && fabs((double)a->position.y - b->position.y) < 1e-4 + && fabs((double)a->position.z - b->position.z) < 1e-4 + && fabs(p->x - s * q->x) < 1e-4 && fabs(p->y - s * q->y) < 1e-4 + && fabs(p->z - s * q->z) < 1e-4 && fabs(p->w - s * q->w) < 1e-4; +} + +/* Hinged exactly at Hinge Gap 2 under screen 1, flat and folded 30 + * degrees, and held still: the grip opening turns the aim in pitch or + * yaw, or, which the hinge holds anyway, in roll or across. Let go, it + * rests hinged at the gap, saved where it is let go. */ +static void test_edit_release_twitch(void) +{ + /* Pitch, yaw and roll in degrees, and metres across. */ + static const double twitch[][4] = { + { 0.4, 0.0, 0.0, 0.0 }, { -0.4, 0.0, 0.0, 0.0 }, + { 0.6, 0.0, 0.0, 0.0 }, { -0.6, 0.0, 0.0, 0.0 }, + { 0.0, 0.4, 0.0, 0.0 }, { 0.0, -0.4, 0.0, 0.0 }, + { 0.0, 1.0, 0.0, 0.0 }, { 0.0, -1.0, 0.0, 0.0 }, + { 0.0, 2.0, 0.0, 0.0 }, { 0.0, -2.0, 0.0, 0.0 }, + { 0.0, 0.0, 2.0, 0.0 }, { 0.0, 0.0, -2.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.01 }, { 0.0, 0.0, 0.0, -0.01 } }; + unsigned i, j; + const video_xr_pose_t *at = &t_grabs.let_go[1].last; + for (j = 0; j < 2; j++) + for (i = 0; i < sizeof(twitch) / sizeof(twitch[0]); i++) + { + bool hinged; + double gap, plane; + video_xr_pose_t saved; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_2, j * 30.0, 0.02); + t_tilt_polls(9, 0.0f, false); + hinged = input_openxr_edit.hand[1].hinged; + t_turn_aim(twitch[i][0], twitch[i][1], twitch[i][2]); + t_aim[2].position.y += (float)twitch[i][3]; + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &saved); + CHECK( hinged && t_grabs.num_rests == 1 + && fabs(gap - 0.02) < 1e-5 && t_pose_close(&saved, at), + "twitch, fold %u, pitch %+.1f yaw %+.1f roll %+.1f across " + "%+.0f mm: hinged %d, %u at rest, gap %.5f, saved at " + "%.4f,%.4f,%.4f, let go at %.4f,%.4f,%.4f", j * 30, + twitch[i][0], twitch[i][1], twitch[i][2], + twitch[i][3] * 1000.0, (int)hinged, t_grabs.num_rests, gap, + saved.position.x, saved.position.y, saved.position.z, + at->position.x, at->position.y, at->position.z); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 1 taken at x 0 and carried n polls us apart, the hand dx + * further along x each, the last lost of them untracked, then let go + * with the hand still moving: where it is let go, and in *hand where + * the hand was then. */ +static double t_rewind(unsigned n, retro_time_t us, double dx, + unsigned lost, double *hand) +{ + unsigned i; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += us; + t_edit_poll(); + for (i = 1; i <= n + 1; i++) + { + if (i > n) + t_let_go(); + if (i + lost > n) + t_aim_tracked[2] = false; + t_aim[2].position.x = (float)(dx * i); + t_clock += us; + t_edit_poll(); + } + *hand = t_aim[2].position.x; + return t_grabs.let_go[0].last.position.x; +} + +/* A grip let go puts its item where it showed the newest trail entry + * at least INPUT_OPENXR_EDIT_REWIND_US back, entries at least + * INPUT_OPENXR_EDIT_TRAIL_US apart, untracked polls adding none: past + * the hand's twitch as the grip opens. A hold shorter than that lets go + * where it was taken. The size is the rewound entry's too. */ +static void test_edit_release_rewind(void) +{ + unsigned i; + double x, hand, gap, plane, edge; + float width, scale, grown; + video_xr_pose_t saved; + x = t_rewind(30, 10000, 0.001, 0, &hand); + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && fabs(hand - x - 0.008) < 1e-5 && fabs(t_xr[0].x - x) < 1e-5, + "rewind at 10 ms polls: let go at %.4f, saved %.4f, the hand at " + "%.4f, want 8 mm short", x, t_xr[0].x, hand); + t_edit_end(); + x = t_rewind(300, 1000, 0.0001, 0, &hand); + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && hand - x > 0.008 - 1e-5 && hand - x < 0.0089 + 1e-5 + && fabs(t_xr[0].x - x) < 1e-5, + "rewind at 1 ms polls: let go at %.5f, saved %.5f, the hand at " + "%.5f, want 8.0 to 8.9 mm short", x, t_xr[0].x, hand); + t_edit_end(); + x = t_rewind(5, 10000, 0.001, 0, &hand); + CHECK(fabs(x) < 1e-5 && fabs(t_xr[0].x) < 1e-5, + "held 50 ms: let go at %.4f, saved %.4f, want where it was taken", + x, t_xr[0].x); + t_edit_end(); + x = t_rewind(30, 10000, 0.001, 3, &hand); + CHECK(fabs(x - 0.023) < 1e-5 && fabs(t_xr[0].x - x) < 1e-5, + "untracked for 3 polls: let go at %.4f, saved %.4f, want 0.023", + x, t_xr[0].x); + t_edit_end(); + + /* Grown on the stick for the last 50 ms: undone. */ + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + for (i = 0; i < 10; i++) + { + t_clock += 10000; + t_edit_poll(); + } + width = input_openxr_edit.hand[1].g.width; + scale = t_xr[0].scale; + t_hold("edit/stick", 2, 1.0f, 0.0f); + for (i = 0; i < 5; i++) + { + t_clock += 10000; + t_edit_poll(); + } + grown = input_openxr_edit.hand[1].g.width; + t_let_go(); + t_clock += 10000; + t_edit_poll(); + CHECK( grown > 1.65f && t_near(t_grabs.let_go[0].width, width) + && t_near(t_xr[0].scale, scale) + && t_near(t_grabs.let_go[0].last.position.x, 0.0f), + "grown to %.3f as let go: %.3f wide, scale %.3f, at %.3f, want " + "%.3f, %.3f", grown, t_grabs.let_go[0].width, t_xr[0].scale, + t_grabs.let_go[0].last.position.x, width, scale); + t_edit_end(); + + /* Hinged at Hinge Gap 2, then grown in the grip's last 80 ms: let go + * as it showed before, its edge at the gap. */ + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.0, 0.02); + t_tilt_polls(9, 0.0f, false); + width = input_openxr_edit.hand[1].g.width; + scale = t_xr[1].scale; + t_hold("edit/stick", 2, 1.0f, 0.0f); + for (i = 0; i < 7; i++) + { + t_clock += 10000; + t_edit_poll(); + } + grown = input_openxr_edit.hand[1].g.width; + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + /* t_under_gap's edge is 0.3 over the middle: flat, the let-go's own + * is half its height. */ + edge = gap + 0.3 - 0.5 * t_grabs.let_go[1].height; + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &saved); + CHECK( grown > width * 1.04f + && t_near(t_grabs.let_go[1].width, width) + && t_near(t_xr[1].scale, scale) && fabs(gap - 0.02) < 1e-5 + && fabs(edge - 0.02) < 1e-5 && t_grabs.num_rests == 1 + && t_pose_close(&saved, &t_grabs.let_go[1].last), + "hinged, grown to %.3f as let go: %.3f wide, scale %.3f, want " + "%.3f, %.3f; gap %.5f, its edge %.5f, %u at rest, saved at " + "%.4f,%.4f,%.4f, let go at %.4f,%.4f,%.4f", grown, + t_grabs.let_go[1].width, t_xr[1].scale, width, scale, gap, edge, + t_grabs.num_rests, + saved.position.x, saved.position.y, saved.position.z, + t_grabs.let_go[1].last.position.x, + t_grabs.let_go[1].last.position.y, + t_grabs.let_go[1].last.position.z); + t_take_end(); + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Frames and polls 10 ms apart, then none for 85 or 340 ms while the + * hand moves 5 cm on, then again: let go 20 ms after, the rewind stops + * at the stall, never stepping back across it. */ +static void test_edit_release_stall(void) +{ + static const retro_time_t stall[] = { 85000, 340000 }; + unsigned i, j; + for (j = 0; j < sizeof(stall) / sizeof(stall[0]); j++) + { + double x; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + for (i = 1; i <= 20; i++) + { + t_aim[2].position.x = (float)(0.001 * i); + t_clock += 10000; + t_edit_poll(); + } + t_aim[2].position.x = 0.07f; + t_clock += stall[j]; + t_edit_poll(); + t_aim[2].position.x = 0.071f; + t_clock += 10000; + t_edit_poll(); + t_let_go(); + t_aim[2].position.x = 0.072f; + t_clock += 10000; + t_edit_poll(); + x = t_grabs.let_go[0].last.position.x; + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && fabs(x - 0.07) < 1e-5 && fabs(t_xr[0].x - x) < 1e-5, + "stalled %u ms: let go at %.4f, saved %.4f, want 0.070, not " + "before the stall (0.020)", (unsigned)(stall[j] / 1000), x, + t_xr[0].x); + t_edit_end(); + } +} + +/* 120 Hz frames seen by 60 Hz polls, one a frame on and the next three, + * the hand twitching as the grip opens: no stall, so the let-go looks + * the whole 80 ms back, to where it showed before the twitch. */ +static void test_edit_release_fast_frames(void) +{ + unsigned i; + double x; + t_period = 8333333; + t_edit_start(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 16667; + t_edit_poll(); + for (i = 1; i <= 20; i++) + { + t_aim[2].position.x = (float)(i <= 18 ? 0.001 * i + : (i == 19 ? 0.03 : 0.04)); + t_clock += i == 18 ? 8333 : (i == 19 ? 25000 : 16667); + t_edit_poll(); + } + t_let_go(); + t_aim[2].position.x = 0.05f; + t_clock += 16667; + t_edit_poll(); + x = t_grabs.let_go[0].last.position.x; + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && fabs(x - 0.016) < 1e-5 && fabs(t_xr[0].x - x) < 1e-5, + "120 Hz frames, 60 Hz polls: let go at %.4f, saved %.4f, want " + "0.016, before the twitch (0.030 on)", x, t_xr[0].x); + t_edit_end(); + t_period = 11111111; +} + +/* Edit Smoothing on: each frame's aims add one trail entry, dated by + * the frame, however many polls see them. */ +static void test_edit_trail_frames(void) +{ + unsigned i, j; + video_xr_pose_t a; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + bool ok = true; + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + t_edit_start(); + t_clock += 10000; + t_frame_aim(&a); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_clock += 8000; + for (i = 1; i <= 6 && ok; i++) + { + XrTime frame; + t_aim[2].position.x = (float)(0.002 * i); + t_clock += 3111; + t_frame_aim(&a); + frame = openxr_frame_predicted_time(&t_xr_frame); + for (j = 0; j < 4; j++) + { + t_clock += 2000; + t_edit_poll(); + } + ok = hold->g.state == VIDEO_XR_GRAB_HELD && hold->trails == i + 1 + && hold->trail[hold->trail_at].t == frame; + CHECK(ok, "frame %u seen by 4 polls: %u entries, the newest dated " + "%.0f, want %u, %.0f", i, hold->trails, + (double)hold->trail[hold->trail_at].t, i + 1, (double)frame); + } + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_OFF; +} + +/* Edit Smoothing on: a hand's trail is dated by the frame its own aim + * is from, not by a newer one the other hand's was stepped for first. */ +static void test_edit_trail_hands(void) +{ + bool ok; + XrTime first, next; + video_xr_pose_t a; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + t_edit_start(); + t_clock += 10000; + t_frame_aim(&a); + first = openxr_frame_predicted_time(&t_xr_frame); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_clock += 10000; + next = openxr_frame_predicted_time(&t_xr_frame); + t_hooks.aim(t_hooks.user, 0, t_xr_session.local_space, next, &a); + t_edit_poll(); + ok = hold->trails == 1 && hold->trail[hold->trail_at].t == first; + CHECK(ok, "the left hand's aim stepped first: %u entries, the newest " + "dated %.0f, want 1, %.0f", hold->trails, + (double)hold->trail[hold->trail_at].t, (double)first); + t_frame_aim(&a); + t_edit_poll(); + CHECK(hold->trails == 2 && hold->trail[hold->trail_at].t == next, + "then the right's: %u entries, the newest dated %.0f, want 2, " + "%.0f", hold->trails, (double)hold->trail[hold->trail_at].t, + (double)next); + t_let_go(); + t_edit_poll(); + t_edit_end(); + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_OFF; +} + +/* Edit Smoothing on: carried at 0.3 m/s, stopped 100 ms before the grip + * opens and grown on the stick meanwhile, screen 1 is let go where it + * shows as the grip opens, that large: smoothing has damped the + * twitch, and a rewind would step back by what it still lagged. */ +static void test_edit_release_smoothed(void) +{ + unsigned i; + video_xr_pose_t a, want; + video_xr_grab_t held; + video_screen_xr_t s; + float scale; + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + t_edit_start(); + t_clock += 10000; + t_frame_aim(&a); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + for (i = 1; i <= 30; i++) + { + if (i <= 20) + t_aim[2].position.x = (float)(0.003 * i); + if (i == 26) + t_hold("edit/stick", 2, 1.0f, 0.0f); + t_clock += 10000; + t_frame_aim(&a); + t_edit_poll(); + } + t_let_go(); + t_clock += 10000; + t_frame_aim(&a); + held = *g; + scale = input_openxr_edit.hand[1].scale; + video_xr_grab_pose(&held, &a, NULL, 0, NULL, &want, &s, NULL); + t_edit_poll(); + CHECK( t_grabs.let_go[0].state == VIDEO_XR_GRAB_RELEASED + && held.width > 1.62f + && t_pose_close(&t_grabs.let_go[0].last, &want) + && fabs(t_xr[0].x - s.x) < 1e-4 + && fabs(t_grabs.let_go[0].width - held.width) < 1e-5 + && fabs(t_xr[0].scale - scale) < 1e-5, + "smoothed let-go at %.4f, %.3f wide, saved x %.4f scale %.4f; " + "want where it shows, %.4f, %.3f wide, x %.4f scale %.4f", + t_grabs.let_go[0].last.position.x, t_grabs.let_go[0].width, + t_xr[0].x, t_xr[0].scale, want.position.x, held.width, s.x, + scale); + t_edit_end(); + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_OFF; +} + +/* As test_edit_release_twitch with Edit Smoothing on, the twitch over + * the grip's last four frames, as a hand opening one does: the hinge + * and the settle still look back past it. */ +static void test_edit_release_smoothed_twitch(void) +{ + /* Pitch and yaw in degrees. */ + static const double twitch[][2] = { + { 0.6, 0.0 }, { -0.6, 0.0 }, { 0.0, 2.0 }, { 0.0, -2.0 } }; + unsigned i, k; + const video_xr_pose_t *at = &t_grabs.let_go[1].last; + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM; + for (i = 0; i < sizeof(twitch) / sizeof(twitch[0]); i++) + { + bool hinged; + double gap, plane, along, x; + video_xr_pose_t a, saved; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.0, 0.02); + t_tilt_polls(40, 0.0f, false); + hinged = input_openxr_edit.hand[1].hinged; + x = t_edge_along(true); + for (k = 0; k < 3; k++) + { + t_turn_aim(twitch[i][0] / 4.0, twitch[i][1] / 4.0, 0.0); + t_tilt_polls(1, 0.0f, false); + } + t_turn_aim(twitch[i][0] / 4.0, twitch[i][1] / 4.0, 0.0); + t_let_go(); + t_clock += 10000; + t_frame_aim(&a); + t_edit_poll(); + gap = t_under_gap(&plane); + along = t_edge_along(true); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &saved); + CHECK( hinged && t_grabs.num_rests == 1 + && fabs(gap - 0.02) < 1e-5 && fabs(along - x) < 1e-3 + && t_pose_close(&saved, at), + "smoothed twitch, pitch %+.1f yaw %+.1f: hinged %d, %u at " + "rest, gap %.5f, along %.5f from %.5f, saved at " + "%.4f,%.4f,%.4f, let go at %.4f,%.4f,%.4f", twitch[i][0], + twitch[i][1], (int)hinged, t_grabs.num_rests, gap, along, x, + saved.position.x, saved.position.y, saved.position.z, + at->position.x, at->position.y, at->position.z); + t_take_end(); + } + t_smooth = VIDEO_OPENXR_EDIT_SMOOTHING_OFF; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* n polls 10 ms apart, the right hand gripping, and the left on the + * menu's handle too if menu. */ +static void t_menu_polls(unsigned n, bool menu) +{ + unsigned i; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + if (menu) + t_hold("edit/grab", 1, 1.0f, 0.0f); + for (i = 0; i < n; i++) + { + t_clock += 10000; + t_edit_poll(); + } +} + +/* Where the right hand puts screen 2 with no snap: its top edge's + * middle under screen 1's bottom edge, and in *along its x. */ +static double t_hand_gap(double *along) +{ + video_xr_pose_t pose; + video_xr_vec3_t m, w; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + video_xr_grab_pose(&hold->g, &hold->aim, NULL, 0, NULL, &pose, NULL, + NULL); + m.x = 0.0f; + m.y = 0.3f; + m.z = 0.0f; + video_xr_rotate(&pose.orientation, &m, &w); + *along = (double)pose.position.x + w.x; + return -0.48 - ((double)pose.position.y + w.y); +} + +/* The right hand, gripping screen 2, carried in 1 mm steps until it + * puts it apart metres under screen 1 (negative: past touching), its + * middle at x along, as t_hand_gap measures it. */ +static void t_carry_under(double apart, double along, bool menu) +{ + unsigned k, n; + double x; + double dy = t_hand_gap(&x) - apart; + double dx = along - x; + double y0 = t_aim[2].position.y; + double x0 = t_aim[2].position.x; + n = (unsigned)ceil((fabs(dy) > fabs(dx) ? fabs(dy) : fabs(dx)) / 0.001); + for (k = 1; k <= n; k++) + { + t_aim[2].position.y = (float)(y0 + dy * k / n); + t_aim[2].position.x = (float)(x0 + dx * k / n); + t_menu_polls(1, menu); + } +} + +/* Screen 2 hinged under screen 1, flat and folded 30, carried to where + * a row's hand puts it, held still and let go. Within + * VIDEO_XR_HINGE_SETTLE of Hinge Gap 2 it settles at the gap, from the + * stop's hold at Gap 0 touching, and 4 cm off level, level; listed at + * rest and saved there, and taken again it is hinged at once with no + * long buzz; so too pushed through, let go inside its mark's 150 ms. + * Elsewhere it rests where it shows: listed at a free gap, not past 10 + * cm or with Snap to Screens off. The left hand holding the menu + * changes nothing. */ +static void test_edit_settle(void) +{ + /* The Hinge Gap, Snap to Screens, the left hand on the menu; where + * the hand puts it, metres apart and along; the gap let go (-1: as + * it showed); level; how many listed. Taken 7 cm short of where it + * is let go, so its snaps are whole. */ + static const struct + { + unsigned gap; + bool snap, menu; + double apart, along, want; + bool level; + unsigned rests; + } rows[] = { + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.049, 0.07, 0.02, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.035, 0.07, 0.02, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.003, 0.07, 0.02, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.055, 0.07, 0.055, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, true, false, 0.025, 0.07, 0.0, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, true, false, -0.02, 0.07, 0.0, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.02, 0.04, 0.02, + true, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, 0.109, 0.07, -1.0, + false, 0 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, false, -0.035, 0.07, 0.0, + false, 1 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, false, false, 0.049, 0.07, -1.0, + false, 0 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, true, true, 0.049, 0.07, 0.02, + false, 1 } }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, j, k; + for (j = 0; j < 2; j++) + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) + { + bool through, menu, at_once; + unsigned l; + double gap, along, plane, before, x; + video_xr_pose_t saved; + if (rows[i].snap) + t_hinged_along(rows[i].gap, j * 30.0, + rows[i].gap == VIDEO_OPENXR_EDIT_HINGE_GAP_2 ? 0.02 : 0.0, + rows[i].along - 0.07); + else + { + t_gap = rows[i].gap; + t_snap = false; + t_edit_start(); + t_snap_under(j * 30.0); + t_gap_shift(0.02); + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + } + if (rows[i].menu) + { + t_aim[1].position.x = 2.0f; + t_aim[1].position.y = t_handle_y(0.6f); + } + t_carry_under(rows[i].apart, rows[i].along, rows[i].menu); + t_menu_polls(9, rows[i].menu); + through = hold->g.through != 0; + menu = input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == VIDEO_XR_MENU_SLOT; + before = t_under_gap(&plane); + x = t_edge_along(true); + t_let_go(); + if (rows[i].menu) + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + along = t_edge_along(true); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &saved); + CHECK( t_grabs.let_go[1].state == VIDEO_XR_GRAB_RELEASED + && menu == rows[i].menu + && (rows[i].apart >= 0.0 || rows[i].want >= 0.0 + || (through && before < 0.0)) + && fabs(gap - (rows[i].want < 0.0 ? before : rows[i].want)) + < 1e-5 + && fabs(along - (rows[i].level ? 0.0 : x)) < 1e-5 + && t_grabs.num_rests == rows[i].rests + && t_pose_close(&saved, &t_grabs.let_go[1].last), + "settle row %u, fold %u: menu held %d, through %d, gap " + "%.5f from %.5f, along %.5f from %.5f, %u at rest, saved " + "at %.4f,%.4f,%.4f, let go at %.4f,%.4f,%.4f", i, j * 30, + (int)menu, (int)through, gap, before, along, x, + t_grabs.num_rests, saved.position.x, saved.position.y, + saved.position.z, t_grabs.let_go[1].last.position.x, + t_grabs.let_go[1].last.position.y, + t_grabs.let_go[1].last.position.z); + /* Shown where it was saved, as the driver publishes it. */ + for (k = 0; k < t_quads.num_quads; k++) + if (t_quads.quads[k].slot == 1) + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], + &t_quads.quads[k].pose); + l = t_longs[2]; + t_menu_polls(2, rows[i].menu); + at_once = hold->hinged; + t_menu_polls(1, rows[i].menu); + CHECK( (hold->g.rest_slot != 0) == (rows[i].rests != 0) + && (!rows[i].rests + || (at_once && hold->hinged && t_longs[2] == l)), + "settle row %u, fold %u, taken again: kept %u, hinged %d " + "then %d, %u long buzzes", i, j * 30, + (unsigned)hold->g.rest_slot, (int)at_once, + (int)hold->hinged, t_longs[2] - l); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2, the left hand's, carried 4.9 cm under screen 1 at Hinge + * Gap 2 and showing its mark, as the right hand takes screen 1 and edit + * mode ends before the left's next poll: let go where it shows, never + * settled on a held screen. */ +static void test_edit_settle_target_held(void) +{ + unsigned k; + bool marked, taken; + double y, before, gap, plane; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[0]; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_2; + t_snap = true; + t_edit_start(); + t_snap_under(0.0); + t_gap_shift(0.02); + y = t_aim[2].position.y; + t_aim[1].position.y = (float)y; + t_aim[2].position.y = 0.2f; + t_hold("edit/grab", 1, 1.0f, 0.0f); + /* 7 cm along, so its snaps are whole, then 2.9 cm under and still. */ + for (k = 1; k <= 108; k++) + { + if (k <= 70) + t_aim[1].position.x = (float)(0.001 * k); + else if (k <= 99) + t_aim[1].position.y = (float)(y - 0.001 * (k - 70)); + t_clock += 10000; + t_edit_poll(); + } + marked = hold->buzzed; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + taken = input_openxr_edit.hand[1].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[1].g.slot == 0 && hold->buzzed; + before = t_under_gap(&plane); + input_openxr_edit_toggle(); + gap = t_under_gap(&plane); + CHECK(marked && taken && before > 0.04 && fabs(gap - before) < 1e-5, + "screen 1 taken as edit mode ends: marked %d, taken %d, gap " + "%.5f from %.5f", (int)marked, (int)taken, gap, before); + t_edit_end(); + t_snap = false; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* As test_edit_release_twitch, from 4.9 cm apart at Hinge Gap 2 and + * from 4 cm off level at the gap, the aim turned 0.6 degrees in pitch + * or yaw as the grip opens: each settles at the gap, level from off + * it, listed and saved there. */ +static void test_edit_settle_twitch(void) +{ + static const double turn[][2] = { { 0.6, 0.0 }, { -0.6, 0.0 }, + { 0.0, 0.6 }, { 0.0, -0.6 } }; + /* The hand's metres apart and along. */ + static const double from[][2] = { { 0.049, 0.07 }, { 0.02, 0.04 } }; + unsigned i, j, k; + for (k = 0; k < 2; k++) + for (j = 0; j < 2; j++) + for (i = 0; i < 4; i++) + { + double gap, along, plane, x; + video_xr_pose_t saved; + t_hinged_along(VIDEO_OPENXR_EDIT_HINGE_GAP_2, j * 30.0, 0.02, + from[k][1] - 0.07); + t_carry_under(from[k][0], from[k][1], false); + t_tilt_polls(9, 0.0f, false); + x = t_edge_along(true); + t_turn_aim(turn[i][0], turn[i][1], 0.0); + t_let_go(); + t_clock += 10000; + t_edit_poll(); + gap = t_under_gap(&plane); + along = t_edge_along(true); + video_xr_item_pose(&t_quads.slots[1].base, &t_xr[1], &saved); + CHECK( t_grabs.num_rests == 1 && fabs(gap - 0.02) < 1e-5 + && fabs(along - (k ? 0.0 : x)) < 1e-5 + && t_pose_close(&saved, &t_grabs.let_go[1].last), + "settle twitch from %.3f apart at %.2f, fold %u, pitch " + "%+.1f yaw %+.1f: %u at rest, gap %.5f, along %.5f from " + "%.5f, saved at %.4f,%.4f,%.4f", from[k][0], from[k][1], + j * 30, turn[i][0], turn[i][1], t_grabs.num_rests, gap, + along, x, saved.position.x, saved.position.y, + saved.position.z); + t_take_end(); + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Where t_shake shakes the right hand about. */ +static XrPosef t_still; + +/* t_still shaken as a hand on a device, for poll k of 10 ms: turned + * deg degrees in pitch and yaw at 8 Hz and a fifth of that at 2.3 Hz, + * the yaw a quarter period behind, and moved 0.3 mm each way. At 1.8 m + * 0.5 degrees is 1.57 cm at the screen: pitch carries screen 2 across + * its hinge under screen 1, yaw along it. */ +static void t_shake(unsigned k, double deg) +{ + double t = k * 0.01; + double a = 2.0 * M_PI * 8.0 * t; + double b = 2.0 * M_PI * 2.3 * t + 1.0; + double m = 0.0003 * sin(k * 1.3); + t_aim[2] = t_still; + t_turn_aim(deg * (sin(a) + 0.2 * sin(b)), + deg * (sin(a - M_PI / 2.0) + 0.2 * sin(b - M_PI / 2.0)), 0.0); + t_aim[2].position.x += (float)m; + t_aim[2].position.y += (float)m; + t_aim[2].position.z += (float)m; +} + +/* The right hand, screen 2 hinged as t_hinged_at left it with the hand + * at y and z putting it gap apart, now putting it d metres past + * touching (out 0) or d in front of screen 1's plane (out 1). */ +static void t_mark_out(unsigned out, double y, double z, double gap, + double d) +{ + t_aim[2].position.y = (float)(out ? y : y + gap + d); + t_aim[2].position.z = (float)(out ? z + d : z); +} + +/* Screen 2 hinged under screen 1 flat at Hinge Gap 0, pushed 3.5 cm + * past touching, out of the stop's hold, or carried 3.5 cm in front of + * screen 1's plane: out 140 ms from the first poll seen out and back to + * 2 cm, its hinge stays marked with no long buzz; 150 ms from it, it + * lets go, and back it takes hold again with one. The other hand + * taking screen 1 ends the mark at once, the hand tracked or not, and + * a lid outliving it; so does screen 1 gone. Untracked polls neither + * keep nor end it; out, a stall or a spell untracked between two + * tracked polls counts as 50 ms. With Snap to Screens off it is never + * marked. */ +static void test_edit_mark_debounce(void) +{ + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + unsigned i, k, l; + for (i = 0; i < 2; i++) + { + double x; + double y = 0.0; + double z = 0.0; + double gap = 0.0; + bool kept = true; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + l = t_longs[2]; + CHECK(hold->buzzed, "out %u: hinged %d, buzzed %d", i, + (int)hold->hinged, (int)hold->buzzed); + t_mark_out(i, y, z, gap, 0.035); + for (k = 1; k <= 15; k++) + { + t_tilt_polls(1, 0.0f, false); + kept = kept && hold->buzzed; + } + t_mark_out(i, y, z, gap, 0.02); + t_tilt_polls(1, 0.0f, false); + CHECK(kept && hold->buzzed && t_longs[2] == l, + "out %u, 140 ms out and back: kept %d, buzzed %d, %u long " + "buzzes", i, (int)kept, (int)hold->buzzed, t_longs[2] - l); + t_mark_out(i, y, z, gap, 0.035); + for (k = 1; k <= 16; k++) + { + t_tilt_polls(1, 0.0f, false); + if (k == 15) + kept = hold->buzzed; + } + CHECK(kept && !hold->buzzed, + "out %u, 150 ms out: buzzed %d at 140 ms, %d at 150", i, + (int)kept, (int)hold->buzzed); + t_mark_out(i, y, z, gap, 0.02); + t_tilt_polls(1, 0.0f, false); + CHECK(hold->buzzed && t_longs[2] == l + 1, + "out %u, back after 150 ms: buzzed %d, %u long buzzes", i, + (int)hold->buzzed, t_longs[2] - l); + t_take_end(); + } + + /* The other hand takes screen 1. */ + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 && !hold->buzzed, + "screen 1 taken: held %u, buzzed %d", + input_openxr_edit.hand[0].g.slot, (int)hold->buzzed); + t_take_end(); + + /* The same with the right hand lost: its mark ends as screen 1 is + * taken, and that hand letting go does not bring it back. */ + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + t_aim_tracked[2] = false; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 && !hold->buzzed + && !t_grabs.hand[1].mark_slot, + "lost, screen 1 taken: held %u, buzzed %d, published %u", + input_openxr_edit.hand[0].g.slot, (int)hold->buzzed, + (unsigned)t_grabs.hand[1].mark_slot); + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state != VIDEO_XR_GRAB_HELD + && hold->g.state == VIDEO_XR_GRAB_HELD && !hold->buzzed + && !t_grabs.hand[1].mark_slot, + "lost, screen 1 let go: holding %d, buzzed %d, published %u", + (int)(hold->g.state == VIDEO_XR_GRAB_HELD), (int)hold->buzzed, + (unsigned)t_grabs.hand[1].mark_slot); + t_aim_tracked[2] = true; + t_take_end(); + + /* Its lid outliving its mark: hinged flush beside screen 1, 12.5 cm + * out and still till the mark has gone. */ + t_take_flush(); + t_aim[2].position.x += 0.125f; + t_turn_polls(16, 0.0f, 10000, false); + CHECK(hold->lidded && !hold->buzzed, + "12.5 cm out 150 ms: lidded %d, buzzed %d", (int)hold->lidded, + (int)hold->buzzed); + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0 && !hold->lidded, + "screen 1 taken under a lid: held %u, lidded %d", + input_openxr_edit.hand[0].g.slot, (int)hold->lidded); + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; + + /* The same with the right hand lost, screen 1 then let go: the lid + * stays gone. */ + { + bool lidded, taken; + t_take_flush(); + t_aim[2].position.x += 0.125f; + t_turn_polls(16, 0.0f, 10000, false); + lidded = hold->lidded && !hold->buzzed; + t_aim_tracked[2] = false; + t_hold("edit/grab", 1, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + taken = input_openxr_edit.hand[0].g.state == VIDEO_XR_GRAB_HELD + && input_openxr_edit.hand[0].g.slot == 0; + t_let_go(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_clock += 10000; + t_edit_poll(); + CHECK( lidded && taken + && input_openxr_edit.hand[0].g.state != VIDEO_XR_GRAB_HELD + && hold->g.state == VIDEO_XR_GRAB_HELD && !hold->lidded, + "lost, screen 1 taken under a lid and let go: lidded %d " + "before, taken %d, holding %d, lidded %d", (int)lidded, + (int)taken, (int)(hold->g.state == VIDEO_XR_GRAB_HELD), + (int)hold->lidded); + t_aim_tracked[2] = true; + t_take_end(); + t_angles = VIDEO_OPENXR_EDIT_SNAP_ANGLES_15; + } + + /* Screen 1 gone from the quads. */ + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + memmove(&t_quads.quads[0], &t_quads.quads[1], + (t_quads.num_quads - 1) * sizeof(t_quads.quads[0])); + t_quads.num_quads--; + t_tilt_polls(1, 0.0f, false); + CHECK(!hold->buzzed, "screen 1 gone: buzzed %d", (int)hold->buzzed); + t_take_end(); + + /* Untracked 300 ms while out, then tracked and still out: the gap + * counts 50 ms, so 100 ms more of tracked polls let it go. */ + { + double x; + double y, z, gap; + bool kept = true; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + t_mark_out(0, y, z, gap, 0.035); + t_tilt_polls(1, 0.0f, false); + t_aim_tracked[2] = false; + for (k = 0; k < 30; k++) + { + t_tilt_polls(1, 0.0f, false); + kept = kept && hold->buzzed; + } + t_aim_tracked[2] = true; + for (k = 1; k <= 11; k++) + { + t_tilt_polls(1, 0.0f, false); + if (k <= 10) + kept = kept && hold->buzzed; + } + CHECK(kept && !hold->buzzed, + "untracked 300 ms out: kept %d to 140 ms seen out, buzzed %d " + "at 150", (int)kept, (int)hold->buzzed); + t_take_end(); + } + + /* A stall while out: 50 ms out, then one poll 200 ms on, which + * counts 50 ms, so 50 ms more of polls let it go. */ + { + double x; + double y, z, gap; + bool stalled; + bool kept = true; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + t_mark_out(0, y, z, gap, 0.035); + t_tilt_polls(6, 0.0f, false); + t_turn_polls(1, 0.0f, 200000, false); + stalled = hold->buzzed; + for (k = 1; k <= 5; k++) + { + t_tilt_polls(1, 0.0f, false); + if (k <= 4) + kept = kept && hold->buzzed; + } + CHECK(stalled && kept && !hold->buzzed, + "stalled 200 ms out: buzzed %d after it, kept %d to 140 ms " + "seen out, buzzed %d at 150", (int)stalled, (int)kept, + (int)hold->buzzed); + t_take_end(); + } + + /* Untracked 200 ms in range, then tracked and out: 150 ms from that + * poll. */ + { + double x; + double y, z, gap; + bool kept; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + t_aim_tracked[2] = false; + t_tilt_polls(20, 0.0f, false); + kept = hold->buzzed; + t_aim_tracked[2] = true; + t_mark_out(0, y, z, gap, 0.035); + for (k = 1; k <= 16; k++) + { + t_tilt_polls(1, 0.0f, false); + if (k <= 15) + kept = kept && hold->buzzed; + } + CHECK(kept && !hold->buzzed, + "untracked 200 ms in range, then out: kept %d to 140 ms, " + "buzzed %d at 150", (int)kept, (int)hold->buzzed); + t_take_end(); + } + + /* Snap to Screens off. */ + { + double x; + double y, z, gap; + bool marked = false; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; + t_snap = false; + t_edit_start(); + t_snap_under(0.0); + t_tilt_polls(1, 0.0f, false); + t_aim[2].position.x = 0.07f; + t_tilt_polls(1, 0.0f, false); + marked = hold->buzzed; + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + for (k = 0; k < 40; k++) + { + t_mark_out(k / 20, y, z, gap, (k % 20 < 10) ? 0.035 : 0.02); + t_tilt_polls(1, 0.0f, false); + marked = marked || hold->buzzed; + } + CHECK(!marked, "Snap to Screens off: marked %d", (int)marked); + t_take_end(); + } +} + +/* Screen 2 hinged under screen 1 flat, its hand shaken as on a device + * (t_shake, 0.5 degrees) for 640 polls, held about 2 mm short of where + * a let-go stops settling, the shake straddling it: the stop's end at + * Hinge Gap 0, 2 and 1, and screen 1's plane, there shaken in depth + * too; short of 10 cm apart. Inside the click's fade and at the settle + * band's end at Gap 2, and at the level band's end, where a let-go + * settles in it either side. Marked there, its mark never flips, with + * one long buzz, and every publish carries it. Swept from 1 cm inside + * the stop's end or the plane's to 2 cm out, past the shake's reach, + * and back, it flips twice, with two. Three frames a poll keep their + * layer count, every layer's index and every outline's crop, but in + * the polls where it flips. */ +static void test_edit_mark_shaken(void) +{ + /* The Hinge Gap; the hand's mean metres apart (negative: past + * touching), along and in front of the plane; swept, across 0 or + * in depth 1, else 2; whether the shake takes it out of the mark's + * range. */ + static const struct + { + unsigned gap; + double apart, along, front; + unsigned sweep; + bool edge; + } spots[] = { + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, -0.028, 0.07, 0.0, 2, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, -0.028, 0.07, 0.0, 2, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_1, -0.033, 0.07, 0.0, 2, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.07, 0.028, 2, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.098, 0.07, 0.0, 2, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.035, 0.07, 0.0, 2, false }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.05, 0.07, 0.0, 2, false }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.02, 0.06, 0.0, 2, false }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, -0.02, 0.07, 0.0, 0, true }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.07, 0.02, 1, true } }; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + video_xr_pose_t pose; + unsigned i, k; + for (i = 0; i < sizeof(spots) / sizeof(spots[0]); i++) + { + t_layout_t lay; + unsigned fails = 0; + unsigned flips = 0; + unsigned pubs = 0; + unsigned counts = 0; + unsigned moved = 0; + unsigned cuts = 0; + unsigned stray = 0; + unsigned l = t_longs[2]; + unsigned want = (spots[i].sweep < 2) ? 2u : 0u; + double y, z; + bool was; + t_hinged_at(spots[i].gap, 0.0, 0.0); + t_carry_under(spots[i].apart, spots[i].along, false); + for (k = 1; k <= (unsigned)(spots[i].front * 1000.0 + 0.5); k++) + { + t_aim[2].position.z += 0.001f; + t_tilt_polls(1, 0.0f, false); + } + t_tilt_polls(2, 0.0f, false); + was = hold->buzzed; + CHECK(was, "spot %u: not marked before the shake", i); + lay.n = 0; + lay.bent = 0; + t_frames(&lay, &counts, &moved); + t_still = t_aim[2]; + y = t_still.position.y; + z = t_still.position.z; + for (k = 0; k < 640; k++) + { + unsigned c; + /* 3 cm further out by poll 320, back by 640. */ + double d = 0.03 * ((k < 320) ? k : 640 - k) / 320.0; + if (spots[i].sweep == 0) + t_still.position.y = (float)(y + d); + else if (spots[i].sweep == 1) + t_still.position.z = (float)(z + d); + t_shake(k, 0.5); + if (spots[i].front > 0.0) + t_aim[2].position.z += (float)(1.8 * tan(0.5 * M_PI / 180.0) + * sin(2.0 * M_PI * 8.0 * k * 0.01)); + t_tilt_polls(1, 0.0f, false); + if (!video_xr_grab_settle(&hold->g, &hold->aim, &t_quads, + input_openxr_edit_holding(&input_openxr_edit), + &hold->mark, &pose)) + fails++; + c = t_frames(&lay, &counts, &moved); + cuts += c; + stray += (c && hold->buzzed == was) ? c : 0u; + flips += (hold->buzzed != was) ? 1u : 0u; + was = hold->buzzed; + if ( t_grabs.hand[1].mark_slot + != (hold->buzzed ? hold->mark.slot + 1u : 0u) + || t_grabs.hand[1].mark_on + != (hold->buzzed ? hold->mark.on : 0u) + || t_grabs.hand[1].mark_edge + != (hold->buzzed ? hold->mark.edge : 0u)) + pubs++; + } + CHECK( flips == want && t_longs[2] - l == (want ? 2u : 1u) + && (fails > 0) == spots[i].edge && !pubs, + "shaken at spot %u: %u flips, %u long buzzes, out %u polls, " + "%u published off its mark", i, flips, t_longs[2] - l, fails, + pubs); + CHECK(!counts && !moved && cuts == want && !stray && !lay.bent, + "shaken at spot %u, its frames: %u with another layer count, " + "%u with layers moved, %u cut otherwise, %u of them in polls " + "it did not flip, want %u; %u with an outline not whole", i, + counts, moved, cuts, stray, want, lay.bent); + t_take_end(); + } + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Edit mode's hinge lines are slots: one for each hand holding a + * screen, hand 0 first, then one for each pair at rest. A slot whose + * line does not show is drawn in place from its screen's middle, at + * rows of the image transparent either way up: a mark taken or let go + * changes no count or index, and a pair whose screen a hand holds is + * hidden in its slot. Short of room the beams go first, then the tag, + * then slots from the end, then outlines. */ +static void test_edit_hinge_slots(void) +{ + unsigned i, n, at, c, k; + int32_t cut; + uint8_t *px; + video_xr_quad_set_t shown; + const XrCompositionLayerQuad *q; + const XrCompositionLayerBaseHeader *layers[OPENXR_MAX_EXTRA_LAYERS]; + XrSpace local; + float sx[4], sy[4]; + unsigned first = 0; + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + + /* Screen 2 held 20 cm under screen 1, then in its hinge, then out + * past its mark's 150 ms: one slot after the outlines throughout. */ + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + local = t_xr_session.local_space; + t_head_tracked = true; + t_cursor = true; + for (i = 0; i < 3; i++) + { + bool on = i == 1; + t_carry_under(on ? 0.0 : 0.2, 0.07, false); + t_tilt_polls(on ? 2 : 20, 0.0f, false); + shown = t_quads; + t_follow(&shown); + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + if (!i) + first = n; + for (at = 0, c = 0; at < n; at++) + c += t_kind(layers[at], &cut) == 3; + q = (const XrCompositionLayerQuad*)layers[4]; + CHECK( hold->buzzed == on + && t_grabs.hand[1].mark_slot == (on ? 1u : 0u) + && n == first && t_under == 2 && c == 1 + && t_kind(layers[4], &cut) == 3 + && q->subImage.imageRect.offset.y + == (on ? VIDEO_XR_HINGE_Y : VIDEO_XR_HINGE_HIDDEN_Y), + "held, %s: marked %d (%u published), %u layers of %u, %u " + "under, %u slots, layer 4 at row %d", on ? "hinged" : "free", + (int)hold->buzzed, t_grabs.hand[1].mark_slot, n, first, + t_under, c, (int)q->subImage.imageRect.offset.y); + } + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_cursor = false; + t_head_tracked = false; + t_take_end(); + + /* Hands 0 and 1 holding screens 4 and 5, screens 2 and 3 resting on + * screen 1's right and bottom edges. */ + t_edit_start(); + local = t_xr_session.local_space; + t_head_tracked = true; + t_cursor = true; + shown = t_quads; + for (i = 2; i < 6; i++) + { + shown.quads[i] = shown.quads[0]; + shown.quads[i].slot = i - 1; + shown.quads[i].screen = i - 1; + } + shown.quads[2].pose.position.x = 1.6f; + shown.quads[3].pose.position.y = -0.96f; + shown.quads[4].pose.position.x = -2.0f; + shown.quads[5].pose.position.x = -2.0f; + shown.quads[5].pose.position.y = 1.2f; + shown.num_quads = 6; + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_xr_frame.grabs.hand[0].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[0].slot = 3; + t_xr_frame.grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + t_xr_frame.grabs.hand[1].slot = 4; + t_xr_frame.grabs.num_rests = 2; + t_xr_frame.grabs.rests[0].slot = 0; + t_xr_frame.grabs.rests[0].edge = 2; + t_xr_frame.grabs.rests[0].target = 1; + t_xr_frame.grabs.rests[0].on = 3; + t_xr_frame.grabs.rests[1].slot = 0; + t_xr_frame.grabs.rests[1].edge = 1; + t_xr_frame.grabs.rests[1].target = 2; + t_xr_frame.grabs.rests[1].on = 0; + n = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + for (at = 0, c = 0; at < n; at++) + c += t_kind(layers[at], &cut) == 3; + CHECK( n > 10 && c == 4 && t_under == 5 + && t_slot_is((const XrCompositionLayerQuad*)layers[7], true, + -2.0f, 0.0f, -1.8f, -2.0f + VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[8], true, + -2.0f, 1.2f, -1.8f, -2.0f + VIDEO_XR_HINGE_WIDTH, 1.2f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[9], false, + 0.8f, -0.48f, -1.8f, 0.8f, 0.48f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[10], false, + -0.8f, -0.48f, -1.8f, 0.8f, -0.48f, -1.8f), + "two hands, two pairs at rest: %u layers, %u under, %u slots; " + "want hand 0's and hand 1's hidden on screens 4 and 5, then both " + "lines, after seven outlines", n, t_under, c); + for (i = 0; i < 4; i++) + { + q = (const XrCompositionLayerQuad*)layers[(n > 10) ? 7 + i : 0]; + sx[i] = q->pose.position.x; + sy[i] = q->pose.position.y; + } + + /* Short of room, one layer at a time: the beams go, then the tag, + * then slots from the end; the outlines last. */ + for (c = n; c >= OPENXR_KEPT_EXTRA_LAYERS; c--) + { + unsigned kinds[7]; + unsigned r = c - OPENXR_KEPT_EXTRA_LAYERS; + unsigned o = (r < 7) ? r : 7; + unsigned l = (r - o < 4) ? r - o : 4; + unsigned t = (r - o - l > 0) ? 1u : 0u; + unsigned b = (r - o - l - t < 2) ? r - o - l - t : 2u; + unsigned j = 0; + unsigned slid = 0; + memset(kinds, 0, sizeof(kinds)); + at = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + c, &t_under); + for (k = 0; k < at; k++) + { + unsigned kind = t_kind(layers[k], &cut); + kinds[kind]++; + if (kind != 3) + continue; + q = (const XrCompositionLayerQuad*)layers[k]; + if ( k != 7 + j || j >= 4 || !t_near(q->pose.position.x, sx[j]) + || !t_near(q->pose.position.y, sy[j])) + slid++; + j++; + } + CHECK( at == o + l + t + b + 2 && kinds[1] + kinds[2] == o + && kinds[3] == l && kinds[5] == t && kinds[4] == b + && kinds[6] == 2 && !slid, + "room for %u: %u layers, %u outlines, %u slots (%u out of " + "place), %u tags, %u beams, %u dots; want %u, %u, %u, %u, %u", + c, at, kinds[1] + kinds[2], kinds[3], slid, kinds[5], kinds[4], + kinds[6], o, l, t, b, 2u); + } + + /* Hand 1 on screen 2 instead: the right edge's pair hidden in its + * slot on screen 1, the count the same. */ + t_xr_frame.grabs.hand[1].slot = 1; + at = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK( at == n && n > 10 + && t_slot_is((const XrCompositionLayerQuad*)layers[7], true, + -2.0f, 0.0f, -1.8f, -2.0f + VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[8], true, + 1.6f, 0.0f, -1.8f, 1.6f + VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[9], true, + 0.0f, 0.0f, -1.8f, VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[10], false, + -0.8f, -0.48f, -1.8f, 0.8f, -0.48f, -1.8f), + "screen 2 held: %u layers of %u, want its pair hidden in place", + at, n); + + /* The bottom edge's pair listed from screen 3, then hand 1 on + * screen 3: that pair hidden in its slot on screen 3, the count the + * same. */ + t_xr_frame.grabs.rests[1].slot = 2; + t_xr_frame.grabs.rests[1].edge = 0; + t_xr_frame.grabs.rests[1].target = 0; + t_xr_frame.grabs.rests[1].on = 1; + for (i = 0; i < 2; i++) + { + bool own = i == 1; + float hx = own ? 0.0f : -2.0f; + float hy = own ? -0.96f : 1.2f; + t_xr_frame.grabs.hand[1].slot = own ? 2 : 4; + at = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + CHECK( at == n && n > 10 + && t_slot_is((const XrCompositionLayerQuad*)layers[7], true, + -2.0f, 0.0f, -1.8f, -2.0f + VIDEO_XR_HINGE_WIDTH, 0.0f, + -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[8], true, + hx, hy, -1.8f, hx + VIDEO_XR_HINGE_WIDTH, hy, -1.8f) + && t_slot_is((const XrCompositionLayerQuad*)layers[9], false, + 0.8f, -0.48f, -1.8f, 0.8f, 0.48f, -1.8f) + && (own + ? t_slot_is((const XrCompositionLayerQuad*)layers[10], true, + 0.0f, -0.96f, -1.8f, VIDEO_XR_HINGE_WIDTH, -0.96f, -1.8f) + : t_slot_is((const XrCompositionLayerQuad*)layers[10], + false, -0.8f, -0.48f, -1.8f, 0.8f, -0.48f, -1.8f)), + "screen 3's pair, screen 3 %s: %u layers of %u, want it %s", + own ? "held" : "free", at, n, + own ? "hidden in place on screen 3" : "shown"); + } + + /* Hand 0 holding screen 4 on screen 1's left edge, hand 1 screen 5 + * on its top, the right edge's pair at rest: each hand's mark taken + * and let go in turn moves no slot and changes no count. */ + shown.quads[4].pose.position.x = -1.6f; + shown.quads[5].pose.position.x = 0.0f; + shown.quads[5].pose.position.y = 0.96f; + memset(&t_grabs, 0, sizeof(t_grabs)); + for (i = 0; i < 2; i++) + { + video_xr_grab_t *g = &t_grabs.hand[i]; + g->state = VIDEO_XR_GRAB_HELD; + g->slot = 3 + i; + g->last = shown.quads[4 + i].pose; + g->width = shown.quads[4 + i].width; + g->height = shown.quads[4 + i].height; + g->mark_on = i ? 0 : 3; + g->mark_edge = i ? 1 : 2; + } + t_grabs.num_rests = 1; + t_grabs.rests[0] = t_xr_frame.grabs.rests[0]; + for (i = 0; i < 5; i++) + { + /* Per hand, hidden on its screen, then its line. */ + static const float ends[2][2][6] = { + { { -1.6f, 0.0f, -1.8f, -1.6f + VIDEO_XR_HINGE_WIDTH, 0.0f, -1.8f }, + { -0.8f, -0.48f, -1.8f, -0.8f, 0.48f, -1.8f } }, + { { 0.0f, 0.96f, -1.8f, VIDEO_XR_HINGE_WIDTH, 0.96f, -1.8f }, + { -0.8f, 0.48f, -1.8f, 0.8f, 0.48f, -1.8f } } }; + static const unsigned marks[5] = { 0, 1, 3, 2, 0 }; + unsigned j; + unsigned m = marks[i]; + unsigned slid = 0; + bool placed; + t_grabs.hand[0].mark_slot = (m & 1u) ? 1 : 0; + t_grabs.hand[1].mark_slot = (m & 2u) ? 1 : 0; + t_follow(&shown); + at = t_hooks.frame_layers(t_hooks.user, t_time, &shown, local, layers, + OPENXR_MAX_EXTRA_LAYERS, &t_under); + if (!i) + first = at; + for (k = 0, c = 0; k < at; k++) + if (t_kind(layers[k], &cut) == 3) + { + c++; + if (k < 7 || k > 9) + slid++; + } + placed = at > 9 && t_slot_is((const XrCompositionLayerQuad*)layers[9], + false, 0.8f, -0.48f, -1.8f, 0.8f, 0.48f, -1.8f); + for (j = 0; j < 2 && placed; j++) + { + unsigned on = (m >> j) & 1u; + const float *e = ends[j][on]; + placed = t_slot_is( + (const XrCompositionLayerQuad*)layers[7 + j], !on, e[0], + e[1], e[2], e[3], e[4], e[5]); + } + CHECK( at == first && c == 3 && !slid && placed + && t_xr_frame.hinged == m, + "marks %#x (shown %#x): %u layers of %u, %u slots, %u out of " + "place, %s", m, t_xr_frame.hinged, at, first, c, slid, + placed ? "each where it shows" : "one not where it shows"); + } + memset(&t_grabs, 0, sizeof(t_grabs)); + memset(&t_xr_frame.grabs, 0, sizeof(t_xr_frame.grabs)); + t_xr_frame.hinged = 0; + t_cursor = false; + t_head_tracked = false; + t_edit_end(); + + /* The hidden rows, either way up: nothing drawn. */ + px = (uint8_t*)malloc((size_t)VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4); + CHECK(px != NULL, "no room for the image"); + if (!px) + return; + video_xr_cursor_pixels(px); + for (i = 0, c = 0; i < VIDEO_XR_HINGE_ROWS; i++) + for (k = VIDEO_XR_HINGE_X + 1; + k < VIDEO_XR_HINGE_X + VIDEO_XR_HINGE_COLS - 1; k++) + { + const uint8_t *a = px + ((size_t)(VIDEO_XR_HINGE_HIDDEN_Y + i) + * VIDEO_XR_CURSOR_W + k) * 4; + const uint8_t *b = px + ((size_t)(VIDEO_XR_CURSOR_H - 1 + - VIDEO_XR_HINGE_HIDDEN_Y - i) * VIDEO_XR_CURSOR_W + k) + * 4; + c += (a[0] | a[1] | a[2] | a[3]) ? 1u : 0u; + c += (b[0] | b[1] | b[2] | b[3]) ? 1u : 0u; + } + free(px); + CHECK(c == 0, "%u texels drawn in the hidden rows", c); +} + +/* Hand 2 lets go: by the grip (how 0), a recenter (1) or edit mode's + * end (2). */ +static void t_let_go_by(unsigned how) +{ + if (how == 1) + input_openxr_edit_release(); + else if (how == 2) + input_openxr_edit_toggle(); + else + { + t_let_go(); + t_clock += 10000; + t_edit_poll(); + } +} + +/* Screen 2 hinged under screen 1 flat, its hand 12 polls out of its + * hinge's range, the mark still shown, and let go by the grip, a + * recenter or edit mode's end: it settles from the newest place it + * would, touching at Hinge Gap 0 pushed 3.5 cm past, at the gap at Gap 2 + * carried 3.5 cm off the plane, listed at rest till edit mode ends. 20 + * polls out, the mark gone, it rests where it shows, none listed. In + * range at Gap 2 and carried 1 cm along in its last 100 ms, the grip + * settles it from 80 ms before; a recenter or edit mode's end from + * where it shows. */ +static void test_edit_mark_tail(void) +{ + static const struct + { + unsigned gap; + unsigned out; + double want; + } rows[] = { + { VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0, 0.0 }, + { VIDEO_OPENXR_EDIT_HINGE_GAP_2, 1, 0.02 } }; + unsigned i, j, how; + for (how = 0; how < 3; how++) + { + for (j = 0; j < 2; j++) + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) + { + double x, y, z, gap, plane, before, held_plane; + unsigned polls = j ? 20u : 12u; + t_hinged_at(rows[i].gap, 0.0, + rows[i].gap == VIDEO_OPENXR_EDIT_HINGE_GAP_2 ? 0.02 : 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + t_mark_out(rows[i].out, y, z, gap, 0.035); + t_tilt_polls(polls, 0.0f, false); + before = t_under_gap(&held_plane); + t_let_go_by(how); + gap = t_under_gap(&plane); + CHECK( t_grabs.let_go[1].state == VIDEO_XR_GRAB_RELEASED + && (j + ? (t_grabs.num_rests == 0 && fabs(gap - before) < 1e-5 + && fabs(plane - held_plane) < 1e-5) + : (t_grabs.num_rests == (how < 2 ? 1u : 0u) + && fabs(gap - rows[i].want) < 1e-5 + && fabs(plane) < 1e-5)), + "tail row %u, %u polls out, let go by %u: %u at rest, " + "gap %.5f from %.5f, plane %.5f from %.5f", i, polls, + how, t_grabs.num_rests, gap, before, plane, held_plane); + t_take_end(); + } + { + double gap, plane, along, x; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_2, 0.0, 0.02); + t_carry_under(0.035, 0.07, false); + t_tilt_polls(9, 0.0f, false); + t_carry_under(0.035, 0.08, false); + x = t_edge_along(true); + t_let_go_by(how); + gap = t_under_gap(&plane); + along = t_edge_along(true); + CHECK( t_grabs.let_go[1].state == VIDEO_XR_GRAB_RELEASED + && fabs(gap - 0.02) < 1e-5 && fabs(plane) < 1e-5 + && (how ? fabs(along - x) < 1e-5 : fabs(along - x) > 0.005) + && t_grabs.num_rests == (how < 2 ? 1u : 0u), + "in range, carried along, let go by %u: gap %.5f, plane " + "%.5f, along %.5f from %.5f, %u at rest", how, gap, plane, + along, x, t_grabs.num_rests); + t_take_end(); + } + } + t_gap = VIDEO_OPENXR_EDIT_HINGE_GAP_0; +} + +/* Screen 2 hinged under screen 1 flat at Hinge Gap 0 and pushed 3.5 cm + * past touching, its mark's 150 ms running, then carried flush right of + * screen 1: that hinge takes the mark at once, with the long buzz, and + * its time out starts afresh. */ +static void test_edit_mark_switch(void) +{ + const input_openxr_hold_t *hold = &input_openxr_edit.hand[1]; + double x, y, z, gap; + bool tail; + unsigned l; + video_xr_hinge_t was; + t_hinged_at(VIDEO_OPENXR_EDIT_HINGE_GAP_0, 0.0, 0.0); + y = t_aim[2].position.y; + z = t_aim[2].position.z; + gap = t_hand_gap(&x); + was = hold->mark; + t_mark_out(0, y, z, gap, 0.035); + t_tilt_polls(5, 0.0f, false); + tail = hold->buzzed && hold->out && !hold->hinged; + l = t_longs[2]; + t_aim[2].position.x = 1.20f; + t_aim[2].position.y = 0.07f; + t_tilt_polls(1, 0.0f, false); + CHECK( tail && hold->hinged && hold->buzzed && !hold->out + && hold->mark.slot == 0 && hold->mark.edge != was.edge + && t_longs[2] == l + 1 && t_dur[2] == INPUT_OPENXR_HINGE_BUZZ_NS, + "switched mid-tail: from the tail %d, hinged %d, buzzed %d, out " + "%d, edge %u from %u, %u long buzzes", (int)tail, + (int)hold->hinged, (int)hold->buzzed, (int)(hold->out != 0), + hold->mark.edge, was.edge, t_longs[2] - l); + t_take_end(); +} + +/* A hand lets go and grips the same item again before quads planned at + * the release come: it takes the item where the headset shows it, where + * the hand left it, not where the last quads had it. Once they come, + * the let-go hold is theirs: a reset since shows. */ +static void test_edit_regrab(void) +{ + const video_xr_grab_t *g = &input_openxr_edit.hand[1].g; + t_edit_start(); + t_quads_seq = t_edit_seq; + t_quads_behind = true; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + t_aim[2].position.x = 0.3f; + t_edit_poll(); + t_let_go(); + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK( g->state == VIDEO_XR_GRAB_HELD && g->slot == 0 + && t_near(g->last.position.x, 0.3f) + && t_near(t_grabs.hand[1].last.position.x, 0.3f), + "regrabbed at %.3f, not where it was let go", g->last.position.x); + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.3f), "the regrab moved it to %.3f", t_xr[0].x); + t_let_go(); + t_edit_poll(); + /* The quads catch up, screen 1 since reset: they show it. */ + t_quads_behind = false; + config_set_video_screen_xr(0, NULL); + t_aim[2].position.x = 0.0f; + t_edit_poll(); + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(g->state == VIDEO_XR_GRAB_HELD && t_near(g->last.position.x, 0.0f), + "caught up, the let-go hold still applied: at %.3f", + g->last.position.x); + t_edit_end(); +} + +/* The simple controller: grab and the menu, no tilt, stick or reset; + * Follow Head locates the hands in VIEW, as the screens are. */ +static void test_edit_simple_and_view(void) +{ + const char *simple = "/interaction_profiles/khr/simple_controller"; + t_edit_start(); + CHECK(t_binds(simple, "edit/grab", "/user/hand/right/input/select/click") + && t_binds(simple, "edit/menu", "/user/hand/left/input/menu/click") + && !t_binds(simple, "edit/tilt", NULL) + && !t_binds(simple, "edit/stick", NULL) + && !t_binds(simple, "edit/reset", NULL), + "the simple controller's edit bindings"); + t_space = t_xr_session.view_space; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(t_located_base == t_xr_session.view_space, + "the aim was not located where the screens are"); + t_aim[2].position.x = 0.1f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, 0.1f), "grab only: %.3f", t_xr[0].x); + t_edit_end(); +} + +/* Fixed in Room: a hand in the room moves an item in the anchor's + * frame. The anchor turned a quarter left, a hand moved 20 cm along the + * room's +Z moves screen 1 20 cm to its left. */ +static void test_edit_room(void) +{ + t_edit_start(); + t_screens.orientation.y = 0.70710678f; + t_screens.orientation.w = 0.70710678f; + t_aim[2].orientation.y = 0.70710678f; + t_aim[2].orientation.w = 0.70710678f; + t_hold("edit/grab", 2, 1.0f, 0.0f); + t_edit_poll(); + CHECK(input_openxr_edit.hand[1].g.state == VIDEO_XR_GRAB_HELD + && t_located_base == t_xr_session.local_space, + "the hand aiming where the anchor looks took nothing"); + t_aim[2].position.z = 0.2f; + t_edit_poll(); + CHECK(t_near(t_xr[0].x, -0.2f) && t_near(t_xr[0].z, 0.0f), + "moved to %.3f %.3f", t_xr[0].x, t_xr[0].z); + t_edit_end(); +} + int main(void) { t_forget_devices(); @@ -1808,6 +10156,7 @@ int main(void) test_hotkeys(); test_nothing_held(); test_start_menu(); + test_start_menu_after_edit(); test_laser(); test_hands(); test_rumble(); @@ -1819,6 +10168,121 @@ int main(void) test_joypad_wrapped(); test_joints(); test_session_restart_trigger(); + test_edit_mode(); + test_edit_trigger_held(); + test_edit_unavailable(); + test_edit_profile_refused(); + test_edit_pause_refused(); + test_edit_repause(); + test_edit_unbound(); + test_edit_oversized(); + test_edit_pause(); + test_edit_grab(); + test_edit_turn(); + test_edit_tilt_stick(); + test_edit_stick_lock(); + test_edit_step_stick(); + test_edit_step_waver(); + test_edit_step_sizes(); + test_edit_tilt_from_grab(); + test_edit_limits(); + test_edit_spot_moved(); + test_edit_two_hands(); + test_edit_menu_handle(); + test_edit_reset(); + test_edit_held_at_start(); + test_edit_recenter(); + test_edit_held_moves(); + test_edit_stereo(); + test_edit_fallback_frame(); + test_edit_simple_and_view(); + test_edit_hold_record(); + test_edit_release_moving(); + test_edit_release_rewind(); + test_edit_release_twitch(); + test_edit_release_stall(); + test_edit_release_fast_frames(); + test_edit_trail_frames(); + test_edit_trail_hands(); + test_edit_release_smoothed(); + test_edit_release_smoothed_twitch(); + test_edit_settle(); + test_edit_settle_target_held(); + test_edit_settle_twitch(); + test_edit_mark_debounce(); + test_edit_mark_shaken(); + test_edit_mark_tail(); + test_edit_mark_switch(); + test_edit_regrab(); + test_edit_frame_aim(); + test_edit_outlines(); + test_edit_backs(); + test_edit_handle_sticky(); + test_edit_beams(); + test_edit_select_bindings(); + test_edit_select_cycle(); + test_edit_select_fallback(); + test_edit_select_grab(); + test_edit_select_grips(); + test_edit_held_lit(); + test_edit_select_lit(); + test_edit_tag(); + test_edit_snap(); + test_edit_snap_two_hands(); + test_edit_turn_snap(); + test_edit_step_snap(); + test_edit_snap_edge(); + test_edit_snap_commit(); + test_edit_hinge_swing(); + test_edit_hinge_hold(); + test_edit_hinge_rearm(); + test_edit_hinge_drop(); + test_edit_hinge_cells(); + test_edit_hinge_crops(); + test_edit_hinge_slots(); + test_edit_rest_take(); + test_edit_rest_swing(); + test_edit_rest_take_two(); + test_edit_rest_take_other(); + test_edit_rest_take_sides(); + test_edit_rest_take_target(); + test_edit_rest_take_over(); + test_edit_rest_take_out(); + test_edit_gap_push_rest(); + test_edit_hinge_under(); + test_edit_hinge_flick(); + test_edit_hinge_lid_ends(); + test_edit_hinge_glide(); + test_edit_hinge_lid_far(); + test_edit_hinge_door(); + test_edit_hinge_lid_reach(); + test_edit_hinge_lid_moved(); + test_edit_hinge_lid_new_hold(); + test_edit_hinge_diagonal(); + test_edit_hinge_level(); + test_edit_hinge_rolled(); + test_edit_gap_swing(); + test_edit_gap_stop(); + test_edit_gap_stop_one(); + test_edit_gap_push(); + test_edit_gap_push_shaken(); + test_edit_gap_push_buzz(); + test_edit_gap_push_off(); + test_edit_gap_tick(); + test_edit_gap_tick_turned(); + test_edit_gap_steps(); + test_edit_gap_menu(); + test_edit_gap_buzz(); + test_edit_hinge_buzz(); + test_edit_hinge_buzz_skip(); + test_edit_hinge_buzz_flick(); + test_edit_hinge_buzz_again(); + test_edit_hinge_take_target(); + test_edit_hinge_take_hop(); + test_edit_hinge_buzz_depth(); + test_edit_hinge_attach(); + test_edit_smoothing(); + test_edit_room(); if (failures) { printf("%d failure(s)\n", failures); diff --git a/samples/menu/stereo_headset/stereo_headset_test.c b/samples/menu/stereo_headset/stereo_headset_test.c index 3651d71894..a1cdd4645b 100644 --- a/samples/menu/stereo_headset/stereo_headset_test.c +++ b/samples/menu/stereo_headset/stereo_headset_test.c @@ -19,7 +19,7 @@ * playlist_nav pattern). A headset gives each eye its own image, so * the list leaves Stereo Mode out while Headset Output is on, and * pressing Headset Output rebuilds the open list with the cursor still - * on it. Headset Refresh Rate sits under Headset Output, and with no + * on it. Headset Refresh Rate sits below Headset Output, and with no * headset lists the fixed rates. * * The video driver stays null: the list reads only the driver's name, @@ -219,11 +219,12 @@ static void lane_headset_rows(void) static const char *const rows[] = { MENU_ENUM_LABEL_VIDEO_OPENXR_ENABLE_STR, MENU_ENUM_LABEL_VIDEO_OPENXR_REFRESH_RATE_STR, - MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE_STR, - MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH_STR, - MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT_STR, MENU_ENUM_LABEL_VIDEO_OPENXR_CONTROLLERS_STR, - MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_STR }; + MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_STR, + MENU_ENUM_LABEL_VIDEO_OPENXR_LASER_BEAM_STR, + MENU_ENUM_LABEL_VIDEO_OPENXR_PLACEMENT_STR, + MENU_ENUM_LABEL_VIDEO_OPENXR_DISTANCE_STR, + MENU_ENUM_LABEL_VIDEO_OPENXR_WIDTH_STR }; unsigned i, d; #ifdef HAVE_VULKAN @@ -340,9 +341,9 @@ static void lane_toggle(void) "the cursor left Headset Output when Start turned it off"); } -/* Right under Headset Output. With no headset to list its rates it - * steps through Auto, Headset's Choice and 72, 90, 120 and 144 Hz, - * round, and its dropdown lists the same. */ +/* Right after the edit rows Headset Output shows. With no headset to + * list its rates it steps through Auto, Headset's Choice and 72, 90, + * 120 and 144 Hz, round, and its dropdown lists the same. */ static void lane_refresh_rate(void) { static const unsigned want[] = { 0, 1, 72, 90, 120, 144, 0 }; @@ -350,7 +351,7 @@ static void lane_refresh_rate(void) "72 Hz", "90 Hz", "120 Hz", "144 Hz", "Auto" }; char s[64]; unsigned i; - int enable, rate; + int edit, rate; file_list_t *buf; settings_t *settings = config_get_ptr(); rarch_setting_t *setting = menu_setting_find_enum( @@ -359,11 +360,11 @@ static void lane_refresh_rate(void) set_headset(true); set_driver(HEADSET_DRIVER); open_stereo_list(); - enable = row(MENU_ENUM_LABEL_VIDEO_OPENXR_ENABLE_STR); + edit = row(msg_hash_to_str(MENU_ENUM_LABEL_VIDEO_OPENXR_EDIT_PAUSE)); rate = row(MENU_ENUM_LABEL_VIDEO_OPENXR_REFRESH_RATE_STR); - CHECK(rate >= 0 && rate == enable + 1, - "Headset Refresh Rate is not right under Headset Output " - "(rows %d and %d)", enable, rate); + CHECK(rate >= 0 && rate == edit + 1, + "Headset Refresh Rate is not right under While Editing Screens " + "(rows %d and %d)", edit, rate); CHECK(setting != NULL, "fixture: no Headset Refresh Rate setting"); if (!setting) return; diff --git a/samples/network/command_options/command_hardcore_test.c b/samples/network/command_options/command_hardcore_test.c index 38f8808fdf..e7d6378566 100644 --- a/samples/network/command_options/command_hardcore_test.c +++ b/samples/network/command_options/command_hardcore_test.c @@ -22,8 +22,10 @@ static runloop_state_t runloop_st; static bool hardcore; bool rcheevos_hardcore_active(void) { return hardcore; } +bool rcheevos_is_pause_allowed(void) { return !hardcore; } settings_t *config_get_ptr(void) { return &settings; } runloop_state_t *runloop_state_get_ptr(void) { return &runloop_st; } +uint32_t runloop_get_flags(void) { return runloop_st.flags; } void RARCH_LOG(const char *fmt, ...) { (void)fmt; } bool command_event(enum event_command cmd, void *data) { (void)cmd; (void)data; return false; } diff --git a/samples/openxr/core/openxr_core_test.c b/samples/openxr/core/openxr_core_test.c index 39b2a46357..037476c9da 100644 --- a/samples/openxr/core/openxr_core_test.c +++ b/samples/openxr/core/openxr_core_test.c @@ -45,7 +45,23 @@ void RARCH_LOG(const char *fmt, ...) { va_list ap; va_start(ap, fmt); t_append(f void RARCH_WARN(const char *fmt, ...) { va_list ap; va_start(ap, fmt); t_append(fmt, ap); va_end(ap); } void RARCH_ERR(const char *fmt, ...) { va_list ap; va_start(ap, fmt); t_append(fmt, ap); va_end(ap); } static int t_notified = -1; -const char *msg_hash_to_str(enum msg_hash_enums msg) { t_notified = (int)msg; return "msg"; } +/* The tags' words each their own, with descenders (g, Cyrillic er). */ +const char *msg_hash_to_str(enum msg_hash_enums msg) +{ + t_notified = (int)msg; + switch (msg) + { + case MSG_OPENXR_EDIT_TAG_LASER: + return "Lg"; + case MSG_OPENXR_EDIT_TAG_SCREEN: + return "S%u"; + case MSG_OPENXR_EDIT_TAG_MENU: + return "M\xd1\x80"; + default: + break; + } + return "msg"; +} void runloop_msg_queue_push(const char *msg, size_t len, unsigned prio, unsigned duration, bool flush, char *title, enum message_queue_icon icon, enum message_queue_category category) { } @@ -563,6 +579,7 @@ static XrCompositionLayerProjection t_proj; static XrCompositionLayerProjectionView t_proj_views[2]; static XrStructureType t_layer_types[4]; static XrCompositionLayerQuad t_quad; /* the first quad */ +static XrSwapchain t_layer_sc[16]; /* each quad's */ static XRAPI_ATTR XrResult XRAPI_CALL t_end_frame(XrSession s, const XrFrameEndInfo *ei) { @@ -574,6 +591,11 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_end_frame(XrSession s, memset(t_layer_types, 0, sizeof(t_layer_types)); for (i = 0; i < ei->layerCount && i < 4; i++) t_layer_types[i] = ei->layers[i]->type; + memset(t_layer_sc, 0, sizeof(t_layer_sc)); + for (i = 0; i < ei->layerCount && i < 16; i++) + if (ei->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_QUAD) + t_layer_sc[i] = ((const XrCompositionLayerQuad*) + ei->layers[i])->subImage.swapchain; for (i = 0; i < ei->layerCount; i++) if (ei->layers[i]->type == XR_TYPE_COMPOSITION_LAYER_QUAD) { @@ -590,15 +612,6 @@ static XRAPI_ATTR XrResult XRAPI_CALL t_end_frame(XrSession s, t_frame_unlocked++; return t_end_frame_res; } -static unsigned t_extra_layers(void *user, XrTime time, - const XrCompositionLayerBaseHeader **layers, unsigned cap) -{ - static XrCompositionLayerQuad dot; - t_extra_cap = cap; - layers[0] = (const XrCompositionLayerBaseHeader*)˙ - return 1; -} - static XrResult t_gl_reqs_res = XR_SUCCESS; static unsigned t_gl_reqs_asked; static XRAPI_ATTR XrResult XRAPI_CALL t_gl_reqs(XrInstance i, XrSystemId s, @@ -729,6 +742,296 @@ static bool t_locate_hands(void *user, XrSpace base, XrTime time, return true; } +/* The cursor's tags' font: none unless a test gives one. The fake + * draws every glyph as a solid 6 x 10 box on its baseline, 8 apart, + * but g and Cyrillic er as 6 x 14, 5 rows of it below. With line + * metrics, its ascender and descender are these many a pixel of the + * size asked. It keeps the sizes asked and the codes drawn. */ +static const font_renderer_driver_t *t_font_drv; +static uint8_t t_font_px[6 * 14]; +static struct font_atlas t_font_atlas; +static struct font_glyph t_font_glyph; +static struct font_glyph t_font_desc; +static struct font_line_metrics t_font_metrics; +static float t_font_asc, t_font_dsc; +static unsigned t_font_sizes[4], t_font_made; +static uint32_t t_font_code, t_font_drawn[64]; +static unsigned t_font_num_drawn; +int font_renderer_create_default(const font_renderer_driver_t **drv, + void **handle, const char *font_path, unsigned font_size, + enum font_atlas_format fmt) +{ + (void)font_path; (void)fmt; + if (!t_font_drv) + return 0; + if (t_font_made < 4) + t_font_sizes[t_font_made] = font_size; + t_font_made++; + t_font_metrics.ascender = t_font_asc * font_size; + t_font_metrics.descender = t_font_dsc * font_size; + *drv = t_font_drv; + *handle = &t_font_atlas; + return 1; +} +/* Asked for once a glyph is laid out, to draw it. */ +static struct font_atlas *t_font_get_atlas(void *data) +{ + (void)data; + if (t_font_num_drawn < 64) + t_font_drawn[t_font_num_drawn++] = t_font_code; + return &t_font_atlas; +} +static const struct font_glyph *t_font_get_glyph(void *data, uint32_t code) +{ + (void)data; + t_font_code = code; + return (code == 'g' || code == 0x440) ? &t_font_desc : &t_font_glyph; +} +static void t_font_get_metrics(void *data, + struct font_line_metrics **metrics) +{ (void)data; *metrics = &t_font_metrics; } +static void t_font_free(void *data) { (void)data; } +static font_renderer_driver_t t_font; + +static void t_font_make(void) +{ + memset(t_font_px, 255, sizeof(t_font_px)); + memset(&t_font_atlas, 0, sizeof(t_font_atlas)); + t_font_atlas.buffer = t_font_px; + t_font_atlas.width = 6; + t_font_atlas.height = 14; + t_font_atlas.format = FONT_ATLAS_FORMAT_A8; + memset(&t_font_glyph, 0, sizeof(t_font_glyph)); + t_font_glyph.dims = VIDEO_SCALE_PACK(6, 10); + t_font_glyph.draw_offset_y = -10; + t_font_glyph.advance_x = 8; + t_font_desc = t_font_glyph; + t_font_desc.dims = VIDEO_SCALE_PACK(6, 14); + t_font_desc.draw_offset_y = -9; + memset(&t_font, 0, sizeof(t_font)); + t_font.get_atlas = t_font_get_atlas; + t_font.get_glyph = t_font_get_glyph; + t_font.free = t_font_free; + t_font_made = 0; + t_font_num_drawn = 0; +} + +/* The laser's image with edit mode's tags: none without a font; with + * one, each tag's words centred in its cell, light with a dark halo, + * the rest of the image as video_xr_cursor_pixels and + * video_xr_cut_pixels make it; bottom up, + * each cell's rows mirrored. Before a font is made the strings are not + * even looked up. */ +static void test_cursor_tags(void) +{ + static uint8_t px[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + static uint8_t up[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + static uint8_t plain[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + unsigned k, x, y, light = 0, dark = 0, mirrored = 1; + video_xr_cursor_pixels(plain); + video_xr_cut_pixels(plain, false); + t_font_drv = NULL; + t_notified = 0; + openxr_frame_cursor_pixels(px, VIDEO_XR_CURSOR_W, false); + CHECK(!memcmp(px, plain, sizeof(px)) && !t_notified, + "no font: the image changed, or a string was asked for"); + t_font_make(); + t_font_drv = &t_font; + openxr_frame_cursor_pixels(px, VIDEO_XR_CURSOR_W, false); + openxr_frame_cursor_pixels(up, VIDEO_XR_CURSOR_W, true); + t_font_drv = NULL; + for (k = 0; k < VIDEO_XR_TAGS; k++) + for (y = 0; y < VIDEO_XR_TAG_ROWS; y++) + for (x = 0; x < VIDEO_XR_TAG_COLS; x++) + { + const uint8_t *p = px + (((size_t)VIDEO_XR_TAG_Y + y) + * VIDEO_XR_CURSOR_W + VIDEO_XR_TAG_X(k) + x) * 4; + const uint8_t *q = up + (((size_t)VIDEO_XR_TAG_Y + + VIDEO_XR_TAG_ROWS - 1 - y) * VIDEO_XR_CURSOR_W + + VIDEO_XR_TAG_X(k) + x) * 4; + light += p[0] == VIDEO_XR_TAG_FAINT; + dark += p[0] == 0 && p[3] == VIDEO_XR_TAG_FAINT; + mirrored &= !memcmp(p, q, 4); + } + CHECK(light && dark && mirrored, + "tags: %u light, %u dark texels, mirrored %u", light, dark, + mirrored); + for (x = 0; x < VIDEO_XR_CURSOR_W; x++) + for (y = 0; y < VIDEO_XR_CURSOR_H; y++) + if ( (x < VIDEO_XR_TAG_X(0) || x > VIDEO_XR_CURSOR_TAGGED_W) + && memcmp(px + ((size_t)y * VIDEO_XR_CURSOR_W + x) * 4, + plain + ((size_t)y * VIDEO_XR_CURSOR_W + x) * 4, 4)) + { + CHECK(0, "the tags touched %u,%u", x, y); + return; + } +} + +/* A runtime whose swapchains are narrower than the whole image: from + * VIDEO_XR_CURSOR_W wide, the image with its cut outlines and tags; from + * VIDEO_XR_CURSOR_TAGGED_W, without the cut outlines; else the image + * without the tags too, VIDEO_XR_CURSOR_UNTAGGED_W wide, and no font is + * made. Each the full width's columns as far as it goes. */ +static void test_cursor_narrow(void) +{ + static uint8_t px[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + static uint8_t plain[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + static const unsigned max[][2] = { + { 0, VIDEO_XR_CURSOR_TAGGED_W }, + { 16384, VIDEO_XR_CURSOR_W }, + { VIDEO_XR_CURSOR_W, VIDEO_XR_CURSOR_W }, + { VIDEO_XR_CURSOR_W - 1, VIDEO_XR_CURSOR_TAGGED_W }, + { VIDEO_XR_CURSOR_TAGGED_W, VIDEO_XR_CURSOR_TAGGED_W }, + { VIDEO_XR_CURSOR_TAGGED_W - 1, VIDEO_XR_CURSOR_UNTAGGED_W }, + { VIDEO_XR_CURSOR_UNTAGGED_W, VIDEO_XR_CURSOR_UNTAGGED_W }, + { VIDEO_XR_CURSOR_UNTAGGED_W - 1, VIDEO_XR_CURSOR_UNTAGGED_W } }; + openxr_runtime_t rt; + openxr_frame_t f; + unsigned i, k, y; + memset(&rt, 0, sizeof(rt)); + memset(&f, 0, sizeof(f)); + f.rt = &rt; + for (i = 0; i < sizeof(max) / sizeof(max[0]); i++) + { + rt.max_width = max[i][0]; + CHECK(openxr_frame_cursor_w(&f) == max[i][1], + "widest %u: %u wide, want %u", max[i][0], + openxr_frame_cursor_w(&f), max[i][1]); + } + CHECK(VIDEO_XR_CURSOR_UNTAGGED_W == VIDEO_XR_HINGE_X + VIDEO_XR_HINGE_COLS, + "untagged: %u wide", VIDEO_XR_CURSOR_UNTAGGED_W); + t_font_make(); + t_font_drv = &t_font; + openxr_frame_cursor_pixels(plain, VIDEO_XR_CURSOR_W, false); + for (k = 0; k < 2; k++) + { + unsigned w = k ? VIDEO_XR_CURSOR_UNTAGGED_W : VIDEO_XR_CURSOR_TAGGED_W; + unsigned same = 1; + t_font_made = 0; + openxr_frame_cursor_pixels(px, w, false); + for (y = 0; y < VIDEO_XR_CURSOR_H; y++) + same &= !memcmp(px + (size_t)y * w * 4, + plain + (size_t)y * VIDEO_XR_CURSOR_W * 4, (size_t)w * 4); + CHECK(same && t_font_made == (k ? 0u : 1u), + "%u wide: rows differ (%u), %u fonts made", w, same, + t_font_made); + } + t_font_drv = NULL; +} + +/* Light texels in tag k's cell. */ +static unsigned t_tag_light(const uint8_t *px, unsigned k) +{ + unsigned x, y, light = 0; + for (y = 0; y < VIDEO_XR_TAG_ROWS; y++) + for (x = 0; x < VIDEO_XR_TAG_COLS; x++) + light += px[(((size_t)VIDEO_XR_TAG_Y + y) * VIDEO_XR_CURSOR_W + + VIDEO_XR_TAG_X(k) + x) * 4] == VIDEO_XR_TAG_FAINT; + return light; +} + +/* Each cell's own words: the laser's, screens 1 to 8, the menu's; and + * with a font's line metrics, every row of a full line inside the + * halo's inset, descenders too (a 16 px Noto Sans line: 18 rows up, 5 + * down). A font whose line is taller is drawn smaller. */ +static void test_cursor_tag_lines(void) +{ + static uint8_t px[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + static const char *words[VIDEO_XR_TAGS] = { "Lg", "S1", "S2", "S3", + "S4", "S5", "S6", "S7", "S8", "M\xd1\x80" }; + uint32_t want[64]; + unsigned k, n = 0, full = 1; + for (k = 0; k < VIDEO_XR_TAGS; k++) + { + want[n++] = (uint32_t)(unsigned char)words[k][0]; + want[n++] = k ? (k < VIDEO_XR_TAGS - 1 + ? (uint32_t)(unsigned char)words[k][1] : 0x440) : 'g'; + } + t_font_make(); + t_font.get_line_metrics = t_font_get_metrics; + t_font_asc = 18.0f / 16.0f; + t_font_dsc = 5.0f / 16.0f; + t_font_drv = &t_font; + openxr_frame_cursor_pixels(px, VIDEO_XR_CURSOR_W, false); + CHECK( t_font_num_drawn == n + && !memcmp(t_font_drawn, want, n * sizeof(*want)), + "the tags drew %u glyphs, not Lg S1 .. S8 Mer", t_font_num_drawn); + for (k = 0; k < VIDEO_XR_TAGS; k++) + full &= t_tag_light(px, k) == (k && k < VIDEO_XR_TAGS - 1 + ? 2 * 6 * 10 : 6 * 10 + 6 * 14); + CHECK(full && t_font_made == 1 && t_font_sizes[0] == VIDEO_XR_TAG_FONT, + "a 23 row line: glyphs cut (laser %u, menu %u light), or %u " + "fonts made", t_tag_light(px, 0), + t_tag_light(px, VIDEO_XR_TAGS - 1), t_font_made); + + /* A line half again as tall as its size, and a third. */ + t_font_make(); + t_font.get_line_metrics = t_font_get_metrics; + t_font_asc = 1.25f; + t_font_dsc = 0.5f; + openxr_frame_cursor_pixels(px, VIDEO_XR_CURSOR_W, false); + t_font_drv = NULL; + CHECK( t_font_made == 2 && t_font_sizes[0] == VIDEO_XR_TAG_FONT + && t_font_sizes[1] == VIDEO_XR_TAG_FONT + * (VIDEO_XR_TAG_ROWS - 2 * VIDEO_XR_TAG_INSET) + / (unsigned)(1.75f * VIDEO_XR_TAG_FONT + 0.5f) + && t_tag_light(px, 0) == 6 * 10 + 6 * 14, + "a tall line: %u fonts made, sizes %u then %u, %u laser texels", + t_font_made, t_font_sizes[0], t_font_sizes[1], t_tag_light(px, 0)); +} + +/* The input's hooks, stood in for. */ +static video_xr_quad_set_t t_extra_shown; +static XrSpace t_extra_space; +static unsigned t_extra_layers(void *user, XrTime time, + const video_xr_quad_set_t *shown, XrSpace space, + const XrCompositionLayerBaseHeader **layers, unsigned cap, + unsigned *under) +{ + static XrCompositionLayerQuad dot; + t_extra_cap = cap; + t_extra_shown = *shown; + t_extra_space = space; + layers[0] = (const XrCompositionLayerBaseHeader*)˙ + *under = 0; + return 1; +} + +/* Edit mode's: two screens' rims, to go under the screens, then the + * menu's rim, its handle, a beam and a dot; swapchains 0x700 up. */ +static unsigned t_split_layers(void *user, XrTime time, + const video_xr_quad_set_t *shown, XrSpace space, + const XrCompositionLayerBaseHeader **layers, unsigned cap, + unsigned *under) +{ + static XrCompositionLayerQuad l[6]; + unsigned i; + for (i = 0; i < 6 && i < cap; i++) + { + l[i].type = XR_TYPE_COMPOSITION_LAYER_QUAD; + l[i].subImage.swapchain = (XrSwapchain)(uintptr_t)(0x700 + i); + layers[i] = (const XrCompositionLayerBaseHeader*)&l[i]; + } + *under = 2; + return i; +} + +/* The aim hook: a pose, or untracked; the hand and space asked last. */ +static bool t_aim_ok; +static video_xr_pose_t t_aim_pose; +static int t_aim_hand = -1; +static XrSpace t_aim_space; +static bool t_aim_hook(void *user, unsigned hand, XrSpace space, + XrTime time, video_xr_pose_t *pose) +{ + t_aim_hand = (int)hand; + t_aim_space = space; + if (!t_aim_ok) + return false; + *pose = t_aim_pose; + return true; +} + static void t_runtime_defaults(void) { t_num_exts = 3; @@ -831,6 +1134,7 @@ static void test_runtime(void) CHECK(rt.DestroyInstance && rt.RequestDisplayRefreshRateFB, "functions not loaded"); CHECK(openxr_runtime_find_system(&rt) == OPENXR_RUNTIME_OK, "system"); CHECK(rt.max_dim == 2048, "max_dim %u: the smaller side, at most 4096", rt.max_dim); + CHECK(rt.max_width == 8192, "max_width %u", rt.max_width); CHECK(rt.rec_width == 1728, "rec_width %u", (unsigned)rt.rec_width); CHECK(rt.rec_height == 1824, "rec_height %u", (unsigned)rt.rec_height); CHECK(LOGGED("[OpenXR] Headset: Stand-in HMD.\n"), "log: %s", t_log); @@ -1349,7 +1653,7 @@ static void test_frame(void) { XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; - CHECK(openxr_frame_layers(&f, layers, ptrs) == 1, + CHECK(openxr_frame_layers(&f, 0, layers, ptrs) == 1, "a quad without content shown"); } @@ -1379,7 +1683,8 @@ static void test_frame(void) /* The host's layers follow the screens. */ f.extra_layers = t_extra_layers; openxr_frame_run(&f); - CHECK(t_last_layer_count == 2 && t_extra_cap == OPENXR_MAX_EXTRA_LAYERS, + CHECK( t_last_layer_count == 2 + && t_extra_cap == XR_MIN_COMPOSITION_LAYERS_SUPPORTED - 1, "%u layers with the host's, cap %u", t_last_layer_count, t_extra_cap); /* Each frame counts toward the tick: the ninth at interval 3 makes @@ -1408,7 +1713,7 @@ static void test_frame(void) { XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; - CHECK(openxr_frame_layers(&f, layers, ptrs) == 3, "menu and stereo"); + CHECK(openxr_frame_layers(&f, 0, layers, ptrs) == 3, "menu and stereo"); CHECK(layers[0].layerFlags == XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT && layers[0].eyeVisibility == XR_EYE_VISIBILITY_BOTH, @@ -1424,6 +1729,24 @@ static void test_frame(void) (int)layers[2].eyeVisibility); } + /* The screens' rims before the first quad; the menu's rim, the + * handle, the beam and the dot after the last. */ + f.extra_layers = t_split_layers; + openxr_frame_run(&f); + CHECK( t_last_layer_count == 9 + && t_layer_sc[0] == (XrSwapchain)(uintptr_t)0x700 + && t_layer_sc[1] == (XrSwapchain)(uintptr_t)0x701 + && t_layer_sc[2] == sc.slots[1].swapchains[0] + && t_layer_sc[3] == sc.slots[3].swapchains[0] + && t_layer_sc[4] == sc.slots[3].swapchains[1] + && t_layer_sc[5] == (XrSwapchain)(uintptr_t)0x702 + && t_layer_sc[6] == (XrSwapchain)(uintptr_t)0x703 + && t_layer_sc[7] == (XrSwapchain)(uintptr_t)0x704 + && t_layer_sc[8] == (XrSwapchain)(uintptr_t)0x705, + "%u layers: the rims not under the screens, or the rest not over", + t_last_layer_count); + f.extra_layers = t_extra_layers; + /* A recenter makes the tracked head's level pose the anchor; the * runtime's recenter and a new session put it back and drop one * asked for. The head here is turned a quarter left: level already. */ @@ -1944,6 +2267,17 @@ static void test_projection(void) && t_layer_types[1] == XR_TYPE_COMPOSITION_LAYER_QUAD, "order: %u layers, the first %d", t_last_layer_count, (int)t_layer_types[0]); + /* Edit mode's screen rims between the projection and the quads. */ + f.extra_layers = t_split_layers; + openxr_frame_run(&f); + CHECK( t_last_layer_count == 8 + && t_layer_types[0] == XR_TYPE_COMPOSITION_LAYER_PROJECTION + && t_layer_sc[1] == (XrSwapchain)(uintptr_t)0x700 + && t_layer_sc[2] == (XrSwapchain)(uintptr_t)0x701 + && t_layer_sc[3] == sc.slots[1].swapchains[0] + && t_layer_sc[4] == (XrSwapchain)(uintptr_t)0x702, + "%u layers: the rims not between the projection and the quads", + t_last_layer_count); f.extra_layers = NULL; /* A new track space leaves out what was drawn in the last, until @@ -3385,20 +3719,22 @@ static void test_frame_inline(void) openxr_frame_end(&f, &b); CHECK(t_last_layer_count == 3, "%u layers where the runtime takes 3", t_last_layer_count); + CHECK(t_extra_cap == OPENXR_KEPT_EXTRA_LAYERS, + "no room left, and the host's layers capped at %u", t_extra_cap); { XrCompositionLayerQuad q[4]; const XrCompositionLayerBaseHeader *kept[4]; memset(q, 0, sizeof(q)); for (i = 0; i < 4; i++) kept[i] = (const XrCompositionLayerBaseHeader*)&q[i]; - q[0].subImage.swapchain = sc.slots[1].swapchains[0]; - q[1].subImage.swapchain = sc.slots[2].swapchains[0]; - q[2].subImage.swapchain = sc.slots[VIDEO_XR_MENU_SLOT].swapchains[0]; - CHECK( openxr_frame_fit(&f, kept, 4, 0, 3) == 3 + /* The frame's end left the menu third. */ + CHECK( f.menu_layer == 2 + && openxr_frame_fit(&f, kept, 4, 0, 3) == 3 && kept[0] == (const XrCompositionLayerBaseHeader*)&q[0] && kept[1] == (const XrCompositionLayerBaseHeader*)&q[2] && kept[2] == (const XrCompositionLayerBaseHeader*)&q[3], - "not the second screen dropped"); + "the menu at %d; not the second screen dropped", + f.menu_layer); } openxr_frame_stop_inline(&f); openxr_slot_destroy(&sc, 1); @@ -3473,7 +3809,7 @@ static void test_slot_resize(void) video_xr_quad_set_t set; XrSwapchain old0; uint32_t max_layers; - unsigned n = 0, i, destroyed, idx[2]; + unsigned n = 0, destroyed, idx[2]; const unsigned menu = VIDEO_XR_MENU_SLOT; int cb_user = 0; @@ -3622,13 +3958,13 @@ static void test_slot_resize(void) { XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; - unsigned got = openxr_frame_layers(&f, layers, ptrs); + unsigned got = openxr_frame_layers(&f, 0, layers, ptrs); CHECK( got == 1 && layers[0].subImage.swapchain == old0 && layers[0].eyeVisibility == XR_EYE_VISIBILITY_LEFT, "stereo: %u layers from the old mono set", got); CHECK(openxr_slot_acquire(&sc, 2, idx), "stereo: acquire"); openxr_slot_release(&sc, 2); - got = openxr_frame_layers(&f, layers, ptrs); + got = openxr_frame_layers(&f, 0, layers, ptrs); CHECK( got == 2 && layers[0].subImage.swapchain == sc.slots[2].swapchains[0] && layers[1].subImage.swapchain == sc.slots[2].swapchains[1], @@ -3657,6 +3993,10 @@ static void test_slot_resize(void) /* The menu resized: its old swapchain is still the menu's at the * runtime's layer count. */ + CHECK( openxr_slot_create(&sc, 1, 44, false, VIDEO_SCALE_PACK(64, 64), + 1, t_images_cb, &cb_user, &n) + && openxr_slot_acquire(&sc, 1, idx), "menu: a screen"); + openxr_slot_release(&sc, 1); CHECK( openxr_slot_create(&sc, menu, 44, false, VIDEO_SCALE_PACK(64, 64), 1, t_images_cb, &cb_user, &n) && openxr_slot_acquire(&sc, menu, idx), "menu"); @@ -3664,21 +4004,25 @@ static void test_slot_resize(void) old0 = sc.slots[menu].swapchains[0]; CHECK(openxr_slot_create(&sc, menu, 44, false, VIDEO_SCALE_PACK(96, 64), 1, t_images_cb, &cb_user, &n), "menu: resize"); + memset(&set, 0, sizeof(set)); + set.num_quads = 3; + set.quads[0].slot = 1; + set.quads[1].slot = 1; + set.quads[2].kind = VIDEO_XR_QUAD_MENU; + set.quads[2].slot = menu; + openxr_frame_publish(&f, &set); max_layers = rt.max_layers; rt.max_layers = 3; { - XrCompositionLayerQuad q[4]; - const XrCompositionLayerBaseHeader *kept[4]; - memset(q, 0, sizeof(q)); - for (i = 0; i < 4; i++) - kept[i] = (const XrCompositionLayerBaseHeader*)&q[i]; - q[0].subImage.swapchain = sc.slots[1].swapchains[0]; - q[1].subImage.swapchain = sc.slots[1].swapchains[0]; - q[2].subImage.swapchain = old0; - CHECK( openxr_frame_fit(&f, kept, 4, 0, 3) == 3 - && kept[0] == (const XrCompositionLayerBaseHeader*)&q[0] - && kept[1] == (const XrCompositionLayerBaseHeader*)&q[2] - && kept[2] == (const XrCompositionLayerBaseHeader*)&q[3], + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS], host; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS + 1]; + unsigned got = openxr_frame_layers(&f, 0, layers, ptrs); + ptrs[got] = (const XrCompositionLayerBaseHeader*)&host; + CHECK( got == 3 && layers[2].subImage.swapchain == old0 + && openxr_frame_fit(&f, ptrs, 4, 0, 3) == 3 + && ptrs[0] == (const XrCompositionLayerBaseHeader*)&layers[0] + && ptrs[1] == (const XrCompositionLayerBaseHeader*)&layers[2] + && ptrs[2] == (const XrCompositionLayerBaseHeader*)&host, "fit: the menu's old swapchain dropped as a screen"); } rt.max_layers = max_layers; @@ -4509,6 +4853,7 @@ static void test_input_hooks(void) hooks.session_created = t_in_created_fn; hooks.session_destroying = t_in_destroying_fn; hooks.frame_layers = t_extra_layers; + hooks.aim = t_aim_hook; hooks.user = &user; openxr_frame_set_input_hooks(&hooks); t_in_created = t_in_destroying = 0; @@ -4525,22 +4870,25 @@ static void test_input_hooks(void) t_in_handles.session == s.session, t_in_handles.get_proc == rt.GetInstanceProcAddr, (int)t_in_handles.frame_controller); - CHECK(f.extra_layers == t_extra_layers && f.extra_user == &user, - "the input's layers are not the frame's"); + CHECK( f.extra_layers == t_extra_layers && f.aim == t_aim_hook + && f.extra_user == &user, + "the input's layers and aim are not the frame's"); /* Once a session: a second would leak the first's action sets. */ openxr_frame_input_attach(&f); CHECK(t_in_created == 1, "attached %u times", t_in_created); openxr_session_set_alive(&s, true); retro_atomic_store_release_int(&s.state, XR_SESSION_STATE_FOCUSED); openxr_frame_run(&f); - CHECK(t_last_layer_count == 1 && t_extra_cap == OPENXR_MAX_EXTRA_LAYERS, + CHECK( t_last_layer_count == 1 + && t_extra_cap == XR_MIN_COMPOSITION_LAYERS_SUPPORTED, "%u layers with the input's, cap %u", t_last_layer_count, t_extra_cap); /* Detached once, however often asked; its layers go with it. */ openxr_frame_input_detach(&f); openxr_frame_input_detach(&f); - CHECK(t_in_destroying == 1 && !f.extra_layers && !f.extra_user, + CHECK( t_in_destroying == 1 && !f.extra_layers && !f.aim + && !f.extra_user, "detached %u times, layers %d", t_in_destroying, f.extra_layers != NULL); openxr_frame_run(&f); @@ -4693,6 +5041,128 @@ static void test_swapchain_filled(void) openxr_runtime_deinit(&rt); } +static void test_custom_screen_recenter(void) +{ + struct custom_fixture + { + openxr_runtime_t rt; + openxr_session_t session; + openxr_swapchains_t sc; + openxr_frame_t frame; + video_xr_params_t params; + video_xr_quad_set_t set; + video_xr_quad_set_t shown; + video_views_map_t map; + video_screen_xr_t screens[RETRO_VIDEO_VIEWS_MAX]; + openxr_projection_t projection; + } *fixture = (struct custom_fixture*)calloc(1, sizeof(*fixture)); + struct retro_video_view views[2]; + unsigned i, n = 0; + int user = 0; + XrPosef origin; + XrSpace space; + enum retro_vr_reference_space track = RETRO_VR_REFERENCE_SPACE_LOCAL; + CHECK(fixture, "custom fixture"); + if (!fixture) + return; + t_frame_fresh(&fixture->rt, &fixture->session, &fixture->sc, &fixture->frame); + memset(views, 0, sizeof(views)); + for (i = 0; i < 2; i++) + { + views[i].x = i * 400; + views[i].width = 400; + views[i].height = 240; + views[i].eye = i ? RETRO_VIDEO_VIEW_EYE_RIGHT : RETRO_VIDEO_VIEW_EYE_LEFT; + } + CHECK(video_views_validate(views, 2, &fixture->map), "custom stereo map"); + video_views_xr_reset(fixture->screens, RETRO_VIDEO_VIEWS_MAX); + fixture->screens[0].x = 0.25f; + fixture->screens[0].y = 0.1f; + fixture->screens[0].z = 0.3f; + fixture->screens[0].yaw = 30.0f; + fixture->screens[0].scale = 1.5f; + fixture->params.map = &fixture->map; + fixture->params.screens = fixture->screens; + fixture->params.distance = 1.8f; + fixture->params.width = 1.6f; + fixture->params.px_per_rad = 1000.0f; + fixture->params.max_dim = 4096; + video_xr_pose_identity(&fixture->params.anchor); + video_xr_place(&fixture->params, &fixture->set); + CHECK(fixture->set.num_quads == 2, "custom stereo quads"); + CHECK(openxr_slot_create(&fixture->sc, 0, 44, false, + fixture->set.slots[0].dims, 2, t_images_cb, &user, &n), "quad slot"); + retro_atomic_store_release_int(&fixture->sc.slots[0].content, 1); + openxr_frame_publish(&fixture->frame, &fixture->set); + openxr_session_set_alive(&fixture->session, true); + retro_atomic_store_release_int(&fixture->session.state, XR_SESSION_STATE_FOCUSED); + t_head_tracked = true; + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + t_head.position.x = 0.5f; + t_head.position.y = 0.2f; + t_head.position.z = 0.3f; + openxr_frame_request_recenter(&fixture->frame); + openxr_frame_run(&fixture->frame); + openxr_frame_get_shown_quads(&fixture->frame, &fixture->shown, &space); + CHECK(fixture->shown.num_quads == 2, "recenter kept both eyes"); + for (i = 0; i < 2; i++) + { + const video_xr_pose_t *pose = &fixture->shown.quads[i].pose; + CHECK(t_near(pose->position.x, -1.0f) + && t_near(pose->position.y, 0.3f) + && t_near(pose->position.z, 0.05f) + && t_near(pose->orientation.x, 0.0f) + && t_near(pose->orientation.y, 0.8660254f) + && t_near(pose->orientation.z, 0.0f) + && t_near(pose->orientation.w, 0.5f), + "eye %u lost its relative adjustment", i); + CHECK(t_near(fixture->shown.quads[i].width, 2.4f) + && t_near(fixture->shown.quads[i].height, 1.44f), "custom size"); + } + memset(&origin, 0, sizeof(origin)); + origin.orientation.w = 1.0f; + openxr_session_swap_track_space(&fixture->session, &track, &origin, NULL, NULL); + CHECK(openxr_slot_create(&fixture->sc, OPENXR_PROJECTION_SLOT, 44, false, + VIDEO_SCALE_PACK(400, 240), 2, t_images_cb, &user, &n), "eye slot"); + retro_atomic_store_release_int(&fixture->sc.slots[OPENXR_PROJECTION_SLOT].content, 1); + fixture->projection.active = true; + fixture->projection.width = 400; + fixture->projection.height = 240; + fixture->projection.track_seq = retro_atomic_load_acquire_int(&fixture->session.track_seq); + for (i = 0; i < 2; i++) + { + fixture->projection.pose[i].orientation.w = 1.0f; + fixture->projection.pose[i].position.x = i ? 0.032f : -0.032f; + fixture->projection.fov[i].angleLeft = -0.9f; + fixture->projection.fov[i].angleRight = 0.8f; + fixture->projection.fov[i].angleUp = 0.7f; + fixture->projection.fov[i].angleDown = -0.6f; + } + openxr_frame_publish_projection(&fixture->frame, &fixture->projection); + fixture->screens[0].x = 10.0f; + fixture->screens[0].yaw = 180.0f; + fixture->screens[0].scale = 10.0f; + fixture->screens[0].visible = false; + video_xr_place(&fixture->params, &fixture->set); + openxr_frame_publish(&fixture->frame, &fixture->set); + openxr_frame_run(&fixture->frame); + CHECK(t_proj.viewCount == 2, "projection kept both eyes"); + for (i = 0; i < 2; i++) + { + CHECK(!memcmp(&t_proj_views[i].pose, &fixture->projection.pose[i], + sizeof(t_proj_views[i].pose)), "tracked eye %u pose changed", i); + CHECK(!memcmp(&t_proj_views[i].fov, &fixture->projection.fov[i], + sizeof(t_proj_views[i].fov)), "tracked eye %u FOV changed", i); + } + openxr_slot_destroy(&fixture->sc, 0); + openxr_slot_destroy(&fixture->sc, OPENXR_PROJECTION_SLOT); + t_head_tracked = false; + t_frame_free(&fixture->rt, &fixture->session, &fixture->frame); + free(fixture); +} + static void test_screen_placement(void) { openxr_runtime_t rt; @@ -4747,7 +5217,7 @@ static void test_screen_placement(void) -1.3f, 0.2f, 0.3f), "Fixed: at %.2f,%.2f,%.2f", shown.quads[0].pose.position.x, shown.quads[0].pose.position.y, shown.quads[0].pose.position.z); - CHECK(openxr_frame_layers(&f, layers, ptrs) == 1 + CHECK(openxr_frame_layers(&f, 0, layers, ptrs) == 1 && layers[0].space == s.local_space && t_near(layers[0].pose.position.x, -1.3f) && t_near(layers[0].pose.position.z, 0.3f) @@ -4757,6 +5227,17 @@ static void test_screen_placement(void) openxr_frame_get_quads(&f, &shown); CHECK(!memcmp(&shown, &set, sizeof(set)), "the driver's quads were changed"); + /* The input's layers get them where the frame shows them. */ + f.extra_layers = t_extra_layers; + openxr_frame_run(&f); + f.extra_layers = NULL; + CHECK( t_extra_space == s.local_space + && t_pose_is(&t_extra_shown.quads[0].pose, 0.70710678f, + 0.70710678f, -1.3f, 0.2f, 0.3f), + "the input's layers see screen 1 at %.2f,%.2f,%.2f", + t_extra_shown.quads[0].pose.position.x, + t_extra_shown.quads[0].pose.position.y, + t_extra_shown.quads[0].pose.position.z); /* Follow Head: in VIEW, where the driver put them; published once in * the frame. */ @@ -4772,7 +5253,7 @@ static void test_screen_placement(void) "Follow: at %.2f,%.2f,%.2f in %s", shown.quads[0].pose.position.x, shown.quads[0].pose.position.y, shown.quads[0].pose.position.z, space == s.view_space ? "VIEW" : "another space"); - CHECK(openxr_frame_layers(&f, layers, ptrs) == 1 + CHECK(openxr_frame_layers(&f, 0, layers, ptrs) == 1 && layers[0].space == s.view_space && t_near(layers[0].pose.position.z, -1.8f), "Follow: the layer is not in VIEW in front of the eyes"); @@ -4895,7 +5376,7 @@ static void test_screen_placement_seated(void) "Fixed: before the seated world, at %.2f,%.2f,%.2f", shown.quads[0].pose.position.x, shown.quads[0].pose.position.y, shown.quads[0].pose.position.z); - CHECK( openxr_frame_layers(&f, layers, ptrs) == 1 + CHECK( openxr_frame_layers(&f, 0, layers, ptrs) == 1 && t_near(layers[0].pose.position.x, 0.0f) && t_near(layers[0].pose.position.z, -1.8f), "Fixed: the layer before the seated world, at %.2f,%.2f", @@ -6696,7 +7177,29 @@ static void test_gl_host_records(void) && t_gl_draw(xr, 0, VIDEO_SCALE_PACK(64, 32), 1, 3, NULL) && t_gl_draw(xr, 0, VIDEO_SCALE_PACK(64, 32), 1, 4, NULL), "draws 2-4"); + /* A paused edit copies the latest retained pixels into its own + * target before either record is consumed. */ + { + uint32_t tex[2] = { 0, 0 }; + video_xr_quad_set_t set; + CHECK(gl_openxr_slot_target(xr, 0, VIDEO_SCALE_PACK(64, 32), 1, tex), + "repeat target"); + slock_lock(t_gl_lock); + t_tex_content[tex[0]] = 40; + slock_unlock(t_gl_lock); + memset(&set, 0, sizeof(set)); + set.slots[0].dims = VIDEO_SCALE_PACK(64, 32); + set.slots[0].layers = 1; + set.quads[0].kind = VIDEO_XR_QUAD_SCREEN; + set.quads[0].pose.orientation.w = 1.0f; + set.quads[0].pose.position.x = 0.75f; + set.quads[0].width = 1.0f; + set.quads[0].height = 1.0f; + set.num_quads = 1; + gl_openxr_slot_publish(xr, 0, &set, NULL); + } CHECK(t_gate_step(1), "frame"); + CHECK(t_quad.pose.position.x == 0.75f, "repeat pose"); CHECK(t_gl_copies == 2 && t_gl_image_content(xr, 0, 0) == 40, "copies %u, image %u", t_gl_copies, t_gl_image_content(xr, 0, 0)); @@ -7025,6 +7528,2107 @@ static void test_gl_host_projection(void) } #endif +/* A frame with screen 0 a stereo pair 1.8 m ahead and screen 1 below + * it, planned at edit count 2, their slots holding images, the session + * shown. */ +static void t_grab_frame(openxr_runtime_t *rt, openxr_session_t *s, + openxr_swapchains_t *sc, openxr_frame_t *f, video_xr_quad_set_t *set) +{ + unsigned n = 0; + int cb_user = 0; + t_frame_fresh(rt, s, sc, f); + openxr_slot_create(sc, 0, 44, false, VIDEO_SCALE_PACK(64, 64), 2, + t_images_cb, &cb_user, &n); + openxr_slot_create(sc, 1, 44, false, VIDEO_SCALE_PACK(64, 64), 1, + t_images_cb, &cb_user, &n); + retro_atomic_store_release_int(&sc->slots[0].content, 1); + retro_atomic_store_release_int(&sc->slots[1].content, 1); + memset(set, 0, sizeof(*set)); + set->num_quads = 3; + video_xr_pose_identity(&set->quads[0].pose); + set->quads[0].pose.position.z = -1.8f; + set->quads[0].width = 1.6f; + set->quads[0].height = 0.96f; + set->quads[0].eye = VIDEO_XR_EYE_LEFT; + set->quads[1] = set->quads[0]; + set->quads[1].eye = VIDEO_XR_EYE_RIGHT; + set->quads[1].layer = 1; + set->quads[2] = set->quads[0]; + set->quads[2].eye = VIDEO_XR_EYE_BOTH; + set->quads[2].pose.position.y = -1.0f; + set->quads[2].screen = 1; + set->quads[2].slot = 1; + set->slots[0].layers = 2; + set->slots[1].layers = 1; + set->slots[0].edit_seq = 2; + set->slots[1].edit_seq = 2; + openxr_frame_publish(f, set); + f->aim = t_aim_hook; + f->extra_user = NULL; + openxr_session_set_alive(s, true); + retro_atomic_store_release_int(&s->state, XR_SESSION_STATE_FOCUSED); +} + +static void t_grab_free(openxr_runtime_t *rt, openxr_session_t *s, + openxr_swapchains_t *sc, openxr_frame_t *f) +{ + openxr_slot_destroy(sc, 0); + openxr_slot_destroy(sc, 1); + t_frame_free(rt, s, f); + t_aim_ok = false; +} + +/* A right hand holding screen 0 1 m out from the eyes. */ +static void t_grab_held(openxr_grabs_t *g) +{ + memset(g, 0, sizeof(*g)); + video_xr_pose_identity(&g->hand[1].rel); + g->hand[1].rel.position.z = -1.0f; + g->hand[1].turn = g->hand[1].rel.orientation; + video_xr_pose_identity(&g->hand[1].last); + g->hand[1].last.position.z = -1.8f; + g->hand[1].base.z = -1.8f; + g->hand[1].width = 2.0f; + g->hand[1].height = 1.2f; + g->hand[1].state = VIDEO_XR_GRAB_HELD; +} + +/* Hand h's mark as the main thread publishes it: on target slot's edge + * on by its own edge edge; slot ~0u, none. */ +static void t_mark(openxr_grabs_t *g, unsigned h, unsigned slot, + unsigned on, unsigned edge) +{ + bool none = slot == ~0u; + g->hand[h].mark_slot = none ? 0 : (unsigned char)(slot + 1u); + g->hand[h].mark_on = none ? 0 : (unsigned char)on; + g->hand[h].mark_edge = none ? 0 : (unsigned char)edge; +} + +/* Hand 1 lets go of what it holds at count seq, where it last put it, + * as the main thread hands it over, its mark gone. */ +static void t_grab_let_go(openxr_grabs_t *g, unsigned seq, float x) +{ + g->hand[1].state = VIDEO_XR_GRAB_RELEASED; + g->hand[1].release_seq = seq; + g->hand[1].last.position.x = x; + t_mark(g, 1, ~0u, 0, 0); + g->let_go[g->hand[1].slot] = g->hand[1]; +} + +/* Edit mode on the frame loop: a held item goes where the hand's aim at + * the frame's own time puts it, both eyes of a stereo screen, and the + * input's layers see it there; an untracked hand leaves it where the + * main thread last put it; let go, it stays until quads planned at its + * release show it; a fixed slot is no item to hold. */ +static void test_frame_grabs(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + openxr_frame_publish_grabs(&f, &grabs); + /* Not shown yet and untracked: where the main thread put it. */ + t_aim_ok = false; + openxr_frame_place_screens(&f, 500); + openxr_frame_layers(&f, 500, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.0f) + && t_near(layers[0].pose.position.z, -1.8f) + && t_near(layers[0].size.width, 2.0f), + "untracked before any frame showed it, at %.2f,%.2f", + layers[0].pose.position.x, layers[0].pose.position.z); + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.x = 0.3f; + t_aim_ok = true; + t_aim_hand = -1; + openxr_frame_place_screens(&f, 1000); + CHECK(openxr_frame_layers(&f, 1000, layers, ptrs) == 3, "not three quads"); + CHECK( t_near(layers[0].pose.position.x, 0.3f) + && t_near(layers[0].pose.position.z, -1.0f) + && t_near(layers[1].pose.position.x, 0.3f) + && t_near(layers[1].pose.position.z, -1.0f) + && t_near(layers[0].size.width, 2.0f) + && t_near(layers[1].size.height, 1.2f), + "held at %.2f,%.2f and %.2f,%.2f, %.2f wide", + layers[0].pose.position.x, layers[0].pose.position.z, + layers[1].pose.position.x, layers[1].pose.position.z, + layers[0].size.width); + CHECK(t_near(layers[2].pose.position.x, 0.0f) + && t_near(layers[2].size.width, 1.6f), "screen 1 moved with it"); + CHECK(t_aim_hand == 1, "asked hand %d's aim", t_aim_hand); + CHECK(t_near(f.shown.quads[1].pose.position.x, 0.3f), + "the input's layers would see it elsewhere"); + + /* A whole headset frame: the screens and the input's layers. */ + f.extra_layers = t_extra_layers; + openxr_frame_run(&f); + CHECK( t_near(t_quad.pose.position.x, 0.3f) + && t_near(t_extra_shown.quads[0].pose.position.x, 0.3f) + && t_near(t_extra_shown.quads[1].pose.position.x, 0.3f) + && t_extra_space == s.local_space, + "the frame submitted it at %.2f, the input saw %.2f", + t_quad.pose.position.x, t_extra_shown.quads[0].pose.position.x); + f.extra_layers = NULL; + + /* Tracking lost: still where the last frame showed it, not back + * where the main thread's last poll put it. */ + t_aim_ok = false; + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.3f) + && t_near(layers[0].pose.position.z, -1.0f) + && t_near(layers[1].pose.position.x, 0.3f) + && t_near(layers[0].size.width, 2.0f), + "untracked at %.2f,%.2f", layers[0].pose.position.x, + layers[0].pose.position.z); + /* The hand on another item: a new hold, untracked, is where the main + * thread put it, and the first item back where its quads are. */ + { + openxr_grabs_t other = grabs; + other.hand[1].slot = 1; + other.hand[1].last.position.y = -1.0f; + other.hand[1].last.position.z = -1.8f; + openxr_frame_publish_grabs(&f, &other); + openxr_frame_place_screens(&f, 2500); + openxr_frame_layers(&f, 2500, layers, ptrs); + CHECK( t_near(layers[2].pose.position.x, 0.0f) + && t_near(layers[2].pose.position.y, -1.0f) + && t_near(layers[2].pose.position.z, -1.8f) + && t_near(layers[0].pose.position.x, 0.0f) + && t_near(layers[0].pose.position.z, -1.8f), + "a new hold untracked at %.2f,%.2f,%.2f, the first item at " + "%.2f", layers[2].pose.position.x, layers[2].pose.position.y, + layers[2].pose.position.z, layers[0].pose.position.x); + openxr_frame_publish_grabs(&f, &grabs); + } + + t_grab_let_go(&grabs, 3, 0.5f); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 3000); + openxr_frame_layers(&f, 3000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.5f) + && t_near(layers[1].pose.position.x, 0.5f) + && t_near(layers[0].size.width, 2.0f), + "let go, at %.2f before its quads came", layers[0].pose.position.x); + set.slots[0].edit_seq = 3; + set.quads[0].pose.position.x = 0.5f; + set.quads[1].pose.position.x = 0.5f; + openxr_frame_publish(&f, &set); + openxr_frame_place_screens(&f, 4000); + openxr_frame_layers(&f, 4000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.5f) + && t_near(layers[0].size.width, 1.6f), + "not handed back to the published quads"); + + /* A count that wrapped still hands over. */ + set.slots[0].edit_seq = 1; + openxr_frame_publish(&f, &set); + grabs.let_go[0].release_seq = 0xfffffffeu; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 5000); + openxr_frame_layers(&f, 5000, layers, ptrs); + CHECK(t_near(layers[0].size.width, 1.6f), "a wrapped count held it"); + + /* A frame shown whole without the screens' settings is no item. */ + set.slots[0].fixed = true; + openxr_frame_publish(&f, &set); + t_grab_held(&grabs); + openxr_frame_publish_grabs(&f, &grabs); + t_aim_ok = true; + openxr_frame_place_screens(&f, 6000); + openxr_frame_layers(&f, 6000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.5f) + && t_near(layers[0].size.width, 1.6f), + "a fixed slot held at %.2f", layers[0].pose.position.x); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Edit Smoothing's aims reach the main thread whole, as published; a + * new session starts with none. */ +static void test_frame_aims(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_aims_t a, got; + t_grab_frame(&rt, &s, &sc, &f, &set); + memset(&a, 0, sizeof(a)); + video_xr_pose_identity(&a.pose[0]); + video_xr_pose_identity(&a.pose[1]); + a.pose[1].position.x = 0.25f; + a.valid = 2u; + openxr_frame_publish_aims(&f, &a); + openxr_frame_get_aims(&f, &got); + CHECK(got.valid == 2u && got.pose[1].position.x == 0.25f + && got.pose[1].orientation.w == 1.0f, + "read back valid %#x at %.3f", got.valid, got.pose[1].position.x); + openxr_frame_session_created(&f); + openxr_frame_get_aims(&f, &got); + CHECK(got.valid == 0u, "a new session kept aims %#x", got.valid); + t_grab_free(&rt, &s, &sc, &f); +} + +/* The hand in the room, the screens elsewhere: Fixed in Room at an + * anchor turned a quarter left, the held item goes where the hand puts + * it in LOCAL; Follow Head asks the hand in VIEW. */ +static void test_frame_grabs_spaces(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + t_head.position.x = 0.5f; + t_head.position.y = 0.2f; + t_head.position.z = 0.3f; + t_head_tracked = true; + openxr_frame_request_recenter(&f); + openxr_frame_run(&f); + t_head_tracked = false; + t_grab_held(&grabs); + openxr_frame_publish_grabs(&f, &grabs); + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.x = 0.3f; + t_aim_ok = true; + t_aim_space = XR_NULL_HANDLE; + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK( t_aim_space == s.local_space + && t_near(layers[0].pose.position.x, 0.3f) + && t_near(layers[0].pose.position.y, 0.0f) + && t_near(layers[0].pose.position.z, -1.0f) + && t_near(layers[0].pose.orientation.w, 1.0f), + "in the room: at %.2f,%.2f,%.2f", layers[0].pose.position.x, + layers[0].pose.position.y, layers[0].pose.position.z); + openxr_frame_set_placement(&f, VIDEO_OPENXR_PLACEMENT_FOLLOW); + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK( t_aim_space == s.view_space + && layers[0].space == s.view_space + && t_near(layers[0].pose.position.x, 0.3f) + && t_near(layers[0].pose.position.z, -1.0f), + "Follow Head: the hand not asked in VIEW, or held at %.2f,%.2f", + layers[0].pose.position.x, layers[0].pose.position.z); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Snap to Screens on the frame loop: taken where it is, the held + * screen does not hop; taken further off, it snaps to the one no hand + * holds exactly as the main thread's call places it, from the grab's + * pose the grabs record carries; off, it goes where the hand puts it. */ +static void test_frame_grabs_snap(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t want; + video_xr_hinge_t wh; + video_xr_pose_t pw; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + /* Screen 0, held 2 x 1.2 m, 1 cm above standing on screen 1, and + * taken there. */ + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.09f; + t_aim_pose.position.z = -0.8f; + t_aim_ok = true; + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.y, 0.09f), + "taken where it was, it hopped to %.4f", layers[0].pose.position.y); + /* Taken 10 cm lower: the pull is whole. */ + grabs.start[1].position.y -= 0.1f; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, &set, 1u, + &grabs.start[1], &want, NULL, NULL); + CHECK(t_near(want.position.y, 0.08f) + && fabs(layers[0].pose.position.y - want.position.y) < 1e-6 + && fabs(layers[1].pose.position.y - want.position.y) < 1e-6 + && fabs(layers[0].pose.position.z - want.position.z) < 1e-6, + "the frame loop snapped it to %.5f, the main thread's call to %.5f", + layers[0].pose.position.y, want.position.y); + /* Snapped flush on screen 1's top edge it hinges there, and the + * frame loop draws the hinge the main thread's call finds and marks + * where it draws it. */ + memset(&wh, 0, sizeof(wh)); + CHECK(video_xr_grab_hinge(&grabs.hand[1], &t_aim_pose, &set, 1u, + &grabs.start[1], &want, &wh) & VIDEO_XR_SNAP_HINGE, + "the main thread's call finds no hinge"); + t_mark(&grabs, 1, wh.slot, wh.on, wh.edge); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 2500); + openxr_frame_layers(&f, 2500, layers, ptrs); + CHECK(f.hinged == 2u && f.held[1].hinged, + "hinged %#x on the frame loop, %d kept", f.hinged, + (int)f.held[1].hinged); + video_xr_pose_identity(&pw); + pw.position = wh.point; + video_xr_compose(&f.screens.pose, &pw, &pw); + CHECK( f.hinges[1].slot == wh.slot && f.hinges[1].on == wh.on + && f.hinges[1].edge == wh.edge && wh.slot == 1 && wh.on == 0 + && wh.edge == 1 + && t_near(f.hinges[1].point.x, pw.position.x) + && t_near(f.hinges[1].point.y, pw.position.y) + && t_near(f.hinges[1].point.z, pw.position.z) + && t_near(f.hinges[1].gap, wh.gap), + "the frame loop's hinge on %u edge %u at %.4f, the main thread's " + "on %u edge %u at %.4f", f.hinges[1].slot, f.hinges[1].on, + f.hinges[1].point.y, wh.slot, wh.on, pw.position.y); + grabs.snap = false; + t_mark(&grabs, 1, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 3000); + openxr_frame_layers(&f, 3000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.y, 0.09f), + "snapped at %.4f with Snap to Screens off", + layers[0].pose.position.y); + CHECK(f.hinged == 0u && !f.held[1].hinged, + "hinged %#x with Snap to Screens off", f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Screen 0 pushed down past touching screen 1's top edge: the frame + * loop shows it touching, as the main thread's call does, 0.5 and 1 cm + * past at Hinge Gap 0, 2 and 10, and 0.5, 1 and 1.4 cm past at Hinge + * Gap 1, whose stop holds from where its click's fade shows touching; + * Snap to Screens off, where the hand has it. */ +static void test_frame_grabs_stop(void) +{ + static const unsigned char gaps[] = { 0, 0, 2, 2, 10, 10, 1, 1, 1, 2, + 2 }; + static const double past[] = { 0.005, 0.01, 0.005, 0.01, 0.005, 0.01, + 0.005, 0.01, 0.014, 0.005, 0.01 }; + unsigned i; + for (i = 0; i < 11; i++) + { + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t want; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + double c = past[i]; + double y = (i < 9) ? 0.08 : 0.08 - c; + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = i < 9; + grabs.hand[1].gap = gaps[i]; + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.08f; + t_aim_pose.position.z = -0.8f; + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + t_aim_pose.position.y -= (float)c; + t_aim_ok = true; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, + grabs.snap ? &set : NULL, 1u, &grabs.start[1], &want, NULL, + NULL); + CHECK( fabs(layers[0].pose.position.y - y) < 1e-5 + && fabs(want.position.y - layers[0].pose.position.y) < 1e-6, + "gap %u, snap %d, %.3f past: the frame loop at %.5f, the main " + "thread at %.5f, want %.5f", gaps[i], (int)grabs.snap, c, + layers[0].pose.position.y, want.position.y, y); + t_grab_free(&rt, &s, &sc, &f); + } +} + +/* The main thread's mark for hand 1, as its poll keeps it. */ +typedef struct t_main +{ + video_xr_hinge_t mark; + XrTime out; + bool marked; +} t_main_t; + +/* INPUT_OPENXR_HINGE_DEBOUNCE_US, in XrTime's nanoseconds. */ +#define T_MARK_NS 150000000 + +/* As the main thread's poll at time with hand 1's aim where it is + * (input_openxr_edit_move): the hinge it was taken in kept while + * video_xr_grab_kept; a hinge a let-go there settles in marked at + * once, the mark gone T_MARK_NS out of that; through set as a push + * lets the marked hinge go, cleared as one takes hold; published as + * any of them changes. */ +static void t_main_poll(openxr_frame_t *f, openxr_grabs_t *grabs, + const video_xr_quad_set_t *set, XrTime time, t_main_t *m) +{ + video_xr_pose_t pose; + video_xr_hinge_t hinge; + const video_xr_hinge_t *c = NULL; + video_xr_grab_t *g = &grabs->hand[1]; + video_xr_grab_t was = *g; + bool hinged = (video_xr_grab_hinge(g, &t_aim_pose, set, 1u, + &grabs->start[1], &pose, &hinge) & VIDEO_XR_SNAP_HINGE) != 0; + if (g->rest_slot) + { + video_xr_hinge_t kept; + if ( !video_xr_grab_kept(g, &t_aim_pose, set, &grabs->start[1], + &kept) + || (hinged && (hinge.slot != kept.slot || hinge.on != kept.on + || hinge.edge != kept.edge))) + g->rest_slot = 0; + } + if ( hinged + && video_xr_grab_settle(g, &t_aim_pose, set, 1u, &hinge, &pose)) + c = &hinge; + else if (m->marked + && video_xr_grab_settle(g, &t_aim_pose, set, 1u, &m->mark, &pose)) + c = &m->mark; + if (c) + { + m->mark = *c; + m->marked = true; + m->out = 0; + } + else if (m->marked && !m->out) + m->out = time; + else if (m->marked && time - m->out >= T_MARK_NS) + m->marked = false; + if (hinged) + g->through = 0; + else if (m->marked + && video_xr_grab_through(g, &t_aim_pose, set, &m->mark)) + g->through = 1; + t_mark(grabs, 1, m->marked ? m->mark.slot : ~0u, m->mark.on, + m->mark.edge); + if ( g->through != was.through || g->rest_slot != was.rest_slot + || g->mark_slot != was.mark_slot || g->mark_on != was.mark_on + || g->mark_edge != was.mark_edge) + openxr_frame_publish_grabs(f, grabs); +} + +/* Screen 0 hinged on screen 1's top edge at Hinge Gap 0, pushed down + * through it 1 mm a frame to 5 cm past and back, then shaken 3 mm at + * 8 Hz about 3 cm past and about 2.5 cm, the main thread polling each + * frame (t_main_poll): no frame moves it more than the stop's fade, + * 3.4 times the hand, and the line follows the published mark on + * every frame. Marked 2.8 cm past, a through published there keeps + * the line; the mark dropped takes it at once. Main's side: + * test_edit_gap_push and test_edit_gap_push_shaken. */ +static void test_frame_grabs_push(void) +{ + unsigned i; + unsigned ends = 0; + unsigned takes = 0; + unsigned off = 0; + bool was = false; + bool on = false; + float last = 0.08f; + t_main_t m; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + memset(&m, 0, sizeof(m)); + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.08f; + t_aim_pose.position.z = -0.8f; + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + t_aim_ok = true; + openxr_frame_publish_grabs(&f, &grabs); + for (i = 0; i <= 500; i++) + { + XrTime time = 1000 + (XrTime)i * 11111111; + unsigned k = (i <= 50) ? i : 100 - i; + double x = 0.001 * k; + if (i > 100) + x = ((i <= 300) ? 0.03 : 0.025) + 0.003 * sin((i - 101) * 0.5); + t_aim_pose.position.y = (float)(0.08 - x); + t_main_poll(&f, &grabs, &set, time, &m); + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + on = (f.hinged & 2u) != 0; + if (i && i <= 100) + CHECK(fabs(layers[0].pose.position.y - last) <= 0.00341, + "%s %u mm past: moved %.2f mm", (i <= 50) ? "in" : "back", + k, fabs(layers[0].pose.position.y - last) * 1e3); + off += (on != m.marked) ? 1u : 0u; + ends += (was && !on) ? 1u : 0u; + takes += (i && !was && on) ? 1u : 0u; + last = layers[0].pose.position.y; + was = on; + } + CHECK(!off && ends && takes, + "%u frames off the published mark; it ended %u, was taken %u", + off, ends, takes); + t_aim_pose.position.y = 0.08f - 0.028f; + t_mark(&grabs, 1, 1, 0, 1); + grabs.hand[1].through = 0; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000 + 501 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 501 * (XrTime)11111111, layers, ptrs); + on = (f.hinged & 2u) != 0; + grabs.hand[1].through = 1; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000 + 502 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 502 * (XrTime)11111111, layers, ptrs); + was = (f.hinged & 2u) != 0; + t_mark(&grabs, 1, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000 + 503 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 503 * (XrTime)11111111, layers, ptrs); + CHECK(on && was && f.hinged == 0u, + "2.8 cm past: hinged %d, through: hinged %d, the mark dropped: " + "hinged %#x", (int)on, (int)was, f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Screen 0 hinged on screen 1's top edge at Hinge Gap 0, the main + * thread polling every third frame (t_main_poll). Held 2.9 cm past, + * the frames between graze 3.02 cm past; pushed through to 3.2 cm and + * back to 2.6, the frames between dip to 2.45; then held at 2.45: the + * line follows the published mark on every frame, whatever the frames + * between show. Main's side: test_edit_mark_shaken. */ +static void test_frame_grabs_graze(void) +{ + unsigned i; + unsigned blinks = 0; + t_main_t mk; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + memset(&mk, 0, sizeof(mk)); + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.08f; + t_aim_pose.position.z = -0.8f; + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + t_aim_ok = true; + openxr_frame_publish_grabs(&f, &grabs); + for (i = 0; i <= 160; i++) + { + bool on; + XrTime time = 1000 + (XrTime)i * 11111111; + double m = (i < 30) ? 0.001 * i : 0.029; + double x; + if (i >= 60) + m = (i < 63) ? 0.03 + 0.001 * (i - 60) + : (i < 69) ? 0.032 - 0.001 * (i - 63) : 0.026; + if (i >= 120) + m = 0.0245; + x = m; + if (i % 3 && i >= 30 && i < 60) + x = 0.0302; + if (i % 3 && i >= 90 && i < 120) + x = 0.0245; + if (!(i % 3)) + { + t_aim_pose.position.y = (float)(0.08 - m); + t_main_poll(&f, &grabs, &set, time, &mk); + } + t_aim_pose.position.y = (float)(0.08 - x); + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + on = (f.hinged & 2u) != 0; + blinks += (on != mk.marked) ? 1u : 0u; + if (i == 59 || i == 119 || i == 160) + CHECK(blinks == 0, "frame %u: %u frames off the published mark", + i, blinks); + } + t_grab_free(&rt, &s, &sc, &f); +} + +/* Snapping on, each hand holding a screen: the frame loop snaps neither + * to the other. Screen 0 held 1 cm above standing on screen 1, both + * taken 10 cm off, so any pull would be whole. */ +static void test_frame_grabs_snap_two(void) +{ + unsigned h; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + grabs.hand[0] = grabs.hand[1]; + grabs.hand[0].slot = 1; + grabs.hand[0].rel.position.y = -1.09f; + grabs.hand[0].last.position.y = -1.0f; + grabs.hand[0].width = 1.6f; + grabs.hand[0].height = 0.96f; + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.09f; + t_aim_pose.position.z = -0.8f; + t_aim_ok = true; + for (h = 0; h < 2; h++) + { + video_xr_grab_pose(&grabs.hand[h], &t_aim_pose, NULL, 0, NULL, + &grabs.start[h], NULL, NULL); + grabs.start[h].position.y -= 0.1f; + } + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK( t_near(layers[0].pose.position.y, 0.09f) + && t_near(layers[2].pose.position.y, -1.0f), + "both held: screen 0 snapped to %.4f, screen 1 to %.4f", + layers[0].pose.position.y, layers[2].pose.position.y); + CHECK(f.hinged == 0u, "both held: hinged %#x", f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +/* The hinge where the frame draws: the anchor a quarter turn left, + * screen 0 held flush on screen 1's top edge as test_frame_grabs_snap + * holds it, marked there, the hinge on screen 0's drawn bottom edge, + * its line along that edge, flat the way screen 1's drawn top faces. + * Let go, no hinge. Main's side: test_edit_mark_switch. */ +static void test_frame_grabs_hinge(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t aim; + video_xr_quat_t o; + video_xr_vec3_t v, edge, x, up; + const video_xr_hinge_t *k = &f.hinges[1]; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + t_head_tracked = true; + openxr_frame_request_recenter(&f); + openxr_frame_run(&f); + t_head_tracked = false; + t_grab_held(&grabs); + grabs.snap = true; + video_xr_pose_identity(&aim); + aim.position.y = 0.09f; + aim.position.z = -0.8f; + video_xr_grab_pose(&grabs.hand[1], &aim, NULL, 0, NULL, &grabs.start[1], + NULL, NULL); + grabs.start[1].position.y -= 0.1f; + t_mark(&grabs, 1, 1, 0, 1); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + video_xr_compose(&f.screens.pose, &aim, &t_aim_pose); + t_aim_ok = true; + openxr_frame_layers(&f, 1000, layers, ptrs); + o.x = layers[0].pose.orientation.x; + o.y = layers[0].pose.orientation.y; + o.z = layers[0].pose.orientation.z; + o.w = layers[0].pose.orientation.w; + v.x = 0.0f; + v.y = -0.6f; + v.z = 0.0f; + video_xr_rotate(&o, &v, &edge); + edge.x += layers[0].pose.position.x; + edge.y += layers[0].pose.position.y; + edge.z += layers[0].pose.position.z; + v.x = 1.0f; + v.y = 0.0f; + video_xr_rotate(&o, &v, &x); + o.x = layers[2].pose.orientation.x; + o.y = layers[2].pose.orientation.y; + o.z = layers[2].pose.orientation.z; + o.w = layers[2].pose.orientation.w; + v.x = 0.0f; + v.y = 1.0f; + video_xr_rotate(&o, &v, &up); + CHECK( f.hinged == 2u + && t_near(k->point.x, edge.x) && t_near(k->point.y, edge.y) + && t_near(k->point.z, edge.z) + && t_near((float)fabs(k->dir.x * x.x + k->dir.y * x.y + + k->dir.z * x.z), 1.0f) + && t_near(k->flat.x, up.x) && t_near(k->flat.y, up.y) + && t_near(k->flat.z, up.z), + "hinged %#x at %.3f,%.3f,%.3f, the drawn edge at %.3f,%.3f,%.3f", + f.hinged, k->point.x, k->point.y, k->point.z, edge.x, edge.y, + edge.z); + t_grab_let_go(&grabs, 3, 0.0f); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK(f.hinged == 0u, "let go: hinged %#x", f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Lost, the hand keeps its marked hinge as it keeps the pose, drawn + * where a recenter puts the screens; the other hand taking the hinge's + * target ends it, and that hand letting go does not bring it back. + * Main's side: test_edit_mark_debounce's rows for screen 1 taken. */ +static void test_frame_grabs_hinge_kept(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t at, inv; + video_xr_vec3_t was; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.y = 0.09f; + t_aim_pose.position.z = -0.8f; + t_aim_ok = true; + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.1f; + t_mark(&grabs, 1, 1, 0, 1); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + was = f.hinges[1].point; + video_xr_pose_invert(&f.screens.pose, &inv); + CHECK(f.hinged == 2u && f.held[1].hinge.slot == 1, + "held: hinged %#x on slot %u", f.hinged, f.held[1].hinge.slot); + /* Lost, the head turned a quarter left and recentered on. */ + t_aim_ok = false; + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + t_head_tracked = true; + openxr_frame_request_recenter(&f); + openxr_frame_run(&f); + t_head_tracked = false; + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + video_xr_pose_identity(&at); + at.position = was; + video_xr_compose(&inv, &at, &at); + video_xr_compose(&f.screens.pose, &at, &at); + CHECK( f.hinged == 2u && f.held[1].hinged + && t_near(f.hinges[1].point.x, at.position.x) + && t_near(f.hinges[1].point.y, at.position.y) + && t_near(f.hinges[1].point.z, at.position.z) + && !t_near(f.hinges[1].point.x, was.x), + "lost: hinged %#x at %.3f,%.3f,%.3f, the recentered hinge at " + "%.3f,%.3f,%.3f", f.hinged, f.hinges[1].point.x, + f.hinges[1].point.y, f.hinges[1].point.z, at.position.x, + at.position.y, at.position.z); + /* The left hand takes screen 1, which ends the mark in the same + * publish, then lets it go. */ + grabs.hand[0] = grabs.hand[1]; + t_mark(&grabs, 0, ~0u, 0, 0); + t_mark(&grabs, 1, ~0u, 0, 0); + grabs.hand[0].slot = 1; + grabs.hand[0].rel.position.y = -1.09f; + grabs.hand[0].last.position.y = -1.0f; + grabs.hand[0].width = 1.6f; + grabs.hand[0].height = 0.96f; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 3000); + openxr_frame_layers(&f, 3000, layers, ptrs); + CHECK(f.hinged == 0u && !f.held[1].hinged, + "its target taken: hinged %#x, %d kept", f.hinged, + (int)f.held[1].hinged); + grabs.hand[0].state = VIDEO_XR_GRAB_RELEASED; + grabs.hand[0].release_seq = 3; + grabs.let_go[1] = grabs.hand[0]; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 4000); + openxr_frame_layers(&f, 4000, layers, ptrs); + CHECK(f.hinged == 0u, "its target let go: hinged %#x", f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +#define T_PI 3.14159265358979323846 + +/* A hand's shake at t: three sines, within about 1.8 either way, axis + * k in its own phase. */ +static double t_shake(double t, unsigned k) +{ + static const double hz[3] = { 4.3, 7.1, 10.9 }; + unsigned i; + double s = 0.0; + for (i = 0; i < 3; i++) + s += sin(2.0 * T_PI * hz[i] * t + 1.7 * (k + 1) * (i + 1)); + return s / 1.7; +} + +/* aim shaken at t by about m metres and deg degrees about the hand. */ +static void t_shaken(video_xr_pose_t *aim, double t, double m, double deg) +{ + video_xr_pose_t by, was; + double x = t_shake(t, 3); + double y = t_shake(t, 4); + double z = t_shake(t, 5); + double n = sqrt(x * x + y * y + z * z); + double a = deg * n / 1.7 * T_PI / 360.0; + video_xr_pose_identity(&by); + video_xr_pose_identity(&was); + if (n > 0.0) + { + by.orientation.x = (float)(x / n * sin(a)); + by.orientation.y = (float)(y / n * sin(a)); + by.orientation.z = (float)(z / n * sin(a)); + by.orientation.w = (float)cos(a); + } + was.orientation = aim->orientation; + video_xr_compose(&by, &was, &was); + aim->orientation = was.orientation; + aim->position.x += (float)(m * t_shake(t, 0)); + aim->position.y += (float)(m * t_shake(t, 1)); + aim->position.z += (float)(m * t_shake(t, 2)); +} + +/* Whether hand 1's hinge where f draws it is k's, slot and edges, its + * line within 0.5 mm of midway between the edges where f shows them: + * the held one's middle and the nearest point of set's target edge; + * crossed, across on the target's, drawn over the held screen. */ +static bool t_hinge_midway(const openxr_frame_t *f, + const video_xr_quad_set_t *set, const video_xr_hinge_t *k) +{ + unsigned i; + double m[3], n[3], q[3], g[3], ab[3], s, x, len, off; + video_xr_vec3_t mid, v, e, u, a, b; + video_xr_quad_t target; + video_xr_pose_t at; + const video_xr_hinge_t *h = &f->hinges[1]; + const video_xr_pose_t *p = &f->held[1].pose; + const video_xr_grab_t *gr = &f->grabs.hand[1]; + if (h->slot != k->slot || h->on != k->on || h->edge != k->edge) + return false; + for (i = 0; i < set->num_quads; i++) + if ( set->quads[i].slot == h->slot + && set->quads[i].kind != VIDEO_XR_QUAD_MENU) + break; + if (i == set->num_quads) + return false; + target = set->quads[i]; + mid.x = (h->edge == 2) ? gr->width * 0.5f + : (h->edge == 3) ? -gr->width * 0.5f : 0.0f; + mid.y = (h->edge == 0) ? gr->height * 0.5f + : (h->edge == 1) ? -gr->height * 0.5f : 0.0f; + mid.z = 0.0f; + video_xr_rotate(&p->orientation, &mid, &v); + m[0] = (double)p->position.x + v.x; + m[1] = (double)p->position.y + v.y; + m[2] = (double)p->position.z + v.z; + mid.x = (h->on == 2) ? 1.0f : (h->on == 3) ? -1.0f : 0.0f; + mid.y = (h->on == 0) ? 1.0f : (h->on == 1) ? -1.0f : 0.0f; + video_xr_rotate(&target.pose.orientation, &mid, &u); + mid.x = (h->on < 2) ? 1.0f : 0.0f; + mid.y = (h->on < 2) ? 0.0f : 1.0f; + video_xr_rotate(&target.pose.orientation, &mid, &e); + mid.x = (h->on == 2) ? target.width * 0.5f + : (h->on == 3) ? -target.width * 0.5f : 0.0f; + mid.y = (h->on == 0) ? target.height * 0.5f + : (h->on == 1) ? -target.height * 0.5f : 0.0f; + video_xr_rotate(&target.pose.orientation, &mid, &v); + n[0] = (double)target.pose.position.x + v.x; + n[1] = (double)target.pose.position.y + v.y; + n[2] = (double)target.pose.position.z + v.z; + s = (m[0] - n[0]) * e.x + (m[1] - n[1]) * e.y + (m[2] - n[2]) * e.z; + n[0] += s * e.x; + n[1] += s * e.y; + n[2] += s * e.z; + for (i = 0; i < 3; i++) + g[i] = m[i] - n[i]; + x = g[0] * u.x + g[1] * u.y + g[2] * u.z; + if (x < 0.0) + { + g[0] -= x * u.x; + g[1] -= x * u.y; + g[2] -= x * u.z; + } + for (i = 0; i < 3; i++) + q[i] = n[i] + g[i] * 0.5; + video_xr_pose_identity(&at); + at.position.x = (float)q[0]; + at.position.y = (float)q[1]; + at.position.z = (float)q[2]; + video_xr_compose(&f->screens.pose, &at, &at); + video_xr_compose(&f->screens.pose, &target.pose, &target.pose); + if (!video_xr_hinge_line(h, gr->width, gr->height, &target, &a, &b)) + return false; + ab[0] = (double)b.x - a.x; + ab[1] = (double)b.y - a.y; + ab[2] = (double)b.z - a.z; + len = sqrt(ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2]); + q[0] = (double)at.position.x - a.x; + q[1] = (double)at.position.y - a.y; + q[2] = (double)at.position.z - a.z; + s = (q[0] * ab[0] + q[1] * ab[1] + q[2] * ab[2]) / (len * len); + off = 0.0; + for (i = 0; i < 3; i++) + off += (q[i] - s * ab[i]) * (q[i] - s * ab[i]); + return sqrt(off) <= 5e-4; +} + +/* Hand 1 holding screen 0 as grabs has it, n frames at 90 Hz, its aim + * at t from path shaken by m metres and deg degrees, the main thread + * polling every third frame (t_main_poll): how often the frame loop's + * hinge for it came or went, in *on whether it shows at the end, in + * *off_line the frames it shows other than the published mark or its + * line off midway between the edges. */ +static unsigned t_hinge_shaken(openxr_frame_t *f, + const video_xr_quad_set_t *set, openxr_grabs_t *grabs, + void (*path)(double t, video_xr_pose_t *aim), unsigned n, double m, + double deg, bool *on, unsigned *off_line) +{ + unsigned i; + t_main_t mk; + unsigned changes = 0; + bool shown = false; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + memset(&mk, 0, sizeof(mk)); + *off_line = 0; + for (i = 0; i < n; i++) + { + double t = i / 90.0; + XrTime time = 1000 + (XrTime)i * 11111111; + bool was = shown; + path(t, &t_aim_pose); + t_shaken(&t_aim_pose, t, m, deg); + if (!(i % 3)) + t_main_poll(f, grabs, set, time, &mk); + openxr_frame_place_screens(f, time); + openxr_frame_layers(f, time, layers, ptrs); + shown = ((f->hinged >> 1) & 1u) != 0; + if (i && shown != was) + changes++; + if ( shown != mk.marked + || (shown && !t_hinge_midway(f, set, &mk.mark))) + (*off_line)++; + } + *on = shown; + return changes; +} + +/* Screen 0 flush on screen 1's top edge, held there. */ +static void t_path_still(double t, video_xr_pose_t *aim) +{ + (void)t; + video_xr_pose_identity(aim); + aim->position.y = 0.08f; + aim->position.z = -0.8f; +} + +/* Over screen 1's top edge, 5 cm in front of its plane, carried back at + * 1 cm/s into it. */ +static void t_path_depth(double t, video_xr_pose_t *aim) +{ + t_path_still(t, aim); + aim->position.z += (float)(0.05 - 0.01 * (t < 5.0 ? t : 5.0)); +} + +/* 14 cm over screen 1's top edge, lowered at 1 cm/s to 8 cm: into the + * free gap past the 10 cm where its hinge holds whole. */ +static void t_path_gap(double t, video_xr_pose_t *aim) +{ + t_path_still(t, aim); + aim->position.y += (float)(0.14 - 0.01 * (t < 6.0 ? t : 6.0)); +} + +/* Flush, carried 8 cm along the edge at 2 cm/s and back. */ +static void t_path_slide(double t, video_xr_pose_t *aim) +{ + double x = (t < 4.0) ? 0.02 * t : 0.08 - 0.02 * (t - 4.0); + t_path_still(t, aim); + aim->position.x = (float)((x > 0.0) ? x : 0.0); +} + +/* The hand shaking 1 mm and 0.15 degrees, the frame loop's hinge comes + * on once with the published mark and stays at each of the hinge's + * limits: carried in from in front of screen 1's plane, lowered across + * the free gap, taken where it hinges and carried along and back + * through the grab's fade, and held at a square corner. While it + * shows, its line lies midway between the edges. Main's side: + * test_edit_mark_shaken. */ +static void test_frame_grabs_hinge_shaken(void) +{ + static const char *what[4] = { "depth", "gap", "fade", "square" }; + unsigned i; + for (i = 0; i < 4; i++) + { + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t aim; + unsigned changes, off_line; + bool on; + void (*path)(double t, video_xr_pose_t *aim) = t_path_still; + unsigned n = 270; + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + t_path_still(0.0, &aim); + video_xr_grab_pose(&grabs.hand[1], &aim, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + if (i < 2) + grabs.start[1].position.y -= 0.3f; + if (i == 0) + { + path = t_path_depth; + n = 540; + } + else if (i == 1) + { + path = t_path_gap; + n = 630; + } + else if (i == 2) + { + path = t_path_slide; + n = 720; + } + else + { + video_xr_pose_t pose; + video_xr_hinge_t h; + grabs.start[1].position.y -= 0.3f; + CHECK(video_xr_grab_hinge(&grabs.hand[1], &aim, &set, 1u, + &grabs.start[1], &pose, &h) & VIDEO_XR_SNAP_HINGE, + "fixture: not hinged flush"); + video_xr_grab_put(&grabs.hand[1], &aim, &pose); + video_xr_grab_swing(&grabs.hand[1], &aim, &h, 90.0); + } + t_aim_ok = true; + openxr_frame_publish_grabs(&f, &grabs); + changes = t_hinge_shaken(&f, &set, &grabs, path, n, 0.001, 0.15, + &on, &off_line); + CHECK(changes <= 1 && on && !off_line, + "%s: the hinge came or went %u times, shows %d at the end, " + "%u frames off its line", what[i], changes, (int)on, off_line); + t_grab_free(&rt, &s, &sc, &f); + } +} + +/* Flush, then carried 4 cm in front of screen 1's plane, 5 cm, back to + * 4 cm and flush, 0.2 s at each, the main thread polling each frame + * (t_main_poll): the line follows the published mark on every frame, + * midway between the edges 4 cm in front while it shows, gone by the + * end of 5 cm and back flush. Main's side: test_edit_mark_debounce. */ +static void test_frame_grabs_hinge_reach(void) +{ + static const float out[5] = { 0.0f, 0.04f, 0.05f, 0.04f, 0.0f }; + unsigned i, j; + unsigned off = 0; + unsigned shown = 0; + unsigned apart = 0; + t_main_t mk; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + memset(&mk, 0, sizeof(mk)); + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + t_path_still(0.0, &t_aim_pose); + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + t_aim_ok = true; + openxr_frame_publish_grabs(&f, &grabs); + for (i = 0; i < 5; i++) + { + for (j = 0; j < 18; j++) + { + XrTime time = 1000 + (XrTime)(i * 18 + j) * 11111111; + bool on; + t_path_still(0.0, &t_aim_pose); + t_aim_pose.position.z += out[i]; + t_main_poll(&f, &grabs, &set, time, &mk); + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + on = ((f.hinged >> 1) & 1u) != 0; + off += (on != mk.marked) ? 1u : 0u; + if (i == 1 && on) + { + shown++; + apart += t_hinge_midway(&f, &set, &mk.mark) ? 1u : 0u; + } + } + CHECK( !off && (i != 1 || (shown && apart == shown)) + && (i == 1 || i == 3 || mk.marked == (out[i] == 0.0f)), + "%.0f cm out: %u frames off the published mark, %u of %u " + "shown midway, marked %d", out[i] * 100.0f, off, apart, shown, + (int)mk.marked); + } + t_grab_free(&rt, &s, &sc, &f); +} + +/* The line and rims follow the published mark (t_mark), nothing of + * the frame's own: marked, screen 0 shows that hinge on every frame, + * midway between the edges, flush on screen 1's top edge, 4 cm in front + * of its plane or 12 cm above it; unmarked, flush where it hinges + * exactly, none; the mark switched to screen 2's left edge, that one + * from the publish's first frame; its target not in the quads or held + * by the other hand, none. */ +static void test_frame_grabs_mark(void) +{ + static const float up[3] = { 0.0f, 0.0f, 0.12f }; + static const float toward[3] = { 0.0f, 0.04f, 0.0f }; + unsigned i, j; + video_xr_hinge_t k; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + /* Screen 2 70 cm right of screen 0, off any pull. */ + set.quads[3] = set.quads[2]; + set.quads[3].pose.position.x = 2.5f; + set.quads[3].pose.position.y = 0.0f; + set.quads[3].screen = 2; + set.quads[3].slot = 2; + set.num_quads = 4; + set.slots[2].layers = 1; + set.slots[2].edit_seq = 2; + openxr_frame_publish(&f, &set); + t_grab_held(&grabs); + grabs.snap = true; + t_path_still(0.0, &t_aim_pose); + video_xr_grab_pose(&grabs.hand[1], &t_aim_pose, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + t_aim_ok = true; + memset(&k, 0, sizeof(k)); + k.slot = 1; + k.on = 0; + k.edge = 1; + t_mark(&grabs, 1, 1, 0, 1); + openxr_frame_publish_grabs(&f, &grabs); + for (i = 0; i < 3; i++) + for (j = 0; j < 3; j++) + { + XrTime time = 1000 + (XrTime)(i * 3 + j) * 11111111; + t_path_still(0.0, &t_aim_pose); + t_aim_pose.position.y += up[i]; + t_aim_pose.position.z += toward[i]; + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + CHECK(f.hinged == 2u && t_hinge_midway(&f, &set, &k), + "marked %.0f cm up, %.0f cm toward, frame %u: hinged %#x " + "on %u/%u/%u", up[i] * 100.0f, toward[i] * 100.0f, j, + f.hinged, f.hinges[1].slot, f.hinges[1].on, + f.hinges[1].edge); + } + /* Flush, where it hinges whole: the frame's own snap holds it, but + * with no mark there is no line. */ + t_mark(&grabs, 1, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + t_path_still(0.0, &t_aim_pose); + openxr_frame_place_screens(&f, 1000 + 9 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 9 * (XrTime)11111111, layers, ptrs); + CHECK(f.hinged == 0u && !f.held[1].hinged + && t_near(layers[0].pose.position.y, 0.08f), + "unmarked flush: hinged %#x at %.4f", f.hinged, + layers[0].pose.position.y); + /* The mark switched to screen 2's left edge, by screen 0's right. */ + k.slot = 2; + k.on = 3; + k.edge = 2; + t_mark(&grabs, 1, 2, 3, 2); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000 + 10 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 10 * (XrTime)11111111, layers, ptrs); + CHECK( f.hinged == 2u && f.hinges[1].slot == 2 + && f.hinges[1].on == 3 && f.hinges[1].edge == 2 + && t_hinge_midway(&f, &set, &k), + "switched: hinged %#x on %u/%u/%u", f.hinged, f.hinges[1].slot, + f.hinges[1].on, f.hinges[1].edge); + /* Marked on a screen not in the frame's quads: no line, no rim cut. */ + t_mark(&grabs, 1, 3, 0, 1); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000 + 11 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 11 * (XrTime)11111111, layers, ptrs); + CHECK(f.hinged == 0u, "its target not in the quads: hinged %#x", + f.hinged); + t_mark(&grabs, 1, 2, 3, 2); + /* The left hand holds screen 2, the mark not yet gone. */ + grabs.hand[0] = grabs.hand[1]; + grabs.hand[0].slot = 2; + grabs.hand[0].last = set.quads[3].pose; + grabs.hand[0].width = 1.6f; + grabs.hand[0].height = 0.96f; + t_mark(&grabs, 0, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + t_aim_ok = false; + openxr_frame_place_screens(&f, 1000 + 12 * (XrTime)11111111); + openxr_frame_layers(&f, 1000 + 12 * (XrTime)11111111, layers, ptrs); + CHECK(f.hinged == 0u, "its target held: hinged %#x", f.hinged); + t_grab_free(&rt, &s, &sc, &f); +} + +/* still shaken as openxr_input_test.c's t_shake shakes a hand on a + * device, at t seconds, k its step: turned deg degrees in pitch and yaw + * at 8 Hz and a fifth of that at 2.3 Hz, the yaw a quarter period + * behind, and moved 0.3 mm each way. */ +static void t_shake_device(video_xr_pose_t *aim, const video_xr_pose_t *still, + double t, unsigned k, double deg) +{ + unsigned i; + double turn[2]; + double a = 2.0 * T_PI * 8.0 * t; + double b = 2.0 * T_PI * 2.3 * t + 1.0; + double m = 0.0003 * sin(k * 1.3); + turn[0] = deg * (sin(a) + 0.2 * sin(b)); + turn[1] = deg * (sin(a - T_PI / 2.0) + 0.2 * sin(b - T_PI / 2.0)); + *aim = *still; + for (i = 0; i < 2; i++) + { + video_xr_pose_t d; + double half = turn[i] * T_PI / 360.0; + video_xr_pose_identity(&d); + d.orientation.w = (float)cos(half); + if (i) + d.orientation.y = (float)sin(half); + else + d.orientation.x = (float)sin(half); + video_xr_compose(aim, &d, aim); + } + aim->position.x += (float)m; + aim->position.y += (float)m; + aim->position.z += (float)m; +} + +/* Screen 0 marked on screen 1's top edge, held 2.8 cm past touching, + * at the end of the stop's hold, its aim shaken every frame as + * t_shake_device shakes it (0.5 degrees), the main thread publishing + * every third frame with its mark steady: in 1920 frames the line + * never flips. The mark dropped at publish 300, the line goes from + * that publish's first frame, never before. */ +static void test_frame_grabs_mark_shaken(void) +{ + unsigned i; + unsigned before = 0; + unsigned after = 0; + video_xr_pose_t still; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + t_path_still(0.0, &still); + video_xr_grab_pose(&grabs.hand[1], &still, NULL, 0, NULL, + &grabs.start[1], NULL, NULL); + grabs.start[1].position.y -= 0.3f; + still.position.y -= 0.028f; + t_mark(&grabs, 1, 1, 0, 1); + t_aim_ok = true; + for (i = 0; i < 1920; i++) + { + XrTime time = 1000 + (XrTime)i * 11111111; + if (!(i % 3)) + { + if (i / 3 == 300) + t_mark(&grabs, 1, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + } + t_shake_device(&t_aim_pose, &still, i / 90.0, i, 0.5); + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + if (i < 900) + before += (f.hinged != 2u) ? 1u : 0u; + else + after += (f.hinged != 0u) ? 1u : 0u; + } + CHECK(!before && !after, + "shaken 2.8 cm past: %u frames off while marked, %u on after", + before, after); + t_grab_free(&rt, &s, &sc, &f); +} + +/* A session that ended mid-grab leaves its last record behind; the + * next session holds nothing, whatever its input does (here its setup + * publishes nothing): no item held, and Lazy Follow glides. */ +static void test_frame_grabs_new_session(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + openxr_input_hooks_t hooks; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + unsigned i; + + t_grab_frame(&rt, &s, &sc, &f, &set); + openxr_frame_set_placement(&f, VIDEO_OPENXR_PLACEMENT_LAZY); + t_grab_held(&grabs); + openxr_frame_publish_grabs(&f, &grabs); + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.x = 0.3f; + t_aim_ok = true; + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK(t_near(layers[0].pose.position.x, 0.3f), "fixture: not held"); + + memset(&hooks, 0, sizeof(hooks)); + hooks.session_created = t_in_created_fn; + hooks.aim = t_aim_hook; + openxr_frame_set_input_hooks(&hooks); + openxr_frame_session_created(&f); + openxr_frame_input_attach(&f); + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK( t_near(layers[0].pose.position.x, 0.0f) + && t_near(layers[0].size.width, 1.6f), + "the new session held the last one's item at %.2f", + layers[0].pose.position.x); + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + t_head_tracked = true; + for (i = 1; i <= 10; i++) + openxr_frame_place_screens(&f, 1000000000LL + (XrTime)i * 100000000LL); + CHECK(f.screens.pose.orientation.y > 0.01f, + "Lazy Follow held in the new session: %.3f", + f.screens.pose.orientation.y); + t_head_tracked = false; + openxr_frame_input_detach(&f); + openxr_frame_set_input_hooks(NULL); + t_grab_free(&rt, &s, &sc, &f); +} + +/* A hand lets go of screen 0 and takes screen 1 at its next poll, which + * reuses its hold: screen 0 stays where it let go until quads planned + * at that release show it, and screen 1 goes with the hand. */ +static void test_frame_grabs_reused(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + t_grab_let_go(&grabs, 3, 0.5f); + grabs.hand[1].state = VIDEO_XR_GRAB_HELD; + grabs.hand[1].slot = 1; + grabs.hand[1].base.y = -1.0f; + grabs.hand[1].last.position = set.quads[2].pose.position; + openxr_frame_publish_grabs(&f, &grabs); + video_xr_pose_identity(&t_aim_pose); + t_aim_pose.position.x = -0.4f; + t_aim_ok = true; + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK( t_near(layers[0].pose.position.x, 0.5f) + && t_near(layers[1].pose.position.x, 0.5f) + && t_near(layers[0].size.width, 2.0f), + "screen 0 stepped back to %.2f while its quads lagged", + layers[0].pose.position.x); + CHECK( t_near(layers[2].pose.position.x, -0.4f) + && t_near(layers[2].pose.position.z, -1.0f), + "screen 1 not with the hand: %.2f,%.2f", layers[2].pose.position.x, + layers[2].pose.position.z); + set.slots[0].edit_seq = 3; + set.slots[1].edit_seq = 3; + set.quads[0].pose.position.x = 0.5f; + set.quads[1].pose.position.x = 0.5f; + openxr_frame_publish(&f, &set); + openxr_frame_place_screens(&f, 2000); + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK( t_near(layers[0].size.width, 1.6f) + && t_near(layers[2].pose.position.x, -0.4f), + "screen 0 not handed back, or screen 1 dropped"); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Lazy Follow holds its glide while anything is held. */ +static void test_frame_grabs_lazy(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + unsigned i; + + t_grab_frame(&rt, &s, &sc, &f, &set); + openxr_frame_set_placement(&f, VIDEO_OPENXR_PLACEMENT_LAZY); + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.w = 1.0f; + t_head_tracked = true; + openxr_frame_place_screens(&f, 1000000000LL); + t_grab_held(&grabs); + openxr_frame_publish_grabs(&f, &grabs); + t_head.orientation.y = 0.70710678f; + t_head.orientation.w = 0.70710678f; + for (i = 1; i <= 5; i++) + openxr_frame_place_screens(&f, 1000000000LL + (XrTime)i * 100000000LL); + CHECK(fabs(f.screens.pose.orientation.y) < 1e-4, + "glided to %.3f while held", f.screens.pose.orientation.y); + t_grab_let_go(&grabs, 1, 0.0f); + openxr_frame_publish_grabs(&f, &grabs); + for (i = 6; i <= 10; i++) + openxr_frame_place_screens(&f, 1000000000LL + (XrTime)i * 100000000LL); + CHECK(f.screens.pose.orientation.y > 0.01f, + "no glide after the release: %.3f", f.screens.pose.orientation.y); + t_head_tracked = false; + t_grab_free(&rt, &s, &sc, &f); +} + +/* The main thread publishes while the frame loop reads: every frame + * shows one whole record, never half of two. The reads start at the + * writer's first record, however late a loaded machine runs it. */ +typedef struct t_grab_writer +{ + openxr_frame_t *f; + retro_atomic_int_t done; + retro_atomic_int_t started; +} t_grab_writer_t; + +static void t_grab_writer_run(void *data) +{ + unsigned i; + openxr_grabs_t g; + t_grab_writer_t *w = (t_grab_writer_t*)data; + t_grab_held(&g); + for (i = 0; !retro_atomic_load_acquire_int(&w->done); i++) + { + float k = (float)(1 + (i & 1)); + g.hand[1].rel.position.z = -k; + g.hand[1].width = k; + g.hand[1].height = k; + openxr_frame_publish_grabs(w->f, &g); + if (!i) + retro_atomic_store_release_int(&w->started, 1); + } +} + +static void test_frame_grabs_threaded(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + t_grab_writer_t w; + sthread_t *thread; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + retro_time_t deadline; + unsigned i, held = 0, torn = 0; + + t_grab_frame(&rt, &s, &sc, &f, &set); + video_xr_pose_identity(&t_aim_pose); + t_aim_ok = true; + w.f = &f; + retro_atomic_store_release_int(&w.done, 0); + retro_atomic_store_release_int(&w.started, 0); + thread = sthread_create(t_grab_writer_run, &w); + CHECK(thread != NULL, "no writer thread"); + deadline = cpu_features_get_time_usec() + 10000000; + while ( thread && !retro_atomic_load_acquire_int(&w.started) + && cpu_features_get_time_usec() < deadline) + sthread_yield(); + for (i = 0; i < 4000; i++) + { + float z, width; + openxr_frame_place_screens(&f, 1000 + i); + if (!openxr_frame_layers(&f, 1000 + i, layers, ptrs)) + continue; + z = -layers[0].pose.position.z; + width = layers[0].size.width; + /* Before the first record: the published quads. */ + if (t_near(z, 1.8f)) + continue; + held++; + if (!t_near(z, width)) + torn++; + } + retro_atomic_store_release_int(&w.done, 1); + if (thread) + sthread_join(thread); + CHECK(held > 0 && !torn, "%u of %u held frames mixed two records", + torn, held); + t_grab_free(&rt, &s, &sc, &f); +} + +/* The input's layers: the room the screens leave, at least the dots' + * and at most what it can use. */ +static void test_frame_layer_budget(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + + t_max_layers = 8; + t_grab_frame(&rt, &s, &sc, &f, &set); + t_max_layers = 0; + f.extra_layers = t_extra_layers; + openxr_frame_run(&f); + CHECK(t_extra_cap == 8 - 3, "with 3 quads of 8 layers, cap %u", + t_extra_cap); + set.num_quads = 7; + set.quads[3] = set.quads[2]; + set.quads[4] = set.quads[2]; + set.quads[5] = set.quads[2]; + set.quads[6] = set.quads[2]; + openxr_frame_publish(&f, &set); + openxr_frame_run(&f); + CHECK(t_extra_cap == OPENXR_KEPT_EXTRA_LAYERS, + "with 7 quads of 8 layers, cap %u", t_extra_cap); + t_grab_free(&rt, &s, &sc, &f); + t_max_layers = 32; + t_grab_frame(&rt, &s, &sc, &f, &set); + t_max_layers = 0; + f.extra_layers = t_extra_layers; + openxr_frame_run(&f); + CHECK(t_extra_cap == OPENXR_MAX_EXTRA_LAYERS + && OPENXR_MAX_EXTRA_LAYERS == OPENXR_KEPT_EXTRA_LAYERS + + OPENXR_HINGE_LAYERS + OPENXR_BEAM_LAYERS + OPENXR_TAG_LAYERS + + VIDEO_XR_MAX_SLOTS + 1, + "with room to spare, cap %u", t_extra_cap); + t_grab_free(&rt, &s, &sc, &f); +} + +/* The screens at rest in a hinge ride the hands' record to the frame + * loop, the whole list. */ +static void test_frame_grabs_rests(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + memset(&grabs, 0, sizeof(grabs)); + grabs.num_rests = OPENXR_REST_HINGES; + grabs.rests[0].edge = 1; + grabs.rests[0].target = 1; + grabs.rests[OPENXR_REST_HINGES - 1].slot = 1; + grabs.rests[OPENXR_REST_HINGES - 1].on = 3; + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK( f.grabs.num_rests == OPENXR_REST_HINGES + && f.grabs.rests[0].edge == 1 && f.grabs.rests[0].target == 1 + && f.grabs.rests[OPENXR_REST_HINGES - 1].slot == 1 + && f.grabs.rests[OPENXR_REST_HINGES - 1].on == 3, + "the frame loop sees %u at rest", f.grabs.num_rests); + t_grab_free(&rt, &s, &sc, &f); +} + +/* A hold taken where its screen rests hinged, marked in its first + * publish, shows that hinge from its first frame, tracked or not, and + * stays where it was taken while the grab's fade is none; the same + * hold without it is not hinged there; 5 cm toward the viewer it shows + * till the mark goes. The held item's bottom edge rests on screen 2's + * top edge, touching. Main's side: test_edit_rest_take. */ +static void test_frame_grabs_rest_kept(void) +{ + unsigned i; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t aim; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + for (i = 0; i < 2; i++) + { + unsigned want = i ? 0u : 2u; + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + grabs.hand[1].last.position.y = 0.08f; + grabs.start[1] = grabs.hand[1].last; + if (!i) + { + grabs.hand[1].rest_slot = 2; + grabs.hand[1].rest_on = 0; + grabs.hand[1].rest_edge = 1; + t_mark(&grabs, 1, 1, 0, 1); + } + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 1000); + t_aim_ok = false; + openxr_frame_layers(&f, 1000, layers, ptrs); + CHECK( f.hinged == want + && (i || (f.hinges[1].slot == 1 && f.hinges[1].on == 0 + && f.hinges[1].edge == 1)), + "%s, untracked first frame: hinged %#x", + i ? "taken free" : "taken hinged", f.hinged); + video_xr_pose_identity(&aim); + aim.position.y = 0.08f; + aim.position.z = -0.8f; + video_xr_compose(&f.screens.pose, &aim, &t_aim_pose); + t_aim_ok = true; + openxr_frame_layers(&f, 2000, layers, ptrs); + CHECK( f.hinged == want + && t_near(layers[0].pose.position.y, 0.08f) + && t_near(layers[0].pose.position.z, -1.8f), + "%s, tracked where taken: hinged %#x at %.4f,%.4f", + i ? "taken free" : "taken hinged", f.hinged, + layers[0].pose.position.y, layers[0].pose.position.z); + if (!i) + { + aim.position.z += 0.05f; + video_xr_compose(&f.screens.pose, &aim, &t_aim_pose); + openxr_frame_layers(&f, 3000, layers, ptrs); + CHECK(f.hinged == 2u, "5 cm toward the viewer, marked: hinged %#x", + f.hinged); + t_mark(&grabs, 1, ~0u, 0, 0); + openxr_frame_publish_grabs(&f, &grabs); + openxr_frame_place_screens(&f, 4000); + openxr_frame_layers(&f, 4000, layers, ptrs); + CHECK(f.hinged == 0u, "the mark gone: hinged %#x", f.hinged); + } + t_grab_free(&rt, &s, &sc, &f); + } +} + +/* A hold taken where its screen rests hinged, pulled straight off + * screen 2's plane 2 mm a frame to 4.8 cm and back, the main thread + * polling each frame (t_main_poll), marked from the grab: no frame + * moves it more than twice the hand, though the hinge's reach ends + * with the grab's fade short of whole. The line follows the published + * mark on every frame, gone out there and back in the plane. Main's + * side: test_edit_rest_take_out. */ +static void test_frame_grabs_rest_out(void) +{ + unsigned i; + unsigned off = 0; + bool gone = false; + float last = 0.0f; + t_main_t mk; + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t grabs; + video_xr_pose_t aim; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + + t_grab_frame(&rt, &s, &sc, &f, &set); + t_grab_held(&grabs); + grabs.snap = true; + grabs.hand[1].last.position.y = 0.08f; + grabs.start[1] = grabs.hand[1].last; + grabs.hand[1].rest_slot = 2; + grabs.hand[1].rest_on = 0; + grabs.hand[1].rest_edge = 1; + memset(&mk, 0, sizeof(mk)); + mk.mark.slot = 1; + mk.mark.on = 0; + mk.mark.edge = 1; + mk.marked = true; + t_mark(&grabs, 1, 1, 0, 1); + openxr_frame_publish_grabs(&f, &grabs); + t_aim_ok = true; + for (i = 0; i <= 48; i++) + { + bool on; + unsigned k = (i <= 24) ? i : 48 - i; + XrTime time = 1000 + (XrTime)i * 11111111; + video_xr_pose_identity(&aim); + aim.position.y = 0.08f; + aim.position.z = -0.8f + 0.002f * k; + video_xr_compose(&f.screens.pose, &aim, &t_aim_pose); + t_main_poll(&f, &grabs, &set, time, &mk); + openxr_frame_place_screens(&f, time); + openxr_frame_layers(&f, time, layers, ptrs); + if (i) + CHECK(fabs(layers[0].pose.position.z - last) <= 0.00401, + "%s %.1f cm off: moved %.2f mm", (i <= 24) ? "out" : "back", + 0.2 * k, fabs(layers[0].pose.position.z - last) * 1e3); + last = layers[0].pose.position.z; + on = f.hinged == 2u; + off += (on != mk.marked || (f.hinged & ~2u)) ? 1u : 0u; + gone = gone || !mk.marked; + } + CHECK(!off && gone && mk.marked, + "%u frames off the published mark; gone %d, back in the plane %d", + off, (int)gone, (int)mk.marked); + t_grab_free(&rt, &s, &sc, &f); +} + +/* Whether l is the back of q, a quad facing +Z: the laser image's + * dark square in q's place at its size, opaque, facing -Z, up still + * up. */ +static bool t_back_of(const XrCompositionLayerQuad *l, + const video_xr_quad_t *q, XrSwapchain cursor) +{ + video_xr_quat_t o; + video_xr_vec3_t y, z; + o.x = l->pose.orientation.x; + o.y = l->pose.orientation.y; + o.z = l->pose.orientation.z; + o.w = l->pose.orientation.w; + y.x = 0.0f; y.y = 1.0f; y.z = 0.0f; + z.x = 0.0f; z.y = 0.0f; z.z = 1.0f; + video_xr_rotate(&o, &y, &y); + video_xr_rotate(&o, &z, &z); + return l->subImage.swapchain == cursor + && l->subImage.imageRect.offset.x + == VIDEO_XR_BACK_X + VIDEO_XR_BACK_MARGIN + && l->subImage.imageRect.offset.y + == VIDEO_XR_BACK_Y + VIDEO_XR_BACK_MARGIN + && l->subImage.imageRect.extent.width == VIDEO_XR_BACK_DIM + && l->subImage.imageRect.extent.height == VIDEO_XR_BACK_DIM + && !l->layerFlags + && t_near(l->size.width, q->width) + && t_near(l->size.height, q->height) + && t_near(l->pose.position.x, q->pose.position.x) + && t_near(l->pose.position.y, q->pose.position.y) + && t_near(l->pose.position.z, q->pose.position.z) + && t_near(y.y, 1.0f) && t_near(z.z, -1.0f); +} + +/* Outside edit mode a screen seen from behind keeps its front, and no + * head is located for it. The frame loop copies the main thread's + * grabs only once their stamp moves, and a new session passes over + * what was published before it without publishing itself: the main + * thread stays the grabs' one writer. */ +static void test_frame_grabs_owner(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + openxr_grabs_t g; + unsigned i, n, locates; + int stamp; + bool fronts = true; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + const XrSwapchain cursor = (XrSwapchain)(uintptr_t)0x900; + + t_grab_frame(&rt, &s, &sc, &f, &set); + f.cursor = cursor; + t_head_tracked = true; + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.w = 1.0f; + t_head.position.z = -3.0f; + locates = t_head_locates; + openxr_frame_place_screens(&f, 1000); + n = openxr_frame_layers(&f, 1000, layers, ptrs); + for (i = 0; i < n; i++) + fronts &= layers[i].subImage.swapchain != cursor; + CHECK(n == 3 && fronts && !f.backs && t_head_locates == locates, + "not editing, behind: %u layers, fronts %d, backs %x, %u locates", + n, (int)fronts, (unsigned)f.backs, t_head_locates - locates); + + /* Nothing published: the frame loop's copy is not read again. */ + memset(&g, 0, sizeof(g)); + g.chosen = 3; + openxr_frame_publish_grabs(&f, &g); + openxr_frame_place_screens(&f, 2000); + CHECK(f.grabs.chosen == 3, "published grabs not read: %u", + f.grabs.chosen); + f.grabs.chosen = 7; + openxr_frame_place_screens(&f, 3000); + CHECK(f.grabs.chosen == 7, "unchanged grabs copied again"); + g.chosen = 4; + openxr_frame_publish_grabs(&f, &g); + openxr_frame_place_screens(&f, 4000); + CHECK(f.grabs.chosen == 4, "republished grabs not read: %u", + f.grabs.chosen); + + /* A new session: the stale grabs are passed over, not rewritten. */ + g.editing = true; + g.hand[0].state = VIDEO_XR_GRAB_HELD; + openxr_frame_publish_grabs(&f, &g); + stamp = retro_atomic_load_acquire_int(&f.grabs_seq); + openxr_frame_session_created(&f); + CHECK(retro_atomic_load_acquire_int(&f.grabs_seq) == stamp, + "a new session published the grabs"); + openxr_frame_place_screens(&f, 5000); + CHECK( !f.grabs.editing + && f.grabs.hand[0].state != VIDEO_XR_GRAB_HELD, + "a new session holds the last one's grabs"); + g.chosen = 5; + openxr_frame_publish_grabs(&f, &g); + openxr_frame_place_screens(&f, 6000); + CHECK(f.grabs.chosen == 5 && f.grabs.editing, + "the new session's first grabs not read"); + t_grab_free(&rt, &s, &sc, &f); +} + +/* A head behind a quad's plane, where runtimes draw nothing of it: the + * back in the quad's place, in no more layers, the menu's too, and kept + * as the menu by a runtime short of layers; the front from in front, + * with no head or with no laser image, each quad as the head is to it; + * Follow Head's head is its space's origin. The input sees the items + * where they are, and which show their backs. */ +static void test_frame_backs(void) +{ + openxr_runtime_t rt; + openxr_session_t s; + openxr_swapchains_t sc; + openxr_frame_t f; + video_xr_quad_set_t set; + unsigned i, n, count; + uint32_t max_layers; + unsigned made = 0; + int cb_user = 0; + bool fronts = true; + bool backs = true; + XrCompositionLayerQuad layers[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *ptrs[VIDEO_XR_MAX_QUADS]; + const XrCompositionLayerBaseHeader *kept[5]; + const XrSwapchain cursor = (XrSwapchain)(uintptr_t)0x900; + openxr_grabs_t editing; + + t_grab_frame(&rt, &s, &sc, &f, &set); + max_layers = rt.max_layers; + openxr_slot_create(&sc, VIDEO_XR_MENU_SLOT, 44, false, + VIDEO_SCALE_PACK(64, 64), 1, t_images_cb, &cb_user, &made); + retro_atomic_store_release_int(&sc.slots[VIDEO_XR_MENU_SLOT].content, 1); + set.num_quads = 4; + set.quads[3] = set.quads[2]; + set.quads[3].kind = VIDEO_XR_QUAD_MENU; + set.quads[3].slot = VIDEO_XR_MENU_SLOT; + set.quads[3].pose.position.y = 0.0f; + set.quads[3].pose.position.z = -1.7f; + set.quads[3].width = 1.0f; + set.quads[3].height = 0.6f; + set.slots[VIDEO_XR_MENU_SLOT].layers = 1; + openxr_frame_publish(&f, &set); + f.cursor = cursor; + f.extra_layers = t_extra_layers; + t_head_tracked = true; + memset(&t_head, 0, sizeof(t_head)); + t_head.orientation.w = 1.0f; + /* Backs are edit mode's. */ + memset(&editing, 0, sizeof(editing)); + editing.editing = true; + openxr_frame_publish_grabs(&f, &editing); + + openxr_frame_place_screens(&f, 1000); + n = openxr_frame_layers(&f, 1000, layers, ptrs); + for (i = 0; i < n; i++) + fronts &= layers[i].subImage.swapchain != cursor; + CHECK(n == 4 && fronts && !f.backs + && layers[3].layerFlags + == XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT, + "in front: %u layers, fronts %d, backs %x", n, (int)fronts, + (unsigned)f.backs); + /* Short of layers, after a projection: a screen goes, never the + * menu. */ + kept[0] = NULL; + memcpy(&kept[1], ptrs, 4 * sizeof(*ptrs)); + rt.max_layers = 3; + CHECK( openxr_frame_fit(&f, kept, 5, 1, 5) == 3 + && kept[2] == (const XrCompositionLayerBaseHeader*)&layers[3], + "in front, 3 layers: the menu dropped"); + rt.max_layers = max_layers; + openxr_frame_run(&f); + count = t_last_layer_count; + + /* 1.2 m behind the screens. */ + t_head.position.z = -3.0f; + openxr_frame_place_screens(&f, 2000); + n = openxr_frame_layers(&f, 2000, layers, ptrs); + for (i = 0; i < n; i++) + backs &= t_back_of(&layers[i], &set.quads[i], cursor); + CHECK(n == 4 && backs && f.backs == 0xfu + && layers[0].eyeVisibility == XR_EYE_VISIBILITY_LEFT + && layers[1].eyeVisibility == XR_EYE_VISIBILITY_RIGHT, + "behind: %u layers, backs %d (%x), first at %.2f facing %.2f", + n, (int)backs, (unsigned)f.backs, layers[0].pose.position.z, + layers[0].pose.orientation.y); + /* Short of layers, a screen's back goes, never the menu's. */ + rt.max_layers = 3; + CHECK( openxr_frame_fit(&f, ptrs, 4, 0, 4) == 3 + && ptrs[2] == (const XrCompositionLayerBaseHeader*)&layers[3], + "behind, 3 layers: the menu's back dropped"); + rt.max_layers = max_layers; + openxr_frame_run(&f); + CHECK(t_last_layer_count == count && t_last_layer_count == 5 + && t_near(t_extra_shown.quads[0].pose.orientation.w, 1.0f) + && t_near(t_extra_shown.quads[3].pose.orientation.w, 1.0f) + && f.backs == 0xfu, + "a whole frame from behind: %u layers, %u in front; the input " + "saw screen 1 turned %.2f", t_last_layer_count, count, + t_extra_shown.quads[0].pose.orientation.w); + + /* Between the menu and the screens: the menu's back alone. */ + t_head.position.z = -1.75f; + openxr_frame_place_screens(&f, 3000); + n = openxr_frame_layers(&f, 3000, layers, ptrs); + CHECK(n == 4 && f.backs == 0x8u + && t_back_of(&layers[3], &set.quads[3], cursor) + && layers[0].subImage.swapchain != cursor, + "behind the menu only: backs %x", (unsigned)f.backs); + + /* A runtime too narrow for the tags keeps the back. */ + t_head.position.z = -3.0f; + rt.max_width = 2048; + openxr_frame_place_screens(&f, 4000); + n = openxr_frame_layers(&f, 4000, layers, ptrs); + CHECK(openxr_frame_cursor_w(&f) == VIDEO_XR_CURSOR_UNTAGGED_W + && n == 4 && t_back_of(&layers[0], &set.quads[0], cursor) + && layers[0].subImage.imageRect.offset.x + + layers[0].subImage.imageRect.extent.width + + VIDEO_XR_BACK_MARGIN <= openxr_frame_cursor_w(&f), + "narrow: %u wide, the back from %d", openxr_frame_cursor_w(&f), + (int)layers[0].subImage.imageRect.offset.x); + + /* No head, or no laser image: the fronts. */ + t_head_tracked = false; + openxr_frame_place_screens(&f, 5000); + n = openxr_frame_layers(&f, 5000, layers, ptrs); + CHECK(n == 4 && !f.backs && layers[0].subImage.swapchain != cursor, + "no head: backs %x", (unsigned)f.backs); + t_head_tracked = true; + f.cursor = XR_NULL_HANDLE; + openxr_frame_place_screens(&f, 6000); + n = openxr_frame_layers(&f, 6000, layers, ptrs); + CHECK(n == 4 && !f.backs && layers[0].subImage.swapchain, + "no laser image: backs %x", (unsigned)f.backs); + f.cursor = cursor; + + /* Follow Head, screen 2 turned away: its back alone, with no locate. */ + t_head_tracked = false; + set.quads[2].pose.orientation.y = 1.0f; + set.quads[2].pose.orientation.w = 0.0f; + openxr_frame_publish(&f, &set); + openxr_frame_set_placement(&f, VIDEO_OPENXR_PLACEMENT_FOLLOW); + openxr_frame_place_screens(&f, 7000); + n = openxr_frame_layers(&f, 7000, layers, ptrs); + CHECK(n == 4 && f.backs == 0x4u + && layers[2].subImage.swapchain == cursor + && layers[0].subImage.swapchain != cursor, + "Follow Head: backs %x", (unsigned)f.backs); + + /* The menu closed: no layer is the menu's. */ + set.num_quads = 3; + openxr_frame_publish(&f, &set); + n = openxr_frame_layers(&f, 8000, layers, ptrs); + CHECK(n == 3 && f.menu_layer == -1, "the menu closed: the menu at %d", + f.menu_layer); + f.extra_layers = NULL; + openxr_slot_destroy(&sc, VIDEO_XR_MENU_SLOT); + t_grab_free(&rt, &s, &sc, &f); + t_head_tracked = false; +} + +/* The back's square and the texel round it filtering reaches: its + * grey, opaque, at full width and without the tags, top down and + * bottom up. */ +static void test_cursor_backs(void) +{ + static uint8_t px[VIDEO_XR_CURSOR_W * VIDEO_XR_CURSOR_H * 4]; + const unsigned x0 = VIDEO_XR_BACK_X + VIDEO_XR_BACK_MARGIN - 1; + const unsigned y0 = VIDEO_XR_BACK_Y + VIDEO_XR_BACK_MARGIN - 1; + unsigned k, x, y; + for (k = 0; k < 4; k++) + { + unsigned w = (k & 1) ? VIDEO_XR_CURSOR_UNTAGGED_W : VIDEO_XR_CURSOR_W; + unsigned bad = 0; + openxr_frame_cursor_pixels(px, w, k >= 2); + for (y = y0; y < y0 + VIDEO_XR_BACK_DIM + 2; y++) + for (x = x0; x < x0 + VIDEO_XR_BACK_DIM + 2; x++) + { + const uint8_t *t = px + ((size_t)y * w + x) * 4; + if ( t[0] != VIDEO_XR_BACK_LEVEL || t[1] != t[0] + || t[2] != t[0] || t[3] != 255) + bad++; + } + CHECK(!bad, "%u wide%s: %u texels round the back's square are not " + "its grey", w, (k >= 2) ? ", bottom up" : "", bad); + } +} + int main(void) { test_runtime(); @@ -7069,11 +9673,40 @@ int main(void) test_vulkan_inline(); test_input_hooks(); test_swapchain_filled(); + test_custom_screen_recenter(); test_screen_placement(); test_screen_placement_seated(); test_screen_placement_inline(); test_frame_repeat(); test_screen_placement_repeat(); + test_frame_grabs(); + test_frame_aims(); + test_frame_grabs_reused(); + test_frame_grabs_spaces(); + test_frame_grabs_new_session(); + test_frame_grabs_lazy(); + test_frame_grabs_snap(); + test_frame_grabs_stop(); + test_frame_grabs_push(); + test_frame_grabs_graze(); + test_frame_grabs_snap_two(); + test_frame_grabs_hinge(); + test_frame_grabs_hinge_kept(); + test_frame_grabs_hinge_shaken(); + test_frame_grabs_hinge_reach(); + test_frame_grabs_mark(); + test_frame_grabs_mark_shaken(); + test_frame_grabs_threaded(); + test_frame_layer_budget(); + test_frame_grabs_rests(); + test_frame_grabs_rest_kept(); + test_frame_grabs_rest_out(); + test_frame_backs(); + test_frame_grabs_owner(); + test_cursor_backs(); + test_cursor_tags(); + test_cursor_tag_lines(); + test_cursor_narrow(); #ifdef HAVE_OPENXR_GL /* Last: they leave the stand-in's formats and extensions as GL's. */ test_gl_host_start_failure(); diff --git a/samples/openxr/e2e/feedback.slang b/samples/openxr/e2e/feedback.slang new file mode 100644 index 0000000000..6eee1af555 --- /dev/null +++ b/samples/openxr/e2e/feedback.slang @@ -0,0 +1,16 @@ +#version 450 +layout(std140, set = 0, binding = 0) uniform UBO { mat4 MVP; uint FrameCount; } global; +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; +void main() { gl_Position = global.MVP * Position; vTexCoord = TexCoord; } +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D PassFeedback0; +void main() +{ + FragColor = vec4(fract(texture(PassFeedback0, vTexCoord).r + 0.03125), + float(global.FrameCount % 251u) / 250.0, 0.25, 1.0); +} diff --git a/samples/openxr/e2e/feedback.slangp b/samples/openxr/e2e/feedback.slangp new file mode 100644 index 0000000000..7130d9d04e --- /dev/null +++ b/samples/openxr/e2e/feedback.slangp @@ -0,0 +1,5 @@ +shaders = 2 +shader0 = feedback.slang +feedback_pass = 0 +float_framebuffer0 = true +shader1 = feedback.slang diff --git a/samples/openxr/e2e/run.py b/samples/openxr/e2e/run.py index a10d9a6af8..fed14c72e3 100644 --- a/samples/openxr/e2e/run.py +++ b/samples/openxr/e2e/run.py @@ -9,6 +9,12 @@ Monado, and a RetroArch built with HAVE_OPENXR. Usage: run.py [--validate|--gl] [case ...] +The custom-repeat case needs the opt-in shipping harness. Build it with +python3 samples/settings/video_screens/build.py --runtime --output-dir +and pass /video_screens_test as . The runner supplies +RA_SCREEN_TEST_SCRIPT and RA_SCREEN_TEST_ACK for bounded, acknowledged +main-thread edits; the ordinary frontend and its objects are unchanged. + Build the test core and layer first (make -C samples/cores/video_views, make -C samples/openxr/test_layer). monado.env.example here runs Monado's null compositor and simulated headset; RetroArch reaches Monado @@ -145,8 +151,11 @@ class Result(object): self.releases = [] self.chains = {} self.marks = {} + self.screen_frames = {} self.shot = None self.events = [] + # What each 'until' step that timed out waited for. + self.missed = [] # A pixel of the window inside the 2D map's top screen. self.top_px = (800, 240) # Seconds after a video reinit's hotkey that its new session's @@ -201,6 +210,33 @@ def wait_shot(d): return None +def wait_frame(d, pick, timeout): + """Whether a frame recorded from now on, within timeout seconds, has + something pick finds.""" + path = os.path.join(d, 'xr', 'frames.jsonl') + deadline = time.monotonic() + timeout + pos = os.path.getsize(path) if os.path.exists(path) else 0 + part = b'' + while time.monotonic() < deadline: + data = b'' + if os.path.exists(path): + with open(path, 'rb') as f: + f.seek(pos) + data = f.read() + pos += len(data) + lines = (part + data).split(b'\n') + part = lines.pop() + for line in lines: + try: + e = json.loads(line) + except ValueError: + continue # the rest of a line begun before the wait + if isinstance(e, dict) and e.get('ev') == 'frame' and pick(e): + return True + time.sleep(0.01) + return False + + def write_overlay(d): """An overlay whose top-left sixteenth is opaque magenta, and the settings that show it over the menu.""" @@ -254,9 +290,17 @@ def run_case(retroarch, root, monado, case, validate): os.makedirs(os.path.join(d, sub)) os.chmod(os.path.join(d, RUNTIME_DIR), 0o700) + # A screen carried 5 cm or more snaps: in the 3DS layout the other + # screen's edge, 3.2 cm off, would pull it 2.8 cm. The snap cases + # turn it on. Edit Smoothing too: a smoothed screen lags the scripted + # hand; the smoothing cases turn it on. The menu in front of screen + # 1, as the edit cases expect; custom-menu-spot checks it off. settings = {'video_openxr_enable': 'true', 'video_openxr_distance': '1.8', - 'video_openxr_width': '1.6'} + 'video_openxr_width': '1.6', + 'video_openxr_edit_snap': 'false', + 'video_openxr_edit_smoothing': '0', + 'video_openxr_menu_follow': 'true'} if case.get('overlay'): settings.update(write_overlay(d)) settings.update(case.get('settings', {})) @@ -273,6 +317,9 @@ def run_case(retroarch, root, monado, case, validate): 'video_window_save_positions': 'true', 'video_windowed_position_width': str(size[0]), 'video_windowed_position_height': str(size[1])}) + screen_script = os.path.join(d, 'screens.txt') + screen_ack = os.path.join(d, 'screens.ack') + open(screen_script, 'w').close() port = free_port() cfg = os.path.join(d, 'retroarch.cfg') write_cfg(cfg, d, case.get('driver', 'vulkan'), settings, port) @@ -307,6 +354,8 @@ def run_case(retroarch, root, monado, case, validate): 'XDG_RUNTIME_DIR': RUNTIME_DIR, 'WAYLAND_DISPLAY': 'openxr-e2e-no-socket', } + child.update(RA_SCREEN_TEST_SCRIPT=screen_script, + RA_SCREEN_TEST_ACK=screen_ack) child.update(case.get('env', {})) child.update(pairs(monado.get('CLIENT_ENV', ''))) if validate: @@ -386,6 +435,24 @@ def run_case(retroarch, root, monado, case, validate): with open(tmp, 'w') as f: f.write(arg + '\n') os.replace(tmp, script) + elif kind == 'screen': + serial = str(arg[0]) + tmp = screen_script + '.tmp' + with open(tmp, 'w') as f: + f.write(' '.join(map(str, arg))) + os.replace(tmp, screen_script) + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + if os.path.exists(screen_ack): + with open(screen_ack) as f: + ack = f.read().split() + if len(ack) == 2 and ack[0] == serial: + res.screen_frames[int(serial)] = int(ack[1]) + break + time.sleep(0.01) + else: + raise RuntimeError('screen edit not acknowledged; use the ' + 'video_screens --runtime test binary') elif kind == 'unfocus': open(os.path.join(d, 'unfocus'), 'w').close() elif kind == 'touch': @@ -399,11 +466,18 @@ def run_case(retroarch, root, monado, case, validate): elif kind == 'shot': send('SCREENSHOT', port) res.shot = wait_shot(d) - send('QUIT', port) - try: - p.wait(10) - except subprocess.TimeoutExpired: - res.marks['hung'] = time.monotonic() + elif kind == 'until': + what, pick, timeout = arg + if not wait_frame(d, pick, timeout): + res.missed.append('%s within %g s' % (what, timeout)) + # Without its command interface it cannot be told to quit: stop() + # ends it. + if settings.get('network_cmd_enable') != 'false': + send('QUIT', port) + try: + p.wait(10) + except subprocess.TimeoutExpired: + res.marks['hung'] = time.monotonic() finally: stop(p) stop(svc) @@ -415,6 +489,8 @@ def run_case(retroarch, root, monado, case, validate): if os.path.basename(runtime_dir).startswith('ra-e2e-'): shutil.rmtree(runtime_dir, ignore_errors=True) load(res) + with open(os.path.join(d, 'marks.json'), 'w') as f: + json.dump({'marks': res.marks, 'screen_frames': res.screen_frames}, f) return res @@ -1556,7 +1632,7 @@ def check_menu_closed(res): # RGUI, OK and Cancel unswapped, from the Quick Menu: back to the Main # Menu, Settings (row 5), Video (1), Stereo 3D & Screens (5), and down -# to Screen Distance (row 5). +# to Screen Distance (row 15). DISTANCE_NAV = ([('send', 'MENU_B'), ('wait', 0.8)] + [('send', 'MENU_DOWN'), ('wait', 0.3)] * 5 + [('send', 'MENU_A'), ('wait', 0.8)] @@ -1564,7 +1640,7 @@ DISTANCE_NAV = ([('send', 'MENU_B'), ('wait', 0.8)] + [('send', 'MENU_A'), ('wait', 0.8)] + [('send', 'MENU_DOWN'), ('wait', 0.3)] * 5 + [('send', 'MENU_A'), ('wait', 0.8)] - + [('send', 'MENU_DOWN'), ('wait', 0.3)] * 5) + + [('send', 'MENU_DOWN'), ('wait', 0.3)] * 15) STEP_RIGHT = [('send', 'MENU_RIGHT'), ('wait', 2)] STEP_LEFT = [('send', 'MENU_LEFT'), ('wait', 2)] # Screen Distance out and in, Screen Width out and back, and Width once @@ -2021,14 +2097,17 @@ def sync_errors(res, want): def cursors(frame): - """The laser's dots: small blended quads.""" + """The laser's dots: small blended quads from the image's dot.""" return [q for q in quads(frame) - if q['flags'] & BLEND and q['size'][0] < 0.1] + if q['flags'] & BLEND and q['size'][0] < 0.1 + and q['rect'][0] == 0] def menus(frame): + """The menu: large blended quads, not edit mode's outlines.""" return [q for q in quads(frame) - if q['flags'] & BLEND and q['size'][0] >= 0.1] + if q['flags'] & BLEND and q['size'][0] >= 0.1 + and q['rect'][0] == 0] ALL_BUTTONS = script(*['action combined/%s 1' % n for n in ( @@ -2053,7 +2132,7 @@ _FL = '/user/hand/left/input/' _FR = '/user/hand/right/input/' # The Frame's right controller has A, B, X and Y, the left a D-pad; the # right stick's click opens the RetroArch menu in place of R3, and the -# grips stay unbound. +# grips are left to edit mode, whose Reset is the left D-pad's down. FRAME_BINDS = set([ ('combined/b', _FR + 'a/click'), ('combined/a', _FR + 'b/click'), ('combined/y', _FR + 'x/click'), ('combined/x', _FR + 'y/click'), @@ -2080,6 +2159,13 @@ FRAME_BINDS = set([ ('separate/stick', _FL + 'thumbstick'), ('separate/stick', _FR + 'thumbstick'), ('separate/menu', _FL + 'view/click'), + ('edit/grab', _FL + 'squeeze/value'), ('edit/grab', _FR + 'squeeze/value'), + ('edit/tilt', _FL + 'trigger/value'), ('edit/tilt', _FR + 'trigger/value'), + ('edit/stick', _FL + 'thumbstick'), ('edit/stick', _FR + 'thumbstick'), + ('edit/reset', _FL + 'dpad_down/click'), ('edit/reset', _FR + 'a/click'), + ('edit/menu', _FR + 'thumbstick/click'), + ('edit/select_next', _FL + 'dpad_right/click'), + ('edit/select_previous', _FL + 'dpad_left/click'), ('pointer/aim', _FL + 'aim/pose'), ('pointer/aim', _FR + 'aim/pose'), ('pointer/rumble', '/user/hand/left/output/haptic'), ('pointer/rumble', '/user/hand/right/output/haptic')]) @@ -2473,6 +2559,16 @@ def trigger_errors(res): return [] +def rect_colour(res, fr, q, fx, fy): + """colour() at a fraction of the part of its image a quad shows.""" + w, h, bpp, rows = read_png(image(res, fr, q)) + x0, y0, rw, rh = q['rect'] + x = min(w - 1, x0 + int(fx * rw)) + y = min(h - 1, y0 + int(fy * rh)) + px = rows[y][x * bpp:x * bpp + bpp] + return tuple(px[:3]), (px[3] if bpp == 4 else 255) + + def cursor_errors(res, want): shown = [f for f in res.frames if cursors(f)] if not shown: @@ -2487,10 +2583,10 @@ def cursor_errors(res, want): else: fr = snaps[-1] dot = cursors(fr)[0] - rgb, alpha = colour(image(res, fr, dot), 0.5, 0.5) + rgb, alpha = rect_colour(res, fr, dot, 0.5, 0.5) if not near(rgb, WHITE) or alpha < 250: errors.append('the dot\'s centre is %s alpha %d' % (rgb, alpha)) - _, alpha = colour(image(res, fr, dot), 0.02, 0.02) + _, alpha = rect_colour(res, fr, dot, 0.02, 0.02) if alpha: errors.append('the dot\'s corner is not transparent') if cursors(res.frames[-1]): @@ -3459,10 +3555,11 @@ def check_kept_retry(res): if seq != ['live', 'released', 'live']: errors.append('controllers went %s, want live, released when the ' 'session ended, live in the new one' % seq) - # The laser's dot is VIDEO_XR_CURSOR_DIM square; each start makes - # one, and each start here made a session. + # The laser's image is VIDEO_XR_CURSOR_W by VIDEO_XR_CURSOR_H (its + # dot, edit mode's outlines, the menu's handle bars and the beam's + # strips); each start makes one, and each start here made a session. dots = [e for e in events(res, 'swapchain') - if (e['w'], e['h']) == (32, 32)] + if (e['w'], e['h']) == (CURSOR_W, CURSOR_H)] if len(dots) != 2: errors.append('%d laser dot swapchains, want 2 (one per start)' % len(dots)) @@ -3836,6 +3933,3526 @@ THREADED_HW = [ ('VUID-vkQueueSubmit-pSignalSemaphores-00067', 'pSubmits[0].pSignalSemaphores[0]')] +def check_custom(kind, legacy=False): + def check(res): + if kind == 'tracked': + return check_tracked(beside=not legacy)(res) + fr = last_snap(res) + if not fr: + return no_quads(res) + qs = [q for q in quads(fr) if not q['flags'] & BLEND] + if kind == 'menu': + if legacy: + errors = window_quad(res, fr) + if not errors and near(colour(image(res, fr, quads(fr)[0]), .25, .25)[0], RED): + errors.append('combined fallback is missing its menu') + return errors + ms = menus(fr) + return ([] if not qs and len(ms) == 1 and + at(ms[0], (0, 0, -1.7)) else + ['hidden screens must leave the centered menu available']) + if kind == 'hidden': + return ([] if len(qs) == 1 and qs[0]['eye'] == 'both' else + ['hidden stereo screen still submitted']) + eyes = {q['eye']: q for q in qs} + if not all(e in eyes for e in ('left', 'right')): + return ['custom stereo pair missing'] + errors = [] + if kind == 'offset': + presses = [f for f in kinds(core_events(res), 'pointer') if f['pressed']] + if not presses or not close((presses[0]['cx'], presses[0]['cy']), (320, 420), 3): + errors.append('custom screen did not map the touch to 320,420') + position = (0.25, 0.1, -1.5) + rotation = (0, 0, 0, 1) + size = (1.6, 0.96) + if kind == 'rotation': + rotation = (0, math.sin(math.pi / 8), 0, math.cos(math.pi / 8)) + size = (2.4, 1.44) + if kind == 'repeat': + position = (0.75, 0.1, -1.5) + size = (6.4, 3.84) + if kind == 'recenter': + position = (-1.0, 0.3, 0.05) + rotation = (0, math.sin(math.pi / 4), 0, math.cos(math.pi / 4)) + for eye in ('left', 'right'): + q = eyes[eye] + if not at(q, position) or not facing(q, rotation) or not sized(q, size): + errors.append('%s custom pose/size: %s %s' % + (eye, q['pose'], q['size'])) + if kind == 'repeat': + held = {sc for sc, c in res.chains.items() if (c['w'], c['h']) == (400, 240)} + if not held or any(r['sc'] in held and + res.marks['busy'] * 1e6 < r['t_us'] < res.marks['free'] * 1e6 + for r in res.releases): + errors.append('screen images were not held during the repeat') + if not legacy: + for eye in ('left', 'right'): + errors += image_size(res, eyes[eye], (400, 240), + distance=math.sqrt(sum(v * v for v in position))) + resized = [f for f in res.frames if f['snap'] and + res.marks['resized'] * 1e6 < f['t_us'] < + res.marks['restored'] * 1e6 and len(quads(f)) == 3] + if not resized: + errors.append('three resized layers did not become available') + elif not legacy: + for q in quads(resized[-1]): + native = (320, 240) if q['eye'] == 'both' else (400, 240) + errors += image_size(res, q, native, + distance=math.sqrt(sum(v * v for v in q['pose'][:3]))) + hidden = [f for f in res.frames if + (res.marks['hidden'] + .3) * 1e6 < f['t_us'] < + res.marks['shown'] * 1e6] + first_hidden = next((i for i, f in enumerate(hidden) + if quads(f) and all(q['eye'] == 'both' for q in quads(f))), None) + if first_hidden is None or any(q['eye'] != 'both' for f in hidden[first_hidden:] for q in quads(f)): + errors.append('paused screen did not stay hidden') + before = [f for f in res.frames if f['snap'] and + res.marks['paused'] * 1e6 < f['t_us'] < + res.marks['moved'] * 1e6 and quads(f)] + if not before: + errors.append('no paused image before moving') + else: + old = next(q for q in quads(before[-1]) if q['eye'] == 'left') + feedback_before = colour(image(res, before[-1], old), .5, .5) + feedback_after = colour(image(res, fr, eyes['left']), .5, .5) + if feedback_after != feedback_before: + errors.append('paused feedback image advanced') + if not legacy: + expected = round((res.screen_frames[4] % 251) * 255 / 250) + if abs(feedback_before[0][1] - expected) > 3: + errors.append('re-shown frame tag %d, want %d from latest source' + % (feedback_before[0][1], expected)) + if at(old, eyes['left']['pose'][:3]): + errors.append('paused quad did not move') + return errors + return check + + +def check_menu_spot(res): + """Menu in Front of Screen 1 off: with screen 1 moved, the menu + opens at its own spot straight ahead, as before screens moved.""" + fr = last_snap(res) + if not fr: + return no_quads(res) + ms = menus(fr) + if len(ms) != 1 or not at(ms[0], (0, 0, -1.7)): + return ['menu at %s, want its spot' % + ([m['pose'][:3] for m in ms],)] + return [] + + +def check_frame_custom(window=False): + """Without a map the whole frame, or gl's window, is screen 1: moved + by its offsets and sized for where it is.""" + def check(res): + fr = last_snap(res) + if not fr: + return no_quads(res) + q = [x for x in quads(fr) if not x['flags'] & BLEND] + if len(q) != 1 or q[0]['eye'] != 'both': + return ['want one quad for both eyes, got %s' % [x['eye'] for x in q]] + errors = [] + position = (0.25, 0.1, -1.5) + if not at(q[0], position) or not sized(q[0], (1.6, 0.96)): + errors.append('frame at %s size %s' % (q[0]['pose'][:3], q[0]['size'])) + return errors + image_size(res, q[0], (W, H) if window else None, + distance=math.sqrt(sum(v * v for v in position))) + return check + + +CUSTOM_OFFSET = {'video_screen1_xr_x': '0.25', + 'video_screen1_xr_y': '0.1', 'video_screen1_xr_z': '0.3', + 'video_screen2_xr_x': '-0.2', 'video_screen2_xr_y': '0.1', + 'video_screen2_xr_z': '0.2'} +CUSTOM_AIM = 'aim right 0.2 -0.4 -0.3 -0.52 -1.132 -1.6' +CUSTOM_CASES = [ + {'name': 'custom-offset', 'settings': CUSTOM_OFFSET, + 'steps': [('wait', 6), ('script', CUSTOM_AIM), ('wait', 1), + ('script', script(CUSTOM_AIM, R2)), ('wait', 1), + ('script', ''), ('wait', 2)], 'check': check_custom('offset')}, + {'name': 'custom-rotation', + 'settings': dict(CUSTOM_OFFSET, video_screen1_xr_yaw='45', + video_screen1_xr_scale='1.5'), + 'steps': SETTLE, 'check': check_custom('rotation')}, + {'name': 'custom-hidden', 'settings': {'video_screen1_xr_visible': 'false'}, + 'steps': SETTLE, 'check': check_custom('hidden')}, + {'name': 'custom-recenter', 'settings': CUSTOM_OFFSET, + 'steps': [('wait', 6), ('script', 'head 0.5 0.2 0.3 90'), ('wait', 1), + ('send', 'HEADSET_RECENTER'), ('wait', 3)], + 'check': check_custom('recenter')}, + {'name': 'custom-menu', + 'settings': {'video_screen1_xr_visible': 'false', + 'video_screen2_xr_visible': 'false', 'menu_driver': 'ozone'}, + 'steps': [('wait', 6), ('send', 'MENU_TOGGLE'), ('wait', 4)], + 'check': check_custom('menu')}, + {'name': 'custom-menu-spot', + 'settings': dict(CUSTOM_OFFSET, video_openxr_menu_follow='false', + menu_driver='ozone'), + 'steps': [('wait', 6), ('send', 'MENU_TOGGLE'), ('wait', 4)], + 'check': check_menu_spot}, + {'name': 'custom-tracked', + 'settings': {'video_screen1_xr_x': '10', 'video_screen1_xr_yaw': '180', + 'video_screen1_xr_scale': '10', 'video_screen1_xr_visible': 'false'}, + 'options': {'video_views_test_vr': 'local'}, + 'steps': SETTLE, 'check': check_custom('tracked')}, + {'name': 'custom-repeat', 'settings': CUSTOM_OFFSET, + 'script': 'eyes 400 240', + 'args': ['--set-shader=' + os.path.join(HERE, 'feedback.slangp')], + 'steps': [('wait', 6), ('screen', (1, 8, 0.25, 4.0, 1)), + ('mark', 'resized'), ('wait', 2), ('mark', 'restored'), + ('screen', (2, 8, 0.25, 1.0, 1)), ('wait', 2), + ('mark', 'running_hidden'), + ('script', 'state 3\neyes 400 240\nbusy eyes'), + ('screen', (3, 0, 0.25, 1.0, 0)), ('wait', 2), + ('send', 'PAUSE_TOGGLE'), ('wait', 1), + ('script', 'state 5'), ('screen', (4, 0, 0.25, 1.0, 1)), + ('mark', 'busy'), ('wait', 0.5), ('mark', 'free'), + ('script', 'state 5\nbusy off'), ('wait', 2), + ('mark', 'paused'), ('wait', 2), + ('mark', 'moved'), ('screen', (5, 0, 0.75, 1.0, 1)), ('wait', 2), + ('screen', (6, 0, 0.75, 1.0, 0)), ('mark', 'hidden'), ('wait', 2), + ('mark', 'shown'), ('screen', (7, 0, 0.75, 4.0, 1)), ('wait', 3)], + 'check': check_custom('repeat')}, + {'name': 'no-map-custom', 'map': 'none', 'settings': CUSTOM_OFFSET, + 'steps': SETTLE, 'check': check_frame_custom()}, + # A views core's frame shown whole is as laid out, not screen 1. + {'name': 'crop-threaded-custom', 'map': 'crop', + 'settings': dict(CUSTOM_OFFSET, video_threaded='true'), 'steps': SETTLE, + 'check': threaded(check_crop)}, +] + + +# ---- Edit Screens ---- +# The 3DS quads as in the laser cases. While editing, the menu's quad is +# up 10 cm in front of screen 1 and as large: it is taken only by its +# handle bar, centred under it, so aims through its middle reach the +# screen behind. A right hand at x,y,0 aiming straight ahead moves what +# it holds by its own moves. +EDIT = 'HEADSET_EDIT_SCREENS' +EDIT_ON = '[OpenXR] Edit Screens on.' +EDIT_OFF = '[OpenXR] Edit Screens off.' +EDIT_GRIP = 'action edit/grab@right 1' +EDIT_GRIP_LEFT = 'action edit/grab@left 1' +EDIT_TILT = 'action edit/tilt@right 1' +EDIT_RESET = 'action edit/reset@right 1' +EDIT_NEXT = 'action edit/select_next 1' +EDIT_SETS = ['edit', 'pointer'] +GAME_SETS = ['combined', 'pointer'] +SAVE = {'config_save_on_exit': 'true'} +ID = (0.0, 0.0, 0.0, 1.0) +ROLL30 = (0.0, 0.0, math.sin(math.pi / 12.0), math.cos(math.pi / 12.0)) +S0 = (0.0, 0.0, -1.8) +S1 = (0.0, -0.992, -1.8) +MENU_SPOT = (0.0, 0.0, -1.7) +MENU_W, MENU_H = 1.6, 0.96 +# The laser's image, laid out as gfx/video_xr.h lays it out: its dot, +# edit mode's faint and bright outlines, the menu's faint and bright +# handle bars, the beam's strips to the dot and into nothing. +CURSOR_DIM = 32 +OUTLINE_DIM = 256 +OUTLINE_BAND = 2 +OUTLINE_EDGE = 3 * OUTLINE_BAND +HANDLE_COLS, HANDLE_ROWS = 128, 8 +# VIDEO_XR_OUTLINE_GAP, and the rim's fraction of an outline with the +# half texel filtering adds. +OUTLINE_GAP = 0.005 +OUTLINE_F = (OUTLINE_EDGE + 0.5) / OUTLINE_DIM +BEAM_LEN, BEAM_ROWS = 128, 4 +OUTLINE_X = tuple(CURSOR_DIM + 1 + k * (OUTLINE_DIM + 1) for k in (0, 1)) +HANDLE_X = tuple(OUTLINE_X[0] + 2 * (OUTLINE_DIM + 1) + k * (HANDLE_COLS + 1) + for k in (0, 1)) +BEAM_X = tuple(HANDLE_X[0] + 2 * (HANDLE_COLS + 1) + k * (BEAM_LEN + 1) + for k in (0, 1)) +# A back's solid cell after the beam strips; a back shows the square +# BACK_MARGIN inside it, centred on the middle row. +BACK_DIM, BACK_MARGIN, BACK_LEVEL = 8, 4, 40 +BACK_CELL = BACK_DIM + 2 * BACK_MARGIN +BACK_X = BEAM_X[1] + BEAM_LEN + 1 +BACK_Y = (OUTLINE_DIM - BACK_CELL) // 2 +BACK_RECT = (BACK_X + BACK_MARGIN, BACK_Y + BACK_MARGIN, BACK_DIM, BACK_DIM) +# A hinge's green strip after the back's cell, centred on the middle +# row. +HINGE_COLS, HINGE_ROWS = 128, 4 +HINGE_X = BACK_X + BACK_CELL + 1 +HINGE_Y = (OUTLINE_DIM - HINGE_ROWS) // 2 +# A hidden hinge slot's rows, empty either way up. +HINGE_HIDDEN_Y = 0 +# The hinge's line, VIDEO_XR_HINGE_WIDTH wide, each end +# VIDEO_XR_HINGE_LIFT nearer the head; its strip's green, +# VIDEO_XR_HINGE_RB/_G at VIDEO_XR_HINGE_LEVEL, premultiplied. An +# outline hides an edge's rim VIDEO_XR_OUTLINE_CROP texels deep. +HINGE_WIDTH, HINGE_LIFT = 0.006, 0.01 +HINGE_LEVEL, HINGE_RB, HINGE_G = 166, 60, 220 +OUTLINE_CROP = 8 +# Edit mode's tags, one cell for each item it can choose (0, the laser; +# a slot plus one), after the hinge's strip, each centred on the middle +# row. +TAG_COLS, TAG_ROWS, TAGS = 128, 30, 10 +TAG_X = tuple(HINGE_X + HINGE_COLS + 1 + k * (TAG_COLS + 1) + for k in range(TAGS)) +TAG_Y = (OUTLINE_DIM - TAG_ROWS) // 2 +# Edit mode's cut outlines after the tags, faint ones then bright, one +# for each cut: the outline's square without those edges' rims. +CUTS = ('top', 'bottom', 'right', 'left', 'top-bottom', 'right-left') +CUT_X = tuple(TAG_X[-1] + TAG_COLS + 1 + k * (OUTLINE_DIM + 1) + for k in range(2 * len(CUTS))) +CURSOR_W = CUT_X[-1] + OUTLINE_DIM +# Its width without the cut outlines, then without the tags too, for +# runtimes too narrow for them. +CURSOR_TAGGED_W = TAG_X[-1] + TAG_COLS +CURSOR_PLAIN_W = HINGE_X + HINGE_COLS +TAG_HEIGHT, TAG_LIFT, TAG_FAINT = 0.02, 0.05, 160 +CURSOR_H = OUTLINE_DIM +# The beam: VIDEO_XR_BEAM_WIDTH, _REACH and _LEVEL; AIM_GUN's and +# AIM_MISS's rays; the head where the beams' cases put it. +BEAM_WIDTH = 0.004 +BEAM_REACH = 1.5 +BEAM_LEVEL = 160 +GUN_FROM = (0.2, -0.4, -0.3) +GUN_AT = (-0.4, 0.24, -1.8) +MISS_AT = (3.0, 0.0, -1.8) +HEAD_AT_ORIGIN = 'head 0 0 0 0' +EDIT_POS = 0.012 + + +def qrot(q, v): + return qmul(qmul(q, tuple(v) + (0.0,)), (-q[0], -q[1], -q[2], q[3]))[:3] + + +def compose(a, b): + """Pose b, in a's frame, in a's space; a pose is (position, quat).""" + return (tuple(x + y for x, y in zip(a[0], qrot(a[1], b[0]))), + qmul(a[1], b[1])) + + +def inverse(a): + q = (-a[1][0], -a[1][1], -a[1][2], a[1][3]) + return (tuple(-v for v in qrot(q, a[0])), q) + + +def euler(q): + """Yaw, pitch and roll in degrees, as video_xr_euler has them.""" + x, y, z, w = q + sp = max(-1.0, min(1.0, -2.0 * (y * z - w * x))) + if abs(sp) > 0.99999: + return (math.degrees(math.atan2(-2.0 * (x * z - w * y), + 1.0 - 2.0 * (y * y + z * z))), + math.degrees(math.asin(sp)), 0.0) + return (math.degrees(math.atan2(2.0 * (x * z + w * y), + 1.0 - 2.0 * (x * x + y * y))), + math.degrees(math.asin(sp)), + math.degrees(math.atan2(2.0 * (x * y + w * z), + 1.0 - 2.0 * (x * x + z * z)))) + + +def from_euler(yaw, pitch, roll): + """The turn yaw, pitch and roll settings give an item.""" + cy, sy = math.cos(math.radians(yaw) / 2), math.sin(math.radians(yaw) / 2) + cp, sp = (math.cos(math.radians(pitch) / 2), + math.sin(math.radians(pitch) / 2)) + cr, sr = (math.cos(math.radians(roll) / 2), + math.sin(math.radians(roll) / 2)) + return (cy * sp * cr + sy * cp * sr, sy * cp * cr - cy * sp * sr, + cy * cp * sr - sy * sp * cr, cy * cp * cr + sy * sp * sr) + + +def aim_pose(p, t, roll=0.0): + """The test layer's aim at p towards t.""" + f = [b - a for a, b in zip(p, t)] + n = math.sqrt(sum(v * v for v in f)) + return (tuple(p), from_euler(math.degrees(math.atan2(-f[0], -f[2])), + math.degrees(math.asin(f[1] / n)), roll)) + + +def held(item, aim0, aim1, tilt): + """Where a hand that took item at aim0 puts it at aim1: turned as + the aim's heading turned, its pitch and roll kept, unless the + trigger tilts it. Neither aim points near straight up or down.""" + pose = compose(aim1, compose(inverse(aim0), item)) + if tilt: + return pose + turn = euler(aim1[1])[0] - euler(aim0[1])[0] + return (pose[0], qmul(from_euler(turn, 0.0, 0.0), item[1])) + + +def aim_line(p, t, hand='right', roll=None): + line = 'aim %s %g %g %g %g %g %g' % ((hand,) + tuple(p) + tuple(t)) + return line if roll is None else '%s %g' % (line, roll) + + +def edit_aim(x=0.0, y=0.0, roll=None, z=-1.8): + """The right hand at x,y,0 aiming straight ahead, rolled.""" + return aim_line((x, y, 0.0), (x, y, z), roll=roll) + + +def edit_aim_turned(x=0.0, y=0.0, pitch=0.0, yaw=0.0, z=-1.8): + """edit_aim()'s hand, its aim turned pitch degrees up and yaw degrees + left, as the unit test's t_turn_aim turns them.""" + return aim_line((x, y, 0.0), + (x - 1.8 * math.tan(math.radians(yaw)), + y + 1.8 * math.tan(math.radians(pitch)), z)) + + +def edit_shake(x, y, deg, secs): + """Steps gripping with edit_aim_turned()'s hand at x, y for secs + seconds, its aim turned every 0.02 s as the unit tests' t_shake + turns it: deg degrees in pitch and yaw at 8 Hz and a fifth of that + at 2.3 Hz, the yaw a quarter period behind.""" + steps = [] + for k in range(int(round(secs / 0.02))): + t = k * 0.02 + a = 2.0 * math.pi * 8.0 * t + b = 2.0 * math.pi * 2.3 * t + 1.0 + p = deg * (math.sin(a) + 0.2 * math.sin(b)) + w = deg * (math.sin(a - math.pi / 2.0) + + 0.2 * math.sin(b - math.pi / 2.0)) + steps += [('script', script(edit_aim_turned(x, y, pitch=p, yaw=w), + EDIT_GRIP)), + ('wait', 0.02)] + return steps + + +def outline_size(w, h): + """An outline's quad round an item w by h.""" + return tuple((s + 2.0 * OUTLINE_GAP) / (1.0 - 2.0 * OUTLINE_F) + for s in (w, h)) + + +def handle_y(menu_y, h): + """The height of the handle under a menu at menu_y, h high: 5 mm + below its outline.""" + return menu_y - (outline_size(0.0, h)[1] + 0.03) / 2.0 - 0.005 + + +def edit_screen0(frame): + """Screen 1's two eyes.""" + return [q for q in quads(frame) if not q['flags'] & BLEND + and q['eye'] in ('left', 'right')] + + +def screen2(frame): + q = [x for x in quads(frame) if x['eye'] == 'both' and not x['flags']] + return q[0] if q else None + + +def frame_quad(frame): + """A whole frame: the one quad that is not blended.""" + qs = [q for q in quads(frame) if not q['flags'] & BLEND] + return qs[0] if len(qs) == 1 else None + + +def menu_quad(frame): + ms = menus(frame) + return ms[0] if ms else None + + +def outlines(frame, lit=None): + """Edit mode's outlines, whole, a cut's cell or cut to hide an edge's + rim; lit: only the bright (1) or faint (0).""" + lits = (0, 1) if lit is None else (lit,) + xs = [x for k in lits for x in (OUTLINE_X[k], OUTLINE_X[k] + OUTLINE_CROP) + + CUT_X[k * len(CUTS):(k + 1) * len(CUTS)]] + return [q for q in quads(frame) + if q['flags'] & BLEND and q['rect'][3] == OUTLINE_DIM + and q['rect'][0] in xs] + + +def outline_cut(o): + """Whether outline o hides a rim: a cut's cell, or cut.""" + return o['rect'][0] not in OUTLINE_X or o['rect'][2] != OUTLINE_DIM + + +def outline_pose_errors(frame): + """Every outline in frame its item's whole outline, cut or not: the + item's middle, lifted toward the head, its axes and the size round + it. A cut changes the outline's image alone.""" + items = [q for q in quads(frame) if not q['flags'] & BLEND] \ + + menus(frame) + errors = [] + for o in outlines(frame): + if not items: + return ['frame %d: no items under the outlines' % frame['n']] + q = min(items, key=lambda i: sum( + (a - b) ** 2 for a, b in zip(i['pose'][:3], o['pose'][:3]))) + axes = [abs(sum(a * b for a, b in zip( + qrot(tuple(o['pose'][3:]), v), qrot(tuple(q['pose'][3:]), v)))) + for v in ((1.0, 0.0, 0.0), (0.0, 0.0, 1.0))] + size = outline_size(*q['size']) + off = math.sqrt(sum((a - b) ** 2 + for a, b in zip(o['pose'][:3], q['pose'][:3]))) + if (min(axes) < 0.9999 or off > 0.003 + or any(abs(a - b) > 1e-4 for a, b in zip(o['size'], size))): + errors.append('frame %d: the outline %s at %s, %s, axes %s, not ' + 'its item\'s whole outline at %s, %s' % ( + frame['n'], o['rect'], + [round(v, 4) for v in o['pose'][:3]], + [round(v, 4) for v in o['size']], + [round(v, 5) for v in axes], + [round(v, 4) for v in q['pose'][:3]], + [round(v, 4) for v in size])) + return errors + + +def hinge_lines(frame): + """Edit mode's green hinge lines, the slots shown.""" + return [q for q in hinge_slots(frame) if q['rect'][1] == HINGE_Y] + + +def hinge_slots(frame): + """Edit mode's hinge slots, shown or hidden.""" + return [q for q in quads(frame) + if q['flags'] & BLEND and q['rect'][0] == HINGE_X + 1] + + +def handles(frame, lit=None): + """The menu's handle bar; lit as for outlines().""" + xs = HANDLE_X if lit is None else (HANDLE_X[lit],) + return [q for q in quads(frame) + if q['flags'] & BLEND and q['rect'][0] in xs] + + +def beams(frame, miss=None): + """The laser's beams; miss: only those to a dot (0) or into nothing + (1).""" + xs = BEAM_X if miss is None else (BEAM_X[miss],) + return [q for q in quads(frame) + if q['flags'] & BLEND and q['rect'][0] in xs] + + +def tags(frame): + """Edit mode's tag naming the chosen item.""" + return [q for q in quads(frame) if q['flags'] & BLEND + and tuple(q['rect'][2:]) == (TAG_COLS, TAG_ROWS)] + + +def unit(v): + n = math.sqrt(sum(x * x for x in v)) + return tuple(x / n for x in v) + + +def rect_texel(res, fr, q, tx, ty): + """Texel tx, ty of the part of its image a quad shows.""" + w, h, bpp, rows = read_png(image(res, fr, q)) + x, y = q['rect'][0] + tx, q['rect'][1] + ty + px = rows[y][x * bpp:x * bpp + bpp] + return tuple(px[:3]), (px[3] if bpp == 4 else 255) + + +def beam_errors(what, b, p, t, miss, head=(0.0, 0.0, 0.0)): + """Beam b on the ray from p towards t: its strip, its middle and + length (to t, or BEAM_REACH into nothing), its x down the ray and + its face towards head.""" + d = unit(tuple(y - x for x, y in zip(p, t))) + length = (BEAM_REACH if miss + else math.sqrt(sum((y - x) ** 2 for x, y in zip(p, t)))) + mid = tuple(x + y * length / 2.0 for x, y in zip(p, d)) + to = tuple(a - m for a, m in zip(head, mid)) + k = sum(a * c for a, c in zip(to, d)) + face = unit(tuple(a - k * c for a, c in zip(to, d))) + q = tuple(b['pose'][3:]) + errors = [] + if b['rect'][0] != BEAM_X[miss] or tuple(b['rect'][2:]) != (BEAM_LEN, + BEAM_ROWS): + errors.append('%s shows %s of the image' % (what, b['rect'])) + if not near3(b['pose'][:3], mid) \ + or not near3(b['size'], (length, BEAM_WIDTH), 0.002): + errors.append('%s at %s, %s, want %s, %s' % ( + what, [round(v, 3) for v in b['pose'][:3]], b['size'], + [round(v, 3) for v in mid], (round(length, 3), BEAM_WIDTH))) + if sum(a * c for a, c in zip(qrot(q, (1.0, 0.0, 0.0)), d)) < 0.999: + errors.append('%s does not run down the ray' % what) + if sum(a * c for a, c in zip(qrot(q, (0.0, 0.0, 1.0)), face)) < 0.999: + errors.append('%s does not face the head' % what) + return errors + + +def strip_errors(res, f, b, miss, what): + """The beam's strip: white at BEAM_LEVEL in the middle, faded in at + the hand, and on a miss faded out at the far end.""" + errors = [] + rgb, alpha = rect_texel(res, f, b, BEAM_LEN // 2, BEAM_ROWS // 2) + if abs(alpha - BEAM_LEVEL) > 2 or abs(rgb[0] - BEAM_LEVEL) > 2: + errors.append('%s shows %s alpha %d mid-way, want %d' % ( + what, rgb, alpha, BEAM_LEVEL)) + if rect_texel(res, f, b, 0, BEAM_ROWS // 2)[1] >= BEAM_LEVEL // 8: + errors.append('%s does not fade in at the hand' % what) + if miss and rect_texel(res, f, b, BEAM_LEN - 1, + BEAM_ROWS // 2)[1] >= BEAM_LEVEL // 32: + errors.append('%s does not fade out at its end' % what) + return errors + + +def between(res, start, end=None, pick=quads, delay=0.0): + """Frames from delay seconds after mark start until mark end that pick + finds something in.""" + t0 = (res.marks[start] + delay) * 1e6 + t1 = res.marks[end] * 1e6 if end else float('inf') + return [f for f in res.frames if t0 < f['t_us'] < t1 and pick(f)] + + +def last_before(res, mark, pick): + fs = [f for f in res.frames + if f['t_us'] < res.marks[mark] * 1e6 and pick(f)] + return pick(fs[-1]) if fs else None + + +def near3(a, b, tol=EDIT_POS): + return all(abs(x - y) <= tol for x, y in zip(a, b)) + + +def pose_errors(what, qs, pos, turn=None, size=None): + """Every quad in qs at pos, turned as turn (yaw, pitch and roll each + within 0.6 degrees) and size if given.""" + if not qs: + return ['%s: no frames' % what] + for q in qs: + if q is None: + return ['%s: the quad is missing' % what] + if not near3(q['pose'][:3], pos): + return ['%s at %s, want %s' % ( + what, [round(v, 3) for v in q['pose'][:3]], + [round(v, 3) for v in pos])] + if turn is not None: + got, want = euler(q['pose'][3:]), euler(turn) + if not all(abs((a - b + 180.0) % 360.0 - 180.0) <= 0.6 + for a, b in zip(got, want)): + return ['%s turned %s, want %s' % ( + what, [round(v, 2) for v in got], + [round(v, 2) for v in want])] + if size is not None and not near3(q['size'], size): + return ['%s sized %s, want %s' % ( + what, [round(v, 4) for v in q['size']], + [round(v, 4) for v in size])] + return [] + + +def stereo_errors(frames): + """Screen 1's eyes together, one item: the same pose and size.""" + for f in frames: + eyes = edit_screen0(f) + if len(eyes) != 2 or not (near3(eyes[0]['pose'], eyes[1]['pose'], + 1e-4) + and near3(eyes[0]['size'], + eyes[1]['size'], 1e-4)): + return ['frame %d: screen 1\'s eyes apart: %s' + % (f['n'], [(q['pose'][:3], q['size']) for q in eyes])] + return [] + + +def saved(res): + """The configuration RetroArch saved at exit.""" + cfg = {} + path = os.path.join(res.dir, 'retroarch.cfg') + if os.path.exists(path): + with open(path) as f: + for line in f: + m = re.match(r'(\w+) = "(.*)"', line.strip()) + if m: + cfg[m.group(1)] = m.group(2) + return cfg + + +def saved_errors(res, what, prefix, base, want, size, last=None, + anchor=None): + """The saved settings put the item where want is, its width and + height at scale 1 size, from anchor (the identity unless given), + and where the headset last showed it.""" + cfg = saved(res) + v = [float(cfg.get(prefix + k, '0')) + for k in ('x', 'y', 'z', 'yaw', 'pitch', 'roll')] + scale = float(cfg.get(prefix + 'scale', '1')) + pose = (tuple(b + o for b, o in zip(base, v[:3])), from_euler(*v[3:])) + if anchor is not None: + pose = compose(anchor, pose) + q = {'pose': list(pose[0]) + list(pose[1]), + 'size': [size[0] * scale, size[1] * scale]} + errors = pose_errors('%s from its saved settings' % what, [q], want[0], + want[1]) + if last is not None: + errors += pose_errors('%s last shown' % what, [last], pose[0], + pose[1], q['size']) + return errors + + +def edit_log_errors(res, want, exact=True): + """Edit mode's on and off lines in order; not exact, a last off may + follow (quitting ends it).""" + got = ['on' if EDIT_ON in line else 'off' + for line in res.log.splitlines() + if EDIT_ON in line or EDIT_OFF in line] + if got == want or (not exact and got == want + ['off']): + return [] + return ['edit mode %s, want %s' % (got, want)] + + +def edit_sync_errors(res, on, off, game=GAME_SETS): + """The game's sets before mark on, edit mode's alone until mark off, + the game's again after.""" + syncs = [(e['t_us'], e['sets']) for e in events(res, 'sync') + if e['sets']] + t_on, t_off = res.marks[on] * 1e6, res.marks[off] * 1e6 + before = [s for t, s in syncs if t < t_on] + during = [s for t, s in syncs if t_on <= t < t_off] + after = [s for t, s in syncs if t >= t_off] + errors = [] + if not before or before[-1] != game: + errors.append('synced %s before %s, want %s' % (before[-1:], on, game)) + if during != [EDIT_SETS]: + errors.append('synced %s between %s and %s, want only %s' + % (during, on, off, EDIT_SETS)) + if not after or after[-1] != game: + errors.append('synced %s after %s, want %s' % (after, off, game)) + return errors + + +def core_frames(res, t0, t1): + """The core's logged frame numbers from t0 to t1 us.""" + return [int(n) for n, t in CORE_FRAME.findall(res.log) + if t0 <= int(t) <= t1] + + +def check_edit_grab(menu=True): + def check(res): + """A grip on screen 1 through the menu's middle takes both its + eyes along with the hand, level; they stay where it let go, + through the release and after edit mode ends, and are saved + there, the menu following them unplaced. While editing every + item is outlined, the pointed-at one brightly, and the menu has + its handle.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 1 before the grab', + [screen1_left(f) for f in + between(res, 'on', 'grab', edit_screen0, + 0.5)], S0, ID, (1.6, 0.96)) + late = between(res, 'moved', None, edit_screen0) + if not late: + return errors + no_quads(res) + want = (0.3, 0.0, -1.8) + errors += pose_errors('screen 1 moved', + [q for f in late for q in edit_screen0(f)], + want, ID, (1.6, 0.96)) + errors += stereo_errors(between(res, 'aimed', None, edit_screen0)) + if not any(f['t_us'] > res.marks['off'] * 1e6 for f in late): + errors.append('no frame after edit mode ended') + items = 2 + if menu: + items = 3 + d = math.sqrt(0.3 * 0.3 + 1.7 * 1.7) / 1.7 + errors += pose_errors( + 'the menu following it', + [menu_quad(f) for f in between(res, 'moved', 'stop', + delay=0.5)], + (0.3, 0.0, -1.7), ID, (MENU_W * d, MENU_H * d)) + lit = [f for f in between(res, 'aimed', 'released', outlines) + if len(outlines(f)) == items and len(outlines(f, 1)) == 1 + and len(handles(f)) == int(menu)] + if not lit: + errors.append('no frame outlined all %d items, one brightly%s' + % (items, ', with a handle' if menu else '')) + if any(outlines(f) or handles(f) + for f in between(res, 'off', delay=0.5)): + errors.append('outlines after edit mode ended') + errors += saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (want, ID), (1.6, 0.96), + edit_screen0(late[-1])[0]) + if saved(res).get('video_openxr_menu_placed') != 'false': + errors.append('grabbing through the menu placed it') + return errors + return check + + +def slot_made(res, slot=0): + """How often the slot's swapchains were made while editing.""" + made, on = 0, False + for line in res.log.splitlines(): + if EDIT_ON in line: + on = True + elif EDIT_OFF in line: + on = False + elif on and '[OpenXR] Slot %d:' % slot in line: + made += 1 + return made + + +def check_edit_follow(res): + """Held, screen 1 moves on nearly every headset frame whose aim moved + (the laser's dot, placed from the frame's own aim, shows it) while + the core draws one frame in two: each frame places it from its own + aim, not from the last video frame. Brought nearer, it keeps its + image: in every headset frame, not made again for each distance.""" + errors = edit_log_errors(res, ['on', 'off']) + held = between(res, 'grab', 'released', lambda f: True) + gaps = [f['n'] for f in held if len(edit_screen0(f)) != 2] + if not held or gaps: + errors.append('screen 1 missing from %d of the %d headset frames ' + 'it was held in: %s' % (len(gaps), len(held), + gaps[:10])) + made = slot_made(res) + if made > 2: + errors.append('screen 1\'s swapchains made %d times while editing' + % made) + sweep = [f for f in between(res, 'sweep', 'swept', edit_screen0) + if cursors(f)] + xs = [(screen1_left(f)['pose'][0], cursors(f)[0]['pose'][0]) + for f in sweep] + pairs = [(a, b) for a, b in zip(xs, xs[1:]) if abs(b[1] - a[1]) > 0.001] + if len(pairs) < 8: + return errors + ['only %d headset frames saw the aim move' + % len(pairs)] + moved = sum(1 for a, b in pairs if abs(b[0] - a[0]) > 0.001) + if moved < 0.9 * len(pairs): + errors.append('screen 1 moved in %d of the %d headset frames whose ' + 'aim moved' % (moved, len(pairs))) + t0, t1 = res.marks['sweep'] * 1e6, res.marks['swept'] * 1e6 + fps = core_fps(res, t0 - 2e6, t1 + 2e6) + hz = len(xs) * 1e6 / (t1 - t0) + if not fps or fps * 1.5 > hz: + errors.append('the core at %.1f fps, the headset at %.1f Hz: the ' + 'core was not the slower' % (fps, hz)) + errors += stereo_errors(between(res, 'grab', None, edit_screen0)) + want = (0.3, 0.0, -1.5) + errors += pose_errors('screen 1 let go', + [edit_screen0(f)[0] for f in + between(res, 'released', None, edit_screen0, + 1.0)], want, ID) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (want, ID), (1.6, 0.96)) + + +def no_step_back(frames, pick, want, what): + """Once at want (moving there along +x), never short of it again.""" + reached = False + for f in frames: + q = pick(f) + if not q: + continue + if near3(q['pose'], want): + reached = True + elif reached or q['pose'][0] > want[0] + EDIT_POS: + return ['%s stepped to %s at frame %d, want %s' + % (what, [round(v, 3) for v in q['pose'][:3]], f['n'], + want)] + return [] if reached else ['%s never reached %s' % (what, want)] + + +def screen1_left(frame): + eyes = edit_screen0(frame) + return eyes[0] if eyes else None + + +def screen1_at_yaw(yaw): + """A pick: screen 1's left eye where it shows exactly yaw.""" + def pick(frame): + q = screen1_left(frame) + if q and abs(euler(tuple(q['pose'][3:]))[0] - yaw) <= 0.05: + return q + return None + return pick + + +def screen2_at_yaw(yaw, past=False): + """A pick: screen 2 where it shows exactly yaw, or once it has + turned past it (yaw rising) if past.""" + def pick(frame): + q = screen2(frame) + if not q: + return None + y = euler(tuple(q['pose'][3:]))[0] + return q if (y >= yaw if past else abs(y - yaw) <= 0.05) else None + return pick + + +def check_edit_release(res): + """Let go after a sweep under threaded video: screen 1 never steps + back from where the hand left it, and is saved there.""" + want = (0.15, 0.0, -1.8) + return (edit_log_errors(res, ['on'], exact=False) + + no_step_back(between(res, 'swept'), screen1_left, want, + 'screen 1') + + stereo_errors(between(res, 'grab', None, edit_screen0)) + + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (want, ID), (1.6, 0.96))) + + +def check_edit_release_moving(res): + """Let go mid-sweep: screen 1 is let go and saved where a frame at + most a poll (100 ms at FPS10) before the release showed it, not + where the release poll's aim puts it, and no frame after the release + shows it further back.""" + errors = edit_log_errors(res, ['on'], exact=False) + xs = [(f['t_us'], screen1_left(f)['pose'][0]) + for f in between(res, 'sweep', None, edit_screen0)] + t_rel = res.marks['released'] * 1e6 + poll, slack = 100000, 20000 + back = [x for t, x in xs if t_rel - poll - slack <= t <= t_rel + slack] + late = [x for t, x in xs if t > t_rel] + rest = [screen1_left(f) for f in + between(res, 'released', None, edit_screen0, 1.0)] + if not back or not late or not rest: + return errors + ['no frames around the release'] + want = (rest[-1]['pose'][0], 0.0, -1.8) + if not any(abs(want[0] - x) <= 0.002 for x in back): + errors.append('screen 1 let go at %.3f, want where a frame showed ' + 'it a poll before the release: %s' + % (want[0], [round(x, 3) for x in back])) + for x in late: + if x < want[0] - 0.002: + errors.append('after the release screen 1 stepped back to ' + '%.3f, past where it was let go' % x) + break + errors += pose_errors('screen 1 let go', rest, want, ID) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (want, ID), (1.6, 0.96)) + + +def check_edit_regrab(res): + """Let go and grip again at once: the regrab takes screen 1 where it + was let go, while quads planned at the release are still coming, + and nudges it on from there.""" + errors = edit_log_errors(res, ['on'], exact=False) + want = (0.15, 0.0, -1.8) + errors += pose_errors('screen 1 let go and taken again', + [screen1_left(f) for f in + between(res, 'released', 'moved_again', + edit_screen0)], want, ID) + again = (0.25, 0.0, -1.8) + errors += pose_errors('screen 1 nudged', + [screen1_left(f) for f in + between(res, 'moved_again', None, edit_screen0, + 1.0)], again, ID) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (again, ID), (1.6, 0.96)) + + +def check_edit_handover(res): + """One hand lets go of screen 1 and takes screen 2 at the next poll: + screen 1 never steps back, screen 2 moves, both are saved.""" + errors = edit_log_errors(res, ['on'], exact=False) + want = (0.15, 0.0, -1.8) + errors += no_step_back(between(res, 'swept'), screen1_left, want, + 'screen 1') + b_want = (0.3, -0.992, -1.8) + errors += pose_errors('screen 2 moved', + [screen2(f) for f in + between(res, 'b_moved', 'end', delay=1.0)], + b_want, ID, (1.28, 0.96)) + errors += saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (want, ID), (1.6, 0.96)) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (b_want, ID), (1.28, 0.96)) + + +def check_edit_tilt(res): + """Level by default: the wrist's roll leaves screen 1 upright; with + the trigger held the roll follows from where it was, and stays when + the trigger is let go.""" + errors = [] + for mark, want, what in ( + ('rolled', ID, 'rolled 30 without the trigger'), + ('tilted', ID, 'the trigger pressed'), + ('turned', ROLL30, 'rolled 30 more with the trigger'), + ('kept', ROLL30, 'the trigger let go, the wrist back')): + q = last_before(res, mark, edit_screen0) + if not q: + errors.append('no frame before %s' % what) + elif not all(facing(e, want) for e in q): + errors.append('%s: screen 1 turned %s, want %s' + % (what, q[0]['pose'][3:], want)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (S0, ROLL30), (1.6, 0.96)) + + +T_HAND = (0.8, 0.0, -0.3) +T0 = (T_HAND, (0.0, 0.0, -1.8)) +T1 = (T_HAND, (0.0, 0.8, -1.8)) + + +def check_edit_turn(tilt): + want = held((S0, ID), aim_pose(*T0), aim_pose(*T1), tilt) + + def check(res): + """A hand beside screen 1 turned up carries it up, as video_xr's + grab does: keeping its yaw, pitch and roll, or with the trigger + held letting it follow the hand; saved as shown.""" + errors = edit_log_errors(res, ['on'], exact=False) + if tilt != (abs(euler(want[1])[2]) > 5.0): + errors.append('the case does not tell level from tilt') + if tilt != (abs(euler(want[1])[1]) > 5.0): + errors.append('the case does not tell a held pitch from one ' + 'that follows') + shown = [screen1_left(f) for f in + between(res, 'moved', None, edit_screen0, 1.5)] + errors += pose_errors('screen 1 turned by the hand', shown, want[0], + want[1], (1.6, 0.96)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', + S0, want, (1.6, 0.96), + shown[-1] if shown else None) + return check + + +def check_edit_stick(res): + """The stick held up pushes screen 1 away along the laser, from a + hand below and beside it, inside its deadzone does nothing, and + held right grows it.""" + held_q = last_before(res, 'held', screen1_left) + pushed = last_before(res, 'pushed', screen1_left) + dead = last_before(res, 'deaded', screen1_left) + grown = last_before(res, 'grown', screen1_left) + if not (held_q and pushed and dead and grown): + return no_quads(res) + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 1 held', [held_q], S0, ID) + ray = [b - a for a, b in zip(*A0)] + ray = [v / math.sqrt(sum(r * r for r in ray)) for v in ray] + disp = [b - a for a, b in zip(S0, pushed['pose'][:3])] + along = sum(a * b for a, b in zip(disp, ray)) + off = math.sqrt(sum((d - along * r) ** 2 for d, r in zip(disp, ray))) + if not 0.3 <= along <= 1.3 or off > 0.02: + errors.append('half a second of push moved screen 1 %.3f along the ' + 'laser and %.3f off it, want 0.3 to 1.3 along' + % (along, off)) + errors += pose_errors('screen 1 after the deadzone', [dead], + pushed['pose'][:3], ID, pushed['size']) + k = grown['size'][0] / pushed['size'][0] + if not 1.2 <= k <= 1.9: + errors.append('half a second right scaled it by %.3f, want 1.2 to ' + '1.9' % k) + errors += pose_errors('screen 1 grown', [grown], pushed['pose'][:3], ID) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (grown['pose'][:3], ID), (1.6, 0.96), + last_before(res, 'off', screen1_left)) + + +def check_edit_reset(menu=True): + def check(res): + """Screen 1 from its offsets, grabbed and moved, then reset as + Reset Screen does, and the menu with it; the menu taken by its + handle and moved, screen 1 staying, then back in front of screen + 1 on its own reset, unplaced.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 1 at its offsets', + [last_before(res, 'offset', screen1_left)], + (0.25, 0.1, -1.5), ID) + errors += pose_errors('screen 1 moved', + [screen1_left(f) for f in + between(res, 'moved', 'reset', edit_screen0, + 1.0)], (0.55, 0.1, -1.5), ID) + late = between(res, 'reset', 'off', edit_screen0, 1.0) + errors += pose_errors('screen 1 reset', + [screen1_left(f) for f in late], S0, ID, + (1.6, 0.96)) + if menu: + errors += pose_errors( + 'the menu after the screen\'s reset', + [menu_quad(f) for f in between(res, 'reset', 'handle', + delay=1.0)], + MENU_SPOT, ID, (MENU_W, MENU_H)) + errors += pose_errors( + 'the menu taken by its handle', + [menu_quad(f) for f in between(res, 'placed', 'back', + delay=1.0)], + (0.5, 0.0, -1.7), ID, (MENU_W, MENU_H)) + errors += pose_errors( + 'its reset', + [menu_quad(f) for f in between(res, 'back', 'off', + delay=1.0)], + MENU_SPOT, ID, (MENU_W, MENU_H)) + if saved(res).get('video_openxr_menu_placed') != 'false': + errors.append('the menu\'s reset left it placed') + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', + S0, (S0, ID), (1.6, 0.96), + screen1_left(late[-1]) if late + else None) + return check + + +def check_edit_menu_front(res): + """Screen 1 moved 60 cm closer takes the menu with it, 10 cm in front, + as large to the eye as at its spot and submitted after the + screens.""" + fr = last_snap(res) + if not fr: + return no_quads(res) + ms = menus(fr) + errors = [] + d = 1.1 / 1.7 + if len(ms) != 1 or not at(ms[0], (0.0, 0.0, -1.1)) \ + or not sized(ms[0], (MENU_W * d, MENU_H * d)): + errors.append('the menu at %s, want 0,0,-1.1 and %.3f wide' + % ([(m['pose'][:3], m['size']) for m in ms], + MENU_W * d)) + order = quads(fr) + if ms and any(order.index(q) > order.index(ms[0]) + for q in edit_screen0(fr)): + errors.append('a screen was submitted after the menu') + return errors + + +F_D = 0.8 / 1.7 +F_HANDLE = handle_y(0.0, MENU_H * F_D) + + +def check_edit_menu_size(res): + """Screen 1 0.9 m nearer: the menu follows 0.8 m away as large to the + eye as at its spot, and taken by its handle it is placed as large + as it showed.""" + errors = edit_log_errors(res, ['on'], exact=False) + size = (MENU_W * F_D, MENU_H * F_D) + errors += pose_errors('the menu following the nearer screen 1', + [menu_quad(f) for f in between(res, 'on', 'grab', + delay=1.0)], + (0.0, 0.0, -0.8), ID, size) + shown = [menu_quad(f) for f in between(res, 'moved', None, menus, 1.5)] + errors += pose_errors('the menu placed', shown, (0.3, 0.0, -0.8), ID, + size) + if saved(res).get('video_openxr_menu_placed') != 'true': + errors.append('the menu was not placed') + return errors + saved_errors(res, 'the menu', 'video_openxr_menu_xr_', + MENU_SPOT, ((0.3, 0.0, -0.8), ID), + (MENU_W, MENU_H), + shown[-1] if shown else None) + + +# Screen 1 at EDIT_OFFSET's 0.25,0.1,-1.5: the menu following 10 cm in +# front, as large to the eye as at its spot. +M_D = math.sqrt(0.25 * 0.25 + 0.1 * 0.1 + 1.4 * 1.4) / 1.7 +M_HANDLE = handle_y(0.1, MENU_H * M_D) + + +def check_edit_menu_reset(res): + """The menu following a moved screen 1, taken by its handle and moved, + screen 1 staying; reset on its handle, it follows screen 1 again, + unplaced.""" + errors = edit_log_errors(res, ['on', 'off']) + size = (MENU_W * M_D, MENU_H * M_D) + errors += pose_errors('the menu following screen 1', + [menu_quad(f) for f in between(res, 'on', 'grab', + delay=1.0)], + (0.25, 0.1, -1.4), ID, size) + errors += pose_errors('the menu taken by its handle', + [menu_quad(f) for f in between(res, 'placed', + 'back', delay=1.0)], + (0.55, 0.1, -1.4), ID, size) + errors += pose_errors('screen 1 while the menu moved', + [screen1_left(f) for f in between( + res, 'grab', 'off', edit_screen0)], + (0.25, 0.1, -1.5), ID) + errors += pose_errors('the menu reset', + [menu_quad(f) for f in between(res, 'back', 'off', + delay=1.0)], + (0.25, 0.1, -1.4), ID, size) + if saved(res).get('video_openxr_menu_placed') != 'false': + errors.append('the menu\'s reset left it placed') + return errors + + +def band_errors(res, f, q, level, what): + """The three bands across the part of the image q shows, from its + top edge in, dark, light, dark, at level: through an outline's rim, + or across the handle bar.""" + if q['rect'][3] == OUTLINE_DIM: + rows = (0, OUTLINE_BAND, 2 * OUTLINE_BAND) + else: + rows = (0, HANDLE_ROWS // 2, HANDLE_ROWS - 1) + errors = [] + for row, light in zip(rows, (False, True, False)): + rgb, alpha = rect_texel(res, f, q, q['rect'][2] // 2, row) + want = level if light else 0 + if abs(alpha - level) > 2 or abs(rgb[0] - want) > 2: + errors.append('%s row %d shows %s alpha %d, want %d alpha %d' + % (what, row, rgb, alpha, want, level)) + return errors + + +def outline_whole(o, q): + """Outline o's size uncut, along item q's own x and y.""" + w = o['size'][0] * OUTLINE_DIM / o['rect'][2] + ox = qrot(tuple(o['pose'][3:]), (1.0, 0.0, 0.0)) + qx = qrot(tuple(q['pose'][3:]), (1.0, 0.0, 0.0)) + if abs(sum(a * b for a, b in zip(ox, qx))) < 0.5: + return (o['size'][1], w) + return (w, o['size'][1]) + + +def outline_gap_errors(frame): + """Every outline's clear middle, where filtering leaves its rim, + OUTLINE_GAP wider each side than the item it is round: the rim never + covers the picture. An outline cut to hide an edge's rim counts at + its whole size, turned back for a top or bottom edge.""" + items = [q for q in quads(frame) if not q['flags'] & BLEND] \ + + menus(frame) + if not items: + return ['no items under the outlines'] + errors = [] + for o in outlines(frame): + q = min(items, key=lambda i: sum( + (a - b) ** 2 for a, b in zip(i['pose'][:3], o['pose'][:3]))) + gaps = [(s * (1.0 - 2.0 * OUTLINE_F) - w) / 2.0 + for s, w in zip(outline_whole(o, q), q['size'])] + if any(abs(g - OUTLINE_GAP) > 0.001 for g in gaps): + errors.append('the outline at %s is %s clear of its item, ' + 'want %.3f' % ( + [round(v, 3) for v in o['pose'][:3]], + [round(g, 4) for g in gaps], OUTLINE_GAP)) + return errors + + +EDIT_HANDLE = handle_y(0.0, MENU_H) +# Screen 2 flush against screen 1's right edge, the menu out of the way. +FLUSH = {'video_screen2_xr_x': '1.44', 'video_screen2_xr_y': '0.992', + 'video_openxr_menu_placed': 'true', 'video_openxr_menu_xr_x': '-2'} + + +def texel_at(res, fr, q, x, y): + """The texel quad q, facing +Z, shows at x, y; None off it.""" + o = tuple(q['pose'][3:]) + lx, ly, _ = qrot((-o[0], -o[1], -o[2], o[3]), + (x - q['pose'][0], y - q['pose'][1], 0.0)) + u, v = lx / q['size'][0] + 0.5, 0.5 - ly / q['size'][1] + if not (0.0 <= u < 1.0 and 0.0 <= v < 1.0): + return None + return rect_texel(res, fr, q, int(u * q['rect'][2]), + int(v * q['rect'][3])) + + +def seen_at(res, fr, x, y): + """What frame fr shows at x, y looking down -Z: its layers that face + +Z, each in its turn over those before, from black.""" + out = (0, 0, 0) + for q in quads(fr): + if qrot(tuple(q['pose'][3:]), (0.0, 0.0, 1.0))[2] < 0.999: + continue + t = texel_at(res, fr, q, x, y) + if t is None: + continue + rgb, alpha = t + out = (tuple(min(255, c + (255 - alpha) * b // 255) + for c, b in zip(rgb, out)) + if q['flags'] & BLEND else rgb) + return out + + +def check_edit_outlines_flush(res): + """Screen 2 flush against screen 1's right edge, screen 1 lit: its + outline goes under the screens, so screen 2's picture covers that + side of its rim; the other three sides still show.""" + errors = edit_log_errors(res, ['on', 'off']) + snaps = [f for f in between(res, 'lit', 'off', delay=0.5) + if f['snap'] and len(outlines(f, 1)) == 1 and screen2(f)] + if not snaps: + return errors + ['no snapshot of screen 1 outlined beside screen 2'] + f = snaps[-1] + o, s2 = outlines(f, 1)[0], screen2(f) + if not near3(o['pose'], (0.0, 0.0, -1.798)) \ + or not near3(s2['pose'], (1.44, 0.0, -1.8)): + return errors + ['screen 1\'s outline at %s, screen 2 at %s: not ' + 'flush' % (o['pose'][:3], s2['pose'][:3])] + tw, th = (s / OUTLINE_DIM for s in o['size']) + right = o['pose'][0] + o['size'][0] / 2.0 + covered = [] + for k in range(OUTLINE_EDGE): + for y in (-0.3, 0.0, 0.3): + x = right - (k + 0.5) * tw + want = texel_at(res, f, s2, x, y) + seen = seen_at(res, f, x, y) + if want is None or any(abs(a - b) > 2 + for a, b in zip(seen, want[0])): + covered.append((round(x, 3), y, seen)) + if covered: + errors.append('screen 1\'s rim over screen 2\'s picture: %s' + % covered[:4]) + light = OUTLINE_BAND + 0.5 + for what, x, y in ( + ('left', o['pose'][0] - o['size'][0] / 2.0 + light * tw, 0.0), + ('top', 0.0, o['pose'][1] + o['size'][1] / 2.0 - light * th), + ('bottom', 0.0, o['pose'][1] - o['size'][1] / 2.0 + light * th)): + seen = seen_at(res, f, x, y) + if any(c < 253 for c in seen): + errors.append('screen 1\'s rim shows %s on its %s side, want ' + 'its light band' % (seen, what)) + return errors + + +def check_edit_outlines(menu=True): + def check(res): + """Faint outlines round every item, a bright one round the item + under a laser, the menu's faint handle bar under it and bright + with the menu's outline under a laser; none after edit mode.""" + errors = edit_log_errors(res, ['on', 'off']) + lit = between(res, 'lit', 'handle', delay=0.5) + items = 3 if menu else 2 + good = [f for f in lit if len(outlines(f)) == items + and len(outlines(f, 1)) == 1 + and len(handles(f)) == int(menu)] + if not good: + return errors + ['no frame with %d outlines, one bright: %s' % ( + items, [[q['rect'][0] for q in outlines(f) + handles(f)] + for f in lit[-1:]])] + b = outlines(good[-1], 1)[0] + if not near3(b['pose'], (0.0, 0.0, -1.798)) \ + or not near3(b['size'], outline_size(1.6, 0.96), 0.002): + errors.append('the bright outline at %s, %s, want screen 1\'s' + % (b['pose'][:3], b['size'])) + errors += outline_gap_errors(good[-1]) + snaps = [f for f in good if f['snap']] + if snaps: + s = snaps[-1] + errors += band_errors(res, s, outlines(s, 1)[0], 255, + 'the bright outline') + errors += band_errors(res, s, outlines(s, 0)[0], 64, + 'a faint outline') + if menu and handles(s, 0): + errors += band_errors(res, s, handles(s, 0)[0], 64, + 'the faint handle') + else: + errors.append('no snapshot of the outlines') + if menu: + h = handles(good[-1], 0) + if len(h) != 1 or not near3(h[0]['pose'], + (0.0, EDIT_HANDLE, -1.698)) \ + or not near3(h[0]['size'], (MENU_W * 0.4, 0.03)): + errors.append('the faint handle: %s' + % [(q['pose'][:3], q['size']) for q in h]) + on = [f for f in between(res, 'handle', 'off', delay=0.5) + if len(handles(f, 1)) == 1 and len(outlines(f, 1)) == 1 + and near3(outlines(f, 1)[0]['pose'], (0.0, 0.0, -1.698))] + if not on: + errors.append('the laser on the handle lit no handle and ' + 'menu outline') + elif [f for f in on if f['snap']]: + s = [f for f in on if f['snap']][-1] + errors += band_errors(res, s, handles(s, 1)[0], 255, + 'the bright handle') + elif any(handles(f) for f in lit): + errors.append('a handle without a menu quad') + if any(outlines(f) or handles(f) + for f in between(res, 'off', delay=1.0)): + errors.append('outlines after edit mode ended') + return errors + return check + + +def check_edit_beam(res): + """Edit Mode Only: the dot alone before edit mode; in it a beam from + the hand to the dot facing the head, then 1.5 m into nothing; the dot + alone again after.""" + errors = edit_log_errors(res, ['on', 'off']) + before = between(res, 'before', 'on', delay=0.5) + if not [f for f in before if cursors(f)]: + errors.append('no dot before edit mode') + if any(beams(f) for f in before): + errors.append('a beam before edit mode') + hits = [f for f in between(res, 'aimed', 'miss', delay=0.5) + if len(beams(f)) == 1 and cursors(f)] + if not hits: + return errors + ['no frame with one beam and its dot'] + errors += beam_errors('the beam', beams(hits[-1])[0], GUN_FROM, GUN_AT, 0) + snaps = [f for f in hits if f['snap']] + if snaps: + errors += strip_errors(res, snaps[-1], beams(snaps[-1])[0], 0, + 'the beam') + else: + errors.append('no snapshot of the beam') + misses = [f for f in between(res, 'miss', 'off', delay=0.5) + if len(beams(f, 1)) == 1] + if not misses: + return errors + ['no frame with a beam into nothing'] + errors += beam_errors('the beam into nothing', beams(misses[-1])[0], + GUN_FROM, MISS_AT, 1) + snaps = [f for f in misses if f['snap']] + if snaps: + errors += strip_errors(res, snaps[-1], beams(snaps[-1])[0], 1, + 'the beam into nothing') + after = between(res, 'off', delay=1.0) + if any(beams(f) for f in after): + errors.append('a beam after edit mode') + return errors + + +def check_edit_beam_off(res): + """Laser Beam Off: edit mode's dot and outlines, never a beam.""" + errors = edit_log_errors(res, ['on', 'off']) + if not [f for f in between(res, 'aimed', 'off', delay=0.5) + if cursors(f) and outlines(f)]: + errors.append('no frame with the dot and the outlines') + if any(beams(f) for f in res.frames): + errors.append('a beam with Laser Beam Off') + return errors + + +def check_laser_beam_always(res): + """Laser Beam Always, out of edit mode: a beam to the light gun's + dot, one into nothing beside the screens, none while the laser's + hotkey hides it, and back after.""" + errors = [] + hits = [f for f in between(res, 'aimed', 'miss', delay=0.5) + if len(beams(f)) == 1 and cursors(f)] + if hits: + errors += beam_errors('the beam', beams(hits[-1])[0], GUN_FROM, + GUN_AT, 0) + else: + errors.append('no frame with the beam and its dot') + misses = [f for f in between(res, 'miss', 'off', delay=0.5) + if len(beams(f, 1)) == 1] + if misses: + errors += beam_errors('the beam into nothing', beams(misses[-1])[0], + GUN_FROM, MISS_AT, 1) + else: + errors.append('no frame with a beam into nothing') + if any(beams(f) or cursors(f) + for f in between(res, 'off', 'on', delay=0.5)): + errors.append('a beam or dot with the laser hidden') + if not [f for f in between(res, 'on', delay=0.5) if beams(f)]: + errors.append('no beam once the laser is back') + return errors + + +def check_edit_fallback(menu=True): + def check(res): + """A views core's frame shown whole (threaded video crops the map + away) is no item: grip, move and reset leave it, no dot or + outline shows on it, and screen 1's settings stay.""" + errors = edit_log_errors(res, ['on', 'off']) + if 'cropping to 655 rows' not in res.log: + errors.append('threaded video did not crop the frame') + late = between(res, 'on', 'off', delay=1.0) + errors += pose_errors('the fallback frame through grip, move and ' + 'reset', [screen2(f) for f in late], S0, ID, + (1.6, 0.96)) + dots = [f for f in late if cursors(f)] + if dots: + errors.append('a dot on the fallback frame in %d of %d frames' + % (len(dots), len(late))) + for f in late: + frame = screen2(f) + if frame and any(near3(q['pose'], frame['pose'][:2] + + [frame['pose'][2] + 0.002]) + for q in outlines(f)): + errors.append('frame %d: an outline round the fallback ' + 'frame' % f['n']) + break + n = len(outlines(f)) + len(handles(f)) + if n != (2 if menu else 0): + errors.append('frame %d: %d layers, want %s' % ( + f['n'], n, 'the menu\'s outline and handle alone' + if menu else 'none')) + break + cfg = saved(res) + got = [cfg.get('video_screen1_xr_' + k) + for k in ('x', 'y', 'z', 'scale')] + if [float(v or 'nan') for v in got] != [0.5, 0.0, 0.0, 1.0]: + errors.append('screen 1 saved as x y z scale %s, want 0.5 0 0 1' + % got) + return errors + return check + + +def check_edit_no_map(res): + """A core without a view map is screen 1: its whole frame is grabbed, + stays where it was put, and is saved there.""" + errors = edit_log_errors(res, ['on', 'off']) + before = last_before(res, 'grab', frame_quad) + late = between(res, 'moved', None, frame_quad) + if not before or not late: + return errors + no_quads(res) + want = ((0.3, 0.0, -1.8), ID) + errors += pose_errors('the frame before the grab', [before], S0, ID) + errors += pose_errors('the frame moved', [frame_quad(f) for f in late], + want[0], ID, before['size']) + return errors + saved_errors(res, 'the frame', 'video_screen1_xr_', S0, + want, before['size'], + frame_quad(late[-1])) + + +# Follow Head: the hand's aims in VIEW, through a head turned 30 +# degrees, as LOCAL points. +V_HEAD = 'head 0 0 0 30' +V_ANCHOR = ((0.0, 0.0, 0.0), from_euler(30.0, 0.0, 0.0)) +A0 = ((0.2, -0.4, -0.3), (0.0, 0.0, -1.8)) +A1 = ((0.5, -0.3, -0.3), (0.3, 0.1, -1.8)) +A2 = ((0.9, -0.3, -0.3), (0.7, 0.1, -1.8)) +MOVED = ((0.3, 0.1, -1.8), ID) + + +def turned(anchor, a, shift=(0.0, 0.0, 0.0)): + """Aim a, given in anchor's frame, in LOCAL and moved by shift.""" + return tuple(tuple(x + s for x, s in zip(compose(anchor, (p, ID))[0], + shift)) for p in a) + + +def check_edit_follow_head(res): + """Follow Head: screen 1 grabbed and moved in VIEW space keeps that + spot in view as the head turns, and is saved there.""" + errors = edit_log_errors(res, ['on'], exact=False) + shown = [screen1_left(f) for f in + between(res, 'moved', 'turned', edit_screen0, 1.5)] + late = [screen1_left(f) for f in + between(res, 'turned', None, edit_screen0, 1.0)] + if any(q['space'] != 'view' for q in shown + late): + errors.append('screen 1 out of view space') + errors += pose_errors('screen 1 moved in VIEW', shown, MOVED[0], ID) + errors += pose_errors('screen 1 with the head turned', late, MOVED[0], + ID) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + MOVED, (1.6, 0.96), + shown[-1] if shown else None) + + +def yaw_of(q): + return euler(q['pose'][3:])[0] + + +def check_edit_lazy(res): + """Lazy Follow holds its glide while screen 1 is held, so screen 2, + which nothing holds, stays; once it is let go they glide.""" + errors = edit_log_errors(res, ['on'], exact=False) + held_frames = between(res, 'turned', 'released', screen2, 0.3) + if not held_frames: + return errors + ['no frames while held'] + errors += pose_errors('screen 1 held still', + [screen1_left(f) for f in held_frames], S0, ID) + yaws = [yaw_of(screen2(f)) for f in held_frames] + if any(abs(y) > 0.5 for y in yaws): + errors.append('the screens glided to %.1f degrees while held' + % max(yaws, key=abs)) + late = between(res, 'released', None, screen2, 2.0) + if not late or abs(yaw_of(screen2(late[-1]))) < 45.0: + errors.append('no glide after the release: %s' % ( + late and round(yaw_of(screen2(late[-1])), 1))) + return errors + + +L0 = ((-0.2, -0.4, -0.3), (0.0, 0.0, -1.8)) +L1 = ((0.0, -0.4, -0.3), (0.2, 0.0, -1.8)) +R1 = ((0.6, -0.4, -0.3), (0.4, 0.0, -1.8)) +B0 = ((0.2, -0.6, -0.3), (0.0, -0.992, -1.8)) +B1 = ((0.5, -0.6, -0.3), (0.3, -0.992, -1.8)) +LEFT_AT = (0.2, 0.0, -1.8) +BOTTOM_AT = (0.3, -0.992, -1.8) + + +def check_edit_two_hands(res): + """One item a hand: the right on the left's screen 1 does nothing, + then takes screen 2 while the left still holds screen 1.""" + errors = edit_log_errors(res, ['on'], exact=False) + errors += pose_errors('screen 1 held by the left hand alone', + [screen1_left(f) for f in + between(res, 'right_moved', 'bottom', + edit_screen0, 1.5)], LEFT_AT, ID) + errors += pose_errors('screen 2 before the right hand took it', + [screen2(f) for f in + between(res, 'right_moved', 'bottom', + delay=1.5)], S1, ID) + errors += pose_errors('screen 2 moved by the right hand', + [screen2(f) for f in + between(res, 'bottom_moved', delay=1.5)], + BOTTOM_AT, ID, (1.28, 0.96)) + errors += pose_errors('screen 1 while the right moved screen 2', + [screen1_left(f) for f in + between(res, 'bottom_moved', None, edit_screen0, + 1.5)], LEFT_AT, ID) + errors += saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (LEFT_AT, ID), (1.6, 0.96)) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (BOTTOM_AT, ID), (1.28, 0.96)) + + +def check_edit_recenter(res): + """A recenter during a grab lets go first, then recenters; edit mode + stays on, and the hand moving on leaves screen 1.""" + errors = edit_log_errors(res, ['on'], exact=False) + asked = res.log.find('Headset recenter requested') + if asked < 0: + errors.append('the recenter did not reach the hotkey') + elif EDIT_OFF in res.log[:asked]: + errors.append('edit mode stopped before the recenter') + syncs = [(e['t_us'], e['sets']) for e in events(res, 'sync')] + t = res.marks['recenter'] * 1e6 + then = [s for u, s in syncs if u <= t] + if not then or then[-1] != EDIT_SETS \ + or any(s != EDIT_SETS for u, s in syncs if u > t): + errors.append('edit mode\'s set not synced through the recenter') + want = compose(V_ANCHOR, MOVED) + errors += pose_errors('screen 1 recentered, let go', + [screen1_left(f) for f in + between(res, 'recenter', 'pulled', edit_screen0, + 1.5)], want[0], want[1]) + errors += pose_errors('screen 1 after the hand moved on', + [screen1_left(f) for f in + between(res, 'pulled', None, edit_screen0, + 1.5)], want[0], want[1]) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + MOVED, (1.6, 0.96)) + + +RC0 = turned(V_ANCHOR, A0) +RC1 = turned(V_ANCHOR, A0, (0.3, 0.0, 0.0)) + + +def check_edit_recentered(res): + """Grabbed from a turned anchor, screen 1 moves along the room's X, + and its settings are saved in the anchor's frame.""" + errors = edit_log_errors(res, ['on'], exact=False) + spot = compose(V_ANCHOR, (S0, ID)) + errors += pose_errors('screen 1 at the turned anchor', + [screen1_left(f) for f in + between(res, 'on', 'grab', edit_screen0, 1.0)], + spot[0], spot[1]) + want = (tuple(a + b for a, b in zip(spot[0], (0.3, 0.0, 0.0))), + spot[1]) + shown = [screen1_left(f) for f in + between(res, 'moved', None, edit_screen0, 1.5)] + errors += pose_errors('screen 1 moved along the room\'s X', shown, + want[0], want[1], (1.6, 0.96)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + want, (1.6, 0.96), + shown[-1] if shown else None, V_ANCHOR) + + +def check_edit_pause(res): + """While Editing Screens: Pause stops the core while edit mode runs, + started by the command and then by the headset's action, and lets + it run again after each.""" + errors = edit_log_errors(res, ['on', 'off', 'on', 'off']) + for on, off, end in (('on', 'off', 'on2'), ('on2', 'off2', None)): + t_on, t_off = res.marks[on] * 1e6, res.marks[off] * 1e6 + t_end = res.marks[end] * 1e6 if end else float('inf') + if not core_frames(res, t_on - 3e6, t_on): + errors.append('the core did not run before %s' % on) + mid = core_frames(res, t_on + 1e6, t_off) + if mid: + errors.append('the core ran while edit mode paused it (%s): %s' + % (on, mid)) + if not core_frames(res, t_off + 1e6, t_end): + errors.append('the core did not run again after %s' % off) + return errors + + +AIM_SCREEN0_RIGHT = 'aim right 0.2 -0.4 -0.3 0.4 0.24 -1.8' +AIM_SCREEN0_LEFT = 'aim left -0.2 -0.4 -0.3 -0.4 0.24 -1.8' +DOT_RIGHT, DOT_LEFT = (0.4, 0.24, -1.8), (-0.4, 0.24, -1.8) + + +def check_edit_command(menu=True): + def check(res): + """The command starts and stops edit mode: only its set synced + between, a dot from each hand on screen 1, which Auto leaves + alone, only while editing, the menu's quad kept up (gl has none) + and the core kept running.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += edit_sync_errors(res, 'on', 'off') + t_on, t_off = res.marks['on'] * 1e6, res.marks['off'] * 1e6 + pre = [f for f in res.frames + if res.marks['aim'] * 1e6 + 3e5 < f['t_us'] < t_on] + mid = [f for f in res.frames if t_on + 5e5 < f['t_us'] < t_off] + post = [f for f in res.frames if f['t_us'] > t_off + 5e5] + if not pre or not mid or not post: + return errors + ['no headset frames around the marks'] + if any(cursors(f) for f in pre + post): + errors.append('a dot showed on screen 1 outside edit mode') + for want in (DOT_RIGHT, DOT_LEFT): + if not any(at(c, want) for f in mid for c in cursors(f)): + errors.append('no dot at %s while editing' % (want,)) + if any(menus(f) for f in res.frames + if f['t_us'] < t_on or f['t_us'] > t_off + 1e6): + errors.append('a menu quad outside edit mode') + up = [f for f in mid if menus(f)] + if menu and len(up) != len(mid): + errors.append('the menu quad was up in %d of %d frames while ' + 'editing' % (len(up), len(mid))) + elif not menu and up: + errors.append('gl showed a menu quad while editing') + if len(core_frames(res, t_on + 1e6, t_off)) < 2: + errors.append('the core stopped while editing kept it running') + return errors + return check + + +def check_edit_action(game): + def check(res): + """The headset's Edit Screens action starts edit mode from the + game's set and stops it from edit mode's own.""" + return (edit_log_errors(res, ['on', 'off']) + + edit_sync_errors(res, 'on', 'off', game)) + return check + + +def check_edit_hotkeys(res): + """With no command interface the hotkeys are read by their bits: the + headset's Laser Pointer, Recenter and Edit Screens actions each set + their own.""" + errors = check_laser_toggle('Auto', DOT_BOTTOM)(res) + if res.log.count('Headset recenter requested') != 1: + errors.append('Recenter reached the hotkey %d times, want once' + % res.log.count('Headset recenter requested')) + return (errors + edit_log_errors(res, ['on', 'off']) + + edit_sync_errors(res, 'edit_on', 'edit_off')) + + +def check_edit_menu_open(res): + """Edit mode syncs only its own set with the pointer's, every profile + takes its bindings, and opening the menu ends it and syncs the game's + set again.""" + errors = bindings_errors(res) + edit_log_errors(res, ['on', 'off']) + if 'Edit Screens bindings refused' in res.log: + errors.append('a profile refused edit mode\'s bindings') + return errors + edit_sync_errors(res, 'on', 'menu') + + +def check_edit_menu_ends(res): + """Edit mode ends with what opened the menu or restarted video, and + the game's set comes back.""" + return edit_log_errors(res, ['on', 'off']) + edit_sync_errors( + res, 'on', 'menu') + + +def check_edit_no_content(res): + """Without content the menu cannot close, so edit mode does not + start, and it is not called unavailable.""" + errors = edit_log_errors(res, []) + syncs = [e['sets'] for e in events(res, 'sync')] + if EDIT_SETS in syncs: + errors.append('the edit set was synced without content') + if GAME_SETS not in syncs: + errors.append('no session read the controllers') + if 'Edit Screens unavailable' in res.log: + errors.append('told unavailable, not that content is needed') + return errors + + +# Screen 2 straight behind screen 1, hidden by it from the hand. +BURIED = {'video_screen2_xr_y': '0.992', 'video_screen2_xr_z': '-0.3'} +BURIED_MOVED = (0.6, 0.0, -2.1) +# Screen 2 pulled 30 cm nearer, its top edge 7 mm over the middle of +# the menu's handle bar, in front of it. +FRONT2 = (0.0, EDIT_HANDLE + 0.007 - 0.48, -1.5) + + +def edit_tap(action, aim): + """action pressed once, the hand kept at aim.""" + return [('script', script(aim, action)), ('wait', 0.5), + ('script', aim), ('wait', 0.5)] + + +def on_handle(dx=0.0, dy=0.0): + """The right hand aiming straight ahead at the middle of the + menu's bar, moved dx right and dy up.""" + return aim_line((dx, EDIT_HANDLE + dy, 0.0), + (dx, EDIT_HANDLE + dy, -1.8)) + + +def check_edit_choose(menu=True): + def check(res): + """Select Next twice chooses screen 2, behind screen 1: its + outline alone is bright while the laser is on screen 1; the grip + takes it from there, it moves with the hand and is saved where + the headset last showed it; screen 1 stays.""" + errors = edit_log_errors(res, ['on', 'off']) + lit = between(res, 'chosen', 'grab', delay=0.5) + items = 3 if menu else 2 + good = [f for f in lit if len(outlines(f)) == items + and len(outlines(f, 1)) == 1 + and near3(outlines(f, 1)[0]['pose'], (0.0, 0.0, -2.098))] + if not good: + errors.append('screen 2 not lit alone while chosen: %s' % [ + [(q['rect'][0], [round(v, 3) for v in q['pose'][:3]]) + for q in outlines(f)] for f in lit[-1:]]) + errors += pose_errors('screen 2 taken from behind screen 1', + [screen2(f) for f in + between(res, 'moved', 'released')], + BURIED_MOVED, ID, (1.28, 0.96)) + errors += pose_errors('screen 1 while screen 2 moved', + [screen1_left(f) for f in + between(res, 'grab', 'off', edit_screen0)], + S0, ID, (1.6, 0.96)) + errors += saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (BURIED_MOVED, ID), (1.28, 0.96), + last_before(res, 'off', screen2)) + return errors + return check + + +def check_edit_handle_first(res): + """A grip aimed at the menu's bar, through screen 2 which is + nearer, takes the menu: it moves with the hand and is saved placed; + screen 2 stays.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('the menu taken by its handle', + [menu_quad(f) for f in + between(res, 'moved', 'released', delay=0.5)], + (0.3, 0.0, -1.7), ID, (MENU_W, MENU_H)) + errors += pose_errors('screen 2 in front of the bar', + [screen2(f) for f in + between(res, 'on', 'off', delay=0.5)], + FRONT2, ID, (1.28, 0.96)) + cfg = saved(res) + if cfg.get('video_openxr_menu_placed') != 'true' \ + or abs(float(cfg.get('video_openxr_menu_xr_x', '0')) - 0.3) \ + > EDIT_POS: + errors.append('the menu saved placed %s at x %s, want true at 0.3' + % (cfg.get('video_openxr_menu_placed'), + cfg.get('video_openxr_menu_xr_x'))) + return errors + + +# Screen 2 20 cm nearer than the menu, under its handle bar. +UNDER_BAR = dict(SAVE, video_screen2_xr_y='%g' % (FRONT2[1] - S1[1]), + video_screen2_xr_z='0.3') + + +def sticky_steps(first): + """From first, the laser 3 cm under the bar's middle, in its margin + over screen 2; the grip there carries what it takes 30 cm right.""" + return edit_start(first) + [ + ('script', on_handle(0.0, -0.03)), ('wait', 1), ('mark', 'grab'), + ('script', script(on_handle(0.0, -0.03), EDIT_GRIP)), ('wait', 1), + ('script', script(on_handle(0.3, -0.03), EDIT_GRIP)), ('wait', 1), + ('mark', 'moved'), ('wait', 1), ('script', on_handle(0.3, -0.03)), + ('mark', 'released'), ('wait', 1), ('send', EDIT), ('wait', 2), + ('mark', 'off'), ('wait', 1)] + + +def check_edit_handle_sticky(stuck): + def check(res): + """The laser in the bar's margin over screen 2: come from the + bar (stuck), the menu is the one lit and the grip takes it; + come from screen 2, screen 2 is lit and taken and the menu + stays.""" + errors = edit_log_errors(res, ['on', 'off']) + want = outline_size(MENU_W, MENU_H) if stuck \ + else outline_size(1.28, 0.96) + frames = between(res, 'on', 'grab', delay=2.5) + if not frames: + errors.append('no frame with the laser in the margin') + for f in frames: + got = [q['size'] for q in outlines(f, 1)] + if len(got) != 1 or any(abs(a - b) > 0.002 + for a, b in zip(got[0], want)): + errors.append('in the margin %s lit, want only %s' + % (got, want)) + break + if stuck: + errors += pose_errors('the menu taken by its stuck bar', + [menu_quad(f) for f in + between(res, 'moved', 'released', + delay=0.5)], + (0.3, 0.0, -1.7), ID, (MENU_W, MENU_H)) + errors += pose_errors('screen 2 under the margin', + [screen2(f) for f in + between(res, 'on', 'off', delay=0.5)], + FRONT2, ID, (1.28, 0.96)) + else: + errors += pose_errors('screen 2 taken through the margin', + [screen2(f) for f in + between(res, 'moved', 'released', + delay=0.5)], + (0.3, FRONT2[1], FRONT2[2]), ID, + (1.28, 0.96)) + errors += pose_errors('the menu left in place', + [menu_quad(f) for f in + between(res, 'on', 'released', delay=0.5)], + MENU_SPOT, ID, (MENU_W, MENU_H)) + return errors + return check + + +# Screen 2 grabbed at its spot and held 0.7-2 cm off standing flush +# right of screen 1, level and in its plane. The buzz is +# INPUT_OPENXR_SNAP_TICK_AMP for INPUT_OPENXR_SNAP_TICK_NS; a hinge's, +# INPUT_OPENXR_HINGE_BUZZ_AMP for INPUT_OPENXR_HINGE_BUZZ_NS. +SNAP_GRAB = (0.0, -0.992, 0.0) +SNAP_HAND = (1.447, 0.02, 0.02) +SNAPPED = (1.44, 0.0, -1.8) +SNAP_TICK = (0.5, 20000000) +HINGE_BUZZ = (0.8, 150000000) + + +def buzzes(res, kind, start=None, end=None): + """The right hand's buzzes of kind (SNAP_TICK or HINGE_BUZZ), after + mark start and before mark end.""" + lo = res.marks[start] * 1e6 if start else float('-inf') + hi = res.marks[end] * 1e6 if end else float('inf') + return [e for e in haptics(res, 'right') if e['ev'] == 'haptic' + and lo < e['t_us'] < hi + and abs(e['amplitude'] - kind[0]) < 1e-3 + and e['duration_ns'] == kind[1]] + + +def straight(p): + """The right hand at p aiming straight ahead.""" + return aim_line(p, (p[0], p[1], p[2] - 1.8)) + + +def span(a0, a1, b0, b1): + """The middle and length of where [a0, a1] and [b0, b1] overlap.""" + lo, hi = max(a0, b0), min(a1, b1) + return (lo + hi) / 2.0, hi - lo + + +def hinge_line_errors(frames, want, axis): + """frames each show one hinge line: the green strip a texel in from + each end, HINGE_WIDTH wide, along world axis axis, each end + HINGE_LIFT from where want(frame) puts the line (its middle and + length, on the edges midway across the gap) toward the head the + ribbon faces. The head, both ends and the ribbon's normal share a + plane, so neither end moves out of it.""" + if not frames: + return ['no frames to check for a hinge line'] + for f in frames: + ls = hinge_lines(f) + if len(ls) != 1: + return ['frame %d: %d hinge lines, want 1' % (f['n'], len(ls))] + line = ls[0] + mid, length = want(f) + turn = tuple(line['pose'][3:]) + along = qrot(turn, (1.0, 0.0, 0.0)) + side = qrot(turn, (0.0, 1.0, 0.0)) + facing = qrot(turn, (0.0, 0.0, 1.0)) + way = [0.0, 0.0, 0.0] + way[axis] = 1.0 if along[axis] > 0.0 else -1.0 + lifts = [] + for s in (-1.0, 1.0): + shown = [c + s * line['size'][0] / 2.0 * a + for c, a in zip(line['pose'][:3], along)] + seam = [m + s * length / 2.0 * w for m, w in zip(mid, way)] + lifts.append([a - b for a, b in zip(shown, seam)]) + if (tuple(line['rect']) != (HINGE_X + 1, HINGE_Y, HINGE_COLS - 2, + HINGE_ROWS) + or abs(line['size'][1] - HINGE_WIDTH) > 1e-4 + or abs(abs(along[axis]) - 1.0) > 1e-3 + or any(abs(math.sqrt(sum(v * v for v in d)) - HINGE_LIFT) + > HINGE_GAP_POS + or sum(v * w for v, w in zip(d, facing)) + < 0.8 * HINGE_LIFT + or abs(sum(v * w for v, w in zip(d, side))) + > HINGE_GAP_POS for d in lifts)): + return ['frame %d: the hinge line shows %s at %s, %s, along %s, ' + 'its ends moved %s; want %s, %.3f long, %.3f wide, ' + 'each end %.3f toward the head' % ( + f['n'], line['rect'], + [round(v, 4) for v in line['pose'][:3]], + line['size'], [round(v, 3) for v in along], + [[round(v, 4) for v in d] for d in lifts], + [round(v, 4) for v in mid], length, HINGE_WIDTH, + HINGE_LIFT)] + return [] + + +def cut_outline_errors(frame, want): + """frame's outlines all whole but those want lists, (the middle of + the item it is round, its cut in CUTS): each the cut's whole cell, + faint or bright; every outline its item's whole outline in pose and + size.""" + cut = [o for o in outlines(frame) if outline_cut(o)] + if len(cut) != len(want): + return ['frame %d: %d outlines cut, want %d' + % (frame['n'], len(cut), len(want))] + errors = outline_pose_errors(frame) + for mid, name in want: + o = min(cut, key=lambda c: sum((a - b) ** 2 for a, b in + zip(c['pose'][:3], mid))) + cells = [CUT_X[k * len(CUTS) + CUTS.index(name)] for k in (0, 1)] + if o['rect'][0] not in cells or tuple(o['rect'][1:]) != ( + 0, OUTLINE_DIM, OUTLINE_DIM): + errors.append('frame %d: the outline round %s shows %s; want ' + 'the %s cut\'s cell at %s' % ( + frame['n'], [round(v, 3) for v in mid], + o['rect'], name, cells)) + return errors + + +def crop_outline_errors(frame, want): + """Below the cut outlines' width: frame's outlines all whole but + those want lists, (the middle of the item it is round, the texels + its square starts in, turned a quarter for a top or bottom edge).""" + cut = [o for o in outlines(frame) if outline_cut(o)] + if len(cut) != len(want): + return ['frame %d: %d outlines cut, want %d' + % (frame['n'], len(cut), len(want))] + errors = [] + for mid, off, turned in want: + o = min(cut, key=lambda c: sum((a - b) ** 2 for a, b in + zip(c['pose'][:3], mid))) + base = OUTLINE_X[1] if o['rect'][0] >= OUTLINE_X[1] else OUTLINE_X[0] + x = qrot(tuple(o['pose'][3:]), (1.0, 0.0, 0.0)) + if (o['rect'][0] - base != off + or o['rect'][2] != OUTLINE_DIM - OUTLINE_CROP + or (abs(x[0]) < 1e-3) != turned): + errors.append('frame %d: the outline round %s shows %s, x %s; ' + 'want %d in, turned %s' % ( + frame['n'], [round(v, 3) for v in mid], + o['rect'], [round(v, 3) for v in x], off, + turned)) + return errors + + +def hinge_texel_errors(res, f): + """The hinge line's strip in f's image: green, red equal to blue, + premultiplied at HINGE_LEVEL.""" + line = hinge_lines(f)[0] + rgb, alpha = rect_texel(res, f, line, line['rect'][2] // 2, + HINGE_ROWS // 2) + rb = (HINGE_RB * HINGE_LEVEL + 127) // 255 + g = (HINGE_G * HINGE_LEVEL + 127) // 255 + if (abs(alpha - HINGE_LEVEL) > 2 or abs(rgb[0] - rb) > 2 + or abs(rgb[1] - g) > 2 or rgb[0] != rgb[2]): + return ['the hinge line shows %s alpha %d, want (%d, %d, %d) ' + 'alpha %d' % (rgb, alpha, rb, g, rb, HINGE_LEVEL)] + return [] + + +def check_edit_snap(snap): + def check(res): + """Screen 2, taken at its spot 3.2 cm under screen 1, stays + there until carried; held just off flush right of screen 1: with + Snap to Screens it shows and is saved touching it, level, in + its plane, the hinge marked by a green line and its rims gone + while held and once let go there, and the hand gets the snap's + buzz, the hinge's long buzz once; without, it stays where the + hand put it, with no buzz.""" + want = SNAPPED if snap else (SNAP_HAND[0], SNAP_HAND[1], + SNAP_HAND[2] - 1.8) + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 2 just taken', + [screen2(f) for f in + between(res, 'grab', 'carry', delay=0.5)], + S1, ID, (1.28, 0.96)) + errors += pose_errors('screen 2 held', + [screen2(f) for f in + between(res, 'held', 'released', delay=0.5)], + want, ID, (1.28, 0.96)) + errors += pose_errors('screen 2 let go', + [screen2(f) for f in + between(res, 'released', 'off', delay=0.5)], + want, ID, (1.28, 0.96)) + errors += saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (want, ID), (1.28, 0.96), + last_before(res, 'off', screen2)) + ticks = buzzes(res, SNAP_TICK, 'grab') + buzzes(res, HINGE_BUZZ, + 'grab') + if snap and not ticks: + errors.append('no snap buzz on the right hand: %s' + % haptics(res, 'right')) + if not snap and ticks: + errors.append('a snap buzz with snapping off: %s' % ticks) + hinged = buzzes(res, HINGE_BUZZ, 'grab') + if snap and len(hinged) != 1: + errors.append('%d hinge buzzes on the right hand, want 1 as it ' + 'hinged flush: %s' % (len(hinged), hinged)) + if snap: + # At Hinge Gap 0 its spot, 3.2 cm under screen 1 and + # centred, is a free gap the hinge holds exactly: shown from + # edit mode's start. + def spot(f): + return (0.0, -0.496, -1.8), 1.28 + errors += hinge_line_errors( + [f for f in between(res, 'on', 'grab', delay=0.5) + if screen2(f)], spot, 0) + # The hand is 0.7 cm off, inside EDIT_POS. + xs = [(what, q['pose'][0]) + for a, b, what in (('held', 'released', 'held'), + ('released', 'off', 'let go')) + for q in (screen2(f) for f in between(res, a, b, + delay=0.5)) + if q] + xs.append(('saved', S1[0] + float( + saved(res).get('video_screen2_xr_x', '0')))) + for what, x in xs: + if abs(x - SNAPPED[0]) > HINGE_GAP_POS: + errors.append('screen 2 %s at x %.4f, want %.3f' + % (what, x, SNAPPED[0])) + break + # Flush right of screen 1 it hinges on that edge: the line + # up the gap's middle where the edges overlap, both rims gone. + held = [f for f in between(res, 'held', 'released', delay=0.5) + if screen2(f)] + + def flush(f): + s = screen2(f)['pose'] + y, length = span(s[1] - 0.48, s[1] + 0.48, -0.48, 0.48) + gap = s[0] - 0.64 - 0.8 + return (0.8 + gap / 2.0, y, -1.8), length + errors += hinge_line_errors(held, flush, 1) + for f in held[-1:]: + errors += cut_outline_errors(f, [ + (S0, 'right'), + (tuple(screen2(f)['pose'][:3]), 'left')]) + # Let go flush, it rests hinged: the line and the cut rims + # stay. + rest = [f for f in between(res, 'released', 'resting', + delay=0.5) if screen2(f)] + errors += hinge_line_errors(rest, flush, 1) + for f in rest[-1:]: + errors += cut_outline_errors(f, [ + (S0, 'right'), + (tuple(screen2(f)['pose'][:3]), 'left')]) + if not rest: + errors.append('no frame of screen 2 let go') + else: + for f in between(res, 'released', 'off', delay=0.5): + if hinge_lines(f) or any(outline_cut(o) + for o in outlines(f)): + errors.append('frame %d: a hinge line or cut outline ' + 'with Snap to Screens off' % f['n']) + break + return errors + return check + + +def check_edit_snap_edge(res): + """Screen 2 brought through the fade of its pull to screen 1's + edge and rested at the end of its whole pull, swaying across it: one + buzz as it takes hold and none while it sways; 7 cm off and back, + one more; the hinge's long buzz once, on the way, the hinge holding + from 7 cm off.""" + errors = edit_log_errors(res, ['on', 'off']) + for a, b, what in (('edge', 'away', 'resting at the edge'), + ('away', 'back', 'back from 7 cm off')): + ticks = [e for e in haptics(res, 'right') if e['ev'] == 'haptic' + and res.marks[a] * 1e6 < e['t_us'] < res.marks[b] * 1e6 + and abs(e['amplitude'] - SNAP_TICK[0]) < 1e-3 + and e['duration_ns'] == SNAP_TICK[1]] + if len(ticks) != 1: + errors.append('%d snap buzzes %s, want 1: %s' + % (len(ticks), what, ticks)) + hinged = buzzes(res, HINGE_BUZZ, 'grab', 'edge') + later = buzzes(res, HINGE_BUZZ, 'edge', 'back') + if len(hinged) != 1 or later: + errors.append('%d hinge buzzes on the way to the edge and %d from ' + 'there, want 1 and 0' % (len(hinged), len(later))) + return errors + + +def edit_start(first, wait=2): + """Settle with the hand at first, then start edit mode.""" + return [('wait', 6), ('script', first), ('wait', 1), ('mark', 'on'), + ('send', EDIT), ('wait', wait)] + + +def edit_press(action): + return [('script', action), ('wait', 1), ('script', ''), ('wait', 2)] + + +def sweep(steps, dx, base=edit_aim): + """The grip held while the hand moves dx more each 50 ms.""" + return [step for i in range(1, steps + 1) for step in ( + ('script', script(base(dx * i), EDIT_GRIP)), ('wait', 0.05))] + + +def edit_aim_near(d): + """The right hand d right of and d nearer than edit_aim()'s, aiming + straight ahead: what it holds comes nearer, its image larger.""" + return aim_line((d, 0.0, d), (d, 0.0, d - 1.8)) + + +# Screen 2 taken at its spot and carried round to screen 1's right. +EDIT_SNAP_STEPS = edit_start(straight(SNAP_GRAB), 1) + [ + ('mark', 'grab'), ('script', script(straight(SNAP_GRAB), EDIT_GRIP)), + ('wait', 1.5), ('mark', 'carry')] + [ + step for p in ((0.5, -0.6, 0.0), (1.0, -0.2, 0.0), SNAP_HAND) + for step in (('script', script(straight(p), EDIT_GRIP)), ('wait', 0.3)) +] + [('mark', 'held'), ('wait', 1.5), ('script', straight(SNAP_HAND)), + ('mark', 'released'), ('wait', 1.5), ('mark', 'resting'), + ('send', EDIT), ('wait', 2), + ('mark', 'off'), ('wait', 1)] +# Screen 2 carried 7 cm off flush right of screen 1, then 1.5 cm off, +# in the edge's fade, then 0.9 cm off, inside its whole pull, swaying +# 2 mm across its end four times; then 7 cm off again and back. +EDGE_IN, EDGE_OUT, EDGE_AWAY = (1.449, 0.0, 0.0), (1.451, 0.0, 0.0), \ + (1.51, 0.0, 0.0) +EDGE_NEAR = (1.455, 0.0, 0.0) +EDIT_SNAP_EDGE_STEPS = edit_start(straight(SNAP_GRAB), 1) + [ + ('mark', 'grab'), ('script', script(straight(SNAP_GRAB), EDIT_GRIP)), + ('wait', 1.5)] + [ + step for p in ((0.5, -0.6, 0.0), (1.0, -0.2, 0.0), EDGE_AWAY) + for step in (('script', script(straight(p), EDIT_GRIP)), ('wait', 0.3)) +] + [('wait', 0.5), ('mark', 'edge')] + [ + step for p in (EDGE_NEAR,) + (EDGE_IN, EDGE_OUT) * 4 + for step in (('script', script(straight(p), EDIT_GRIP)), ('wait', 0.25)) +] + [('script', script(straight(EDGE_IN), EDIT_GRIP)), ('wait', 0.5), + ('mark', 'away'), ('script', script(straight(EDGE_AWAY), EDIT_GRIP)), + ('wait', 0.5), ('script', script(straight(EDGE_IN), EDIT_GRIP)), + ('wait', 0.5), ('mark', 'back'), ('script', straight(EDGE_IN)), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), ('wait', 1)] + + +def check_edit_turn_stick(res): + """The left stick held right for half a second turns screen 1, + which the right hand holds, anticlockwise seen from above about its + own middle, about 45 degrees at 90 a second; saved as shown.""" + errors = edit_log_errors(res, ['on', 'off']) + q = last_before(res, 'turned', screen1_left) + if not q: + return errors + ['no frame of screen 1 before the stick let go'] + yaw, pitch, roll = euler(tuple(q['pose'][3:])) + if not 20.0 <= yaw <= 70.0 or abs(pitch) > 0.6 or abs(roll) > 0.6: + errors.append('screen 1 turned to yaw %.1f pitch %.1f roll %.1f, ' + 'want 20 to 70, 0, 0' % (yaw, pitch, roll)) + if any(abs(a - b) > EDIT_POS for a, b in zip(q['pose'][:3], S0)): + errors.append('screen 1 moved to %s as it turned, want %s' + % (q['pose'][:3], S0)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (tuple(q['pose'][:3]), + tuple(q['pose'][3:])), (1.6, 0.96), + last_before(res, 'off', screen1_left)) + + +def check_edit_tilt_stick(res): + """The left stick held up for half a second tips screen 1, which the + right hand holds, back about its own left-right line through its + middle, about 45 degrees at 90 a second: its top edge goes away from + the user and its bottom edge comes nearer; saved as shown.""" + errors = edit_log_errors(res, ['on', 'off']) + q = last_before(res, 'tilted', screen1_left) + if not q: + return errors + ['no frame of screen 1 before the stick let go'] + rot = tuple(q['pose'][3:]) + yaw, pitch, roll = euler(rot) + if not -70.0 <= pitch <= -20.0 or abs(yaw) > 0.6 or abs(roll) > 0.6: + errors.append('screen 1 tilted to yaw %.1f pitch %.1f roll %.1f, ' + 'want 0, -20 to -70, 0' % (yaw, pitch, roll)) + if any(abs(a - b) > EDIT_POS for a, b in zip(q['pose'][:3], S0)): + errors.append('screen 1 moved to %s as it tilted, want %s' + % (q['pose'][:3], S0)) + edge = math.sqrt(sum(v * v for v in S0) + 0.48 ** 2) + top, bottom = (math.sqrt(sum((p + e) ** 2 for p, e in + zip(q['pose'][:3], qrot(rot, (0.0, h, 0.0))))) + for h in (0.48, -0.48)) + if not top > edge + 0.1 or not bottom < edge - 0.1: + errors.append('screen 1 top edge %.3f m and bottom edge %.3f m from ' + 'the user, want the top past %.3f and the bottom ' + 'inside it' % (top, bottom, edge)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (tuple(q['pose'][:3]), + tuple(q['pose'][3:])), (1.6, 0.96), + last_before(res, 'off', screen1_left)) + + +def check_edit_step_stick(res, step=15.0): + """The left stick, held right at an eighth of full speed (11.25 + degrees a second) until a frame shows the step, turns screen 1, + which the right hand holds and which faces the user, the head held + still at the anchor, into the 15 degree step's hold, so it shows and + is saved at exactly 15, and the right hand buzzes once, as it takes + hold of the step. Taken at the 0 step, it does not buzz there. The + step the stick ends in is step. Not a set time: stalled polls cut + the stick's turn short.""" + errors = edit_log_errors(res, ['on', 'off']) + q = last_before(res, 'turned', screen1_left) + if not q: + return errors + ['no frame of screen 1 before the stick let go'] + yaw, pitch, roll = euler(tuple(q['pose'][3:])) + if abs(yaw - step) > 0.05 or abs(pitch) > 0.05 or abs(roll) > 0.05: + errors.append('screen 1 turned to yaw %.3f pitch %.3f roll %.3f, ' + 'want %g, 0, 0' % (yaw, pitch, roll, step)) + if any(abs(a - b) > EDIT_POS for a, b in zip(q['pose'][:3], S0)): + errors.append('screen 1 moved to %s as it turned, want %s' + % (q['pose'][:3], S0)) + ticks = [e for e in haptics(res, 'right') if e['ev'] == 'haptic' + and res.marks['held'] * 1e6 < e['t_us'] + < res.marks['turned'] * 1e6 + and abs(e['amplitude'] - SNAP_TICK[0]) < 1e-3 + and e['duration_ns'] == SNAP_TICK[1]] + if len(ticks) != 1: + errors.append('%d step buzzes on the right hand, want 1: %s' + % (len(ticks), ticks)) + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', S0, + (tuple(q['pose'][:3]), + tuple(q['pose'][3:])), (1.6, 0.96), + last_before(res, 'off', screen1_left)) + + +def check_edit_step_45(res): + """With Snap to Angles at 45 degrees, the stick as in edit-step-stick + turns screen 1 on past 15 and 30 with no hold there, then into the + 45 step's hold, so it shows and is saved at exactly 45, with one + buzz.""" + errors = check_edit_step_stick(res, 45.0) + yaws = [euler(tuple(q['pose'][3:]))[0] for q in + map(screen1_left, between(res, 'held', 'turned', screen1_left))] + for s in (15.0, 30.0): + near = [y for y in yaws if abs(y - s) < 2.0] + if not near or all(abs(y - s) < 0.05 for y in near): + errors.append('screen 1 held at %g on its way to 45: %s' + % (s, ['%.3f' % y for y in near[:8]])) + return errors + + +# Screen 2 at screen 1's right corner, 18 degrees short of square +# (yaw -72, facing left): chosen, gripped, and turned by the left stick +# at an eighth of full speed (11.25 degrees a second) until a frame +# shows it square, it reaches the 5 degree window and clicks square at +# the corner. +SQUARE = dict(SAVE, video_openxr_edit_snap='true', + video_openxr_edit_hinge_gap='0', + video_screen2_xr_x='0.8', video_screen2_xr_y='0.992', + video_screen2_xr_z='0.64', video_screen2_xr_yaw='-72') +SQUARED = (0.8, 0.0, -1.16) +YAW_M90 = (0.0, -math.sqrt(0.5), 0.0, math.sqrt(0.5)) + +# Screen 2 swung back 16 degrees from square about screen 1's right +# edge, the corner's shared edge: its middle on the circle round it. +SWUNG_74 = (0.8 + 0.64 * math.cos(math.radians(74.0)), 0.0, + -1.8 + 0.64 * math.sin(math.radians(74.0))) + +# A hinge holds its gap exactly: 1 mm tells the Hinge Gaps apart. +HINGE_GAP_POS = 0.001 + +# Screen 2 raised 2.5 cm, its top edge 0.7 cm under screen 1's bottom +# edge, inside Hinge Gap 0's click, Snap to Angles at 30 degrees. +HINGE_3DS = dict(SAVE, video_openxr_edit_snap='true', + video_openxr_edit_hinge_gap='0', + video_openxr_edit_snap_angles='2', + video_screen2_xr_y='0.025') + +# The default Hinge Gap, 2 cm: screen 2 raised 1.6 cm, its top edge +# 1.6 cm under screen 1's bottom edge, Snap to Angles at 30 degrees. +HINGE_GAP = dict(SAVE, video_openxr_edit_snap='true', + video_openxr_edit_snap_angles='2', + video_screen2_xr_y='0.016') + + +def quad_point(q, x, y): + """Where the point x, y of quad q's own plane is.""" + qx, qy, qz, qw = q['pose'][3:] + tx = 2.0 * (qy * 0.0 - qz * y) + ty = 2.0 * (qz * x - qx * 0.0) + tz = 2.0 * (qx * y - qy * x) + return (q['pose'][0] + x + qw * tx + (qy * tz - qz * ty), + q['pose'][1] + y + qw * ty + (qz * tx - qx * tz), + q['pose'][2] + qw * tz + (qx * ty - qy * tx)) + + +def screen2_at_pitch(deg): + """A pick: screen 2 where it shows exactly deg from flat, either + way.""" + def pick(frame): + q = screen2(frame) + if q and abs(abs(euler(tuple(q['pose'][3:]))[1]) - deg) <= 0.05: + return q + return None + return pick + + +def check_edit_hinge_3ds(res, gap=0.0, narrow=False): + """Screen 2, taken under screen 1 and carried 6 cm along its edge + so its snaps are whole, hinges flat on screen 1's bottom edge; the + left stick, held at an eighth of full speed until a frame shows it + 30 degrees from flat, swings it about that edge into the 30 step's + hold: it shows and is saved there, its top edge Hinge Gap (gap) + under screen 1's bottom edge, in its plane, and the right hand + gets the hinge's long buzz once as it hinges and one tick for the + step. While hinged a green line runs along the hinge midway across + the gap where the edges overlap, screen 1's bottom rim and screen + 2's top rim gone; let go there, it rests hinged: both stay until + edit mode ends. Taken again where it rests, it is hinged from its + first frame: the line and the cut rims never go, it does not move, + nothing buzzes. On every frame, as it hinges, each outline is its + item's whole one in pose and size. Not a set + time: stalled polls cut the swing short.""" + errors = edit_log_errors(res, ['on', 'off']) + # Below the cut outlines' width, a cut turns and crops its outline. + for f in [] if narrow else between(res, 'on', 'off'): + bent = outline_pose_errors(f) + if bent: + errors += bent[:1] + break + + def cuts(f): + if narrow: + return crop_outline_errors(f, [ + (S0, OUTLINE_CROP, True), + (tuple(screen2(f)['pose'][:3]), 0, True)]) + return cut_outline_errors(f, [ + (S0, 'bottom'), + (tuple(screen2(f)['pose'][:3]), 'top')]) + # Near the hinge but not held there (7 mm into Hinge Gap 0's click, + # 1.6 cm against 2 cm), nothing shows until it is hinged. + for f in between(res, 'on', 'grab', delay=0.5): + if hinge_lines(f) or any(outline_cut(o) + for o in outlines(f)): + errors.append('frame %d: a hinge line or cut outline before ' + 'screen 2 was hinged' % f['n']) + break + q = last_before(res, 'swung', screen2) + if not q: + return errors + ['no frame of screen 2 before the stick let go'] + yaw, pitch, roll = euler(tuple(q['pose'][3:])) + if abs(abs(pitch) - 30.0) > 0.05 or abs(yaw) > 0.05 or abs(roll) > 0.05: + errors.append('screen 2 swung to yaw %.3f pitch %.3f roll %.3f, ' + 'want 0, +-30, 0' % (yaw, pitch, roll)) + edge = quad_point(q, 0.0, 0.48) + if ( abs(edge[1] + 0.48 + gap) > HINGE_GAP_POS + or abs(edge[2] + 1.8) > HINGE_GAP_POS): + errors.append('screen 2 top edge at %s, want on y %.3f, z -1.8' + % ([round(v, 4) for v in edge], -0.48 - gap)) + ticks = [e for e in haptics(res, 'right') if e['ev'] == 'haptic' + and res.marks['held'] * 1e6 < e['t_us'] + < res.marks['swung'] * 1e6 + and abs(e['amplitude'] - SNAP_TICK[0]) < 1e-3 + and e['duration_ns'] == SNAP_TICK[1]] + if len(ticks) != 1: + errors.append('%d buzzes on the right hand in the swing, want 1: %s' + % (len(ticks), ticks)) + held = [f for f in between(res, 'held', 'swung', delay=0.2) + if screen2(f)] + + def hinge(f): + x, length = span(screen2(f)['pose'][0] - 0.64, + screen2(f)['pose'][0] + 0.64, -0.8, 0.8) + return (x, -0.48 - gap / 2.0, -1.8), length + errors += hinge_line_errors(held, hinge, 0) + for f in held[:1] + held[-1:]: + errors += cuts(f) + errors += outline_gap_errors(held[0]) if held else ['no hinged frame'] + snaps = [f for f in held if f['snap'] and hinge_lines(f)] + if snaps: + errors += hinge_texel_errors(res, snaps[-1]) + if narrow: + w = read_png(image(res, snaps[-1], hinge_lines(snaps[-1])[0]))[0] + if w != CURSOR_TAGGED_W: + errors.append('the laser\'s image is %d wide, want %d' + % (w, CURSOR_TAGGED_W)) + elif gap == 0.0: + errors.append('no image of a hinged frame') + # Let go there, it rests hinged: the line and the cut rims stay, + # with no frame between carried and resting. + rest = [f for f in between(res, 'released', 'resting', delay=0.2) + if screen2(f)] + errors += hinge_line_errors( + [f for f in between(res, 'held', 'resting') if screen2(f)], hinge, 0) + for f in rest[:1] + rest[-1:]: + errors += cuts(f) + errors += outline_gap_errors(rest[-1]) if rest else \ + ['no frame of screen 2 let go'] + # Taken again where it rests, it is hinged from its first frame. + again = [f for f in between(res, 'resting', 'dropped') if screen2(f)] + errors += hinge_line_errors(again, hinge, 0) + for f in again[:1] + again[-1:]: + errors += cuts(f) + if rest and again: + at = tuple(screen2(rest[-1])['pose'][:3]) + moved = [f['n'] for f in again + if not near3(screen2(f)['pose'][:3], at, HINGE_GAP_POS)] + if moved: + errors.append('screen 2 moved taken again, frames %s' % moved) + elif not again: + errors.append('no frame of screen 2 taken again') + taken = (buzzes(res, HINGE_BUZZ, 'resting', 'dropped') + + buzzes(res, SNAP_TICK, 'resting', 'dropped')) + if taken: + errors.append('%d buzzes taking screen 2 again where it rests ' + 'hinged, want 0: %s' % (len(taken), taken)) + for f in between(res, 'off', delay=0.5): + if hinge_lines(f): + errors.append('frame %d: a hinge line after edit mode ended' + % f['n']) + break + hinged = buzzes(res, HINGE_BUZZ, 'on', 'held') + swing = buzzes(res, HINGE_BUZZ, 'held', 'swung') + if len(hinged) != 1 or swing: + errors.append('%d hinge buzzes taking screen 2 to the hinge and %d ' + 'in the swing, want 1 and 0' % (len(hinged), len(swing))) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (tuple(q['pose'][:3]), + tuple(q['pose'][3:])), (1.28, 0.96), + last_before(res, 'off', screen2)) + + +def check_edit_hinge_push(res): + """Screen 2 hinged under screen 1 at Hinge Gap 0 and pushed on 4.5 + cm past touching overlaps it with no hinge line; pulled back to 2 cm + past it hinges again touching, with one long buzz; let go there it + rests hinged, the line shown, and taken again it stays hinged with + no buzz; saved touching. On every frame, as it comes off the hinge + and as it hinges again, each outline is its item's whole one in + pose and size.""" + errors = edit_log_errors(res, ['on', 'off']) + for f in between(res, 'on', 'off'): + bent = outline_pose_errors(f) + if bent: + errors += bent[:1] + break + touching = -0.96 + for a, b, want in (('pushed', 'pull', 0), ('back', 'released', 1), + ('released', 'resting', 1), ('regrab', 'dropped', 1)): + frames = [f for f in between(res, a, b, delay=0.2) if screen2(f)] + if not frames: + errors.append('%s to %s: no frames of screen 2' % (a, b)) + continue + for f in frames: + n = len(hinge_lines(f)) + if n != want: + errors.append('%s to %s, frame %d: %d hinge lines, want %d' + % (a, b, f['n'], n, want)) + break + y = screen2(frames[-1])['pose'][1] + if want and abs(y - touching) > 1e-4: + errors.append('%s to %s: screen 2 at y %.4f, want %.4f ' + 'touching' % (a, b, y, touching)) + if not want and y < touching + 0.01: + errors.append('%s to %s: screen 2 at y %.4f, want it 1 cm or ' + 'more into screen 1' % (a, b, y)) + back = buzzes(res, HINGE_BUZZ, 'pull', 'released') + if len(back) != 1: + errors.append('%d hinge buzzes pulled back, want 1: %s' + % (len(back), back)) + taken = buzzes(res, HINGE_BUZZ, 'resting', 'dropped') + if taken: + errors.append('hinge buzzes taken again, want none: %s' % taken) + q = last_before(res, 'off', screen2) + if not q or abs(q['pose'][1] - touching) > 1e-4: + errors.append('screen 2 saved at %s, want y %.4f touching' + % (q and q['pose'][:3], touching)) + return errors + + +def check_edit_hinge_gap(res): + """Screen 2 at the default Hinge Gap: as check_edit_hinge_3ds, its + top edge 2 cm under screen 1's bottom edge.""" + return check_edit_hinge_3ds(res, 0.02) + + +def check_edit_hinge_3ds_narrow(res): + """A runtime whose swapchains are 4096 wide: the laser's image has + its tags but not the cut outlines. As check_edit_hinge_3ds, but each + cut outline is turned a quarter for a top or bottom and cut to its + crop.""" + return check_edit_hinge_3ds(res, narrow=True) + + +def check_edit_hinge_twitch(res, gap=0.02): + """Screen 2 hinged flat at the default Hinge Gap and held still, let + go as the aim turns half a degree up, and again half a degree down + once taken where it rests: each time it rests at the gap where it + showed before the grip opened, its hinge line on every frame after + the release, and is saved there.""" + errors = edit_log_errors(res, ['on', 'off']) + + def hinge(f): + x, length = span(screen2(f)['pose'][0] - 0.64, + screen2(f)['pose'][0] + 0.64, -0.8, 0.8) + return (x, -0.48 - gap / 2.0, -1.8), length + + def at_gap(what, q): + if not q: + return ['%s: no frame of screen 2' % what] + turn = euler(tuple(q['pose'][3:])) + edge = quad_point(q, 0.0, 0.48) + if ( any(abs(a) > 0.05 for a in turn) + or abs(edge[1] + 0.48 + gap) > HINGE_GAP_POS + or abs(edge[2] + 1.8) > HINGE_GAP_POS): + return ['%s turned %s, top edge at %s, want flat on y %.3f, ' + 'z -1.8' % (what, [round(a, 3) for a in turn], + [round(v, 4) for v in edge], -0.48 - gap)] + return [] + for rel, rest in (('released', 'resting'), ('released2', 'resting2')): + frames = [f for f in between(res, rel, rest, delay=0.2) + if screen2(f)] + errors += hinge_line_errors(frames, hinge, 0) + for f in frames: + e = at_gap('frame %d after %s' % (f['n'], rel), screen2(f)) + if e: + errors += e + break + cfg = saved(res) + v = [float(cfg.get('video_screen2_xr_' + k, '0')) + for k in ('x', 'y', 'z', 'yaw', 'pitch', 'roll')] + pose = (tuple(b + o for b, o in zip(S1, v[:3])), from_euler(*v[3:])) + errors += at_gap('screen 2 saved', + {'pose': list(pose[0]) + list(pose[1])}) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + pose, (1.28, 0.96), + last_before(res, 'off', screen2)) + + +def check_edit_hinge_shaken(res): + """Screen 2 hinged at the default Hinge Gap, its aim shaken half a + degree as the unit tests' t_shake shakes a hand, 2.8 cm past + touching, where the stop's hold ends, and 9.8 cm apart, where the + free gap's does: one hinge line on every frame from the hinge to + the last shake, and one long buzz in all, as it took hold. Every + frame from the grab's first held frame has the same layers and + hinge slots, the held screen's slot hidden until the hinge shows; + from the hinge on, the same outline rects too. On every frame, the + hinge's first included, each outline is its item's whole one in + pose and size.""" + errors = edit_log_errors(res, ['on', 'off']) + frames = between(res, 'still', 'shaken2') + if not frames: + errors.append('no frames from the hinge to the last shake') + off = [f['n'] for f in frames if len(hinge_lines(f)) != 1] + if off: + errors.append('%d of %d frames without one hinge line, from frame ' + '%d' % (len(off), len(frames), off[0])) + + def counts(f): + return len(quads(f)), len(hinge_slots(f)) + + def rects(f): + return [tuple(o['rect']) for o in outlines(f)] + # The first held frame is the first with the held screen's slot, + # hidden as the hinge has not shown yet. + held = between(res, 'grab', 'shaken2') + held = held[next((i for i, f in enumerate(held) + if len(hinge_slots(f)) > len(hinge_lines(f))), + len(held)):] + if not held: + errors.append('no frame from the grab to the last shake with the ' + 'held screen\'s hinge slot hidden') + for f in between(res, 'grab', 'shaken2'): + bent = outline_pose_errors(f) + if bent: + errors += bent[:1] + break + for what, fs, key in (('layers and hinge slots', held, counts), + ('outline rects', frames, rects)): + moved = [f for f in fs if key(f) != key(fs[0])] + if moved: + errors.append('%d of %d frames with other %s than frame %d %s, ' + 'from frame %d %s' + % (len(moved), len(fs), what, fs[0]['n'], + key(fs[0]), moved[0]['n'], key(moved[0]))) + taken = buzzes(res, HINGE_BUZZ) + if len(taken) != 1: + errors.append('%d hinge buzzes, want 1' % len(taken)) + return errors + + +def check_edit_hinge_settle(res): + """Screen 2 hinged flat at the default Hinge Gap and carried 4.9 cm + under screen 1, where it shows hinged past the click's reach: let go + there, it settles at the gap, its hinge line on every frame from + the hold to the rest, and is saved there; taken again, nothing + buzzes.""" + errors = edit_log_errors(res, ['on', 'off']) + + def line(gap): + def hinge(f): + x, length = span(screen2(f)['pose'][0] - 0.64, + screen2(f)['pose'][0] + 0.64, -0.8, 0.8) + return (x, -0.48 - gap / 2.0, -1.8), length + return hinge + + def at_gap(what, q, gap): + if not q: + return ['%s: no frame of screen 2' % what] + turn = euler(tuple(q['pose'][3:])) + edge = quad_point(q, 0.0, 0.48) + if ( any(abs(a) > 0.05 for a in turn) + or abs(edge[1] + 0.48 + gap) > HINGE_GAP_POS + or abs(edge[2] + 1.8) > HINGE_GAP_POS): + return ['%s turned %s, top edge at %s, want flat on y %.3f, ' + 'z -1.8' % (what, [round(a, 3) for a in turn], + [round(v, 4) for v in edge], -0.48 - gap)] + return [] + for a, b, gap in (('held4', 'released', 0.049), + ('released', 'resting', 0.02)): + frames = [f for f in between(res, a, b, delay=0.2) if screen2(f)] + errors += hinge_line_errors(frames, line(gap), 0) + for f in frames: + e = at_gap('frame %d from %s' % (f['n'], a), screen2(f), gap) + if e: + errors += e + break + cfg = saved(res) + v = [float(cfg.get('video_screen2_xr_' + k, '0')) + for k in ('x', 'y', 'z', 'yaw', 'pitch', 'roll')] + pose = (tuple(b + o for b, o in zip(S1, v[:3])), from_euler(*v[3:])) + errors += at_gap('screen 2 saved', + {'pose': list(pose[0]) + list(pose[1])}, 0.02) + taken = buzzes(res, HINGE_BUZZ, 'resting', 'dropped') + if taken: + errors.append('%d hinge buzzes taking screen 2 again where it ' + 'settled, want 0: %s' % (len(taken), taken)) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + pose, (1.28, 0.96), + last_before(res, 'off', screen2)) + + +def check_edit_snap_square(res): + """Screen 2, turned by the left stick from 18 degrees short of + square at screen 1's right corner, shows and is saved exactly + square there, and the right hand buzzes.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 2 turned square', + [screen2(f) for f in + between(res, 'held', 'released', delay=0.2)], + SQUARED, YAW_M90, (1.28, 0.96)) + errors += saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (SQUARED, YAW_M90), (1.28, 0.96), + last_before(res, 'off', screen2)) + ticks = buzzes(res, SNAP_TICK, 'grab') + buzzes(res, HINGE_BUZZ, 'grab') + if not ticks: + errors.append('no snap buzz on the right hand: %s' + % haptics(res, 'right')) + hinged = buzzes(res, HINGE_BUZZ, 'grab') + if len(hinged) != 1: + errors.append('%d hinge buzzes on the right hand, want 1 as the ' + 'corner hinged: %s' % (len(hinged), hinged)) + return errors + + +def check_edit_snap_sticks(res): + """Screen 2, turned by the left stick from 18 degrees short of + square at screen 1's right corner until it shows square, stays + square once the stick is let go; pushed back the other way at the + same speed until it shows -74, it swings on from square about the + corner's shared edge, not from where the stick stopped (Snap to + Angles Off, so no step at 75 holds it), and is saved there. The + head is held at the anchor. Not a set time: stalled polls cut the + stick's turn short.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += pose_errors('screen 2 square, the stick let go', + [screen2(f) for f in + between(res, 'square', 'push', delay=0.2)], + SQUARED, YAW_M90, (1.28, 0.96)) + q = last_before(res, 'released', screen2) + if not q: + return errors + ['no frame of screen 2 before the grip let go'] + yaw = euler(tuple(q['pose'][3:]))[0] + if abs(yaw + 74.0) > 1.5: + errors.append('screen 2 pushed back to yaw %.2f, want -74, 16 on ' + 'from square' % yaw) + errors += pose_errors('screen 2 swung back', [q], SWUNG_74) + return errors + saved_errors(res, 'screen 2', 'video_screen2_xr_', S1, + (tuple(q['pose'][:3]), + tuple(q['pose'][3:])), (1.28, 0.96), + last_before(res, 'off', screen2)) + + +# The right hand holding screen 1 sways half a degree either side every +# 50 ms, a 10 Hz shake 1.6 cm either side at the screen, then stops for +# a tenth of a second and lets go, before a smoothed screen catches up. +SWAY = math.tan(math.radians(0.5)) * 1.8 + + +def sway_aim(k): + return aim_line((0.0, 0.0, 0.0), (SWAY if k % 2 else -SWAY, 0.0, -1.8)) + + +EDIT_SWAY_STEPS = edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('mark', 'sway')] + [ + step for k in range(24) + for step in (('script', script(sway_aim(k), EDIT_GRIP)), ('wait', 0.05)) +] + [('wait', 0.1), ('mark', 'still'), ('script', sway_aim(23)), + ('mark', 'released'), + ('wait', 1.5), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)] + + +def check_edit_sway(smooth): + def check(res): + """Screen 1 held by a hand swaying half a degree either side at + 10 Hz: with Edit Smoothing at Medium it moves less than 60% of + the sway's 3.1 cm, Off the whole sway. Let go before it catches + up with the hand, it shows no step from where it was last shown, + and is saved there. Frames further apart than two of the sway's + are not compared: after a stall the smoothing catches up in one + frame.""" + errors = edit_log_errors(res, ['on', 'off']) + sway = between(res, 'sway', 'still', screen1_left, delay=0.3) + xs = [screen1_left(f)['pose'][0] for f in sway] + if len(xs) < 10: + return errors + ['%d frames of the sway' % len(xs)] + span = max(xs) - min(xs) + if smooth and span > 0.6 * 2 * SWAY: + errors.append('smoothed, screen 1 swayed %.4f m, want under ' + '%.4f' % (span, 0.6 * 2 * SWAY)) + if not smooth and span < 0.9 * 2 * SWAY: + errors.append('off, screen 1 swayed %.4f m of %.4f' + % (span, 2 * SWAY)) + gaps = sorted(b['t_us'] - a['t_us'] for a, b in zip(sway, sway[1:])) + period = gaps[len(gaps) // 2] + after = sway[-1:] + between(res, 'still', 'off', screen1_left) + steps = [abs(screen1_left(b)['pose'][0] - screen1_left(a)['pose'][0]) + for a, b in zip(after, after[1:]) + if b['t_us'] - a['t_us'] <= 2 * period] + if len(steps) < 20: + errors.append('%d of %d pairs of frames from the let-go to the ' + 'end without a stall between them, want 20' + % (len(steps), len(after) - 1)) + elif max(steps) > 0.004: + errors.append('screen 1 stepped %.4f m between frames as it ' + 'was let go' % max(steps)) + last = last_before(res, 'off', screen1_left) + if not last: + return errors + ['no frame of screen 1 before the end'] + return errors + saved_errors(res, 'screen 1', 'video_screen1_xr_', + S0, (tuple(last['pose'][:3]), + tuple(last['pose'][3:])), + (1.6, 0.96), last) + return check + + +TAG_LEFT, TAG_RIGHT = (-0.2, -0.3, -0.3), (0.2, -0.3, -0.3) + + +def both_aims(): + """The head at the origin; each hand below the eyes and to its side, + aiming straight ahead at screen 1.""" + return script(HEAD_AT_ORIGIN, + aim_line(TAG_LEFT, (TAG_LEFT[0], TAG_LEFT[1], -1.8), + hand='left'), + aim_line(TAG_RIGHT, (TAG_RIGHT[0], TAG_RIGHT[1], -1.8))) + + +EDIT_TAG_STEPS = edit_start(both_aims()) + [ + ('wait', 1.5), ('mark', 'next'), + ('script', script(both_aims(), 'action edit/select_next@right 1')), + ('wait', 0.5), ('script', both_aims()), ('wait', 1), + ('mark', 'previous'), + ('script', script(both_aims(), 'action edit/select_previous@left 1')), + ('wait', 0.5), ('script', both_aims()), ('wait', 1), + ('mark', 'end'), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)] +EDIT_TAG_NARROW_STEPS = edit_start(both_aims()) + [ + ('wait', 1.5), ('mark', 'end'), ('send', EDIT), ('wait', 2), + ('mark', 'off'), ('wait', 1)] + + +def tag_errors(res, what, frames, k, hand): + """Each frame's one tag shows cell k 5 cm over hand, facing the head + at the origin; its cell has light texels and dark ones.""" + if not frames: + return ['no frames %s' % what] + want = (hand[0], hand[1] + TAG_LIFT, hand[2]) + face = unit(tuple(-a for a in want)) + for f in frames: + t = tags(f) + if len(t) != 1: + return ['%s: %d tags' % (what, len(t))] + q = t[0] + z = qrot(tuple(q['pose'][3:]), (0.0, 0.0, 1.0)) + if q['rect'][0] != TAG_X[k] or q['rect'][1] != TAG_Y \ + or not near3(q['pose'], want, 0.002) \ + or abs(q['size'][1] - TAG_HEIGHT) > 1e-4 \ + or sum(a * b for a, b in zip(z, face)) < 0.999: + return ['%s: tag at %s rect %s facing %s, want cell %d at %s ' + 'facing %s' % (what, q['pose'][:3], q['rect'], z, k, + want, face)] + snaps = [f for f in frames if f['snap']] + if not snaps: + return ['%s: no snapshot of the tag' % what] + q = tags(snaps[-1])[0] + w, h, bpp, rows = read_png(image(res, snaps[-1], q)) + light = dark = 0 + for y in range(q['rect'][1], q['rect'][1] + TAG_ROWS): + for x in range(q['rect'][0], q['rect'][0] + TAG_COLS): + px = rows[y][x * bpp:x * bpp + bpp] + a = px[3] if bpp == 4 else 255 + if a >= TAG_FAINT - 8 and px[0] >= TAG_FAINT - 8: + light += 1 + elif a >= TAG_FAINT - 8 and px[0] <= 8: + dark += 1 + if not light or not dark: + return ['%s: the tag\'s cell has %d light and %d dark texels' + % (what, light, dark)] + return [] + + +def check_edit_tag_narrow(res): + """A runtime whose swapchains are narrower than the laser's image + with its tags: the image without them, as wide as before them; no + tag, and each hand's beam and dot.""" + errors = edit_log_errors(res, ['on', 'off']) + frames = between(res, 'on', 'end', delay=2.5) + if not frames: + return errors + ['no frames in edit mode'] + for f in frames: + dots = cursors(f) + late = [q for q in quads(f) if dots and q['sc'] == dots[0]['sc'] + and q['rect'][0] >= CURSOR_PLAIN_W] + if late or len(beams(f)) != 2 or len(dots) != 2: + return errors + ['%d tags, %d beams, %d dots' + % (len(late), len(beams(f)), len(dots))] + snaps = [f for f in frames if f['snap']] + if not snaps: + return errors + ['no snapshot of the laser\'s image'] + w = read_png(image(res, snaps[-1], cursors(snaps[-1])[0]))[0] + if w != CURSOR_PLAIN_W: + errors.append('the laser\'s image is %d wide, want %d' + % (w, CURSOR_PLAIN_W)) + return errors + + +def check_edit_tag(res): + """The tag starts over the left hand reading Laser; the right hand's + Select Next moves it over the right reading Screen 1; the left's + Select Previous moves it back. Light text with a dark halo.""" + errors = edit_log_errors(res, ['on', 'off']) + errors += tag_errors(res, 'from the start', + between(res, 'on', 'next', delay=2.5), + 0, TAG_LEFT) + errors += tag_errors(res, 'after the right\'s Select Next', + between(res, 'next', 'previous', delay=0.5), + 1, TAG_RIGHT) + errors += tag_errors(res, 'after the left\'s Select Previous', + between(res, 'previous', 'end', delay=0.5), + 0, TAG_LEFT) + return errors + + +def check_edit_back(res): + """From behind, each of screen 1's quads shows its back in its place: + the laser's image's dark square, opaque, at its size, facing the + head; every other item too, in no more layers. Its outline faces + the head too, lit by the hand that points at the back.""" + errors = edit_log_errors(res, ['on', 'off']) + front = between(res, 'front', 'turn', pick=edit_screen0, delay=0.5) + behind = between(res, 'behind', 'end', pick=edit_screen0, delay=0.5) + if not front or not behind: + return errors + ['%d frames in front, %d behind' + % (len(front), len(behind))] + want = edit_screen0(front[-1]) + dots = cursors(front[-1]) + items = [q for q in quads(front[-1]) if dots and q['sc'] != dots[0]['sc']] + if len(want) != 2 or len(items) < 3 \ + or any(tuple(q['rect']) == BACK_RECT for q in items): + return errors + ['in front: %s' % [q['rect'] for q in items]] + for f in behind: + qs = edit_screen0(f) + backs = [q for q in quads(f) if tuple(q['rect']) == BACK_RECT + and not q['flags'] & BLEND] + if len(quads(f)) != len(quads(front[-1])) or len(qs) != 2 \ + or len(backs) != len(items): + return errors + ['from behind %d layers, %d backs; in front %d ' + 'layers, %d items' + % (len(quads(f)), len(backs), + len(quads(front[-1])), len(items))] + for q, w in zip(qs, want): + z = qrot(tuple(q['pose'][3:]), (0.0, 0.0, 1.0)) + if tuple(q['rect']) != BACK_RECT \ + or not near3(q['pose'][:3], w['pose'][:3]) \ + or not near(q['size'], w['size'], 1e-4) or z[2] > -0.999: + return errors + ['screen 1 from behind: rect %s at %s, %s, ' + 'facing %s; in front at %s, %s' + % (q['rect'], q['pose'][:3], q['size'], z, + w['pose'][:3], w['size'])] + rims = [q for q in outlines(f) if near3(q['pose'], S0)] + z = qrot(tuple(rims[0]['pose'][3:]), (0.0, 0.0, 1.0)) if rims \ + else (0.0, 0.0, 1.0) + if len(rims) != 1 or rims[0]['rect'][0] != OUTLINE_X[1] \ + or z[2] > -0.999 or rims[0]['pose'][2] > S0[2]: + return errors + ['screen 1\'s outline from behind: %s facing %s' + % ([(q['rect'][0], q['pose'][:3]) for q in rims], + z)] + snaps = [f for f in behind if f['snap']] + if not snaps: + return errors + ['no snapshot from behind'] + w, h, bpp, rows = read_png(image(res, snaps[-1], + edit_screen0(snaps[-1])[0])) + x0, y0, bw, bh = BACK_RECT + bad = 0 + for y in range(y0, y0 + bh): + for x in range(x0, x0 + bw): + px = rows[y][x * bpp:x * bpp + bpp] + if any(abs(c - BACK_LEVEL) > 2 for c in px[:3]) \ + or (bpp == 4 and px[3] != 255): + bad += 1 + if bad: + errors.append('%d texels of the back are not its dark grey: %s' + % (bad, list(rows[y0][x0 * bpp:x0 * bpp + bpp]))) + return errors + + +EDIT_BACK_HAND = (0.3, 0.0, -3.3) +EDIT_BACK_STEPS = edit_start(script(HEAD_AT_ORIGIN, edit_aim(0.3))) + [ + ('mark', 'front'), ('wait', 1.5), ('mark', 'turn'), + ('script', script('head 0 0 -3.6 180', + aim_line(EDIT_BACK_HAND, (0.3, 0.0, -1.8)))), + ('wait', 0.5), ('mark', 'behind'), ('wait', 1.5), ('mark', 'end'), + ('send', EDIT), ('wait', 2), ('mark', 'off'), ('wait', 1)] + + +EDIT_GRAB_STEPS = edit_start(edit_aim(), 1) + [ + ('mark', 'aimed'), ('wait', 1), ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(0.3), EDIT_GRIP)), ('wait', 1), + ('mark', 'moved'), ('wait', 1), + ('script', edit_aim(0.3)), ('mark', 'released'), ('wait', 1), + ('script', edit_aim(0.6)), ('wait', 1), ('mark', 'stop'), + ('send', EDIT), ('wait', 2), ('mark', 'off'), ('wait', 1)] +EDIT_HOLD = edit_start(edit_aim()) + [ + ('mark', 'grab'), ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'sweep')] +EDIT_OFFSET = dict((k, v) for k, v in CUSTOM_OFFSET.items() + if k.startswith('video_screen1_')) +EDIT_CASES = [ + {'name': 'edit-grab', 'map': '3ds', 'settings': SAVE, + 'steps': EDIT_GRAB_STEPS, 'check': check_edit_grab()}, + {'name': 'edit-grab-threaded', 'map': '3ds', + 'settings': dict(SAVE, video_threaded='true'), 'steps': EDIT_GRAB_STEPS, + 'check': threaded(check_edit_grab())}, + {'name': 'edit-follow', 'map': '3ds', 'options': FPS10, 'settings': SAVE, + 'steps': EDIT_HOLD + sweep(20, 0.015, edit_aim_near) + + [('mark', 'swept'), ('wait', 0.5), ('script', edit_aim_near(0.3)), + ('mark', 'released'), ('wait', 2), ('send', EDIT), ('wait', 1)], + 'check': check_edit_follow}, + {'name': 'edit-release-threaded', 'map': '3ds', + 'settings': dict(SAVE, video_threaded='true'), + 'steps': EDIT_HOLD + sweep(10, 0.015) + + [('mark', 'swept'), ('wait', 0.15), ('script', edit_aim(0.15)), + ('mark', 'released'), ('wait', 1), ('script', edit_aim(0.6)), + ('wait', 1.5)], + 'check': threaded(check_edit_release)}, + # The grip opens on a step that also moves the hand. + {'name': 'edit-release-moving', 'map': '3ds', 'options': FPS10, + 'settings': SAVE, + 'steps': EDIT_HOLD + sweep(9, 0.03) + + [('script', edit_aim(0.3)), ('mark', 'released'), ('wait', 2)], + 'check': check_edit_release_moving}, + {'name': 'edit-regrab-threaded', 'map': '3ds', 'options': FPS10, + 'settings': dict(SAVE, video_threaded='true'), + 'steps': EDIT_HOLD + sweep(10, 0.015) + + [('wait', 0.25), ('script', edit_aim(0.15)), ('mark', 'released'), + ('wait', 0.1), ('script', script(edit_aim(0.15), EDIT_GRIP)), + ('wait', 1), ('script', script(edit_aim(0.25), EDIT_GRIP)), + ('mark', 'moved_again'), ('wait', 1.5), ('script', edit_aim(0.25)), + ('wait', 1.5)], + 'check': threaded(check_edit_regrab)}, + {'name': 'edit-handover-threaded', 'map': '3ds', + 'settings': dict(SAVE, video_threaded='true'), + 'steps': EDIT_HOLD + sweep(10, 0.015) + + [('mark', 'swept'), ('wait', 0.15), ('script', edit_aim(0.15)), + ('mark', 'released'), ('wait', 0.15), + ('script', script(edit_aim(0.0, -0.992), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(0.3, -0.992), EDIT_GRIP)), + ('mark', 'b_moved'), ('wait', 1.5), + ('script', edit_aim(0.3, -0.992)), ('wait', 1.5), + ('mark', 'end')], + 'check': threaded(check_edit_handover)}, + {'name': 'edit-tilt', 'map': '3ds', 'settings': SAVE, + 'steps': edit_start(edit_aim(), 1) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(roll=30), EDIT_GRIP)), ('wait', 1), + ('mark', 'rolled'), + ('script', script(edit_aim(roll=30), EDIT_GRIP, EDIT_TILT)), + ('wait', 1), ('mark', 'tilted'), + ('script', script(edit_aim(roll=60), EDIT_GRIP, EDIT_TILT)), + ('wait', 1), ('mark', 'turned'), + ('script', script(edit_aim(roll=0), EDIT_GRIP)), ('wait', 1), + ('mark', 'kept'), ('script', edit_aim()), ('wait', 1), + ('send', EDIT), ('wait', 1)], + 'check': check_edit_tilt}, +] +for _tilt in (False, True): + _keys = [EDIT_TILT] if _tilt else [] + EDIT_CASES.append( + {'name': 'edit-turn-tilt' if _tilt else 'edit-turn', 'map': '3ds', + 'settings': SAVE, + 'steps': edit_start(aim_line(*T0)) + [ + ('script', script(aim_line(*T0), EDIT_GRIP, *_keys)), + ('wait', 1.5), ('mark', 'moved'), + ('script', script(aim_line(*T1), EDIT_GRIP, *_keys)), + ('wait', 2), ('script', aim_line(*T1)), ('wait', 1.5)], + 'check': check_edit_turn(_tilt)}) +EDIT_STICK = script(aim_line(*A0), EDIT_GRIP) +EDIT_CASES += [ + {'name': 'edit-stick', 'map': '3ds', 'settings': SAVE, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '0'}, + 'steps': edit_start(aim_line(*A0), 1) + [ + ('script', EDIT_STICK), ('wait', 1), ('mark', 'held'), + ('script', script(EDIT_STICK, 'action edit/stick@right 0 1')), + ('wait', 0.5), ('script', EDIT_STICK), ('wait', 1), + ('mark', 'pushed'), + ('script', script(EDIT_STICK, 'action edit/stick@right 0.15 0.15')), + ('wait', 1), ('script', EDIT_STICK), ('wait', 1), + ('mark', 'deaded'), + ('script', script(EDIT_STICK, 'action edit/stick@right 1 0')), + ('wait', 0.5), ('script', EDIT_STICK), ('wait', 1), + ('mark', 'grown'), ('script', aim_line(*A0)), ('wait', 1), + ('mark', 'off'), ('send', EDIT), ('wait', 1)], + 'check': check_edit_stick}, + # Screen 1 from 0.25,0.1,-1.5, the menu following it until the reset. + {'name': 'edit-reset', 'map': '3ds', 'settings': dict(SAVE, + **EDIT_OFFSET), + 'steps': edit_start(edit_aim(0.25, 0.1), 1) + [ + ('mark', 'offset'), + ('script', script(edit_aim(0.25, 0.1), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(0.55, 0.1), EDIT_GRIP)), ('wait', 1), + ('script', edit_aim(0.55, 0.1)), ('wait', 0.5), ('mark', 'moved'), + ('wait', 1.5), ('mark', 'reset'), + ('script', script(edit_aim(0.55, 0.1), EDIT_RESET)), + ('wait', 0.5), ('script', edit_aim(0.55, 0.1)), ('wait', 1.5), + ('script', edit_aim(0.0, EDIT_HANDLE, z=-1.7)), ('wait', 1), + ('mark', 'handle'), + ('script', script(edit_aim(0.0, EDIT_HANDLE, z=-1.7), EDIT_GRIP)), + ('wait', 1), + ('script', script(edit_aim(0.5, EDIT_HANDLE, z=-1.7), EDIT_GRIP)), + ('wait', 1), ('script', edit_aim(0.5, EDIT_HANDLE, z=-1.7)), + ('mark', 'placed'), ('wait', 1.5), ('mark', 'back'), + ('script', script(edit_aim(0.5, EDIT_HANDLE, z=-1.7), EDIT_RESET)), + ('wait', 0.5), ('script', ''), ('wait', 1.5), ('mark', 'off'), + ('send', EDIT), ('wait', 1)], + 'check': check_edit_reset()}, + {'name': 'edit-menu-front', 'map': '3ds', + 'settings': {'video_screen1_xr_z': '0.6', 'menu_driver': 'ozone'}, + 'steps': [('wait', 6), ('send', 'MENU_TOGGLE'), ('wait', 4)], + 'check': check_edit_menu_front}, + {'name': 'edit-menu-size', 'map': '3ds', + 'settings': dict(SAVE, video_screen1_xr_z='0.9'), + 'steps': edit_start(edit_aim(0.0, F_HANDLE, z=-0.8)) + [ + ('mark', 'grab'), + ('script', script(edit_aim(0.0, F_HANDLE, z=-0.8), EDIT_GRIP)), + ('wait', 1.5), ('mark', 'moved'), + ('script', script(edit_aim(0.3, F_HANDLE, z=-0.8), EDIT_GRIP)), + ('wait', 2), ('script', edit_aim(0.3, F_HANDLE, z=-0.8)), + ('wait', 1.5)], + 'check': check_edit_menu_size}, + {'name': 'edit-menu-reset', 'map': '3ds', + 'settings': dict(SAVE, **EDIT_OFFSET), + 'steps': edit_start(edit_aim(0.25, M_HANDLE, z=-1.4)) + [ + ('mark', 'grab'), + ('script', script(edit_aim(0.25, M_HANDLE, z=-1.4), EDIT_GRIP)), + ('wait', 1), + ('script', script(edit_aim(0.55, M_HANDLE, z=-1.4), EDIT_GRIP)), + ('wait', 1), ('script', edit_aim(0.55, M_HANDLE, z=-1.4)), + ('mark', 'placed'), ('wait', 1.5), ('mark', 'back'), + ('script', script(edit_aim(0.55, M_HANDLE, z=-1.4), EDIT_RESET)), + ('wait', 0.5), ('script', ''), ('wait', 1.5), ('mark', 'off'), + ('send', EDIT), ('wait', 1)], + 'check': check_edit_menu_reset}, + {'name': 'edit-outlines', 'map': '3ds', + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, + 'steps': edit_start(edit_aim(), 1) + [ + ('mark', 'lit'), ('wait', 3), + ('script', edit_aim(0.0, EDIT_HANDLE, z=-1.7)), ('mark', 'handle'), + ('wait', 2), ('send', EDIT), ('mark', 'off'), ('wait', 2)], + 'check': check_edit_outlines()}, + {'name': 'edit-outlines-flush', 'map': '3ds', 'settings': FLUSH, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, + 'steps': edit_start(edit_aim(), 1) + [ + ('mark', 'lit'), ('wait', 3), ('send', EDIT), ('mark', 'off'), + ('wait', 2)], + 'check': check_edit_outlines_flush}, + {'name': 'edit-beam', 'map': '3ds', + 'settings': {'video_openxr_laser': '1'}, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, + 'steps': [('wait', 6), ('script', script(HEAD_AT_ORIGIN, AIM_GUN)), + ('wait', 1), ('mark', 'before'), ('wait', 2), ('mark', 'on'), + ('send', EDIT), ('wait', 1), ('mark', 'aimed'), ('wait', 2), + ('mark', 'miss'), ('script', script(HEAD_AT_ORIGIN, AIM_MISS)), + ('wait', 2), ('mark', 'off'), ('send', EDIT), ('wait', 2)], + 'check': check_edit_beam}, + {'name': 'edit-beam-off', 'map': '3ds', + 'settings': {'video_openxr_laser_beam': '0'}, + 'steps': [('wait', 6), ('script', script(HEAD_AT_ORIGIN, AIM_GUN)), + ('wait', 1), ('mark', 'on'), ('send', EDIT), ('wait', 1), + ('mark', 'aimed'), ('wait', 2), ('mark', 'off'), + ('send', EDIT), ('wait', 1)], + 'check': check_edit_beam_off}, + {'name': 'laser-beam-always', 'map': '3ds', + 'settings': {'video_openxr_laser': '1', 'video_openxr_laser_beam': '2'}, + 'steps': [('wait', 6), ('script', script(HEAD_AT_ORIGIN, AIM_GUN)), + ('wait', 1), ('mark', 'aimed'), ('wait', 2), ('mark', 'miss'), + ('script', script(HEAD_AT_ORIGIN, AIM_MISS)), ('wait', 2), + ('mark', 'off'), ('send', 'LASER_POINTER_TOGGLE'), + ('wait', 2), ('mark', 'on'), + ('send', 'LASER_POINTER_TOGGLE'), ('wait', 2)], + 'check': check_laser_beam_always}, + {'name': 'edit-fallback', 'map': 'crop', + 'settings': dict(SAVE, video_threaded='true', video_screen1_xr_x='0.5'), + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1.5), + ('script', script(edit_aim(0.3), EDIT_GRIP)), ('wait', 2), + ('script', edit_aim()), ('wait', 1), + ('script', script(edit_aim(), EDIT_RESET)), ('wait', 0.5), + ('script', edit_aim()), ('wait', 1.5), ('mark', 'off'), + ('send', EDIT), ('wait', 1)], + 'check': threaded(check_edit_fallback())}, + {'name': 'edit-no-map', 'map': 'none', 'settings': SAVE, + 'steps': edit_start(edit_aim(), 1) + [ + ('mark', 'grab'), ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('script', script(edit_aim(0.3), EDIT_GRIP)), + ('wait', 1), ('script', edit_aim(0.3)), ('wait', 1), + ('mark', 'moved'), ('send', EDIT), ('wait', 2)], + 'check': check_edit_no_map}, + {'name': 'edit-follow-head', 'map': '3ds', + 'settings': dict(SAVE, video_openxr_placement='1'), 'script': V_HEAD, + 'steps': edit_start(script(V_HEAD, aim_line(*turned(V_ANCHOR, A0)))) + + [('script', script(V_HEAD, aim_line(*turned(V_ANCHOR, A0)), + EDIT_GRIP)), ('wait', 1.5), ('mark', 'moved'), + ('script', script(V_HEAD, aim_line(*turned(V_ANCHOR, A1)), + EDIT_GRIP)), ('wait', 2), + ('script', script(V_HEAD, aim_line(*turned(V_ANCHOR, A1)))), + ('wait', 1.5), ('mark', 'turned'), ('script', HEAD_TURNED), + ('wait', 2)], + 'check': check_edit_follow_head}, + {'name': 'edit-lazy', 'map': '3ds', + 'settings': {'video_openxr_placement': '2'}, 'script': 'head 0 0 0 0', + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script('head 0 0 0 90', edit_aim(), EDIT_GRIP)), + ('mark', 'turned'), ('wait', 2.5), ('script', 'head 0 0 0 90'), + ('mark', 'released'), ('wait', 3)], + 'check': check_edit_lazy}, + {'name': 'edit-two-hands', 'map': '3ds', 'settings': SAVE, + 'steps': edit_start(script(aim_line(*L0, hand='left'), + aim_line(*A0))) + [ + ('script', script(aim_line(*L0, hand='left'), aim_line(*A0), + EDIT_GRIP_LEFT)), ('wait', 1.5), + ('script', script(aim_line(*L0, hand='left'), aim_line(*A0), + EDIT_GRIP_LEFT, EDIT_GRIP)), ('wait', 1.5), + ('script', script(aim_line(*L1, hand='left'), aim_line(*A0), + EDIT_GRIP_LEFT, EDIT_GRIP)), ('wait', 1.5), + ('mark', 'right_moved'), + ('script', script(aim_line(*L1, hand='left'), aim_line(*R1), + EDIT_GRIP_LEFT, EDIT_GRIP)), ('wait', 2), + ('mark', 'bottom'), + ('script', script(aim_line(*L1, hand='left'), aim_line(*B0), + EDIT_GRIP_LEFT)), ('wait', 1), + ('script', script(aim_line(*L1, hand='left'), aim_line(*B0), + EDIT_GRIP_LEFT, EDIT_GRIP)), ('wait', 1.5), + ('mark', 'bottom_moved'), + ('script', script(aim_line(*L1, hand='left'), aim_line(*B1), + EDIT_GRIP_LEFT, EDIT_GRIP)), ('wait', 2), + ('script', script(aim_line(*L1, hand='left'), aim_line(*B1))), + ('wait', 1.5)], + 'check': check_edit_two_hands}, + {'name': 'edit-recenter', 'map': '3ds', 'settings': SAVE, + 'steps': edit_start(aim_line(*A0)) + [ + ('script', script(aim_line(*A0), EDIT_GRIP)), ('wait', 1.5), + ('script', script(aim_line(*A1), EDIT_GRIP)), ('wait', 2), + ('script', script(V_HEAD, aim_line(*A1), EDIT_GRIP)), ('wait', 1), + ('mark', 'recenter'), ('send', 'HEADSET_RECENTER'), ('wait', 2), + ('mark', 'pulled'), + ('script', script(V_HEAD, aim_line(*A2), EDIT_GRIP)), ('wait', 2)], + 'check': check_edit_recenter}, + {'name': 'edit-recentered', 'map': '3ds', 'settings': SAVE, + 'steps': [('wait', 6), ('script', V_HEAD), ('wait', 1), + ('send', 'HEADSET_RECENTER'), ('wait', 2), + ('script', script(V_HEAD, aim_line(*RC0))), ('wait', 1), + ('mark', 'on'), ('send', EDIT), ('wait', 2), ('mark', 'grab'), + ('script', script(V_HEAD, aim_line(*RC0), EDIT_GRIP)), + ('wait', 1.5), ('mark', 'moved'), + ('script', script(V_HEAD, aim_line(*RC1), EDIT_GRIP)), + ('wait', 2), ('script', script(V_HEAD, aim_line(*RC1))), + ('wait', 1.5)], + 'check': check_edit_recentered}, + # Started by the command, then by the headset's action, which raises + # it: each pause lands between frames. + {'name': 'edit-pause', 'map': '3ds', 'options': FPS16, + 'settings': {'video_openxr_edit_pause': 'true'}, + 'steps': [('wait', 6), ('mark', 'on'), ('send', EDIT), ('wait', 4), + ('mark', 'off'), ('send', EDIT), ('wait', 4), ('mark', 'on2'), + ('script', 'action combined/edit_screens 1'), ('wait', 1), + ('script', ''), ('wait', 3), ('mark', 'off2'), + ('script', 'action edit/edit_screens 1'), ('wait', 1), + ('script', ''), ('wait', 4)], + 'check': check_edit_pause}, + {'name': 'edit-command', 'map': '3ds', 'options': FPS16, + 'steps': [('wait', 6), ('mark', 'aim'), + ('script', script(AIM_SCREEN0_RIGHT, AIM_SCREEN0_LEFT)), + ('wait', 2), ('mark', 'on'), ('send', EDIT), ('wait', 4), + ('mark', 'off'), ('send', EDIT), ('wait', 3)], + 'check': check_edit_command()}, + {'name': 'edit-action', 'map': '3ds', + 'steps': [('wait', 6), ('mark', 'on')] + + edit_press('action combined/edit_screens 1') + [('mark', 'off')] + + edit_press('action edit/edit_screens 1'), + 'check': check_edit_action(GAME_SETS)}, + {'name': 'edit-action-separate', 'map': '3ds', + 'settings': {'video_openxr_controllers': '1'}, + 'steps': [('wait', 6), ('mark', 'on')] + + edit_press('action separate/edit_screens 1') + [('mark', 'off')] + + edit_press('action edit/edit_screens 1'), + 'check': check_edit_action(['separate', 'pointer'])}, + # No command interface, so the hotkeys are read by their bits, and + # RetroArch is stopped, not told to quit. + {'name': 'edit-hotkeys-nocmd', 'map': '3ds', + 'settings': {'network_cmd_enable': 'false'}, + 'steps': laser_action_steps('action combined/laser 1') + [ + ('script', 'action combined/recenter 1'), ('wait', 1), + ('script', ''), ('wait', 1), ('mark', 'edit_on')] + + edit_press('action combined/edit_screens 1') + [('mark', 'edit_off')] + + edit_press('action edit/edit_screens 1'), + 'check': check_edit_hotkeys}, + {'name': 'edit-menu-open', 'map': '3ds', + 'settings': {'menu_driver': 'ozone'}, + 'steps': [('wait', 6), ('mark', 'on'), ('send', EDIT), ('wait', 3), + ('mark', 'menu'), ('send', 'MENU_TOGGLE'), ('wait', 3)], + 'check': check_edit_menu_open}, + {'name': 'edit-menu-action', 'map': '3ds', + 'settings': {'menu_driver': 'ozone'}, + 'steps': [('wait', 6), ('mark', 'on'), ('send', EDIT), ('wait', 3), + ('mark', 'menu')] + edit_press('action edit/menu 1'), + 'check': check_edit_menu_ends}, + # The fullscreen hotkey restarts the driver: edit mode ends with it. + {'name': 'edit-reinit', 'map': '3ds', + 'steps': [('wait', 6), ('mark', 'on'), ('send', EDIT), ('wait', 3), + ('mark', 'menu'), ('send', 'FULLSCREEN_TOGGLE'), ('wait', 6)], + 'check': check_edit_menu_ends}, + # From the open menu: it closes, and edit mode starts. + {'name': 'edit-from-menu', 'map': '3ds', + 'settings': {'menu_driver': 'ozone'}, + 'steps': [('wait', 6), ('send', 'MENU_TOGGLE'), ('wait', 3), + ('mark', 'on'), ('send', EDIT), ('wait', 3), ('mark', 'off'), + ('send', EDIT), ('wait', 3)], + 'check': check_edit_action(GAME_SETS)}, + {'name': 'edit-no-content', 'map': '3ds', + 'core': '/nonexistent/no_core_libretro.so', + 'steps': [('wait', 8), ('send', EDIT), ('wait', 3)], + 'check': check_edit_no_content}, + # Two presses of Select Next choose screen 2, buried behind screen 1. + {'name': 'edit-choose', 'map': '3ds', 'settings': dict(SAVE, **BURIED), + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'chosen'), ('wait', 1.5), ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(0.6), EDIT_GRIP)), ('wait', 1), + ('mark', 'moved'), ('wait', 1), ('script', edit_aim(0.6)), + ('mark', 'released'), ('wait', 1), ('send', EDIT), ('wait', 2), + ('mark', 'off'), ('wait', 1)], + 'check': check_edit_choose()}, + {'name': 'edit-handle-first', 'map': '3ds', 'settings': UNDER_BAR, + 'steps': edit_start(on_handle()) + [ + ('mark', 'grab'), ('script', script(on_handle(), EDIT_GRIP)), + ('wait', 1), ('script', script(on_handle(0.3), EDIT_GRIP)), + ('wait', 1), ('mark', 'moved'), ('wait', 1), + ('script', on_handle(0.3)), ('mark', 'released'), ('wait', 1), + ('send', EDIT), ('wait', 2), ('mark', 'off'), ('wait', 1)], + 'check': check_edit_handle_first}, + {'name': 'edit-handle-sticky', 'map': '3ds', 'settings': UNDER_BAR, + 'steps': sticky_steps(on_handle()), + 'check': check_edit_handle_sticky(True)}, + {'name': 'edit-handle-margin', 'map': '3ds', 'settings': UNDER_BAR, + 'steps': sticky_steps(on_handle(0.0, -0.07)), + 'check': check_edit_handle_sticky(False)}, + {'name': 'edit-snap', 'map': '3ds', + 'settings': dict(SAVE, video_openxr_edit_snap='true', + video_openxr_edit_hinge_gap='0'), + 'steps': EDIT_SNAP_STEPS, 'check': check_edit_snap(True)}, + {'name': 'edit-snap-off', 'map': '3ds', 'settings': SAVE, + 'steps': EDIT_SNAP_STEPS, 'check': check_edit_snap(False)}, + {'name': 'edit-snap-edge', 'map': '3ds', + 'settings': dict(SAVE, video_openxr_edit_snap='true', + video_openxr_edit_hinge_gap='0'), + 'steps': EDIT_SNAP_EDGE_STEPS, 'check': check_edit_snap_edge}, + {'name': 'edit-turn-stick', 'map': '3ds', 'settings': SAVE, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '0'}, + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left 1 0')), + ('wait', 0.5), ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'turned'), ('script', edit_aim()), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_turn_stick}, + {'name': 'edit-tilt-stick', 'map': '3ds', 'settings': SAVE, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '0'}, + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left 0 1')), + ('wait', 0.5), ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'tilted'), ('script', edit_aim()), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_tilt_stick}, + {'name': 'edit-step-stick', 'map': '3ds', 'settings': SAVE, + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left 0.3 0')), + ('until', ('screen 1 at 15', screen1_at_yaw(15.0), 10)), + ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'turned'), ('script', edit_aim()), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_step_stick}, + {'name': 'edit-step-45', 'map': '3ds', + 'settings': dict(SAVE, video_openxr_edit_snap_angles='3'), + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left 0.3 0')), + ('until', ('screen 1 at 45', screen1_at_yaw(45.0), 10)), + ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'turned'), ('script', edit_aim()), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_step_45}, + {'name': 'edit-snap-square', 'map': '3ds', 'settings': SQUARE, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left -0.3 0')), + ('until', ('screen 2 square', screen2_at_yaw(-90.0), 10)), + ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'held'), ('wait', 1), + ('script', edit_aim()), ('mark', 'released'), ('wait', 1), + ('send', EDIT), ('wait', 2), ('mark', 'off'), ('wait', 1)], + 'check': check_edit_snap_square}, + {'name': 'edit-snap-sticks', 'map': '3ds', + 'settings': dict(SQUARE, video_openxr_edit_snap_angles='0'), + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left -0.3 0')), + ('until', ('screen 2 square', screen2_at_yaw(-90.0), 10)), + ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('mark', 'square'), ('wait', 0.5), ('mark', 'push'), + ('script', script(edit_aim(), EDIT_GRIP, + 'action edit/stick@left 0.3 0')), + ('until', ('screen 2 back at -74', screen2_at_yaw(-74.0, True), + 10)), + ('script', script(edit_aim(), EDIT_GRIP)), + ('wait', 1), ('script', edit_aim()), ('mark', 'released'), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_snap_sticks}, + {'name': 'edit-hinge-3ds', 'map': '3ds', 'settings': HINGE_3DS, + 'script': HEAD_AT_ORIGIN, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(x=0.06), EDIT_GRIP, + 'action edit/stick@left 0 -0.3')), + ('until', ('screen 2 at 30', screen2_at_pitch(30.0), 10)), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), + ('wait', 1), ('mark', 'swung'), ('script', edit_aim(x=0.06)), + ('mark', 'released'), + ('wait', 1), ('mark', 'resting'), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('script', edit_aim(x=0.06)), ('mark', 'dropped'), ('wait', 1), + ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_3ds}, + {'name': 'edit-hinge-push', 'map': '3ds', 'settings': HINGE_3DS, + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(x=0.06, y=0.052), EDIT_GRIP)), + ('wait', 1), ('mark', 'pushed'), ('wait', 0.5), ('mark', 'pull'), + ('script', script(edit_aim(x=0.06, y=0.027), EDIT_GRIP)), + ('wait', 1), ('mark', 'back'), ('wait', 0.5), + ('script', edit_aim(x=0.06, y=0.027)), + ('mark', 'released'), ('wait', 1), ('mark', 'resting'), + ('script', script(edit_aim(x=0.06, y=0.027), EDIT_GRIP)), + ('mark', 'regrab'), ('wait', 1), + ('script', edit_aim(x=0.06, y=0.027)), ('mark', 'dropped'), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_push}, + {'name': 'edit-hinge-gap', 'map': '3ds', 'settings': HINGE_GAP, + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), + ('script', script(edit_aim(x=0.06), EDIT_GRIP, + 'action edit/stick@left 0 -0.3')), + ('until', ('screen 2 at 30', screen2_at_pitch(30.0), 10)), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), + ('wait', 1), ('mark', 'swung'), ('script', edit_aim(x=0.06)), + ('mark', 'released'), + ('wait', 1), ('mark', 'resting'), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('script', edit_aim(x=0.06)), ('mark', 'dropped'), ('wait', 1), + ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_gap}, + {'name': 'edit-hinge-twitch', 'map': '3ds', + 'settings': dict(HINGE_GAP, video_openxr_edit_smoothing='0'), + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('mark', 'held'), ('wait', 1), + ('script', edit_aim_turned(x=0.06, pitch=0.5)), + ('mark', 'released'), ('wait', 1), ('mark', 'resting'), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('script', edit_aim_turned(x=0.06, pitch=-0.5)), + ('mark', 'released2'), ('wait', 1), ('mark', 'resting2'), + ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_twitch}, + {'name': 'edit-hinge-settle', 'map': '3ds', + 'settings': dict(HINGE_GAP, video_openxr_edit_smoothing='0'), + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06, y=-0.033), EDIT_GRIP)), + ('wait', 1), ('mark', 'held4'), ('wait', 1), + ('script', edit_aim(x=0.06, y=-0.033)), + ('mark', 'released'), ('wait', 1), ('mark', 'resting'), + ('script', script(edit_aim(x=0.06, y=-0.033), EDIT_GRIP)), + ('wait', 1), ('script', edit_aim(x=0.06, y=-0.033)), + ('mark', 'dropped'), ('wait', 1), + ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_settle}, + {'name': 'edit-hinge-shaken', 'map': '3ds', + 'settings': dict(HINGE_GAP, video_openxr_edit_smoothing='0'), + 'script': HEAD_AT_ORIGIN, + 'steps': edit_start(edit_aim()) + edit_tap(EDIT_NEXT, edit_aim()) + + edit_tap(EDIT_NEXT, edit_aim()) + [ + ('mark', 'grab'), + ('script', script(edit_aim(), EDIT_GRIP)), ('wait', 1), + ('script', script(edit_aim(x=0.06), EDIT_GRIP)), ('wait', 1), + ('mark', 'still'), + ('script', script(edit_aim(x=0.06, y=0.044), EDIT_GRIP)), + ('wait', 0.5)] + edit_shake(0.06, 0.044, 0.5, 2) + [ + ('mark', 'shaken1'), + ('script', script(edit_aim(x=0.06, y=-0.082), EDIT_GRIP)), + ('wait', 0.5)] + edit_shake(0.06, -0.082, 0.5, 2) + [ + ('mark', 'shaken2'), + ('script', edit_aim(x=0.06, y=-0.082)), ('mark', 'released'), + ('wait', 1), ('send', EDIT), ('wait', 2), ('mark', 'off'), + ('wait', 1)], + 'check': check_edit_hinge_shaken}, + {'name': 'edit-smooth', 'map': '3ds', + 'settings': dict(SAVE, video_openxr_edit_smoothing='2'), + 'steps': EDIT_SWAY_STEPS, 'check': check_edit_sway(True)}, + {'name': 'edit-smooth-off', 'map': '3ds', 'settings': SAVE, + 'steps': EDIT_SWAY_STEPS, 'check': check_edit_sway(False)}, + {'name': 'edit-tag', 'map': '3ds', + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, 'steps': EDIT_TAG_STEPS, + 'check': check_edit_tag}, + {'name': 'edit-tag-narrow', 'map': '3ds', + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10', + 'RA_XR_LAYER_MAX_WIDTH': '2048'}, + 'steps': EDIT_TAG_NARROW_STEPS, 'check': check_edit_tag_narrow}, + {'name': 'edit-back', 'map': '3ds', + 'settings': {'video_openxr_placement': '0'}, + 'env': {'RA_XR_LAYER_SNAP_EVERY': '10'}, 'steps': EDIT_BACK_STEPS, + 'check': check_edit_back}, +] +EDIT_CASES.append(dict( + next(c for c in EDIT_CASES if c['name'] == 'edit-hinge-3ds'), + name='edit-hinge-3ds-narrow', + env={'RA_XR_LAYER_SNAP_EVERY': '10', 'RA_XR_LAYER_MAX_WIDTH': '4096'}, + check=check_edit_hinge_3ds_narrow)) + + CASES = [ {'name': '3ds-stereo', 'map': '3ds', 'steps': SETTLE, 'check': check_screens()}, @@ -5046,6 +8663,9 @@ GL_EXTRA = [ ('mark', 'free'), ('script', 'busy off'), ('wait', 3)], 'check': check_tracked_busy}, ] +CASES += CUSTOM_CASES +CASES += EDIT_CASES + # The cases each GL driver's lanes run, by name in CASES or GL_EXTRA. GL_BASE = { 'glcore': ['3ds-stereo', '3ds-swap', '3ds-stereo-index0', @@ -5106,12 +8726,34 @@ GL_BASE = { 'tracked-reinit-local', 'tracked-reinit-local-hw', 'tracked-reinit-poses', 'tracked-reinit-paused-hw'], } +# The edit cases whose work is a host's or a driver's own: the plan and +# its edit count, the hold and hand-over, the outlines, the handle and +# the beam, fixed slots, the menu's quad. gl draws the menu in its +# window: no menu quad to place, outline or take. +GL_EDIT = ['edit-grab', 'edit-grab-threaded', 'edit-command', 'edit-follow', + 'edit-release-threaded', 'edit-release-moving', + 'edit-regrab-threaded', 'edit-handover-threaded', 'edit-reset', + 'edit-outlines', 'edit-outlines-flush', 'edit-beam', + 'edit-fallback', 'edit-no-map', 'edit-lazy', 'edit-two-hands', + 'edit-choose', 'edit-snap', 'edit-tag', 'edit-tag-narrow', + 'edit-back', 'edit-hinge-3ds', 'edit-hinge-shaken', + 'edit-hinge-3ds-narrow'] +GL_EDIT_MENU = ['edit-menu-front', 'edit-menu-size'] +for custom_driver in GL_BASE: + GL_BASE[custom_driver] += [c['name'] for c in CUSTOM_CASES] + GL_EDIT +GL_BASE['glcore'] += GL_EDIT_MENU + # Where a GL host's answer is its own, by driver. GL_CHECKS = { 'glcore': {'session-fails': check_session_fails_gl, 'hw-teardown': check_hw_teardown_gl, 'hw-teardown-threaded': threaded(check_hw_teardown_gl)}, - 'gl': {'session-fails': check_session_fails_gl, + 'gl': {'custom-repeat': check_custom('repeat', legacy=True), + 'custom-menu': check_custom('menu', legacy=True), + 'custom-tracked': check_custom('tracked', legacy=True), + 'no-map-custom': check_frame_custom(window=True), + 'crop-threaded-custom': threaded(check_window_crop), + 'session-fails': check_session_fails_gl, 'hw-teardown': check_hw_teardown_gl, 'hw-teardown-threaded': threaded(check_hw_teardown_gl), '3ds-stereo': check_screens(native=True), @@ -5140,16 +8782,28 @@ GL_CHECKS = { 'tracked-fallback': check_tracked_window_fallback, 'tracked-recenter': check_tracked_recenter_eyes, 'tracked-recenter-paused': - check_tracked_recenter_paused('local', screens=False)}, + check_tracked_recenter_paused('local', screens=False), + 'edit-grab': check_edit_grab(menu=False), + 'edit-grab-threaded': threaded(check_edit_grab(menu=False)), + 'edit-command': check_edit_command(menu=False), + 'edit-reset': check_edit_reset(menu=False), + 'edit-outlines': check_edit_outlines(menu=False), + 'edit-fallback': threaded(check_edit_fallback(menu=False)), + 'edit-choose': check_edit_choose(menu=False)}, } # What a lane cannot run: the unfocus helper sends an X event, and the -# Wayland lane's window is not an X one. -GL_SKIP = {'glcore-egl': ('focus',)} +# Wayland lane's window is not an X one. glcore's GLX lane runs only a +# smoke test of edit mode, whose work differs from EGL's in nothing but +# the binding. +GL_SKIP = {'glcore-egl': ('focus',), + 'glcore-glx': tuple(n for n in GL_EDIT + GL_EDIT_MENU + if n not in ('edit-grab', 'edit-command'))} # A lane that runs only these of its driver's cases: gl's other binding. GL_ONLY = {'gl-glx': ('3ds-stereo', 'no-map', 'menu', 'menu-closes', 'tracked', 'tracked-menu', 'tracked-recenter', 'tracked-reinit', 'pace-2', 'session-lost', - 'hw-teardown')} + 'hw-teardown') + tuple(c['name'] for c in CUSTOM_CASES) + + ('edit-grab',)} def gl_cases(): @@ -5182,6 +8836,8 @@ def gl_cases(): # gl's window shows the packed frame whole: the top screen is # its top-left quarter. if driver == 'gl': + if name == 'custom-repeat': + c['args'] = [] c['top_px'] = (400, 240) # gamescope gives a Wayland client its window back over half # a second after a reinit, where an X one takes some 40 ms: @@ -5274,6 +8930,7 @@ def main(): errors = case['check'](res) + capture_errors(res, case) if 'hung' in res.marks: errors.append('RetroArch did not quit') + errors += ['no frame showed ' + m for m in res.missed] if validate: extra, known = unexpected(res.log, baseline + case.get('baseline', [])) diff --git a/samples/openxr/e2e/test_run.py b/samples/openxr/e2e/test_run.py index 40e211c9ed..5c12e4c109 100644 --- a/samples/openxr/e2e/test_run.py +++ b/samples/openxr/e2e/test_run.py @@ -22,6 +22,18 @@ class GlLanes(unittest.TestCase): [{k: v for k, v in c.items() if k != 'check'} for c in run.CASES]) self.gl_cases = run.gl_cases() + def test_custom_selection(self): + names = ['custom-' + k for k in ('offset', 'rotation', 'hidden', + 'recenter', 'menu', 'tracked', 'repeat')] + selected, error = run.select(names, False, False) + self.assertIsNone(error) + self.assertEqual([c['name'] for c in selected], names) + for lane, driver, context in run.GL_LANES: + wanted = [lane + '-' + n for n in names] + selected, error = run.select(wanted, True, False) + self.assertIsNone(error) + self.assertEqual([c['name'] for c in selected], wanted) + def test_names(self): names = [c['name'] for c in self.gl_cases] self.assertEqual(len(set(names)), len(names)) @@ -107,7 +119,43 @@ class GlLanes(unittest.TestCase): def test_extra_cases_gl_only(self): names = {c['name'] for c in run.CASES} self.assertTrue(all(c['name'] not in names for c in run.GL_EXTRA)) - self.assertEqual(len(run.CASES), 141) + self.assertEqual(len(run.CASES), 213) + + def test_edit_lanes(self): + names = [c['name'] for c in self.gl_cases] + for n in ('edit-grab', 'edit-reset', 'edit-menu-front', + 'edit-follow', 'edit-no-map', 'edit-fallback'): + self.assertIn('glcore-egl-' + n, names) + for n in ('edit-grab', 'edit-reset', 'edit-outlines'): + self.assertIn('gl-egl-' + n, names) + self.assertIn('glcore-egl-edit-beam', names) + self.assertIn('gl-egl-edit-beam', names) + self.assertNotIn('glcore-glx-edit-beam', names) + self.assertIn('laser-beam-always', [c['name'] for c in run.CASES]) + self.assertIn('gl-glx-edit-grab', names) + self.assertIn('glcore-glx-edit-command', names) + self.assertNotIn('glcore-glx-edit-reset', names) + self.assertNotIn('gl-egl-edit-menu-front', names) + self.assertNotIn('gl-egl-edit-menu-size', names) + self.assertNotIn('glcore-egl-edit-tilt', names) + self.assertIn('glcore-egl-edit-choose', names) + self.assertIn('gl-egl-edit-choose', names) + self.assertNotIn('glcore-glx-edit-choose', names) + self.assertNotIn('glcore-egl-edit-handle-first', names) + self.assertIn('glcore-egl-edit-snap', names) + self.assertIn('gl-egl-edit-snap', names) + self.assertIn('glcore-egl-edit-tag', names) + self.assertIn('gl-egl-edit-tag', names) + self.assertNotIn('glcore-glx-edit-tag', names) + self.assertIn('glcore-egl-edit-tag-narrow', names) + self.assertIn('gl-egl-edit-tag-narrow', names) + self.assertIn('glcore-egl-edit-back', names) + self.assertIn('gl-egl-edit-back', names) + selected, error = run.select(['edit-grab', 'edit-pause'], False, + False) + self.assertIsNone(error) + self.assertEqual([c['name'] for c in selected], + ['edit-grab', 'edit-pause']) class Select(unittest.TestCase): diff --git a/samples/openxr/test_layer/xr_test_layer.c b/samples/openxr/test_layer/xr_test_layer.c index 528ef30ca0..bd372547f6 100644 --- a/samples/openxr/test_layer/xr_test_layer.c +++ b/samples/openxr/test_layer/xr_test_layer.c @@ -37,6 +37,8 @@ * RA_XR_LAYER_QUAD_INDEX0 1: quads show array layer 0 whatever their * imageArrayIndex, as SteamVR on the Steam * Frame does + * RA_XR_LAYER_MAX_WIDTH the runtime's maxSwapchainImageWidth, at most + * this; 0 or unset leaves it * * Script lines: * head the VIEW space's pose in LOCAL, yaw in degrees @@ -76,9 +78,11 @@ * action /[@left|@right] [] * an action's state while its set is synced: * boolean x != 0, float x, vector2f x y - * aim + * aim [] * that hand's pose spaces at p, -Z towards t, - * in LOCAL; in VIEW through the scripted head + * rolled roll degrees about the aim's own Z (+X + * towards +Y), in LOCAL; in VIEW through the + * scripted head * aim off that hand untracked * * A session on an OpenGL binding (XR_MNDX_egl_enable's EGL or @@ -93,7 +97,9 @@ * a later line of the same kind replaces them (head off, fail off). state * and space fire once per change of the file. Every read starts from no * actions, no aims and valid views: a script is the whole controller - * state. Action states and hand poses never come from the runtime. + * state. Action states and hand poses never come from the runtime. A + * synced set's actions all read bound; those the script leaves out, at + * rest. */ #include @@ -177,6 +183,7 @@ static struct char dir[1024]; unsigned snap_every; bool quad_index0; + uint32_t max_width; char script[1024]; struct timespec script_mtime; ino_t script_ino; @@ -258,6 +265,7 @@ static struct PFN_xrBeginFrame BeginFrame; PFN_xrEndFrame EndFrame; PFN_xrEnumerateViewConfigurationViews EnumerateViewConfigurationViews; + PFN_xrGetSystemProperties GetSystemProperties; XrInstance instance; bool frame; /* the application enabled FRAME_EXT */ @@ -315,6 +323,7 @@ static void layer_open(void) const char *snap = getenv("RA_XR_LAYER_SNAP_EVERY"); const char *scr = getenv("RA_XR_LAYER_SCRIPT"); const char *idx0 = getenv("RA_XR_LAYER_QUAD_INDEX0"); + const char *maxw = getenv("RA_XR_LAYER_MAX_WIDTH"); char path[1100]; if (dir && *dir && !L.out) { @@ -324,6 +333,7 @@ static void layer_open(void) } L.snap_every = snap ? (unsigned)strtoul(snap, NULL, 10) : 0; L.quad_index0 = idx0 && atoi(idx0) != 0; + L.max_width = maxw ? (uint32_t)strtoul(maxw, NULL, 10) : 0; if (scr) snprintf(L.script, sizeof(L.script), "%s", scr); } @@ -395,8 +405,16 @@ static bool set_synced(XrActionSet s) return false; } +/* Every action of a synced set is bound, as a runtime binds what each + * profile was suggested. Caller holds L.lock. */ +static bool action_bound(XrAction action) +{ + const struct action_rec *a = find_action(action); + return a && set_synced(a->set); +} + /* The script's value for an action asked for a hand, while its set is - * synced; NULL reads inactive. Caller holds L.lock. */ + * synced; NULL reads at rest. Caller holds L.lock. */ static const struct scripted *scripted_value(XrAction action, XrPath sub) { unsigned i; @@ -563,7 +581,9 @@ static void script_aim(const char *args) { char side[16]; float p[3], t[3], fx, fy, fz, len, yaw, pitch; - int hand; + float cy, sy, cp, sp, cr, sr; + float roll = 0.0f; + int hand, n; if (sscanf(args, "%15s", side) != 1 || !(hand = parse_hand(side))) return; if (strstr(args, "off")) @@ -571,8 +591,9 @@ static void script_aim(const char *args) L.aim_set[hand] = false; return; } - if (sscanf(args, "%15s %f %f %f %f %f %f", side, &p[0], &p[1], &p[2], - &t[0], &t[1], &t[2]) != 7) + n = sscanf(args, "%15s %f %f %f %f %f %f %f", side, &p[0], &p[1], &p[2], + &t[0], &t[1], &t[2], &roll); + if (n != 7 && n != 8) return; fx = t[0] - p[0]; fy = t[1] - p[1]; @@ -582,11 +603,19 @@ static void script_aim(const char *args) return; yaw = atan2f(-fx / len, -fz / len); pitch = asinf(fy / len); - /* Pitch about +X, then yaw about +Y: -Z points along (f). */ - L.aim[hand].orientation.x = cosf(yaw / 2.0f) * sinf(pitch / 2.0f); - L.aim[hand].orientation.y = sinf(yaw / 2.0f) * cosf(pitch / 2.0f); - L.aim[hand].orientation.z = -sinf(yaw / 2.0f) * sinf(pitch / 2.0f); - L.aim[hand].orientation.w = cosf(yaw / 2.0f) * cosf(pitch / 2.0f); + roll = roll * 3.14159265f / 180.0f; + /* Yaw about +Y, pitch about +X, roll about the aim's own Z: -Z + * points along (f). */ + cy = cosf(yaw / 2.0f); + sy = sinf(yaw / 2.0f); + cp = cosf(pitch / 2.0f); + sp = sinf(pitch / 2.0f); + cr = cosf(roll / 2.0f); + sr = sinf(roll / 2.0f); + L.aim[hand].orientation.x = cy * sp * cr + sy * cp * sr; + L.aim[hand].orientation.y = sy * cp * cr - cy * sp * sr; + L.aim[hand].orientation.z = cy * cp * sr - sy * sp * cr; + L.aim[hand].orientation.w = cy * cp * cr + sy * sp * sr; L.aim[hand].position.x = p[0]; L.aim[hand].position.y = p[1]; L.aim[hand].position.z = p[2]; @@ -1527,6 +1556,18 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_WaitFrame(XrSession session, return res; } +/* RA_XR_LAYER_MAX_WIDTH: the widest swapchain the runtime says it + * makes, at most this. */ +static XRAPI_ATTR XrResult XRAPI_CALL layer_GetSystemProperties( + XrInstance instance, XrSystemId system, XrSystemProperties *props) +{ + XrResult res = L.GetSystemProperties(instance, system, props); + if ( XR_SUCCEEDED(res) && L.max_width + && props->graphicsProperties.maxSwapchainImageWidth > L.max_width) + props->graphicsProperties.maxSwapchainImageWidth = L.max_width; + return res; +} + static XRAPI_ATTR XrResult XRAPI_CALL layer_EnumerateDisplayRefreshRatesFB( XrSession session, uint32_t cap, uint32_t *count, float *rates) { @@ -1899,7 +1940,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_GetActionStateBoolean(XrSession sess state->currentState = (s && s->v[0] != 0.0f) ? XR_TRUE : XR_FALSE; state->changedSinceLastSync = XR_FALSE; state->lastChangeTime = 0; - state->isActive = s ? XR_TRUE : XR_FALSE; + state->isActive = action_bound(gi->action) ? XR_TRUE + : XR_FALSE; pthread_mutex_unlock(&L.lock); return XR_SUCCESS; } @@ -1915,7 +1957,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_GetActionStateFloat(XrSession sessio state->currentState = s ? s->v[0] : 0.0f; state->changedSinceLastSync = XR_FALSE; state->lastChangeTime = 0; - state->isActive = s ? XR_TRUE : XR_FALSE; + state->isActive = action_bound(gi->action) ? XR_TRUE + : XR_FALSE; pthread_mutex_unlock(&L.lock); return XR_SUCCESS; } @@ -1932,7 +1975,8 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_GetActionStateVector2f(XrSession ses state->currentState.y = s ? s->v[1] : 0.0f; state->changedSinceLastSync = XR_FALSE; state->lastChangeTime = 0; - state->isActive = s ? XR_TRUE : XR_FALSE; + state->isActive = action_bound(gi->action) ? XR_TRUE + : XR_FALSE; pthread_mutex_unlock(&L.lock); return XR_SUCCESS; } @@ -2050,6 +2094,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_GetInstanceProcAddr(XrInstance insta HOOK(WaitFrame) HOOK(EndFrame) HOOK(EnumerateViewConfigurationViews) + HOOK(GetSystemProperties) HOOK(CreateActionSet) HOOK(DestroyActionSet) HOOK(CreateAction) @@ -2117,6 +2162,7 @@ static XRAPI_ATTR XrResult XRAPI_CALL layer_CreateApiLayerInstance( NEXT(BeginFrame); NEXT(EndFrame); NEXT(EnumerateViewConfigurationViews); + NEXT(GetSystemProperties); NEXT(PathToString); NEXT(CreateActionSet); NEXT(DestroyActionSet); diff --git a/samples/settings/video_screens/build.py b/samples/settings/video_screens/build.py index 9640aeb8ff..f2da0afa69 100644 --- a/samples/settings/video_screens/build.py +++ b/samples/settings/video_screens/build.py @@ -12,6 +12,8 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output-dir', type=Path, required=True) parser.add_argument('--build-only', action='store_true') + parser.add_argument('--runtime', action='store_true', + help='build opt-in XR output test frontend') parser.add_argument('--runner', help='executable runner, for example wine') args = parser.parse_args() root = Path(__file__).resolve().parents[3] @@ -47,6 +49,10 @@ def main(): cmd[cmd.index(flag) + 1] = str(obj.with_suffix('.d') if flag == '-MF' else obj) if rename: cmd.append('-Dmain=rarch_harness_unused_main') + if args.runtime: + cmd.append('-Drunloop_iterate=video_screens_test_iterate') + elif args.runtime: + cmd.append('-DVIDEO_SCREENS_RUNTIME_TEST') subprocess.run(cmd, cwd=root, check=True) cmd = [] for token in link_cmd: @@ -57,7 +63,7 @@ def main(): cmd[cmd.index('-o') + 1] = str(binary) subprocess.run(cmd, cwd=root, check=True) print('built ' + str(binary), flush=True) - if not args.build_only: + if not args.build_only and not args.runtime: env = os.environ.copy() for name, directory in (('XDG_CONFIG_HOME', 'config'), ('XDG_CACHE_HOME', 'cache'), ('XDG_DATA_HOME', 'data'), ('XDG_STATE_HOME', 'state')): diff --git a/samples/settings/video_screens/video_screens_test.c b/samples/settings/video_screens/video_screens_test.c index 2368646c2a..7e48f3cc4f 100644 --- a/samples/settings/video_screens/video_screens_test.c +++ b/samples/settings/video_screens/video_screens_test.c @@ -1,5 +1,5 @@ -/* Exercises the screens' own windows through shipping configuration - * and video objects. */ +/* Exercises indexed XR settings and the screens' own windows through + * shipping configuration and video objects. */ #include #include #include @@ -27,6 +27,8 @@ #include "../../../gfx/video_driver.h" #include "../../../gfx/video_thread_wrapper.h" #include "../../../input/input_driver.h" +#include "../../../input/input_defines.h" +#include "../../../input/input_remapping.h" #ifdef HAVE_THREADS #include #include @@ -181,6 +183,984 @@ static void test_window_config(void) CHECK(settings->uints.video_screen_window_dims[2] == VIDEO_SCALE_PACK(65535, 65535)); } +static void test_xr_config_roundtrip(void) +{ + settings_t *settings = config_get_ptr(); + unsigned i; + load_fixture( + "video_screen1_xr_x = \"1.5\"\n" + "video_screen1_xr_y = \"-2\"\n" + "video_screen1_xr_z = \"3\"\n" + "video_screen1_xr_yaw = \"90\"\n" + "video_screen1_xr_pitch = \"-45\"\n" + "video_screen1_xr_roll = \"180\"\n" + "video_screen1_xr_scale = \"2\"\n" + "video_screen1_xr_visible = \"false\"\n" + "video_screen2_xr_x = \"0.25\"\n" + "video_screen8_xr_y = \"4\"\n" + "video_screen8_xr_yaw = \"-30\"\n"); + CHECK(settings->floats.video_screen_xr_x[0] == 1.5f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.25f); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[7]); + settings->bools.config_save_minimal = false; + CHECK(config_save_file(saved_path)); + path_set(RARCH_PATH_CONFIG, saved_path); + config_load(); + CHECK(settings->floats.video_screen_xr_x[0] == 1.5f); + CHECK(settings->floats.video_screen_xr_y[0] == -2.0f); + CHECK(settings->floats.video_screen_xr_z[0] == 3.0f); + CHECK(settings->floats.video_screen_xr_yaw[0] == 90.0f); + CHECK(settings->floats.video_screen_xr_pitch[0] == -45.0f); + CHECK(settings->floats.video_screen_xr_roll[0] == 180.0f); + CHECK(settings->floats.video_screen_xr_scale[0] == 2.0f); + CHECK(!settings->bools.video_screen_xr_visible[0]); + CHECK(settings->floats.video_screen_xr_y[7] == 4.0f); + CHECK(settings->floats.video_screen_xr_yaw[7] == -30.0f); + config_reset_video_screen_xr(settings, 0, 1); + CHECK(settings->floats.video_screen_xr_x[1] == 0.25f); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[7]); + settings->floats.video_screen_xr_x[0] = 3.0f; + config_reset_video_screen_xr(settings, 1, 1); + CHECK(settings->floats.video_screen_xr_x[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_x[0] == 3.0f); + config_reset_video_screen_xr(settings, RETRO_VIDEO_VIEWS_MAX, UINT_MAX); + CHECK(settings->floats.video_screen_xr_y[7] == 4.0f); + config_reset_video_screen_xr(settings, 7, UINT_MAX); + CHECK(settings->floats.video_screen_xr_y[7] == 0.0f); + CHECK(settings->floats.video_screen_xr_x[0] == 3.0f); + config_reset_video_screen_xr(settings, 0, RETRO_VIDEO_VIEWS_MAX); + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + { + CHECK(settings->floats.video_screen_xr_x[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_y[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_z[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_yaw[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_pitch[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_roll[i] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[i] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[i]); + } + + write_config(override_path, "video_screen2_xr_x = \"0.5\"\n"); + path_set(RARCH_PATH_CONFIG_APPEND, override_path); + config_load(); + CHECK(settings->floats.video_screen_xr_x[1] == 0.5f); + write_config(fixture_path, "video_screen2_xr_x = \"0.75\"\n"); + CHECK(config_load_override_file(fixture_path)); + CHECK(settings->floats.video_screen_xr_x[1] == 0.75f); + CHECK(config_unload_override()); + CHECK(settings->floats.video_screen_xr_x[1] == 0.5f); + path_clear(RARCH_PATH_CONFIG_APPEND); +} + +static void test_xr_minimal_save(void) +{ + static const char *fields[] = {"x", "y", "z", "yaw", "pitch", "roll", "scale", "visible"}; + settings_t *settings = config_get_ptr(); + config_file_t *conf; + unsigned i, j; + char key[64]; + float value = 0.0f; + bool visible = true; + load_fixture("config_save_minimal = \"true\"\n"); + CHECK(config_save_file(saved_path)); + conf = config_file_new_from_path_to_string(saved_path); + CHECK(conf != NULL); + if (conf) + { + for (i = 1; i <= RETRO_VIDEO_VIEWS_MAX; i++) + for (j = 0; j < sizeof(fields) / sizeof(fields[0]); j++) + { + snprintf(key, sizeof(key), "video_screen%u_xr_%s", i, fields[j]); + CHECK(config_get_entry(conf, key) == NULL); + } + config_file_free(conf); + } + settings->floats.video_screen_xr_x[1] = 0.25f; + settings->bools.video_screen_xr_visible[7] = false; + CHECK(config_save_file(saved_path)); + conf = config_file_new_from_path_to_string(saved_path); + CHECK(conf != NULL); + if (conf) + { + CHECK(config_get_float(conf, "video_screen2_xr_x", &value)); + CHECK(value == 0.25f); + CHECK(config_get_bool(conf, "video_screen8_xr_visible", &visible)); + CHECK(!visible); + CHECK(config_get_entry(conf, "video_screen8_xr_scale") == NULL); + config_file_free(conf); + } + path_set(RARCH_PATH_CONFIG, saved_path); + config_load(); + CHECK(settings->floats.video_screen_xr_x[1] == 0.25f); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + CHECK(!settings->bools.video_screen_xr_visible[7]); +} + +static void test_xr_override_unload(void) +{ + settings_t *settings = config_get_ptr(); + static rarch_system_info_t system_info; + config_file_t *conf; + float value = -1.0f; + bool visible = false; + unsigned minimal; + memset(&system_info, 0, sizeof(system_info)); + system_info.info.library_name = "video_screens_test"; + path_set(RARCH_PATH_BASENAME, "screen_game.rom"); + for (minimal = 0; minimal < 2; minimal++) + { + load_fixture(minimal + ? "config_save_minimal = \"true\"\nvideo_screen1_xr_x = \"1.5\"\nvideo_screen8_xr_scale = \"2\"\nvideo_screen8_xr_visible = \"false\"\n" + : "config_save_minimal = \"false\"\nvideo_screen1_xr_x = \"1.5\"\nvideo_screen8_xr_scale = \"2\"\nvideo_screen8_xr_visible = \"false\"\n"); + write_config(override_path, + "video_screen1_xr_x = \"0\"\n" + "video_screen2_xr_y = \"-3\"\n" + "video_screen8_xr_scale = \"1\"\n" + "video_screen8_xr_visible = \"true\"\n"); + CHECK(config_load_override_file(override_path)); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_y[1] == -3.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[7]); + CHECK(config_save_overrides(OVERRIDE_GAME, &system_info, false, NULL) == 1); + strlcpy(generated_path, path_get(RARCH_PATH_CONFIG_OVERRIDE), sizeof(generated_path)); + conf = config_file_new_from_path_to_string(generated_path); + CHECK(conf != NULL); + if (conf) + { + CHECK(config_get_float(conf, "video_screen1_xr_x", &value)); + CHECK(value == 0.0f); + CHECK(config_get_float(conf, "video_screen8_xr_scale", &value)); + CHECK(value == 1.0f); + CHECK(config_get_bool(conf, "video_screen8_xr_visible", &visible)); + CHECK(visible); + CHECK(config_get_entry(conf, "video_screen1_xr_scale") == NULL); + config_file_free(conf); + } + CHECK(config_unload_override()); + CHECK(settings->floats.video_screen_xr_x[0] == 1.5f); + CHECK(settings->floats.video_screen_xr_y[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 2.0f); + CHECK(!settings->bools.video_screen_xr_visible[7]); + CHECK(config_load_override_file(generated_path)); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_y[1] == -3.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[7]); + CHECK(config_unload_override()); + CHECK(settings->floats.video_screen_xr_x[0] == 1.5f); + CHECK(settings->floats.video_screen_xr_y[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 2.0f); + CHECK(!settings->bools.video_screen_xr_visible[7]); + } + path_clear(RARCH_PATH_BASENAME); +} + +static void test_xr_config_invalid(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture( + "video_screen1_xr_x = \"100\"\n" + "video_screen1_xr_y = \"-100\"\n" + "video_screen1_xr_z = \"nan\"\n" + "video_screen1_xr_yaw = \"999\"\n" + "video_screen1_xr_pitch = \"-999\"\n" + "video_screen1_xr_roll = \"inf\"\n" + "video_screen1_xr_scale = \"-1\"\n" + "video_screen1_xr_visible = \"false\"\n" + "video_screen2_xr_x = \"nan\"\n" + "video_screen2_xr_y = \"inf\"\n" + "video_screen2_xr_z = \"-inf\"\n" + "video_screen2_xr_yaw = \"nan\"\n" + "video_screen2_xr_pitch = \"inf\"\n" + "video_screen2_xr_roll = \"-inf\"\n" + "video_screen2_xr_scale = \"nan\"\n" + "video_screen8_xr_scale = \"999\"\n" + "video_screen9_xr_x = \"5\"\n"); + CHECK(settings->floats.video_screen_xr_x[0] == 10.0f); + CHECK(settings->floats.video_screen_xr_y[0] == -10.0f); + CHECK(settings->floats.video_screen_xr_z[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_yaw[0] == 180.0f); + CHECK(settings->floats.video_screen_xr_pitch[0] == -180.0f); + CHECK(settings->floats.video_screen_xr_roll[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[0] == 0.1f); + CHECK(!settings->bools.video_screen_xr_visible[0]); + CHECK(settings->floats.video_screen_xr_x[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_y[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_z[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_yaw[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_pitch[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_roll[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[1] == 1.0f); + CHECK(settings->floats.video_screen_xr_scale[7] == 10.0f); + CHECK(settings->floats.video_screen_xr_x[7] == 0.0f); +} + +static void test_xr_menu_place_config(void) +{ + static rarch_system_info_t system_info; + settings_t *settings = config_get_ptr(); + config_file_t *conf; + float value = 0.0f; + bool placed = true; + + load_fixture( + "video_openxr_menu_xr_x = \"0.5\"\n" + "video_openxr_menu_xr_z = \"-0.25\"\n" + "video_openxr_menu_xr_yaw = \"30\"\n" + "video_openxr_menu_xr_pitch = \"-15\"\n" + "video_openxr_menu_xr_roll = \"45\"\n" + "video_openxr_menu_xr_scale = \"0.75\"\n" + "video_openxr_menu_placed = \"true\"\n"); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.5f); + CHECK(settings->floats.video_openxr_menu_xr_z == -0.25f); + CHECK(settings->floats.video_openxr_menu_xr_yaw == 30.0f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 0.75f); + CHECK(settings->bools.video_openxr_menu_placed); + settings->bools.config_save_minimal = false; + CHECK(config_save_file(saved_path)); + path_set(RARCH_PATH_CONFIG, saved_path); + config_load(); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.5f); + CHECK(settings->floats.video_openxr_menu_xr_z == -0.25f); + CHECK(settings->floats.video_openxr_menu_xr_yaw == 30.0f); + CHECK(settings->floats.video_openxr_menu_xr_pitch == -15.0f); + CHECK(settings->floats.video_openxr_menu_xr_roll == 45.0f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 0.75f); + CHECK(settings->bools.video_openxr_menu_placed); + + /* Minimal: only what differs from the defaults. */ + load_fixture("config_save_minimal = \"true\"\nvideo_openxr_menu_xr_y = \"1.5\"\n"); + CHECK(!settings->bools.video_openxr_menu_placed); + CHECK(config_save_file(saved_path)); + conf = config_file_new_from_path_to_string(saved_path); + CHECK(conf != NULL); + if (conf) + { + CHECK(config_get_float(conf, "video_openxr_menu_xr_y", &value)); + CHECK(value == 1.5f); + CHECK(config_get_entry(conf, "video_openxr_menu_xr_x") == NULL); + CHECK(config_get_entry(conf, "video_openxr_menu_xr_scale") == NULL); + CHECK(config_get_entry(conf, "video_openxr_menu_placed") == NULL); + config_file_free(conf); + } + + /* Out of range clamps, non-finite takes the default. */ + load_fixture( + "video_openxr_menu_xr_x = \"100\"\n" + "video_openxr_menu_xr_roll = \"nan\"\n" + "video_openxr_menu_xr_scale = \"0\"\n"); + CHECK(settings->floats.video_openxr_menu_xr_x == 10.0f); + CHECK(settings->floats.video_openxr_menu_xr_roll == 0.0f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 0.1f); + + /* A game override's place, and the base's back on unload. */ + memset(&system_info, 0, sizeof(system_info)); + system_info.info.library_name = "video_screens_test"; + path_set(RARCH_PATH_BASENAME, "menu_game.rom"); + load_fixture("config_save_minimal = \"true\"\n" + "video_openxr_menu_placed = \"true\"\n" + "video_openxr_menu_xr_x = \"1\"\n"); + write_config(override_path, + "video_openxr_menu_placed = \"false\"\n" + "video_openxr_menu_xr_x = \"0\"\n" + "video_openxr_menu_xr_pitch = \"-20\"\n"); + settings->floats.video_openxr_menu_xr_y = 2.0f; + CHECK(config_load_override_file(override_path)); + CHECK(!settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_pitch == -20.0f); + /* In neither file: the default. */ + CHECK(settings->floats.video_openxr_menu_xr_y == 0.0f); + CHECK(config_save_overrides(OVERRIDE_GAME, &system_info, false, NULL) == 1); + strlcpy(generated_path, path_get(RARCH_PATH_CONFIG_OVERRIDE), sizeof(generated_path)); + conf = config_file_new_from_path_to_string(generated_path); + CHECK(conf != NULL); + if (conf) + { + CHECK(config_get_bool(conf, "video_openxr_menu_placed", &placed)); + CHECK(!placed); + CHECK(config_get_float(conf, "video_openxr_menu_xr_pitch", &value)); + CHECK(value == -20.0f); + CHECK(config_get_entry(conf, "video_openxr_menu_xr_scale") == NULL); + config_file_free(conf); + } + CHECK(config_unload_override()); + CHECK(settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_x == 1.0f); + CHECK(settings->floats.video_openxr_menu_xr_pitch == 0.0f); + path_clear(RARCH_PATH_BASENAME); + + config_reset_video_menu_xr(settings); + CHECK(!settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.0f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 1.0f); +} + +/* A presented map fixture, as consumed by the shipping menu. */ +static void set_menu_map(unsigned screens) +{ + video_driver_state_t *video_st = video_state_get_ptr(); + unsigned i; + memset(&video_st->views_core, 0, sizeof(video_st->views_core)); + for (i = 0; i < screens; i++) + { + struct retro_video_view *view = &video_st->views_core.views[i]; + view->screen = i; + view->width = 100; + view->height = 100; + } + video_st->views_core.num_views = screens; + video_st->views_core.num_screens = screens; + video_st->views_presented = screens != 0; + video_st->views_frame_dims = VIDEO_SCALE_PACK(100, 100); +} + +#ifdef HAVE_OPENXR +static bool menu_has_setting(const char *name) +{ + settings_t *settings = config_get_ptr(); + file_list_t *list = (file_list_t*)calloc(1, sizeof(*list)); + bool found = false; + size_t i; + CHECK(list != NULL); + if (!list) + return false; + menu_displaylist_build_list(list, settings, + DISPLAYLIST_VIDEO_STEREO_SETTINGS_LIST, false); + for (i = 0; i < list->size; i++) + if (list->list[i].label && !strcmp(list->list[i].label, name)) + found = true; + file_list_free(list); + return found; +} + +/* Whether the headset list shows name right after prev. */ +static bool menu_setting_follows(const char *name, const char *prev) +{ + settings_t *settings = config_get_ptr(); + file_list_t *list = (file_list_t*)calloc(1, sizeof(*list)); + bool follows = false; + size_t i; + CHECK(list != NULL); + if (!list) + return false; + menu_displaylist_build_list(list, settings, + DISPLAYLIST_VIDEO_STEREO_SETTINGS_LIST, false); + for (i = 1; i < list->size; i++) + if ( list->list[i].label && list->list[i - 1].label + && !strcmp(list->list[i].label, name) + && !strcmp(list->list[i - 1].label, prev)) + follows = true; + file_list_free(list); + return follows; +} + +#endif + +static void menu_action(const char *name, unsigned action) +{ + rarch_setting_t *setting = menu_setting_find(name); + CHECK(setting != NULL); + if (setting) + CHECK(menu_action_handle_setting(setting, 0, action, false) == 0); +} + +static void menu_value(const char *name, const char *value) +{ + rarch_setting_t *setting = menu_setting_find(name); + char text[64]; + CHECK(setting != NULL); + if (!setting) + return; + text[0] = '\0'; + CHECK(setting->actions->repr != NULL); + if (setting->actions->repr) + setting->actions->repr(setting, text, sizeof(text)); + CHECK(!strcmp(text, value)); +} + +static void test_xr_screen_menu(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture("video_driver = \"glcore\"\n"); + set_menu_map(2); +#ifdef HAVE_OPENXR + CHECK(menu_has_setting("video_screen_selected")); +#endif + menu_action("video_screen_selected", MENU_ACTION_RIGHT); + menu_value("video_screen_selected", "2"); + CHECK(menu_state_get_ptr()->video_screen_selected == 1u); + settings->flags &= ~SETTINGS_FLG_MODIFIED; + menu_action("video_screen_xr_x", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.1f); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + menu_value("video_screen_xr_x", "0.1 m"); + settings->floats.video_screen_xr_y[1] = -1.5f; + menu_value("video_screen_xr_y", "-1.5 m"); + menu_action("video_screen_xr_z", MENU_ACTION_LEFT); + CHECK(settings->floats.video_screen_xr_z[1] == -0.1f); + menu_value("video_screen_xr_z", "-0.1 m"); + menu_action("video_screen_xr_pitch", MENU_ACTION_LEFT); + CHECK(settings->floats.video_screen_xr_pitch[1] == -1.0f); + menu_value("video_screen_xr_pitch", "-1 deg"); + menu_action("video_screen_xr_roll", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_roll[1] == 1.0f); + menu_value("video_screen_xr_roll", "1 deg"); + menu_action("video_screen_xr_yaw", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_yaw[1] == 1.0f); + menu_value("video_screen_xr_yaw", "1 deg"); + menu_action("video_screen_xr_scale", MENU_ACTION_RIGHT); + menu_value("video_screen_xr_scale", "1.1"); + menu_action("video_screen_xr_visible", MENU_ACTION_RIGHT); + CHECK(!settings->bools.video_screen_xr_visible[1]); + settings->bools.video_screen_xr_visible[0] = false; +#ifdef HAVE_OPENXR + CHECK(menu_has_setting("video_screen_selected")); + CHECK(menu_has_setting("video_screen_xr_reset")); + CHECK(menu_has_setting("video_screen_xr_reset_all")); +#endif + settings->floats.video_screen_xr_scale[7] = 3.0f; + settings->flags &= ~SETTINGS_FLG_MODIFIED; + menu_action("video_screen_xr_reset", MENU_ACTION_OK); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.0f); + CHECK(menu_state_get_ptr()->video_screen_selected == 1u); + menu_value("video_screen_selected", "2"); + CHECK(settings->floats.video_screen_xr_y[1] == 0.0f); + CHECK(settings->floats.video_screen_xr_scale[1] == 1.0f); + CHECK(settings->bools.video_screen_xr_visible[1]); + CHECK(!settings->bools.video_screen_xr_visible[0]); + CHECK(settings->floats.video_screen_xr_scale[7] == 3.0f); + settings->flags &= ~SETTINGS_FLG_MODIFIED; + menu_action("video_screen_xr_reset_all", MENU_ACTION_OK); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + CHECK(settings->bools.video_screen_xr_visible[0]); + CHECK(settings->floats.video_screen_xr_scale[7] == 1.0f); + settings->bools.menu_navigation_wraparound_enable = false; + settings->floats.video_screen_xr_x[1] = 10.0f; + menu_action("video_screen_xr_x", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_x[1] == 10.0f); + settings->floats.video_screen_xr_yaw[1] = -180.0f; + menu_action("video_screen_xr_yaw", MENU_ACTION_LEFT); + CHECK(settings->floats.video_screen_xr_yaw[1] == -180.0f); + settings->floats.video_screen_xr_scale[1] = 0.1f; + menu_action("video_screen_xr_scale", MENU_ACTION_LEFT); + CHECK(settings->floats.video_screen_xr_scale[1] == 0.1f); + menu_action("video_screen_xr_x", MENU_ACTION_START); + CHECK(settings->floats.video_screen_xr_x[1] == 0.0f); + CHECK(menu_state_get_ptr()->video_screen_selected == 1u); +} + +static void test_xr_menu_map_change(void) +{ + settings_t *settings = config_get_ptr(); + video_driver_state_t *video_st = video_state_get_ptr(); + rarch_setting_t *x; + load_fixture("video_driver = \"glcore\"\n"); + set_menu_map(2); + settings->floats.video_screen_xr_x[1] = 4.0f; + video_st->views_core.views[0].screen = 1; + video_st->views_core.views[1].screen = 0; + menu_action("video_screen_xr_x", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_x[1] == 4.1f); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + x = menu_setting_find("video_screen_xr_x"); + CHECK(x != NULL); + set_menu_map(1); + if (x) + CHECK(menu_action_handle_setting(x, 0, MENU_ACTION_RIGHT, false) == 0); + menu_value("video_screen_selected", "1"); + CHECK(menu_state_get_ptr()->video_screen_selected == 0u); + CHECK(settings->floats.video_screen_xr_x[0] == 0.1f); + CHECK(settings->floats.video_screen_xr_x[1] == 4.1f); + set_menu_map(0); + { + rarch_setting_t *setting = menu_setting_find("video_screen_xr_x"); + CHECK(setting != NULL); + /* Without a map the whole frame is screen 1. */ + if (setting) + CHECK(menu_action_handle_setting(setting, 0, MENU_ACTION_RIGHT, false) == 0); + CHECK(menu_state_get_ptr()->video_screen_selected == 0u); + CHECK(fabs(settings->floats.video_screen_xr_x[0] - 0.2f) < 1e-6); + CHECK(settings->floats.video_screen_xr_x[1] == 4.1f); + } + menu_value("video_screen_selected", "1"); + menu_action("video_screen_selected", MENU_ACTION_RIGHT); + menu_value("video_screen_selected", "1"); +#ifdef HAVE_OPENXR + CHECK(menu_has_setting("video_screen_selected")); + CHECK(menu_has_setting("video_screen_xr_x")); + CHECK(menu_has_setting("video_screen_xr_visible")); + CHECK(menu_has_setting("video_screen_xr_reset")); + CHECK(menu_has_setting("video_openxr_distance")); + CHECK(menu_has_setting("video_screen_xr_reset_all")); +#endif +} + +/* Reset All Screens puts the menu back in front of screen 1. */ +static void test_xr_menu_reset_all(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_menu_placed = \"true\"\n" + "video_openxr_menu_xr_x = \"2\"\n"); + set_menu_map(1); + CHECK(settings->bools.video_openxr_menu_placed); + menu_action("video_screen_xr_reset_all", MENU_ACTION_OK); + CHECK(!settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.0f); +} + +/* Screen 2 moved, turned, scaled and hidden, as before each reset. */ +static void xr_edit_screen2_moved(settings_t *settings) +{ + settings->floats.video_screen_xr_x[1] = 1.5f; + settings->floats.video_screen_xr_y[1] = -0.5f; + settings->floats.video_screen_xr_z[1] = 2.0f; + settings->floats.video_screen_xr_yaw[1] = 30.0f; + settings->floats.video_screen_xr_pitch[1] = -10.0f; + settings->floats.video_screen_xr_roll[1] = 5.0f; + settings->floats.video_screen_xr_scale[1] = 2.5f; + settings->bools.video_screen_xr_visible[1] = false; + settings->flags &= ~SETTINGS_FLG_MODIFIED; +} + +/* Edit mode's writes are the menu rows' writes: the same settings, the + * same saved keys, clamped as a loaded value is; visibility stays the + * menu's. Its reset is Reset Screen's, or the menu's own. */ +static void test_xr_edit_writes(void) +{ + settings_t *settings = config_get_ptr(); + video_screen_xr_t values, got; + config_file_t *conf; + float row_x, row_yaw, row_scale; + float value = 0.0f; + + load_fixture("video_driver = \"glcore\"\nconfig_save_minimal = \"true\"\n"); + set_menu_map(2); + menu_state_get_ptr()->video_screen_selected = 1; + menu_action("video_screen_xr_x", MENU_ACTION_RIGHT); + menu_action("video_screen_xr_yaw", MENU_ACTION_LEFT); + menu_action("video_screen_xr_scale", MENU_ACTION_RIGHT); + row_x = settings->floats.video_screen_xr_x[1]; + row_yaw = settings->floats.video_screen_xr_yaw[1]; + row_scale = settings->floats.video_screen_xr_scale[1]; + + load_fixture("video_driver = \"glcore\"\nconfig_save_minimal = \"true\"\n"); + settings->flags &= ~SETTINGS_FLG_MODIFIED; + video_views_xr_reset(&values, 1); + values.x = row_x; + values.yaw = row_yaw; + values.scale = row_scale; + values.visible = false; + config_set_video_screen_xr(1, &values); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + CHECK(settings->floats.video_screen_xr_x[1] == row_x); + CHECK(settings->floats.video_screen_xr_yaw[1] == row_yaw); + CHECK(settings->floats.video_screen_xr_scale[1] == row_scale); + CHECK(settings->bools.video_screen_xr_visible[1]); + CHECK(settings->floats.video_screen_xr_x[0] == 0.0f); + config_get_video_screen_xr(1, &got); + CHECK(got.x == row_x && got.yaw == row_yaw && got.scale == row_scale + && got.visible); + CHECK(config_save_file(saved_path)); + conf = config_file_new_from_path_to_string(saved_path); + CHECK(conf != NULL); + if (conf) + { + CHECK(config_get_float(conf, "video_screen2_xr_x", &value)); + CHECK(value == row_x); + CHECK(config_get_float(conf, "video_screen2_xr_yaw", &value)); + CHECK(value == row_yaw); + CHECK(config_get_float(conf, "video_screen2_xr_scale", &value)); + CHECK(value == row_scale); + CHECK(config_get_entry(conf, "video_screen2_xr_visible") == NULL); + config_file_free(conf); + } + + values.x = 12.0f; + values.scale = 0.0f; + config_set_video_screen_xr(1, &values); + CHECK(settings->floats.video_screen_xr_x[1] == 10.0f); + CHECK(settings->floats.video_screen_xr_scale[1] == 0.1f); + + /* Reset: what Reset Screen leaves, hidden screens shown again. */ + set_menu_map(2); + menu_state_get_ptr()->video_screen_selected = 1; + xr_edit_screen2_moved(settings); + menu_action("video_screen_xr_reset", MENU_ACTION_OK); + config_get_video_screen_xr(1, &values); + xr_edit_screen2_moved(settings); + config_set_video_screen_xr(1, NULL); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + config_get_video_screen_xr(1, &got); + CHECK(got.x == values.x && got.y == values.y && got.z == values.z + && got.yaw == values.yaw && got.pitch == values.pitch + && got.roll == values.roll && got.scale == values.scale + && got.visible == values.visible); + CHECK(got.visible && got.scale == 1.0f && got.z == 0.0f); + + video_views_xr_reset(&values, 1); + values.x = 0.5f; + values.scale = 0.75f; + settings->flags &= ~SETTINGS_FLG_MODIFIED; + CHECK(!config_get_video_menu_xr(&got)); + config_set_video_menu_xr(&values); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + CHECK(settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.5f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 0.75f); + CHECK(config_get_video_menu_xr(&got) && got.x == 0.5f + && got.scale == 0.75f); + settings->flags &= ~SETTINGS_FLG_MODIFIED; + config_set_video_menu_xr(NULL); + CHECK(settings->flags & SETTINGS_FLG_MODIFIED); + CHECK(!settings->bools.video_openxr_menu_placed); + CHECK(settings->floats.video_openxr_menu_xr_x == 0.0f); + CHECK(settings->floats.video_openxr_menu_xr_scale == 1.0f); + CHECK(!config_get_video_menu_xr(&got)); +} + +#ifdef HAVE_OPENXR +static unsigned menu_test_headset_state(void *user) +{ + return *(unsigned*)user; +} + +static unsigned menu_test_headset_transition(void *user) +{ + unsigned *state = (unsigned*)user; + unsigned current = *state; + *state ^= VIDEO_XR_HEADSET_TRACKING; + return current; +} + +static bool menu_test_headset_space(void *user, + enum retro_vr_reference_space *space, unsigned *width, unsigned *height) +{ + *width = 100; + *height = 100; + return true; +} + +static bool menu_test_headset_content(void *data, + const video_vr_content_info_t *info) +{ + return true; +} + +static void test_xr_menu_tracked_screen(void) +{ + settings_t *settings = config_get_ptr(); + video_driver_state_t *video_st = video_state_get_ptr(); + video_driver_t *saved_driver = video_st->current_video; + static video_driver_t driver; + video_xr_host_t host; + struct retro_video_views views; + unsigned state = VIDEO_XR_HEADSET_TRACKING; + memset(&driver, 0, sizeof(driver)); + driver.set_vr_content_info = menu_test_headset_content; + memset(&host, 0, sizeof(host)); + host.state = menu_test_headset_state; + host.set_space = menu_test_headset_space; + host.user = &state; + video_st->current_video = &driver; + video_driver_set_xr_host(&host); + load_fixture("video_driver = \"glcore\"\n"); + set_menu_map(3); + video_st->views_core.views[0].eye = RETRO_VIDEO_VIEW_EYE_LEFT; + video_st->views_core.views[1].eye = RETRO_VIDEO_VIEW_EYE_RIGHT; + video_st->views_core.views[1].screen = 0; + video_st->views_core.views[2].screen = 1; + video_st->views_core.num_screens = 2; + memset(&views, 0, sizeof(views)); + views.views = video_st->views_core.views; + views.num_views = 3; + views.flags = RETRO_VIDEO_VIEWS_FLAG_TRACKED; + CHECK(video_driver_set_views(&views)); + CHECK(video_driver_vr_content_active()); + menu_state_get_ptr()->video_screen_selected = 0; + settings->floats.video_screen_xr_x[0] = 2.0f; + menu_value("video_screen_selected", "2"); + menu_action("video_screen_selected", MENU_ACTION_RIGHT); + menu_value("video_screen_selected", "2"); + menu_action("video_screen_xr_x", MENU_ACTION_RIGHT); + CHECK(settings->floats.video_screen_xr_x[0] == 2.0f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.1f); + CHECK(menu_has_setting("video_screen_xr_x")); + video_st->views_core.num_views = 2; + video_st->views_core.num_screens = 1; + CHECK(!menu_has_setting("video_screen_selected")); + CHECK(!menu_has_setting("video_screen_xr_x")); + CHECK(!menu_has_setting("video_screen_xr_reset")); + CHECK(menu_has_setting("video_openxr_distance")); + CHECK(menu_has_setting("video_screen_xr_reset_all")); + CHECK(settings->floats.video_screen_xr_x[0] == 2.0f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.1f); + state = 0; + CHECK(menu_has_setting("video_screen_selected")); + menu_value("video_screen_selected", "1"); + { + rarch_setting_t *selector = menu_setting_find("video_screen_selected"); + unsigned i; + CHECK(selector != NULL); + host.state = menu_test_headset_transition; + video_driver_set_xr_host(&host); + if (selector) + for (i = 0; i < 4; i++) + { + state = 0; + CHECK(menu_action_handle_setting(selector, 0, + MENU_ACTION_RIGHT, false) == 0); + CHECK(menu_state_get_ptr()->video_screen_selected == 0u); + state = VIDEO_XR_HEADSET_TRACKING; + CHECK(menu_action_handle_setting(selector, 0, + MENU_ACTION_LEFT, false) == -1); + CHECK(menu_state_get_ptr()->video_screen_selected == 0u); + } + CHECK(settings->floats.video_screen_xr_x[0] == 2.0f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.1f); + } + video_driver_vr_content_clear(); + video_driver_set_xr_host(NULL); + video_st->current_video = saved_driver; +} + +/* Edit Screens, While Editing Screens and Snap to Screens show with + * Headset Output on; the hotkey exists; without a session the row is + * refused and the menu stays. */ +static void test_xr_edit_rows(void) +{ + settings_t *settings = config_get_ptr(); + const char *base = NULL; + unsigned i; + load_fixture("video_driver = \"glcore\"\nvideo_openxr_enable = \"false\"\n"); + set_menu_map(1); + CHECK(!menu_has_setting("video_screen_xr_edit")); + CHECK(!menu_has_setting("video_openxr_edit_pause")); + CHECK(!menu_has_setting("video_openxr_edit_snap")); + CHECK(!menu_has_setting("video_openxr_edit_snap_angles")); + CHECK(!menu_has_setting("video_openxr_edit_hinge_gap")); + CHECK(!menu_has_setting("video_openxr_edit_smoothing")); + settings->bools.video_openxr_enable = true; + CHECK(menu_has_setting("video_screen_xr_edit")); + CHECK(menu_has_setting("video_openxr_edit_pause")); + menu_value("video_openxr_edit_pause", "Keep Running"); + menu_action("video_openxr_edit_pause", MENU_ACTION_RIGHT); + CHECK(settings->bools.video_openxr_edit_pause); + menu_value("video_openxr_edit_pause", "Pause"); + CHECK(menu_has_setting("video_openxr_edit_snap")); + CHECK(settings->bools.video_openxr_edit_snap); + menu_value("video_openxr_edit_snap", "ON"); + menu_action("video_openxr_edit_snap", MENU_ACTION_RIGHT); + CHECK(!settings->bools.video_openxr_edit_snap); + menu_value("video_openxr_edit_snap", "OFF"); + CHECK(menu_setting_follows("video_openxr_edit_snap_angles", + "video_openxr_edit_snap")); + CHECK(settings->uints.video_openxr_edit_snap_angles + == VIDEO_OPENXR_EDIT_SNAP_ANGLES_15); + menu_value("video_openxr_edit_snap_angles", "15°"); + menu_action("video_openxr_edit_snap_angles", MENU_ACTION_RIGHT); + CHECK(settings->uints.video_openxr_edit_snap_angles + == VIDEO_OPENXR_EDIT_SNAP_ANGLES_30); + menu_value("video_openxr_edit_snap_angles", "30°"); + CHECK(menu_setting_follows("video_openxr_edit_hinge_gap", + "video_openxr_edit_snap_angles")); + CHECK(settings->uints.video_openxr_edit_hinge_gap + == VIDEO_OPENXR_EDIT_HINGE_GAP_2); + menu_value("video_openxr_edit_hinge_gap", "2 cm"); + menu_action("video_openxr_edit_hinge_gap", MENU_ACTION_RIGHT); + CHECK(settings->uints.video_openxr_edit_hinge_gap + == VIDEO_OPENXR_EDIT_HINGE_GAP_3); + menu_value("video_openxr_edit_hinge_gap", "3 cm"); + CHECK(menu_setting_follows("video_screen_xr_edit", + "video_openxr_enable")); + CHECK(menu_setting_follows("video_openxr_edit_snap", + "video_screen_xr_edit")); + CHECK(menu_setting_follows("video_openxr_edit_smoothing", + "video_openxr_edit_hinge_gap")); + CHECK(menu_setting_follows("video_openxr_edit_pause", + "video_openxr_edit_smoothing")); + CHECK(menu_setting_follows("video_openxr_refresh_rate", + "video_openxr_edit_pause")); + CHECK(menu_setting_follows("video_openxr_controllers", + "video_openxr_perf_boost")); + CHECK(menu_setting_follows("video_openxr_placement", + "video_openxr_laser_beam")); + CHECK(menu_setting_follows("video_openxr_width", + "video_openxr_distance")); + CHECK(menu_setting_follows("video_openxr_menu_follow", + "video_openxr_width")); + CHECK(menu_setting_follows("video_screen_selected", + "video_openxr_menu_follow")); + /* Off unless asked for: the menu opens where it always did. */ + CHECK(!settings->bools.video_openxr_menu_follow); + menu_value("video_openxr_menu_follow", "OFF"); + menu_action("video_openxr_menu_follow", MENU_ACTION_RIGHT); + CHECK(settings->bools.video_openxr_menu_follow); + menu_value("video_openxr_menu_follow", "ON"); + menu_action("video_openxr_menu_follow", MENU_ACTION_LEFT); + CHECK(!settings->bools.video_openxr_menu_follow); + CHECK(menu_setting_follows("video_screen_xr_visible", + "video_screen_selected")); + CHECK(menu_setting_follows("video_screen_xr_x", + "video_screen_xr_visible")); + CHECK(menu_has_setting("video_openxr_edit_smoothing")); + CHECK(settings->uints.video_openxr_edit_smoothing + == VIDEO_OPENXR_EDIT_SMOOTHING_MEDIUM); + menu_value("video_openxr_edit_smoothing", "Medium"); + menu_action("video_openxr_edit_smoothing", MENU_ACTION_RIGHT); + CHECK(settings->uints.video_openxr_edit_smoothing + == VIDEO_OPENXR_EDIT_SMOOTHING_HIGH); + menu_value("video_openxr_edit_smoothing", "High"); + for (i = 0; input_config_bind_map_get_valid(i); i++) + if (input_config_bind_map_get_retro_key(i) == RARCH_HEADSET_EDIT_SCREENS) + base = input_config_bind_map_get_base(i); + CHECK(base && !strcmp(base, "headset_edit_screens")); + /* The row asks for edit mode, which the desktop is told it cannot + * have; it is no Reset Screen. */ + runloop_msg_queue_init(); + settings->floats.video_screen_xr_x[0] = 2.0f; + menu_action("video_screen_xr_edit", MENU_ACTION_OK); + CHECK(!video_state_get_ptr()->headset_editing); + { + const char *msg = msg_queue_pull(&runloop_state_get_ptr()->msg_queue); + CHECK(msg && !strcmp(msg, + msg_hash_to_str(MSG_OPENXR_EDIT_UNAVAILABLE))); + } + CHECK(settings->floats.video_screen_xr_x[0] == 2.0f); + runloop_msg_queue_deinit(); + settings->bools.video_openxr_enable = false; +} + +/* Laser Beam sits with the headset's rows: Edit Mode Only unless the + * config says otherwise; the menu steps through its values. */ +static void test_xr_laser_beam_row(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture("video_driver = \"glcore\"\n"); + CHECK(menu_has_setting("video_openxr_laser_beam")); + CHECK(settings->uints.video_openxr_laser_beam + == VIDEO_OPENXR_LASER_BEAM_EDIT); + menu_value("video_openxr_laser_beam", "Edit Mode Only"); + menu_action("video_openxr_laser_beam", MENU_ACTION_RIGHT); + CHECK(settings->uints.video_openxr_laser_beam + == VIDEO_OPENXR_LASER_BEAM_ALWAYS); + menu_value("video_openxr_laser_beam", "Always"); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_laser_beam = \"0\"\n"); + CHECK(settings->uints.video_openxr_laser_beam + == VIDEO_OPENXR_LASER_BEAM_OFF); + menu_value("video_openxr_laser_beam", "Off"); +} + +static void test_xr_edit_smoothing_row(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_smoothing = \"0\"\n"); + CHECK(settings->uints.video_openxr_edit_smoothing + == VIDEO_OPENXR_EDIT_SMOOTHING_OFF); + menu_value("video_openxr_edit_smoothing", "Off"); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_smoothing = \"1\"\n"); + menu_value("video_openxr_edit_smoothing", "Low"); +} + +static void test_xr_edit_snap_angles_row(void) +{ + settings_t *settings = config_get_ptr(); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_snap_angles = \"0\"\n"); + CHECK(settings->uints.video_openxr_edit_snap_angles + == VIDEO_OPENXR_EDIT_SNAP_ANGLES_OFF); + menu_value("video_openxr_edit_snap_angles", "Off"); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_snap_angles = \"3\"\n"); + menu_value("video_openxr_edit_snap_angles", "45°"); + load_fixture("video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_snap_angles = \"4\"\n"); + menu_value("video_openxr_edit_snap_angles", "90°"); +} + +/* Each Hinge Gap shows its own value, and survives a save and load. */ +static void test_xr_edit_hinge_gap_row(void) +{ + static const char *const shown[] = { "0 cm", "1 cm", "2 cm", "3 cm", + "5 cm", "10 cm" }; + char fixture[128]; + settings_t *settings = config_get_ptr(); + unsigned i; + for (i = 0; i < 6; i++) + { + snprintf(fixture, sizeof(fixture), "video_driver = \"glcore\"\n" + "video_openxr_enable = \"true\"\n" + "video_openxr_edit_hinge_gap = \"%u\"\n", i); + load_fixture(fixture); + CHECK(settings->uints.video_openxr_edit_hinge_gap == i); + menu_value("video_openxr_edit_hinge_gap", shown[i]); + settings->bools.config_save_minimal = false; + CHECK(config_save_file(saved_path)); + path_set(RARCH_PATH_CONFIG, saved_path); + config_load(); + CHECK(settings->uints.video_openxr_edit_hinge_gap == i); + } +} +#endif + +#ifdef VIDEO_SCREENS_RUNTIME_TEST +int rarch_main(int argc, char *argv[], void *data); + +int video_screens_test_iterate(void) +{ + static unsigned applied; + const char *path = getenv("RA_SCREEN_TEST_SCRIPT"); + int result = runloop_iterate(); + RFILE *file = path ? filestream_open(path, RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_NONE) : NULL; + if (file) + { + char text[128]; + unsigned serial, screen, visible, i; + float x, scale; + if (filestream_gets(file, text, sizeof(text)) + && sscanf(text, "%u %u %f %f %u", + &serial, &screen, &x, &scale, &visible) == 5 + && serial > applied && screen <= RETRO_VIDEO_VIEWS_MAX) + { + settings_t *settings = config_get_ptr(); + const char *ack = getenv("RA_SCREEN_TEST_ACK"); + char value[32]; + for (i = 0; i < RETRO_VIDEO_VIEWS_MAX; i++) + if (screen == i || screen == RETRO_VIDEO_VIEWS_MAX) + { + settings->floats.video_screen_xr_x[i] = x; + settings->floats.video_screen_xr_scale[i] = scale; + settings->bools.video_screen_xr_visible[i] = visible != 0; + } + applied = serial; + snprintf(value, sizeof(value), "%u %u", applied, + (unsigned)video_driver_get_frame_count()); + if (ack) + filestream_write_file(ack, value, strlen(value)); + } + filestream_close(file); + } + return result; +} +#endif + /* A driver that draws views itself, and the screens' own windows while * window_test_flags says so: frame() keeps the windows it was handed. */ static uint32_t window_test_flags; @@ -1162,6 +2142,114 @@ static void test_window_frame_snapshot(void) #endif } +#ifdef HAVE_THREADS +static video_frame_info_t snapshot_queued; + +static bool snapshot_queue_frame(void *data, const void *frame, unsigned dims, + uint64_t count, unsigned pitch, const char *msg, + video_frame_info_t *info) +{ + (void)data; + (void)frame; + (void)dims; + (void)count; + (void)pitch; + (void)msg; + slock_lock(snapshot_lock); + snapshot_queued = *info; + snapshot_done = true; + scond_signal(snapshot_ready); + slock_unlock(snapshot_lock); + return true; +} +#endif + +static void test_screen_frame_snapshot(void) +{ + static video_frame_info_t frame; + settings_t *settings = config_get_ptr(); + load_fixture("video_screen2_xr_x = \"0.25\"\n"); + settings->floats.video_screen_xr_scale[7] = 20.0f; + settings->floats.video_screen_xr_y[1] = -0.3f; + settings->floats.video_screen_xr_z[1] = 0.4f; + settings->floats.video_screen_xr_yaw[1] = 45.0f; + settings->floats.video_screen_xr_pitch[1] = -30.0f; + settings->floats.video_screen_xr_roll[1] = 90.0f; + settings->floats.video_screen_xr_x[7] = (float)HUGE_VAL; + settings->floats.video_openxr_menu_xr_y = 0.5f; + settings->floats.video_openxr_menu_xr_scale = 20.0f; + settings->bools.video_openxr_menu_placed = true; + memset(&frame, 0, sizeof(frame)); + video_driver_build_info(&frame); + CHECK(!frame.headset_menu_follow); + settings->bools.video_openxr_menu_follow = true; + video_driver_build_info(&frame); + CHECK(frame.headset_menu_follow); + settings->bools.video_openxr_menu_follow = false; + CHECK(frame.headset_menu.y == 0.5f); + CHECK(frame.headset_menu.scale == 10.0f); + CHECK(frame.headset_menu.visible); + CHECK(frame.headset_menu_placed); + CHECK(frame.headset_screens[1].x == 0.25f); + CHECK(frame.headset_screens[7].scale == 10.0f); + CHECK(frame.headset_screens[7].x == 0.0f); + CHECK(frame.headset_screens[1].y == -0.3f); + CHECK(frame.headset_screens[1].z == 0.4f); + CHECK(frame.headset_screens[1].yaw == 45.0f); + CHECK(frame.headset_screens[1].pitch == -30.0f); + CHECK(frame.headset_screens[1].roll == 90.0f); + CHECK(frame.headset_screens[0].visible); +#ifdef HAVE_THREADS + { + static video_driver_t driver; + const video_driver_t *threaded = NULL; + void *data = NULL; + video_info_t info; + uint32_t pixel = 0; + bool initialized; + memset(&driver, 0, sizeof(driver)); + memset(&info, 0, sizeof(info)); + driver.init = snapshot_init; + driver.frame = snapshot_queue_frame; + driver.free = snapshot_free; + driver.ident = "snapshot"; + info.input_scale = 1; + info.rgb32 = true; + snapshot_lock = slock_new(); + snapshot_ready = scond_new(); + snapshot_done = false; + initialized = video_init_thread(&threaded, &data, &driver, info); + CHECK(initialized); + if (initialized) + { + frame.views.num_views = 2; + frame.views.views[0].screen = 1; + frame.views.views[1].screen = 0; + slock_lock(snapshot_lock); + CHECK(threaded->frame(data, &pixel, VIDEO_SCALE_PACK(1, 1), + 1, sizeof(pixel), NULL, &frame)); + settings->floats.video_screen_xr_x[1] = 0.75f; + video_state_get_ptr()->views_core.num_views = 1; + video_state_get_ptr()->views_core.views[0].screen = 0; + frame.headset_screens[1].x = 0.9f; + frame.views.views[0].screen = 0; + frame.views.num_views = 1; + if (!snapshot_done) + scond_wait_timeout(snapshot_ready, snapshot_lock, 2000000); + CHECK(snapshot_done); + CHECK(snapshot_queued.headset_screens[1].x == 0.25f); + CHECK(settings->floats.video_screen_xr_x[1] == 0.75f); + CHECK(snapshot_queued.views.num_views == 2); + CHECK(snapshot_queued.views.views[0].screen == 1); + slock_unlock(snapshot_lock); + threaded->free(data); + } + scond_free(snapshot_ready); + slock_free(snapshot_lock); + } +#endif +} + #ifdef HAVE_THREADS static void redraw_viewport(void *data, struct video_viewport *vp) { @@ -1256,7 +2344,11 @@ static void test_window_redraw(void) int main(int argc, char **argv) { - if (argc != 2) +#ifdef VIDEO_SCREENS_RUNTIME_TEST + if (getenv("RA_SCREEN_TEST_SCRIPT")) + return rarch_main(argc, argv, NULL); +#endif + if (argc != 2 && argc != 3) return 2; fill_pathname_join(fixture_path, argv[1], "fixture.cfg", sizeof(fixture_path)); fill_pathname_join(saved_path, argv[1], "saved.cfg", sizeof(saved_path)); @@ -1268,7 +2360,15 @@ int main(int argc, char **argv) frontend_driver_init_first(NULL); menu_state_get_ptr()->entries.list_settings = menu_setting_new(); CHECK(menu_state_get_ptr()->entries.list_settings != NULL); - test_window_config(); + if (argc == 2) + { + test_window_config(); + test_xr_config_roundtrip(); + test_xr_minimal_save(); + test_xr_override_unload(); + test_xr_config_invalid(); + test_xr_menu_place_config(); + } test_window_layout(); test_window_frame(); test_window_displays(); @@ -1278,6 +2378,19 @@ int main(int argc, char **argv) test_window_menu_settings(); test_window_menu_list(); test_window_redraw(); + test_screen_frame_snapshot(); + test_xr_screen_menu(); + test_xr_menu_map_change(); + test_xr_menu_reset_all(); + test_xr_edit_writes(); +#ifdef HAVE_OPENXR + test_xr_menu_tracked_screen(); + test_xr_edit_rows(); + test_xr_laser_beam_row(); + test_xr_edit_smoothing_row(); + test_xr_edit_snap_angles_row(); + test_xr_edit_hinge_gap_row(); +#endif menu_setting_free(menu_state_get_ptr()->entries.list_settings); free(menu_state_get_ptr()->entries.list_settings); menu_state_get_ptr()->entries.list_settings = NULL; diff --git a/settings/settings_def_video_headset.h b/settings/settings_def_video_headset.h index d2aa6d45fd..e12b15db8f 100644 --- a/settings/settings_def_video_headset.h +++ b/settings/settings_def_video_headset.h @@ -29,6 +29,11 @@ S_UINT_EX(video_openxr_placement, VIDEO_OPENXR_PLACEMENT, 0, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 2, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_placement, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, "Screen Placement", "Where the headset keeps the screens. Fixed in Room: where Recenter puts them. Follow Head: always in front of you; on a large screen the edges sit where the lenses blur. Lazy Follow: they stay until you turn well away, then glide back in front of you.") +S_BOOL(video_openxr_menu_follow, VIDEO_OPENXR_MENU_FOLLOW, + "video_openxr_menu_follow", + false, SD_FLAG_NONE, 0, CMD_EVENT_NONE, + "Menu in Front of Screen 1", + "Opens the menu just in front of Screen 1, turned as it is, so moving Screen 1 moves the menu with it. Off: the menu opens straight ahead of the screens' anchor. A menu placed with Edit Screens stays where it was put.") S_BOOL(video_openxr_perf_boost, VIDEO_OPENXR_PERF_BOOST, "video_openxr_perf_boost", false, SD_FLAG_NONE, 0, CMD_EVENT_NONE, @@ -44,3 +49,33 @@ S_UINT_EX(video_openxr_laser, VIDEO_OPENXR_LASER, 0, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 2, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_laser, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, "Laser Pointer", "Where a controller's trigger points and touches instead of pressing L2 or R2. Auto: the menu while it is open, and second screens such as a touch screen. Always: every screen, for light guns. Off: nowhere.") +S_UINT_EX(video_openxr_laser_beam, VIDEO_OPENXR_LASER_BEAM, + "video_openxr_laser_beam", + 1, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 2, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_laser_beam, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, + "Laser Beam", + "A thin line from each controller to where its laser points. Edit Mode Only: while Edit Screens moves the screens. Always: also for the menu and light guns. Off: never.") +S_BOOL_EX(video_openxr_edit_pause, VIDEO_OPENXR_EDIT_PAUSE, + "video_openxr_edit_pause", + false, SD_FLAG_NONE, 0, 0, NULL, setting_get_string_representation_video_openxr_edit_pause, NULL, NULL, NULL, NULL, 0, + "While Editing Screens", + "What the content does while Edit Screens moves the headset's screens: keeps running, or pauses until editing ends. Edit mode resumes only what it paused.") +S_BOOL(video_openxr_edit_snap, VIDEO_OPENXR_EDIT_SNAP, + "video_openxr_edit_snap", + true, SD_FLAG_NONE, 0, 0, + "Snap to Screens", + "Lines up a held screen with the screens near it: parallel or square to one, then in its plane and level, or edge to edge; or hinged to one along a shared edge at any angle. The holding hand buzzes as a snap takes hold.") +S_UINT_EX(video_openxr_edit_snap_angles, VIDEO_OPENXR_EDIT_SNAP_ANGLES, + "video_openxr_edit_snap_angles", + 1, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 4, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_edit_snap_angles, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, + "Snap to Angles", + "Stops a screen the other hand's stick turns or tilts at every step of this angle, with a buzz: turns count from facing you, tilts from upright, and a hinged screen from flat with its neighbour. Snap to Screens wins near another screen. Off: the stick turns and tilts smoothly.") +S_UINT_EX(video_openxr_edit_hinge_gap, VIDEO_OPENXR_EDIT_HINGE_GAP, + "video_openxr_edit_hinge_gap", + 2, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 5, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_edit_hinge_gap, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, + "Hinge Gap", + "Leaves this gap between screens that meet edge to edge, side by side, at a corner or hinged at an angle. A screen clicks to it within 1 cm; elsewhere up to 10 cm the gap stays where you put it. 0: the edges touch.") +S_UINT_EX(video_openxr_edit_smoothing, VIDEO_OPENXR_EDIT_SMOOTHING, + "video_openxr_edit_smoothing", + 2, SD_FLAG_NONE, SDESC_RANGE_MINMAX, 0, 0, 3, 1, 0, setting_action_ok_uint, setting_get_string_representation_video_openxr_edit_smoothing, NULL, NULL, NULL, NULL, ST_UI_TYPE_UINT_COMBOBOX, + "Edit Smoothing", + "Steadies the controllers' aim while Edit Screens moves the screens, so a screen held far out does not shake with the hand: a slow hand is smoothed a lot, a fast one little. Off: the aim as tracked.") diff --git a/settings/settings_def_video_screens.h b/settings/settings_def_video_screens.h new file mode 100644 index 0000000000..5997b45502 --- /dev/null +++ b/settings/settings_def_video_screens.h @@ -0,0 +1,57 @@ +/* Indexed menu templates; configuration uses the indexed XR passes. */ +S_ACTION(VIDEO_SCREEN_SELECTED, + "video_screen_selected", + "Screen", + "Select the floating screen to adjust.") +S_FLOAT(video_screen_xr_x[0], VIDEO_SCREEN_XR_X, + "video_screen_xr_x", + 0.0f, "%.1f m", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -10, 10, .1, NULL, NULL, + "Horizontal Offset", + "Move this screen left or right in metres.") +S_FLOAT(video_screen_xr_y[0], VIDEO_SCREEN_XR_Y, + "video_screen_xr_y", + 0.0f, "%.1f m", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -10, 10, .1, NULL, NULL, + "Vertical Offset", + "Move this screen up or down in metres.") +S_FLOAT(video_screen_xr_z[0], VIDEO_SCREEN_XR_Z, + "video_screen_xr_z", + 0.0f, "%.1f m", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -10, 10, .1, NULL, NULL, + "Depth Offset", + "Move this screen towards or away from you in metres.") +S_FLOAT(video_screen_xr_yaw[0], VIDEO_SCREEN_XR_YAW, + "video_screen_xr_yaw", + 0.0f, "%.0f deg", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -180, 180, 1, NULL, NULL, + "Yaw", + "Turn this screen left or right in degrees.") +S_FLOAT(video_screen_xr_pitch[0], VIDEO_SCREEN_XR_PITCH, + "video_screen_xr_pitch", + 0.0f, "%.0f deg", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -180, 180, 1, NULL, NULL, + "Pitch", + "Tilt this screen up or down in degrees.") +S_FLOAT(video_screen_xr_roll[0], VIDEO_SCREEN_XR_ROLL, + "video_screen_xr_roll", + 0.0f, "%.0f deg", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, -180, 180, 1, NULL, NULL, + "Roll", + "Rotate this screen in degrees.") +S_FLOAT(video_screen_xr_scale[0], VIDEO_SCREEN_XR_SCALE, + "video_screen_xr_scale", + 1.0f, "%.1f", SD_FLAG_NONE, SDESC_RANGE_MINMAX, CMD_EVENT_NONE, 0.1, 10, .1, NULL, NULL, + "Scale", + "Resize this floating screen.") +S_BOOL(video_screen_xr_visible[0], VIDEO_SCREEN_XR_VISIBLE, + "video_screen_xr_visible", + true, SD_FLAG_NONE, 0, 0, + "Visible", + "Show this floating screen. The menu remains available when screens are hidden.") +S_ACTION(VIDEO_SCREEN_XR_RESET, + "video_screen_xr_reset", + "Reset Screen", + "Restore the selected floating screen's position, rotation, scale and visibility.") +S_ACTION(VIDEO_SCREEN_XR_RESET_ALL, + "video_screen_xr_reset_all", + "Reset All Screens", + "Restore every screen's position, rotation, scale and visibility, including screens outside the current map, and return the menu to where it opens unplaced.") +S_ACTION(VIDEO_SCREEN_XR_EDIT, + "video_screen_xr_edit", + "Edit Screens", + "Close the menu and move the headset's screens and menu with the controllers: grip grabs, the trigger tilts, the stick changes distance and size, the free hand's stick turns and tilts, the main button resets, Select Next and Select Previous choose an item.") diff --git a/tools/settings_table.reference b/tools/settings_table.reference index 0714da480c..3a78283f57 100644 --- a/tools/settings_table.reference +++ b/tools/settings_table.reference @@ -202,7 +202,10 @@ bool video_hdr_scanlines 1 1 bool video_stereo_swap_eyes 1 0 bool video_windows_enable 1 1 bool video_openxr_enable 1 0 +bool video_openxr_menu_follow 1 0 bool video_openxr_perf_boost 1 0 +bool video_openxr_edit_pause 1 0 +bool video_openxr_edit_snap 1 1 bool rewind_enable 1 0 bool menu_linear_filter 1 0 bool xmb_vertical_thumbnails 1 0 @@ -577,6 +580,10 @@ uint video_openxr_refresh_rate 1 0 uint video_openxr_placement 1 0 uint video_openxr_controllers 1 0 uint video_openxr_laser 1 0 +uint video_openxr_laser_beam 1 1 +uint video_openxr_edit_snap_angles 1 1 +uint video_openxr_edit_hinge_gap 1 2 +uint video_openxr_edit_smoothing 1 2 uint rewind_granularity 1 1 uint playlist_show_history_icons 1 PLAYLIST_SHOW_HISTORY_ICONS_CONTENT uint rgui_internal_upscale_level 1 RGUI_UPSCALE_NONE @@ -925,7 +932,10 @@ bool ozone_collapse_sidebar 1 0 bool video_hdr_scanlines 1 1 bool video_stereo_swap_eyes 1 0 bool video_openxr_enable 1 0 +bool video_openxr_menu_follow 1 0 bool video_openxr_perf_boost 1 0 +bool video_openxr_edit_pause 1 0 +bool video_openxr_edit_snap 1 1 bool rewind_enable 1 0 bool menu_linear_filter 1 0 bool xmb_vertical_thumbnails 1 0 @@ -1295,6 +1305,10 @@ uint video_openxr_refresh_rate 1 0 uint video_openxr_placement 1 0 uint video_openxr_controllers 1 0 uint video_openxr_laser 1 0 +uint video_openxr_laser_beam 1 1 +uint video_openxr_edit_snap_angles 1 1 +uint video_openxr_edit_hinge_gap 1 2 +uint video_openxr_edit_smoothing 1 2 uint rewind_granularity 1 1 uint playlist_show_history_icons 1 PLAYLIST_SHOW_HISTORY_ICONS_CONTENT uint rgui_internal_upscale_level 1 RGUI_UPSCALE_NONE @@ -1648,7 +1662,10 @@ bool ozone_collapse_sidebar 1 0 bool video_hdr_scanlines 1 1 bool video_stereo_swap_eyes 1 0 bool video_openxr_enable 1 0 +bool video_openxr_menu_follow 1 0 bool video_openxr_perf_boost 1 0 +bool video_openxr_edit_pause 1 0 +bool video_openxr_edit_snap 1 1 bool rewind_enable 1 0 bool menu_linear_filter 1 0 bool xmb_vertical_thumbnails 1 0 @@ -2034,6 +2051,10 @@ uint video_openxr_refresh_rate 1 0 uint video_openxr_placement 1 0 uint video_openxr_controllers 1 0 uint video_openxr_laser 1 0 +uint video_openxr_laser_beam 1 1 +uint video_openxr_edit_snap_angles 1 1 +uint video_openxr_edit_hinge_gap 1 2 +uint video_openxr_edit_smoothing 1 2 uint rewind_granularity 1 1 uint playlist_show_history_icons 1 PLAYLIST_SHOW_HISTORY_ICONS_CONTENT uint rgui_internal_upscale_level 1 RGUI_UPSCALE_NONE