vr.anti_aliasing now takes fxaa, smaa or off. SMAA runs the reference
implementation (third_party/smaa, MIT) at its High preset: luma edge
detection and blending weights into per-eye images, then neighborhood
blending into the runtime's swapchain through FXAA's final pass. Its area and
search lookup textures are uploaded once per device. If SMAA cannot be set
up, the eyes fall back to FXAA.
Also export vr.anti_aliasing from config.yaml: the VR backend reads the
process environment, so the setting only worked as an environment variable.
On a Quest 2 through SteamVR at 120 Hz (2136x2236 per eye), SMAA ran at 118.6
fps against 118.8 for FXAA, with p99 frame times of 10.1 and 9.5 ms.
Emulated images keep a running total of their decoded and ETC2/EAC sizes over every mip and layer. --profile writes both as counters, and summarize_profile_trace.py prints their peaks.
The overlay's fallback fonts were the first few fontconfig sans matches,
which rarely include an emoji font. Fall back to Roblox's bundled
TwemojiMozilla and RobloxEmoji fonts, then the system emoji font, before
the sans list.
Roblox's VR debug device renders both eyes into single-sample textures, so
edges were jagged in the headset. When the eyes go to the runtime's
swapchain, an FXAA draw now replaces the plain copy. It samples the eye
directly, and re-encodes for sRGB swapchains so the guest's bytes round-trip
exactly. Anything it cannot set up falls back to the copy.
vr.anti_aliasing (fxaa, the default, or off) controls it. The SPIR-V is
generated from shaders/vr by scripts/generate_vr_shaders.py and checked in,
so builds need no shader compiler; a test checks it against the sources when
glslc is installed.
On a Quest 2 through SteamVR at 90 Hz (2244x2352 per eye) frame times did not
change. Captured output keeps 97 to 98 percent of pixels byte-identical and
the average brightness unchanged, and smooths the edges.
Roblox's mouse lock and the RMB camera drag put the window into relative
mouse mode with a grab, which locked the cursor inside it while the headset
was in use. In VR the controllers aim and the window only mirrors the left
eye, so the pointer is never captured and the system cursor stays visible.
Without a headset SteamVR refuses the OpenXR client at xrCreateInstance
(VRInitError_Driver_WirelessHmdNotConnected) instead of reporting no system,
so say that it usually means no headset is connected. Describe vr.mode in the
README, docs/VR.md and the changelog.
vr.mode in config.yaml picks how Nightcap starts: off on the desktop, on in
VR, auto in VR only when this user's SteamVR is running and its OpenXR
runtime reports a headset. auto never starts SteamVR, and asks its runtime
from a short-lived copy of the process so the runtime's libraries do not load
before the VR backend arms. vr.enabled still works as on/off, and --vr or
--no-vr still override the file for one launch.
With the eye copies no longer waited for, MOCKTAIL_VR_MIRROR_SHM made the CPU
wait for the whole frame before publishing, which cut the frame rate from 117
to 82 fps at 120 Hz. Publish each readback at the next copy, when the GPU has
finished it. The external viewer sees each frame one frame later.
Move the empty wait-and-signal batch into internal::ChainPresentDependencies,
like WaitForPresentDependencies, and test that it waits on every present
semaphore, signals the replacement, never blocks the CPU, and leaves a failed
submit to the guest.
The frame cycle read the pose source, evidence and mirror settings from the
environment every frame. Read them once. Check the thread-local eye request
before taking the backend lock, so render passes that are not eye passes do
not contend with the frame cycle.
Every frame the CPU waited for the GPU to finish Roblox's whole frame in the
desktop mirror, then again for the eye copy, so CPU and GPU work never
overlapped. The mirror blit now consumes the guest's present semaphores and
signals a per-image semaphore that the present waits on. Present preparation
hands remaining waits to the GPU the same way. Eye copies are submitted from a
small ring of command buffers and fences and rely on queue order, so the CPU
only waits when the GPU is three frames behind or the guest destroys an image
a copy still reads.
On Monado at 120 Hz with 1280x1440 eyes this removed 2 to 5 ms of waiting per
frame and kept the frame rate at 118 to 119 fps instead of 103 to 118.
Also free the copy resources when the guest device goes away without a
session, so a later device never reuses stale handles.
MOCKTAIL_VR_PERF now reports how long the CPU waits in the desktop mirror and
in present preparation, and the time between presents, so per-frame stalls
outside the XR frame cycle show up in the numbers.
GetMethodID maps each name to a JniMethod enum, so dispatch compares
numbers instead of strings. Calls that reach no handler print one
"no handler for" line per class, method and signature. Unknown
gameActivity_ callbacks are recorded on the CallVoidMethod path too.
The FMOD write path reuses a per-thread buffer instead of allocating.
The release environment only accepts main and v* tags, so a workflow pushed on another branch cannot read the keys. Flatpak builds that do not publish run without it.
The Android client drops HapticService:SetMotor on the Gamepad1 LeftHand and
RightHand motors: its haptics backend only consumes Large and Small. VR games
such as VR Hands drive those motors, so they never vibrated.
- Interpose HapticService::SetMotor through its reflection descriptor, call
the original, and forward Gamepad1 LeftHand/RightHand to OpenXR. The
descriptor and the engine's intensity reader are found by pattern at load,
for both the 2998 (tuple span) and 3222 (shared tuple) reader ABIs. The held
level renews a 50 ms lease every presented frame.
- Report the hand motors at x = -1/+1 in GetMotors. Positional HapticEffects
aim at a hand there with a 0.5 radius (VOXEL DESTRUCT 2 does), so at +-0.2
every effect mixed to zero.
- Play SetMotorState buffers sample by sample with trailing silence trimmed.
Roblox sends 100 x 25 ms look-ahead buffers; taking the peak for the whole
buffer turned a short bump into 2.5 s at full strength.
- Stop vibrating from the DebugDeviceVR haptic slot. The engine pulls it every
frame but with each mix buffer's first sample, held about a second, so it
duplicated the buffers as a long buzz.
- Enable SupportGenericHapticsOnVR and FixRegisterMotors with the other haptic
flags.
The Vulkan adapter exports vk* symbols globally, so a runtime loaded again after it bound its Vulkan calls to the adapter and deadlocked the join. Pinning avoided the reload but left SteamVR with its old VkInstance, which aborted the join with "vkGetInstanceProcAddr: Invalid instance". Later instances now load the runtime libraries with RTLD_DEEPBIND and let them unload with their instance.
- Register HapticProtocol and report motors only when MOCKTAIL_VR_ENABLED is on.
- Drop the per-message [haptic] dispatch log; log each OpenXR haptic result once per hand.
- Write a fast flag only when its name pointer matches the expected name.
- Remove the state-sync fallback that dlsym can never resolve.
- Implement RobloxHapticProtocolBridge for MessageBus HapticProtocol
(SupportsHaptics, GetMotors, SetMotorState, UpdateSingletonVibration).
- Wire bridge into RobloxExperienceComposition and direct game runtime.
- Call JNI NativeGLInterface.nativeUpdateMobileHapticsState during startup
and game completion.
- Enable FFlagKeepHapticsOnMobileWhenGamepadConnected and FFlagVRHaptics in
VR client settings overrides to bypass gamepad haptic suppression in libroblox.
- Directly activate KeepHaptics and VRHaptics fast flag bytes in memory
within RobloxVrDeviceBridge::Activate for builds 2998 and 3222.
- Check xrApplyHapticFeedback result code and add diagnostic logging.
New intro and feature list in the README and the site. The site gets a title with Roblox, link preview tags, structured data, a sitemap, robots.txt, a preview image and the Search Console tag. Screenshots are retaken, the old lobby shot is removed and the store listing points at the new ones.
Build 3222 moved the hand selector, hand frame and camera HeadScale fields, so the fixed byte patterns found nothing. Match the code shape instead, read the three offsets from it and write them to the output.
- Remove the MOCKTAIL_VR_TEST_SESSION_LOSS_AFTER fault injection; nothing
used it.
- The scripted head pose and the eye evidence dumper only work in builds
configured with -DMOCKTAIL_VR_DIAGNOSTICS=ON (default OFF); release
builds ignore their environment variables.
- Remove FindFmodInputCaptureProfile and HasExpectedFmodInputCaptureVtable
(only their own tests called them) and the never-defined
PrepareCaptureLocked declaration; the audio bridge files match upstream
again.
- docs/VR.md lists every MOCKTAIL_VR_* variable with its default, linked
from the README.
- MOCKTAIL_VR_HAND_POSE_SOURCE=grip now rotates hand records from the grip
pose. base_ori started as the identity quaternion, so the norm check
never fell back to it and hand rotation was dropped.
- Hand records cancel the engine's stored hand pitch (read from
hand_pitch_storage_rva by the pointer hook, 20 until read) instead of a
hard-coded 20, so they agree with the pointer ray.
- Shutdown never rewrites the aim-pointer prologue while guest threads may
be executing it; the jump stays and only forwards once the bridge is gone,
and a later activation reuses it.
- Guest hooks count themselves in flight before loading the bridge pointer;
Shutdown unpublishes the bridge and waits for in-flight calls to drain,
so no hook keeps a pointer to a destroyed bridge.
- Roblox's original state and eye getters are kept for the process
lifetime. A hook reached with no bridge, including after a failed vtable
restore, forwards to them instead of returning an unfilled state buffer.
Restoring the vtable no longer depends on the pointer hook.
- The activation log names the vtable slots in the right order.