mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-29 04:01:45 +01:00
fix(vr): stop keyboard click-through, redraw churn and upload leaks; build its drawing only with VR
This commit is contained in:
1 parent
f9289d9947
commit
7ec8304d8f
12 files changed
+318
-85
No files matched your search
+11
-8
@@ -32,17 +32,11 @@ add_library(mocktail_vr STATIC
|
||||
src/vr/vr_mirror.cc
|
||||
src/vr/xr_controller.cc
|
||||
src/vr/roblox_vr_device_bridge.cc
|
||||
src/vr/virtual_keyboard.cc
|
||||
src/vr/virtual_keyboard_raster.cc)
|
||||
src/vr/virtual_keyboard.cc)
|
||||
add_library(Mocktail::VR ALIAS mocktail_vr)
|
||||
target_include_directories(mocktail_vr PUBLIC "${CMAKE_SOURCE_DIR}/include")
|
||||
target_include_directories(mocktail_vr PRIVATE "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(mocktail_vr PUBLIC Vulkan::Headers PRIVATE nlohmann_json::nlohmann_json)
|
||||
# The in-headset keyboard draws its labels with the same SDL3_ttf/fontconfig
|
||||
# stack as the TextBox overlay. MocktailInput/MocktailAudio find these
|
||||
# packages later in the same directory scope; links resolve at generate time.
|
||||
target_link_libraries(mocktail_vr PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3
|
||||
PkgConfig::FONTCONFIG)
|
||||
# shm_open for the desktop eye mirror lives in librt on glibc.
|
||||
find_library(MOCKTAIL_RT_LIBRARY rt)
|
||||
if(MOCKTAIL_RT_LIBRARY)
|
||||
@@ -75,7 +69,16 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
endfunction()
|
||||
mocktail_add_openxr_sdk()
|
||||
target_sources(mocktail_vr PRIVATE src/vr/openxr_probe.cc src/vr/openxr_preview.cc
|
||||
src/vr/openxr_backend.cc src/vr/gles_transport.cc src/vr/xr_actions.cc)
|
||||
src/vr/openxr_backend.cc src/vr/gles_transport.cc src/vr/xr_actions.cc
|
||||
src/vr/virtual_keyboard_raster.cc)
|
||||
# The in-headset keyboard draws its labels with the same SDL3_ttf and
|
||||
# fontconfig stack as the TextBox overlay.
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_package(SDL3_ttf REQUIRED CONFIG)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
|
||||
target_link_libraries(mocktail_vr PRIVATE SDL3_ttf::SDL3_ttf SDL3::SDL3
|
||||
PkgConfig::FONTCONFIG)
|
||||
# Error unwinding is confined to these native session implementations; their
|
||||
# public entry points catch errors before returning to the guest runtime.
|
||||
set_source_files_properties(src/vr/openxr_preview.cc src/vr/openxr_backend.cc
|
||||
|
||||
@@ -336,6 +336,7 @@ class OpenXrBackend final {
|
||||
// keyboard swapchain holds a released image ready for a quad layer.
|
||||
bool PrepareVirtualKeyboardLayerLocked(VkQueue queue);
|
||||
bool UploadVirtualKeyboardVulkan(VkQueue queue, VkImage image);
|
||||
void DestroyVirtualKeyboardStaging();
|
||||
void DestroyVirtualKeyboardResources();
|
||||
bool CaptureEyeEvidence(VkQueue queue, int eye);
|
||||
ScriptedPoseSample ComputeScriptedPose(std::uint64_t frame) const;
|
||||
|
||||
@@ -74,7 +74,6 @@ struct VirtualKeyboardPointer {
|
||||
struct VirtualKeyboardOutput {
|
||||
std::vector<std::string> text; // committed in order
|
||||
std::vector<std::uint32_t> scancodes; // pressed in order, after text
|
||||
bool closed = false; // user pressed Close
|
||||
};
|
||||
|
||||
// Ray/quad intersection. The quad is centred on `quad` with its face along
|
||||
@@ -118,8 +117,9 @@ class VirtualKeyboard {
|
||||
const VirtualKeyboardPointer (&pointers)[2]);
|
||||
|
||||
// True when this hand's trigger belongs to the keyboard this frame (aimed
|
||||
// at the panel, or a press that began on it is still held). The backend
|
||||
// withholds that trigger from Roblox so a key press is not also a click.
|
||||
// at the panel, or a press that began on it is still held, including after
|
||||
// the panel hid mid-press). The backend withholds that trigger from Roblox
|
||||
// so a key press is not also a click.
|
||||
bool CapturesTrigger(int hand) const;
|
||||
|
||||
// Current layout, with Shift/Caps Lock applied to the labels.
|
||||
@@ -159,6 +159,9 @@ class VirtualKeyboard {
|
||||
pose::Pose panel_{};
|
||||
VirtualKeyboardText text_;
|
||||
Hand hands_[2];
|
||||
// A press the keyboard owned when it hid; held until that trigger releases.
|
||||
bool release_pending_[2] = {false, false};
|
||||
std::uint64_t cursor_drawn_ns_ = 0;
|
||||
std::vector<VirtualKey> keys_;
|
||||
std::uint64_t revision_ = 1;
|
||||
};
|
||||
|
||||
+3
-5
@@ -46,6 +46,7 @@
|
||||
#include "runtime/roblox_desktop_app_policy.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_fullscreen_runtime_bridge.h"
|
||||
#include "runtime/roblox_text_display_state.h"
|
||||
#include "runtime/roblox_text_editor.h"
|
||||
#include "runtime/runtime_config_bootstrap.h"
|
||||
#include "runtime/runtime_config_file.h"
|
||||
@@ -1082,13 +1083,10 @@ int main(int argc, char* argv[]) {
|
||||
mocktail::vr::ClearVirtualKeyboardText();
|
||||
return;
|
||||
}
|
||||
// RbxKeyboard password input types (see the SDL mapping).
|
||||
const bool password = update.text_input_type == 5 ||
|
||||
update.text_input_type == 9 ||
|
||||
update.text_input_type == 10;
|
||||
mocktail::vr::SetVirtualKeyboardText(
|
||||
update.utf8, update.utf8_size, update.cursor_utf16,
|
||||
password);
|
||||
mocktail::runtime::IsRobloxPasswordTextInputType(
|
||||
update.text_input_type));
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,11 +328,15 @@ Status RobloxTextSurfaceOverlay::UpdateViewport(
|
||||
namespace {
|
||||
std::mutex g_display_mirror_mutex;
|
||||
RobloxTextDisplaySink g_display_mirror;
|
||||
// Lets a text update skip the lock while no mirror is installed.
|
||||
std::atomic<bool> g_display_mirror_set{false};
|
||||
} // namespace
|
||||
|
||||
void SetRobloxTextDisplayMirror(RobloxTextDisplaySink sink) {
|
||||
std::lock_guard<std::mutex> lock(g_display_mirror_mutex);
|
||||
g_display_mirror = sink;
|
||||
g_display_mirror_set.store(sink.update != nullptr,
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
RobloxTextDisplaySink RobloxTextSurfaceOverlay::sink() {
|
||||
@@ -344,6 +348,9 @@ void RobloxTextSurfaceOverlay::UpdateCallback(
|
||||
if (context != nullptr) {
|
||||
static_cast<RobloxTextSurfaceOverlay*>(context)->ApplyUpdate(update);
|
||||
}
|
||||
if (!g_display_mirror_set.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_display_mirror_mutex);
|
||||
if (g_display_mirror.update != nullptr) {
|
||||
g_display_mirror.update(g_display_mirror.context, update);
|
||||
|
||||
@@ -2725,8 +2725,10 @@ void OpenXrBackend::UpdateVirtualKeyboardLocked() {
|
||||
return;
|
||||
}
|
||||
const VirtualKeyboardHost host = CurrentVirtualKeyboardHost();
|
||||
// A panel that cannot be drawn must not take triggers either.
|
||||
const bool requested =
|
||||
host.input_requested != nullptr && host.input_requested(host.context);
|
||||
!keyboard_swapchain_failed_ && host.input_requested != nullptr &&
|
||||
host.input_requested(host.context);
|
||||
if (!requested) {
|
||||
keyboard_.Hide();
|
||||
keyboard_armed_ = true;
|
||||
@@ -2735,10 +2737,9 @@ void OpenXrBackend::UpdateVirtualKeyboardLocked() {
|
||||
keyboard_armed_ = false;
|
||||
Log(" [vr-keyboard] shown for focused TextBox\n");
|
||||
}
|
||||
if (!keyboard_.visible()) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
// A hidden keyboard still runs below: it keeps a trigger that was pressed
|
||||
// on the panel away from Roblox until that trigger is released.
|
||||
if (keyboard_.visible()) {
|
||||
VirtualKeyboardText text = CurrentVirtualKeyboardText();
|
||||
keyboard_.SetText(text);
|
||||
std::fill(text.utf8.begin(), text.utf8.end(), '\0');
|
||||
@@ -2904,6 +2905,8 @@ bool OpenXrBackend::UploadVirtualKeyboardVulkan(VkQueue queue, VkImage image) {
|
||||
if (vk_->CreateBuffer(vk_device_, &info, nullptr, &keyboard_staging_) !=
|
||||
VK_SUCCESS) {
|
||||
keyboard_staging_ = VK_NULL_HANDLE;
|
||||
keyboard_swapchain_failed_ = true;
|
||||
Log(" [vr-keyboard] disabled: no staging buffer\n");
|
||||
return false;
|
||||
}
|
||||
VkMemoryRequirements requirements{};
|
||||
@@ -2932,7 +2935,11 @@ bool OpenXrBackend::UploadVirtualKeyboardVulkan(VkQueue queue, VkImage image) {
|
||||
keyboard_staging_memory_, 0) != VK_SUCCESS ||
|
||||
vk_->MapMemory(vk_device_, keyboard_staging_memory_, 0, size, 0,
|
||||
&keyboard_staging_mapped_) != VK_SUCCESS) {
|
||||
DestroyVirtualKeyboardResources();
|
||||
// The caller still holds an acquired swapchain image, so only the
|
||||
// staging objects go; the latch stops a retry on every frame.
|
||||
DestroyVirtualKeyboardStaging();
|
||||
keyboard_swapchain_failed_ = true;
|
||||
Log(" [vr-keyboard] disabled: no staging memory\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2987,12 +2994,15 @@ bool OpenXrBackend::UploadVirtualKeyboardVulkan(VkQueue queue, VkImage image) {
|
||||
if (vk_->WaitForFences(vk_device_, 1, ©_fence_, VK_TRUE,
|
||||
2'000'000'000ULL) != VK_SUCCESS) {
|
||||
(void)IdleHostDevice(vk_->DeviceWaitIdle, vk_device_);
|
||||
throw std::runtime_error("keyboard upload fence wait failed");
|
||||
// Returning lets the caller release the acquired swapchain image.
|
||||
keyboard_swapchain_failed_ = true;
|
||||
Log(" [vr-keyboard] disabled: upload fence wait failed\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenXrBackend::DestroyVirtualKeyboardResources() {
|
||||
void OpenXrBackend::DestroyVirtualKeyboardStaging() {
|
||||
if (vk_ != nullptr && vk_device_ != VK_NULL_HANDLE) {
|
||||
if (keyboard_staging_mapped_ != nullptr) {
|
||||
vk_->UnmapMemory(vk_device_, keyboard_staging_memory_);
|
||||
@@ -3007,6 +3017,10 @@ void OpenXrBackend::DestroyVirtualKeyboardResources() {
|
||||
keyboard_staging_mapped_ = nullptr;
|
||||
keyboard_staging_ = VK_NULL_HANDLE;
|
||||
keyboard_staging_memory_ = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void OpenXrBackend::DestroyVirtualKeyboardResources() {
|
||||
DestroyVirtualKeyboardStaging();
|
||||
EyeSwapchain& slot = keyboard_swapchain_;
|
||||
slot.images.clear();
|
||||
slot.gl_images.clear();
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
namespace mocktail::vr {
|
||||
OpenXrBackend::~OpenXrBackend() = default;
|
||||
// The backend's keyboard_raster_ member needs this destructor. No rasterizer
|
||||
// is ever created here, so SDL3_ttf and fontconfig stay out of this build.
|
||||
VirtualKeyboardRasterizer::~VirtualKeyboardRasterizer() = default;
|
||||
Status OpenXrBackend::Arm(VrGraphicsApi) {
|
||||
return Status::Error(StatusCode::kUnavailable, "OpenXR support is disabled in this build");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ constexpr std::uint64_t kRepeatDelayNs = 450'000'000ULL;
|
||||
constexpr std::uint64_t kRepeatIntervalNs = 60'000'000ULL;
|
||||
// Cursor moves smaller than this (in UV) do not force a redraw.
|
||||
constexpr float kCursorRedrawStep = 0.004f;
|
||||
// A moving cursor redraws the panel at most this often; each redraw is a
|
||||
// full texture upload on the frame thread.
|
||||
constexpr std::uint64_t kCursorRedrawIntervalNs = 33'000'000ULL;
|
||||
|
||||
constexpr float kPanelDistanceMetres = 0.45f;
|
||||
constexpr float kPanelDropMetres = 0.28f;
|
||||
@@ -239,7 +242,13 @@ void VirtualKeyboard::Hide() {
|
||||
text_.revision = UINT64_MAX; // the next SetText always reloads
|
||||
if (!visible_) return;
|
||||
visible_ = false;
|
||||
for (Hand& hand : hands_) hand = Hand{};
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
// Keeps a press already pending from an earlier hide of this same press.
|
||||
release_pending_[index] =
|
||||
release_pending_[index] ||
|
||||
(hands_[index].trigger_down && hands_[index].captured);
|
||||
hands_[index] = Hand{};
|
||||
}
|
||||
Touch();
|
||||
}
|
||||
|
||||
@@ -255,7 +264,8 @@ int VirtualKeyboard::KeyAt(float u, float v) const {
|
||||
|
||||
bool VirtualKeyboard::CapturesTrigger(int hand) const {
|
||||
const Hand& state = hands_[hand & 1];
|
||||
return visible_ && (state.over_panel || state.captured);
|
||||
return release_pending_[hand & 1] ||
|
||||
(visible_ && (state.over_panel || state.captured));
|
||||
}
|
||||
|
||||
bool VirtualKeyboard::cursor(int hand, float* u, float* v) const {
|
||||
@@ -298,7 +308,6 @@ void VirtualKeyboard::Activate(const VirtualKey& key,
|
||||
return;
|
||||
case VirtualKeyAction::kClose:
|
||||
output->scancodes.push_back(kScancodeEscape);
|
||||
output->closed = true;
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
@@ -307,8 +316,18 @@ void VirtualKeyboard::Activate(const VirtualKey& key,
|
||||
VirtualKeyboardOutput VirtualKeyboard::Update(
|
||||
std::uint64_t now_ns, const VirtualKeyboardPointer (&pointers)[2]) {
|
||||
VirtualKeyboardOutput output;
|
||||
if (!visible_) return output;
|
||||
bool held_over[2] = {false, false};
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
if (!release_pending_[index]) continue;
|
||||
// Losing the aim pose is not a release: the trigger is read regardless.
|
||||
held_over[index] = pointers[index].trigger > kTriggerRelease;
|
||||
release_pending_[index] = held_over[index];
|
||||
}
|
||||
// Decided once per frame, or one moving hand starves the other's cursor.
|
||||
const bool cursor_redraw_due =
|
||||
now_ns - cursor_drawn_ns_ >= kCursorRedrawIntervalNs;
|
||||
for (int index = 0; index < 2 && visible_; ++index) {
|
||||
if (held_over[index]) continue;
|
||||
Hand& hand = hands_[index];
|
||||
const VirtualKeyboardPointer& pointer = pointers[index];
|
||||
float u = 0.f, v = 0.f;
|
||||
@@ -318,12 +337,15 @@ VirtualKeyboardOutput VirtualKeyboard::Update(
|
||||
kPanelHeightMetres, pointer.aim, &u, &v,
|
||||
nullptr);
|
||||
const int hovered = over ? KeyAt(u, v) : -1;
|
||||
const bool cursor_moved =
|
||||
over && (std::abs(u - hand.u) > kCursorRedrawStep ||
|
||||
std::abs(v - hand.v) > kCursorRedrawStep);
|
||||
if (over != hand.over_panel || hovered != hand.hovered ||
|
||||
(over && (std::abs(u - hand.u) > kCursorRedrawStep ||
|
||||
std::abs(v - hand.v) > kCursorRedrawStep))) {
|
||||
(cursor_moved && cursor_redraw_due)) {
|
||||
Touch();
|
||||
hand.u = u;
|
||||
hand.v = v;
|
||||
cursor_drawn_ns_ = now_ns;
|
||||
}
|
||||
hand.over_panel = over;
|
||||
hand.hovered = hovered;
|
||||
|
||||
@@ -179,6 +179,8 @@ struct VirtualInput {
|
||||
};
|
||||
static std::mutex g_virtual_input_mutex;
|
||||
static std::deque<VirtualInput> g_virtual_input;
|
||||
// Set while the queue may hold input, so a pump with none takes no lock.
|
||||
static std::atomic<bool> g_virtual_input_pending{false};
|
||||
static std::unique_ptr<SdlPointerCaptureBackend> g_pointer_capture_backend;
|
||||
// The owner is created and destroyed on the SDL thread, but the experience
|
||||
// composition reaches it from its own thread. The mutex guards the pointer
|
||||
@@ -1699,6 +1701,7 @@ void QueueVirtualTextInput(const char* utf8) {
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_virtual_input_mutex);
|
||||
g_virtual_input.push_back(VirtualInput{utf8, 0});
|
||||
g_virtual_input_pending.store(true, std::memory_order_release);
|
||||
}
|
||||
void QueueVirtualKeyPress(uint32_t scancode) {
|
||||
if (scancode == 0) {
|
||||
@@ -1706,6 +1709,7 @@ void QueueVirtualKeyPress(uint32_t scancode) {
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_virtual_input_mutex);
|
||||
g_virtual_input.push_back(VirtualInput{{}, scancode});
|
||||
g_virtual_input_pending.store(true, std::memory_order_release);
|
||||
}
|
||||
void* GetGLProcAddress(const char* name) {
|
||||
if (!g_state.initialised || name == nullptr || name[0] == '\0') {
|
||||
@@ -2320,7 +2324,7 @@ bool PumpEvents() {
|
||||
g_text_input_owner->active())) {
|
||||
fprintf(stderr, " [input] SDL pointer capture state update failed\n");
|
||||
}
|
||||
{
|
||||
if (g_virtual_input_pending.exchange(false, std::memory_order_acq_rel)) {
|
||||
std::deque<VirtualInput> virtual_input;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_virtual_input_mutex);
|
||||
|
||||
@@ -64,6 +64,12 @@ if(MOCKTAIL_ENABLE_VR)
|
||||
target_include_directories(openxr_backend_test PRIVATE "${PROJECT_SOURCE_DIR}/third_party/OpenXR-SDK/include")
|
||||
target_link_libraries(openxr_backend_test PRIVATE Mocktail::VR GTest::gtest_main)
|
||||
gtest_discover_tests(openxr_backend_test)
|
||||
add_executable(virtual_keyboard_raster_test
|
||||
vr/virtual_keyboard_raster_test.cc)
|
||||
target_link_libraries(virtual_keyboard_raster_test PRIVATE
|
||||
Mocktail::VR GTest::gtest_main)
|
||||
mocktail_apply_compile_options(virtual_keyboard_raster_test)
|
||||
gtest_discover_tests(virtual_keyboard_raster_test)
|
||||
add_executable(vr_queue_submission_test vr/queue_submission_test.cc)
|
||||
target_link_libraries(vr_queue_submission_test PRIVATE
|
||||
Mocktail::VR stub_vulkan GTest::gtest_main)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/vr/virtual_keyboard.h"
|
||||
|
||||
namespace mocktail::vr {
|
||||
namespace {
|
||||
|
||||
using pose::Pose;
|
||||
using pose::Vec3;
|
||||
|
||||
Pose Head() {
|
||||
Pose head;
|
||||
head.position = Vec3{0.f, 1.6f, 0.f};
|
||||
return head;
|
||||
}
|
||||
|
||||
int FindKey(const VirtualKeyboard& keyboard, const std::string& label) {
|
||||
const auto& keys = keyboard.keys();
|
||||
for (std::size_t i = 0; i < keys.size(); ++i)
|
||||
if (keys[i].label == label) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void SetText(VirtualKeyboard* keyboard, const std::string& utf8, bool password,
|
||||
std::uint64_t revision) {
|
||||
VirtualKeyboardText text;
|
||||
text.utf8 = utf8;
|
||||
text.cursor_utf16 = static_cast<int>(utf8.size());
|
||||
text.password = password;
|
||||
text.revision = revision;
|
||||
keyboard->SetText(text);
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardRasterTest, DrawsFieldTextAndCaret) {
|
||||
VirtualKeyboard keyboard;
|
||||
keyboard.Show(Head());
|
||||
VirtualKeyboardRasterizer rasterizer;
|
||||
auto field_light_pixels = [&](const std::vector<std::uint8_t>& rgba) {
|
||||
const int bottom = static_cast<int>(VirtualKeyboard::kFieldFraction *
|
||||
VirtualKeyboard::kTextureHeight);
|
||||
int count = 0;
|
||||
for (int y = 8; y < bottom - 8; ++y)
|
||||
for (int x = 30; x < VirtualKeyboard::kTextureWidth - 30; ++x)
|
||||
if (rgba[(static_cast<std::size_t>(y) *
|
||||
VirtualKeyboard::kTextureWidth + x) * 4 + 1] > 120)
|
||||
++count;
|
||||
return count;
|
||||
};
|
||||
std::vector<std::uint8_t> empty, typed, masked;
|
||||
if (!rasterizer.Draw(keyboard, &empty)) GTEST_SKIP() << "no host font";
|
||||
SetText(&keyboard, "Hello world", false, 1);
|
||||
ASSERT_TRUE(rasterizer.Draw(keyboard, &typed));
|
||||
EXPECT_GT(field_light_pixels(typed), field_light_pixels(empty) + 200);
|
||||
SetText(&keyboard, "Hello world", true, 2);
|
||||
ASSERT_TRUE(rasterizer.Draw(keyboard, &masked));
|
||||
EXPECT_NE(masked, typed); // dots, not the letters
|
||||
EXPECT_GT(field_light_pixels(masked), field_light_pixels(empty));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardRasterTest, PasswordPictureDoesNotDependOnItsLetters) {
|
||||
VirtualKeyboard keyboard;
|
||||
keyboard.Show(Head());
|
||||
VirtualKeyboardRasterizer rasterizer;
|
||||
std::vector<std::uint8_t> first, second;
|
||||
SetText(&keyboard, "Hello world", true, 1);
|
||||
if (!rasterizer.Draw(keyboard, &first)) GTEST_SKIP() << "no host font";
|
||||
SetText(&keyboard, "Jumpy vixen", true, 2);
|
||||
ASSERT_TRUE(rasterizer.Draw(keyboard, &second));
|
||||
EXPECT_EQ(first, second);
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardRasterTest, FillsAnOpaqueTexture) {
|
||||
VirtualKeyboard keyboard;
|
||||
keyboard.Show(Head());
|
||||
VirtualKeyboardRasterizer rasterizer;
|
||||
std::vector<std::uint8_t> rgba;
|
||||
(void)rasterizer.Draw(keyboard, &rgba); // labels depend on host fonts
|
||||
ASSERT_EQ(rgba.size(), static_cast<std::size_t>(
|
||||
VirtualKeyboard::kTextureWidth) *
|
||||
VirtualKeyboard::kTextureHeight * 4);
|
||||
for (std::size_t i = 3; i < rgba.size(); i += 4) ASSERT_EQ(rgba[i], 255);
|
||||
// A key is lighter than the background behind it.
|
||||
const auto& q = keyboard.keys()[FindKey(keyboard, "q")];
|
||||
const int x = static_cast<int>(q.u0 * VirtualKeyboard::kTextureWidth) + 20;
|
||||
const int y = static_cast<int>(q.v0 * VirtualKeyboard::kTextureHeight) + 20;
|
||||
const std::size_t key_px =
|
||||
(static_cast<std::size_t>(y) * VirtualKeyboard::kTextureWidth + x) * 4;
|
||||
EXPECT_GT(rgba[key_px], rgba[0]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
@@ -346,13 +346,141 @@ TEST(VirtualKeyboardTest, LayoutRowsFillThePanelAndCloseKeyHides) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const auto closed = d.Frame(AimAtKey(d.keyboard, "Close"), 1.f);
|
||||
EXPECT_TRUE(closed.closed);
|
||||
EXPECT_EQ(closed.scancodes, std::vector<std::uint32_t>{kEscape});
|
||||
EXPECT_FALSE(d.keyboard.visible());
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
EXPECT_TRUE(d.Frame(AimAtKey(d.keyboard, "q"), 0.f).text.empty());
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, TriggerStaysWithheldAfterCloseUntilReleased) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const Pose close = AimAtKey(d.keyboard, "Close");
|
||||
d.Frame(close, 1.f);
|
||||
ASSERT_FALSE(d.keyboard.visible());
|
||||
// The trigger that pressed Close is still down; Roblox must not see it as
|
||||
// a click on whatever is behind the panel.
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(0));
|
||||
d.Frame(close, 1.f);
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
d.Frame(close, 0.f);
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
d.Frame(close, 1.f); // a new press belongs to Roblox
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, TriggerStaysWithheldWhenThePanelHidesMidPress) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const Pose enter = AimAtKey(d.keyboard, "Enter");
|
||||
d.Frame(enter, 1.f);
|
||||
d.keyboard.Hide(); // Enter made the TextBox release focus
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
d.Frame(enter, 0.f);
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
// A hand that was only hovering is free at once.
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
d.Frame(enter, 0.f);
|
||||
d.keyboard.Hide();
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, HeldTriggerTypesNothingWhenThePanelReopens) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
d.Frame(AimAtKey(d.keyboard, "Close"), 1.f);
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const Pose q = AimAtKey(d.keyboard, "q");
|
||||
EXPECT_TRUE(d.Frame(q, 1.f).text.empty());
|
||||
d.Frame(q, 0.f);
|
||||
EXPECT_EQ(d.Frame(q, 1.f).text, std::vector<std::string>{"q"});
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, CursorMovesRedrawAtMostThirtyTimesASecond) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const VirtualKey space = d.keyboard.keys()[FindKey(d.keyboard, "Space")];
|
||||
const float v = (space.v0 + space.v1) / 2;
|
||||
float u = 0.10f;
|
||||
d.Frame(AimAt(d.keyboard, u, v), 0.f);
|
||||
const std::uint64_t before = d.keyboard.revision();
|
||||
// 30 frames at 90 Hz sliding along one key: 330 ms of pointer motion.
|
||||
for (int frame = 0; frame < 30; ++frame) {
|
||||
u += 0.01f;
|
||||
d.Frame(AimAt(d.keyboard, u, v), 0.f);
|
||||
}
|
||||
const std::uint64_t redraws = d.keyboard.revision() - before;
|
||||
EXPECT_LE(redraws, 11u);
|
||||
EXPECT_GE(redraws, 5u);
|
||||
// Once the pointer rests, the drawn cursor catches up with it.
|
||||
d.Frame(AimAt(d.keyboard, u, v), 0.f, 1, 40 * kMs);
|
||||
float drawn_u = 0.f, drawn_v = 0.f;
|
||||
ASSERT_TRUE(d.keyboard.cursor(1, &drawn_u, &drawn_v));
|
||||
EXPECT_NEAR(drawn_u, u, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, BothCursorsKeepUpWhileBothHandsMove) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const VirtualKey space = d.keyboard.keys()[FindKey(d.keyboard, "Space")];
|
||||
const float v = (space.v0 + space.v1) / 2;
|
||||
float left_u = 0.10f, right_u = 0.50f;
|
||||
auto frame = [&] {
|
||||
VirtualKeyboardPointer pointers[2];
|
||||
pointers[0].valid = pointers[1].valid = true;
|
||||
pointers[0].aim = AimAt(d.keyboard, left_u, v);
|
||||
pointers[1].aim = AimAt(d.keyboard, right_u, v);
|
||||
d.now += 11 * kMs;
|
||||
d.keyboard.Update(d.now, pointers);
|
||||
};
|
||||
frame();
|
||||
// 30 frames at 90 Hz, both hands sliding along one key.
|
||||
for (int step = 0; step < 30; ++step) {
|
||||
left_u += 0.005f;
|
||||
right_u += 0.005f;
|
||||
frame();
|
||||
}
|
||||
// Neither drawn cursor trails its hand by more than one redraw interval.
|
||||
float drawn_u = 0.f, drawn_v = 0.f;
|
||||
ASSERT_TRUE(d.keyboard.cursor(0, &drawn_u, &drawn_v));
|
||||
EXPECT_NEAR(drawn_u, left_u, 0.02f);
|
||||
ASSERT_TRUE(d.keyboard.cursor(1, &drawn_u, &drawn_v));
|
||||
EXPECT_NEAR(drawn_u, right_u, 0.02f);
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, HeldTriggerStaysWithheldWhenAReopenedPanelHides) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
const Pose close = AimAtKey(d.keyboard, "Close");
|
||||
d.Frame(close, 1.f);
|
||||
// The panel reopens and hides again during the same trigger press.
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
d.Frame(close, 1.f);
|
||||
d.keyboard.Hide();
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
d.Frame(close, 1.f);
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
d.Frame(close, 0.f);
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, HeldTriggerStaysWithheldWhileAimTrackingDrops) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
d.Frame(AimAtKey(d.keyboard, "Close"), 1.f);
|
||||
ASSERT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
// The aim pose is lost for a frame; the trigger is still down.
|
||||
VirtualKeyboardPointer lost[2];
|
||||
lost[1].trigger = 1.f;
|
||||
d.now += 11 * kMs;
|
||||
d.keyboard.Update(d.now, lost);
|
||||
EXPECT_TRUE(d.keyboard.CapturesTrigger(1));
|
||||
lost[1].trigger = 0.f;
|
||||
d.now += 11 * kMs;
|
||||
d.keyboard.Update(d.now, lost);
|
||||
EXPECT_FALSE(d.keyboard.CapturesTrigger(1));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, RevisionMovesOnlyWhenTheImageChanges) {
|
||||
Driver d;
|
||||
d.keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
@@ -413,57 +541,5 @@ TEST(VirtualKeyboardTest, TextMirrorFollowsTheFocusedTextBox) {
|
||||
EXPECT_TRUE(CurrentVirtualKeyboardText().utf8.empty());
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, RasterizerDrawsFieldTextAndCaret) {
|
||||
VirtualKeyboard keyboard;
|
||||
keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
VirtualKeyboardRasterizer rasterizer;
|
||||
auto field_light_pixels = [&](const std::vector<std::uint8_t>& rgba) {
|
||||
const int bottom = static_cast<int>(VirtualKeyboard::kFieldFraction *
|
||||
VirtualKeyboard::kTextureHeight);
|
||||
int count = 0;
|
||||
for (int y = 8; y < bottom - 8; ++y)
|
||||
for (int x = 30; x < VirtualKeyboard::kTextureWidth - 30; ++x)
|
||||
if (rgba[(static_cast<std::size_t>(y) *
|
||||
VirtualKeyboard::kTextureWidth + x) * 4 + 1] > 120)
|
||||
++count;
|
||||
return count;
|
||||
};
|
||||
std::vector<std::uint8_t> empty, typed, masked;
|
||||
if (!rasterizer.Draw(keyboard, &empty)) GTEST_SKIP() << "no host font";
|
||||
VirtualKeyboardText text;
|
||||
text.utf8 = "Hello world";
|
||||
text.cursor_utf16 = 11;
|
||||
text.revision = 1;
|
||||
keyboard.SetText(text);
|
||||
ASSERT_TRUE(rasterizer.Draw(keyboard, &typed));
|
||||
// Text adds light glyph pixels; the empty field only has its caret.
|
||||
EXPECT_GT(field_light_pixels(typed), field_light_pixels(empty) + 200);
|
||||
text.password = true;
|
||||
text.revision = 2;
|
||||
keyboard.SetText(text);
|
||||
ASSERT_TRUE(rasterizer.Draw(keyboard, &masked));
|
||||
EXPECT_NE(masked, typed); // dots, not the letters
|
||||
EXPECT_GT(field_light_pixels(masked), field_light_pixels(empty));
|
||||
}
|
||||
|
||||
TEST(VirtualKeyboardTest, RasterizerFillsAnOpaqueTexture) {
|
||||
VirtualKeyboard keyboard;
|
||||
keyboard.Show(HeadAt(0.f, 1.6f, 0.f));
|
||||
VirtualKeyboardRasterizer rasterizer;
|
||||
std::vector<std::uint8_t> rgba;
|
||||
(void)rasterizer.Draw(keyboard, &rgba); // labels depend on host fonts
|
||||
ASSERT_EQ(rgba.size(), static_cast<std::size_t>(
|
||||
VirtualKeyboard::kTextureWidth) *
|
||||
VirtualKeyboard::kTextureHeight * 4);
|
||||
for (std::size_t i = 3; i < rgba.size(); i += 4) ASSERT_EQ(rgba[i], 255);
|
||||
// A key is lighter than the background behind it.
|
||||
const auto& q = keyboard.keys()[FindKey(keyboard, "q")];
|
||||
const int x = static_cast<int>(q.u0 * VirtualKeyboard::kTextureWidth) + 20;
|
||||
const int y = static_cast<int>(q.v0 * VirtualKeyboard::kTextureHeight) + 20;
|
||||
const std::size_t key_px =
|
||||
(static_cast<std::size_t>(y) * VirtualKeyboard::kTextureWidth + x) * 4;
|
||||
EXPECT_GT(rgba[key_px], rgba[0]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mocktail::vr
|
||||
Reference in new issue
Block a user