fix(vr): read the pointer hand and HeadScale offsets per build and check them at start-up

This commit is contained in:
vii committed 2026-10-13 18:37:26 -05:00
1 parent 7294a46e8a
commit f9289d9947
3 files changed
+99 -6

No files matched your search

+41 -6
View File
@@ -403,6 +403,8 @@ constexpr VrBuildContract kContracts[] = {
0x3a8,
0x73677a8,
0x1d48b70,
0x118,
0x140,
"2998 (2.736.1408)"},
{"5f0704edd9064f566ee3d6df2bd2fabbcc709f03",
{0x6d08108, 0x37f9fe4, 0x37fa3c6, 0x37fa3b6, 0x37fa3e8, 0x73f4690,
@@ -413,6 +415,8 @@ constexpr VrBuildContract kContracts[] = {
0x3a0,
0x73f24c8,
0x1d8f8c0,
0x118,
0x140,
"3092 (2.738.1397)"},
};
@@ -431,11 +435,26 @@ bool HasExpectedVrPointerContract(const std::uint8_t* code, std::size_t size,
code[0x59] != 0xe8 || code[0x6d] != 0xff || code[0x6e] != 0x91) {
return false;
}
std::uint32_t camera_slot;
// movsxd rax,[rsi+selector] at +0x24; movss xmm0,[r14+HeadScale] at +0x13d.
static constexpr std::array<std::uint8_t, 3> kSelectorLoad = {0x48, 0x63,
0x86};
static constexpr std::array<std::uint8_t, 5> kHeadScaleLoad = {
0xf3, 0x41, 0x0f, 0x10, 0x86};
if (std::memcmp(code + 0x24, kSelectorLoad.data(), kSelectorLoad.size()) !=
0 ||
std::memcmp(code + 0x13d, kHeadScaleLoad.data(),
kHeadScaleLoad.size()) != 0) {
return false;
}
std::uint32_t camera_slot, selector, head_scale;
std::memcpy(&camera_slot, code + 0x6f, sizeof(camera_slot));
std::memcpy(&selector, code + 0x27, sizeof(selector));
std::memcpy(&head_scale, code + 0x142, sizeof(head_scale));
return RelativeTarget(code + 0x5a, contract.pointer_frame_rva + 0x5e) ==
contract.workspace_getter_rva &&
camera_slot == contract.camera_vtable_offset;
camera_slot == contract.camera_vtable_offset &&
selector == contract.pointer_hand_selector_offset &&
head_scale == contract.camera_head_scale_offset;
}
EyeResourceState InspectEyeResources(const void *object) {
@@ -676,6 +695,24 @@ std::string DescribeSupportedVrBuilds() {
namespace internal {
int ReadPointerHandIndex(const void* service, const VrBuildContract& contract) {
int index = 0;
std::memcpy(&index,
static_cast<const unsigned char*>(service) +
contract.pointer_hand_selector_offset,
sizeof(index));
return index;
}
float ReadCameraHeadScale(const void* camera, const VrBuildContract& contract) {
float scale = 0.f;
std::memcpy(&scale,
static_cast<const unsigned char*>(camera) +
contract.camera_head_scale_offset,
sizeof(scale));
return scale;
}
float GetHandPitchOffsetDeg() {
static const float offset = [] {
if (const char* env = std::getenv("MOCKTAIL_VR_HAND_PITCH_OFFSET")) {
@@ -805,8 +842,7 @@ void RobloxVrDeviceBridge::OnPointerFrameCall(void* result, void* service) {
if (!backend) return;
const auto current = backend->PublishedHeadPose();
if (!current.valid) return;
int index = 0;
std::memcpy(&index, static_cast<unsigned char*>(service) + 0x118, 4);
const int index = internal::ReadPointerHandIndex(service, *contract_);
if (index != 1 && index != 2) return;
if (!current.hands[index - 1].aim_valid) return;
const auto& hand = native_pose.hands[index - 1];
@@ -821,9 +857,8 @@ void RobloxVrDeviceBridge::OnPointerFrameCall(void* result, void* service) {
void* camera = reinterpret_cast<void* (*)(void*)>(
vtable[contract_->camera_vtable_offset / sizeof(void*)])(workspace);
if (!camera) return;
float scale = 0;
const float scale = internal::ReadCameraHeadScale(camera, *contract_);
int degrees = 0;
std::memcpy(&scale, static_cast<unsigned char*>(camera) + 0x140, 4);
std::memcpy(&degrees,
reinterpret_cast<void*>(base + contract_->hand_pitch_storage_rva),
4);
@@ -20,12 +20,20 @@ struct VrBuildContract {
std::uintptr_t camera_vtable_offset;
std::uintptr_t hand_pitch_storage_rva;
std::uintptr_t haptic_sink_rva;
// Field offsets the pointer hook reads: the hand index in the pointer
// service and HeadScale in the workspace camera.
std::uintptr_t pointer_hand_selector_offset;
std::uintptr_t camera_head_scale_offset;
// Roblox version code and name, for messages that list supported builds.
std::string_view version;
};
const VrBuildContract* FindVrBuildContract(std::string_view build_id);
// The hand the pointer service asks about: 1 left, 2 right.
int ReadPointerHandIndex(const void* service, const VrBuildContract& contract);
float ReadCameraHeadScale(const void* camera, const VrBuildContract& contract);
// Computes the record orientation for a hand pose: the aim pose (or, with
// MOCKTAIL_VR_HAND_POSE_SOURCE=grip, the grip pose) pitched by the engine's
// own hand pitch plus the configurable offset.
+50
View File
@@ -317,6 +317,15 @@ TEST(VrPointerContract, PinsWorkspaceAndCameraSlotForEachBuild) {
"5f0704edd9064f566ee3d6df2bd2fabbcc709f03"}) {
const auto& contract = *internal::FindVrBuildContract(id);
std::array<std::uint8_t, 0x1b2> code{};
// movsxd rax,[rsi+selector] and movss xmm0,[r14+HeadScale].
const std::uint8_t selector_load[] = {0x48, 0x63, 0x86};
std::memcpy(code.data() + 0x24, selector_load, sizeof(selector_load));
const std::uint32_t selector = contract.pointer_hand_selector_offset;
std::memcpy(code.data() + 0x27, &selector, sizeof(selector));
const std::uint8_t scale_load[] = {0xf3, 0x41, 0x0f, 0x10, 0x86};
std::memcpy(code.data() + 0x13d, scale_load, sizeof(scale_load));
const std::uint32_t head_scale = contract.camera_head_scale_offset;
std::memcpy(code.data() + 0x142, &head_scale, sizeof(head_scale));
const std::uint8_t prologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41,
0x57, 0x41, 0x56, 0x41, 0x54,
0x53, 0x48, 0x83, 0xec, 0x70};
@@ -336,6 +345,14 @@ TEST(VrPointerContract, PinsWorkspaceAndCameraSlotForEachBuild) {
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
code.size(), contract));
code[0x6f] ^= 8;
code[0x27] ^= 0x10;
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
code.size(), contract));
code[0x27] ^= 0x10;
code[0x142] ^= 0x10;
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
code.size(), contract));
code[0x142] ^= 0x10;
code[0x5a] ^= 1;
EXPECT_FALSE(internal::HasExpectedVrPointerContract(code.data(),
code.size(), contract));
@@ -940,3 +957,36 @@ TEST(VrHandOrientation, GripSourceKeepsTheGripRotation) {
} // namespace
} // namespace mocktail::vr
namespace mocktail::vr {
namespace {
TEST(VrPointerLayout, ReadsHandIndexAndHeadScaleAtTheBuildsOwnOffsets) {
// Roblox moves these fields between builds; a fixed offset reads garbage.
internal::VrBuildContract contract{};
contract.pointer_hand_selector_offset = 0x108;
contract.camera_head_scale_offset = 0x130;
std::array<unsigned char, 0x200> service{}, camera{};
const int hand = 2, stale_hand = 7;
const float scale = 1.5f, stale_scale = 9.f;
std::memcpy(service.data() + 0x108, &hand, 4);
std::memcpy(service.data() + 0x118, &stale_hand, 4);
std::memcpy(camera.data() + 0x130, &scale, 4);
std::memcpy(camera.data() + 0x140, &stale_scale, 4);
EXPECT_EQ(internal::ReadPointerHandIndex(service.data(), contract), 2);
EXPECT_FLOAT_EQ(internal::ReadCameraHeadScale(camera.data(), contract),
1.5f);
}
TEST(VrPointerLayout, VerifiedBuildsKeepTheirOffsets) {
for (const char* id : {"ade08266c67aee88ec9c1d00902150e1684dad3a",
"5f0704edd9064f566ee3d6df2bd2fabbcc709f03"}) {
const auto* contract = internal::FindVrBuildContract(id);
ASSERT_NE(contract, nullptr);
EXPECT_EQ(contract->pointer_hand_selector_offset, 0x118u) << id;
EXPECT_EQ(contract->camera_head_scale_offset, 0x140u) << id;
}
}
} // namespace
} // namespace mocktail::vr