249 Commits
Author SHA1 Message Date
vii adccf4e484 Merge branch 'main' into vr 2026-10-16 01:22:27 -05:00
vii c4be3de14a fix(runtime): start a queued game after leaving the current one 2026-10-16 01:19:29 -05:00
vii 876f8b8c32 perf(runtime): compile menu Lua to native code and switch it off in games 2026-10-16 01:13:49 -05:00
dim-ghub 1854e00934 feat(vr): add SMAA as a vr.anti_aliasing choice
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.
2026-10-16 01:12:21 -05:00
vii c614fba032 feat(profile): trace emulated ETC2 texture VRAM and report its peak
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.
2026-10-16 00:53:05 -05:00
dim-ghub 0221738952 Merge remote-tracking branch 'origin/main' into vr 2026-10-16 00:52:17 -05:00
dim-ghub 046632cf99 fix(input): render emoji in the TextBox overlay (#24)
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.
2026-10-16 00:50:10 -05:00
dim-ghub 753a3f8106 feat(vr): anti-alias the headset eyes with FXAA
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.
2026-10-15 20:19:17 -05:00
dim-ghub 569df68729 fix(vr): stop trapping the desktop cursor in the window during VR
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.
2026-10-15 19:55:35 -05:00
dim-ghub ea248b1ecf feat(vr): add vr.mode with on, off and auto
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.
2026-10-15 19:37:32 -05:00
dim-ghub dea1d5fd59 test(vr): cover handing present dependencies to the GPU
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.
2026-10-15 11:43:51 -05:00
dim-ghub 7296520064 perf(vr): order eye copies and the desktop mirror on the GPU
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.
2026-10-15 04:52:37 -05:00
vii a3a2b76a06 feat(update): add an evidence-scoring solver for HostAbi addresses with a check command 2026-10-15 01:49:19 -05:00
vii f333399e18 test(update): add a 2908 to 3222 regression set for the HostAbi deriver
Share the input checks and FMOD slot tables so the solver reuses them.
2026-10-15 01:49:19 -05:00
vii de0ddf7864 feat(update): solve the VR device bridge and compare it with the VR offset finder 2026-10-15 02:20:13 -05:00
vii e553db2001 fix(vr): point 3092 FixRegisterMotors at the flag Roblox reads
The 3092 table had the address of another field in the same registration, so the flag never turned on.
2026-10-15 02:20:13 -05:00
vii 44ec71f826 Merge branch 'main' into vr
# Conflicts:
#	FAQ.md
2026-10-15 01:57:30 -05:00
vii 3062909209 refactor(jnivm): resolve method names once and log calls with no handler
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.
2026-10-15 01:31:02 -05:00
dim-ghub 42b5138b11 fix(vr): deliver hand-motor and HapticEffect haptics the way games aim them
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.
2026-10-15 01:12:01 -05:00
vii 2eaba9048b fix(vr): reload the OpenXR runtime with its own Vulkan loader instead of pinning it
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.
2026-10-14 22:31:55 -05:00
vii a2ffa5c811 Merge branch 'vr' of CoderDayton/nightcap into vr 2026-10-14 04:29:10 -05:00
vii 2be319a979 fix(vr): keep the OpenXR runtime loaded so a game join does not deadlock 2026-10-14 04:19:58 -05:00
vii 6cf641d472 feat(vr): find the haptic flags with the offset finder and enable them on 3092 2026-10-14 03:14:16 -05:00
vii 3788026d10 fix(vr): limit haptics to VR sessions and check flag names before writing
- 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.
2026-10-14 03:14:16 -05:00
dim-ghub f3e68fc3b7 feat(vr): implement OpenXR controller haptics and bypass mobile suppression
- 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.
2026-10-14 01:32:32 -05:00
vii 87ac6ee572 test(vr): cover the keyboard upload and quad layer on a real OpenXR runtime 2026-10-13 19:58:40 -05:00
vii fcc8b5382a fix(vr): turn off the desktop flyout side panel in VR 2026-10-13 19:58:40 -05:00
vii 9826535102 fix(vr): align the IsVRAppBuild hook so unoptimized builds do not crash Roblox 2026-10-13 19:58:40 -05:00
vii 123968be89 test(assets): pin overlay priority and path safety when the root is the content folder 2026-10-13 18:37:35 -05:00
vii 393297a439 feat(vr): support Roblox 3222 (2.742.1059) 2026-10-13 18:37:35 -05:00
vii 7ec8304d8f fix(vr): stop keyboard click-through, redraw churn and upload leaks; build its drawing only with VR 2026-10-13 18:37:34 -05:00
vii f9289d9947 fix(vr): read the pointer hand and HeadScale offsets per build and check them at start-up 2026-10-13 18:37:26 -05:00
vii 7294a46e8a fix(vr): turn on Vulkan queue locking when the adapter loads after the backend 2026-10-13 18:37:19 -05:00
dim-ghub 228121157b vr: drop test hooks and dead code, document every VR variable
- 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.
2026-10-13 00:28:26 -05:00
dim-ghub a06c7de93e vr: fix bridge hand math and make its shutdown safe for live guest threads
- 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.
2026-10-13 00:24:45 -05:00
dim-ghub 8106203c8e vr: fix eye colour formats, mirror segment safety and frame-wait locking
- Once the guest eye format is known, the eye swapchains are recreated
  once in the format that keeps its bytes: the eye format, else its
  UNORM/SRGB sibling (raw copy), else the other channel order with the same
  sRGB-ness (a blit that only swizzles). Mixed order-and-gamma cases no
  longer go through a gamma-converting blit; if only that remains, it is
  logged.
- The desktop mirror segment is created with O_EXCL after removing our own
  stale segment (a crashed run's leftover), and must be private to this
  user before eye images are written to it.
- A new frame_mutex_ is held for the whole XR frame cycle and by every path
  that can destroy the session (taken before mutex_). That lets the cycle
  release mutex_ while it blocks in xrWaitFrame, so guest render-thread
  hooks no longer wait out the runtime's pacing. Swapchain-image and fence
  waits stay under mutex_; they return promptly unless the GPU or
  compositor stalls, and they run while guest eye bindings are copied.
2026-10-13 00:08:22 -05:00
dim-ghub 0603734ff6 vr: recover from XR failures instead of giving up on headset output
- Every frame error, including the swapchain-image and copy-fence
  timeouts, now tears the session down recoverably instead of ending XR
  output for the rest of the run.
- Session recovery backs off (1, 2, 4, 8 s) and recreates the XrInstance
  after an instance-loss event, an instance/runtime failure, or three
  failed attempts, so VR returns after SteamVR or WiVRn restarts instead
  of retrying forever.
- While no XR session runs (headset idle, session not READY, recovering)
  presents are paced at 60 Hz outside the backend lock instead of running
  unthrottled.
- A PrepareHostPresent failure that consumed nothing (copy resources
  unavailable, failed fence reset or submit) presents normally instead of
  failing the guest's vkQueuePresentKHR; only device loss is propagated.
- A runtime/guest GPU mismatch falls back to the plain host device and
  keeps the game running without headset output, naming both GPUs.
- A failed EGL attach on the GLES path no longer makes window init fail;
  the game runs without headset output and the bind is not retried.
2026-10-13 00:04:42 -05:00
dim-ghub 0b5578d80b vr: keep non-VR launches on their original paths
Queue and device-idle locks in the Vulkan adapter are now taken only after
an armed OpenXR backend calls mocktail_vulkan_enable_queue_sharing(). The
guest synchronizes its own queue use; only the runtime and the eye copies
add a second user. Without VR, and in builds without it, submits and
presents take no locks.

FFlagDebugEnableVREmulator is only written when VR is active, so a user's
own fflags.json value is no longer overridden on every launch.

Config loading is back after the single-instance lock and the support
bundle guard. A non-VR build still rejects --vr before creating any data
or state directories, from the command line alone; a vr.enabled config in
such a build is rejected once the config is loaded.
2026-10-12 23:59:46 -05:00
dim-ghub bafbe4268c vr: name the build and the supported builds when VR is unavailable
An explicit --vr on a Roblox build without VR support now exits with the
build's version and Build ID, the builds VR supports, and how to select one
(--roblox-lib). When VR is only enabled in the config, the launch continues
without VR and logs the same details. The bridge's own Install error names
both as well. The verified build contracts carry their version labels and
move to namespace scope so they can be listed.
2026-10-12 23:55:37 -05:00
dim-ghub 3ef81c3d93 vr: make VR opt-in and build without the OpenXR submodule
VR now stays off at runtime in every build until --vr or vr.enabled: true,
so a normal launch on a VR-capable build never tries to reach an OpenXR
runtime and no longer exits when none is running.

MOCKTAIL_ENABLE_VR defaults to AUTO: OpenXR support is built when the
third_party/OpenXR-SDK submodule is present and skipped with a notice
otherwise, so source packages that do not fetch submodules (Nix, AUR) keep
building. ON still requires the submodule. mocktail-vr-probe is only built
and installed in VR builds.

CI gains an x86_64 job that builds and tests with MOCKTAIL_ENABLE_VR=OFF.
The config docs and README describe the new defaults.
2026-10-12 23:53:31 -05:00
dim-ghub 360ab6af58 vr: fix CI failures from the 3092 profile
Mark the 3092 profile experimental. It still runs when selected with
--roblox-lib, but it no longer becomes the preferred update target for
every x86_64 user, so the shipped payload metadata (2998) is again the
preferred profile.

Select the 2998 entry by Build ID in the partial-input profile test instead
of relying on manifest order, and drop the shipped 3092 entry before the
test re-identifies 2998 as 3092.

Align the example config's VR build wording with the bootstrap template.
2026-10-12 23:48:32 -05:00
dim-ghub 4cc0e41ef3 vr: add in-headset virtual keyboard for TextBox input
When Roblox asks for a keyboard (showKeyboard), a world-locked panel
opens below eye level as an OpenXR quad layer. Controllers point at keys
and the trigger types; text and key presses go through the same platform
events as the desktop keyboard, so the focused TextBox edits identically.

The layout is US QWERTY without the function row or Ctrl/Alt, with one-shot
Shift, Caps Lock, Tab, Backspace auto-repeat, Enter and Close. A trigger
aimed at the panel is withheld from Roblox so a key press is not also a
click. A text field above the keys mirrors the focused TextBox, whose
desktop overlay never reaches the eye images. MOCKTAIL_VR_KEYBOARD=0 turns
it off.
2026-10-12 23:45:51 -05:00
dim-ghub ddc985218b vr: add separate VR frame-rate limit, render scale and unpaced benchmark mode
vr.frame_rate_limit / MOCKTAIL_VR_FRAME_RATE_LIMIT replaces
graphics.frame_rate_limit while VR is active and takes the same values.
It defaults to unlimited so xrWaitFrame paces Roblox at the headset
refresh rate instead of Roblox's own 60 FPS cap.

MOCKTAIL_VR_RENDER_SCALE scales the runtime's recommended eye extent.
SteamVR's recommendation for a Quest 2 at 90 Hz (2244x2352 per eye) missed
the 11.1 ms budget and halved the rate to 45 FPS.

MOCKTAIL_VR_UNPACED=1 stops guest frames from blocking in xrWaitFrame:
the open XR frame spans several guest frames and is submitted once per
display period, for measuring maximum render rate.
2026-10-12 22:44:46 -05:00
dim-ghub 33dd7769f0 Merge remote-tracking branch 'upstream/main' into vr
# Conflicts:
#	stubs/libandroid_stub.cc
2026-10-12 21:50:15 -05:00
dim-ghub 88bb4c940c vr: align pointer ray pitch with hand orientation and reset world scale to 1.0 2026-10-12 21:28:20 -05:00
dim-ghub 68f8ec82d2 vr: fix sRGB double-gamma, home world avatar spawn, and controller aim alignment 2026-10-12 20:25:21 -05:00
dim-ghub a441fa7110 assets: fix shader pack path resolution and root directory normalization 2026-10-12 02:16:21 -05:00
dim-ghub d54d20de64 audio: align input capture test with shared_ptr atomic store and restore FMOD profile functions 2026-10-12 01:45:08 -05:00
vii 81de528668 feat(packaging): publish nightcap and nightcap-bin to the AUR on release
Pushes nothing until the AUR_SSH_PRIVATE_KEY secret is set. Recipes move to 0.3.4.
2026-10-12 01:44:21 -05:00
dim-ghub f9fede1649 Merge remote-tracking branch 'mocktail/vr' into vr 2026-10-12 01:39:13 -05:00